224 lines
7.7 KiB
Go
224 lines
7.7 KiB
Go
package mujica
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// RepeatUnit is how often a rule repeats; zero never does.
|
|
type RepeatUnit int
|
|
|
|
const (
|
|
RepeatNone RepeatUnit = iota
|
|
RepeatDaily
|
|
RepeatWeekly
|
|
RepeatMonthly
|
|
RepeatYearly
|
|
)
|
|
|
|
// RepeatEnd says when a recurrence stops.
|
|
type RepeatEnd int
|
|
|
|
const (
|
|
RepeatForever RepeatEnd = iota
|
|
RepeatCount
|
|
RepeatUntil
|
|
)
|
|
|
|
// Recurrence repeats every Interval units, on Weekdays when weekly.
|
|
type Recurrence struct {
|
|
Unit RepeatUnit
|
|
Interval int
|
|
Weekdays []time.Weekday
|
|
End RepeatEnd
|
|
Count int
|
|
Until Date
|
|
}
|
|
|
|
// RecurrenceEditorOptions configure a RecurrenceEditor.
|
|
type RecurrenceEditorOptions struct {
|
|
// Start is the first occurrence; Until must not precede it.
|
|
Start Date
|
|
WeekStart WeekStart
|
|
Disabled bool
|
|
Label string
|
|
}
|
|
|
|
// RecurrenceResult is the editor, its change and its problem.
|
|
type RecurrenceResult struct {
|
|
Element *ui.Element
|
|
changed bool
|
|
problem string
|
|
}
|
|
|
|
// Changed reports whether the user changed the rule this frame.
|
|
func (r RecurrenceResult) Changed() bool { return r.changed }
|
|
|
|
// Problem explains why the rule cannot be saved.
|
|
func (r RecurrenceResult) Problem() string { return r.problem }
|
|
|
|
const recurrenceCustom = 5
|
|
|
|
// RecurrenceEditor edits a repeat rule and when it ends.
|
|
func RecurrenceEditor(c *ui.Context, rule *Recurrence, opts RecurrenceEditorOptions) RecurrenceResult {
|
|
recurrenceCheck(*rule)
|
|
d := Density(c)
|
|
k := Tokens(c)
|
|
before := recurrenceKey(*rule)
|
|
r := RecurrenceResult{}
|
|
label := opts.Label
|
|
if label == "" {
|
|
label = msg(c, "recurrence_editor.label")
|
|
}
|
|
r.Element = ui.Column(c).Gap(d.Unit() * 3).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled)
|
|
custom := ui.Local(r.Element, "recurrence-custom", func() bool { return rule.Interval > 1 })
|
|
r.Element.Children(func() {
|
|
mode := int(rule.Unit)
|
|
if *custom && rule.Unit != RepeatNone {
|
|
mode = recurrenceCustom
|
|
}
|
|
modes := []SelectOption[int]{}
|
|
for i, key := range []string{"none", "daily", "weekly", "monthly", "yearly", "custom"} {
|
|
modes = append(modes, SelectOption[int]{Value: i, Label: msg(c, "recurrence_editor."+key)})
|
|
}
|
|
if Select(c, &mode, modes, SelectOptions{Label: msg(c, "recurrence_editor.repeat"), Disabled: opts.Disabled}).Changed() {
|
|
*custom = mode == recurrenceCustom
|
|
switch {
|
|
case *custom:
|
|
rule.Unit = max(rule.Unit, RepeatDaily)
|
|
rule.Interval = max(rule.Interval, 2)
|
|
default:
|
|
rule.Unit, rule.Interval = RepeatUnit(mode), 1
|
|
if mode == int(RepeatNone) {
|
|
*rule = Recurrence{}
|
|
}
|
|
}
|
|
if rule.Unit == RepeatWeekly && len(rule.Weekdays) == 0 && !opts.Start.IsZero() {
|
|
rule.Weekdays = []time.Weekday{opts.Start.Weekday()}
|
|
}
|
|
}
|
|
if rule.Unit == RepeatNone {
|
|
return
|
|
}
|
|
if *custom {
|
|
ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() {
|
|
ui.Text(c, msg(c, "recurrence_editor.every"))
|
|
ui.Box(c).Width(d.ControlHeight() * 4).Children(func() {
|
|
n := float64(rule.Interval)
|
|
if NumberInput(c, &n, NumberInputOptions{Min: 1, Max: 999, Label: msg(c, "recurrence_editor.interval"), Disabled: opts.Disabled}).Changed() && !math.IsNaN(n) {
|
|
rule.Interval = int(n)
|
|
}
|
|
})
|
|
units := []SelectOption[RepeatUnit]{}
|
|
for u := RepeatDaily; u <= RepeatYearly; u++ {
|
|
units = append(units, SelectOption[RepeatUnit]{Value: u, Label: msg(c, recurrenceUnitKeys[u])})
|
|
}
|
|
Select(c, &rule.Unit, units, SelectOptions{Label: msg(c, "recurrence_editor.unit"), Disabled: opts.Disabled})
|
|
})
|
|
}
|
|
if rule.Unit == RepeatWeekly {
|
|
first := calendarFirstWeekday(c, opts.WeekStart)
|
|
items := []ToggleItem[time.Weekday]{}
|
|
for i := range 7 {
|
|
wd := time.Weekday((int(first) + i) % 7)
|
|
items = append(items, ToggleItem[time.Weekday]{Value: wd, Label: msg(c, fmt.Sprintf("calendar.weekday_%d", wd)),
|
|
Name: msg(c, fmt.Sprintf("calendar.weekday_long_%d", wd))})
|
|
}
|
|
ToggleGroup(c, &rule.Weekdays, items, ToggleGroupOptions{Multiple: true, AllowEmpty: true, Label: msg(c, "recurrence_editor.on_days"), Disabled: opts.Disabled})
|
|
}
|
|
ends := []RadioItem[RepeatEnd]{{Value: RepeatForever, Label: msg(c, "recurrence_editor.forever")},
|
|
{Value: RepeatCount, Label: msg(c, "recurrence_editor.after")}, {Value: RepeatUntil, Label: msg(c, "recurrence_editor.until")}}
|
|
ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Wrap().Children(func() {
|
|
if RadioGroup(c, &rule.End, ends, RadioGroupOptions{Horizontal: true, Label: msg(c, "recurrence_editor.ends"), Disabled: opts.Disabled}).Changed() {
|
|
if rule.End == RepeatCount && rule.Count == 0 {
|
|
rule.Count = 10
|
|
}
|
|
if rule.End == RepeatUntil && rule.Until.IsZero() && !opts.Start.IsZero() {
|
|
rule.Until = opts.Start.AddMonths(1)
|
|
}
|
|
}
|
|
switch rule.End {
|
|
case RepeatCount:
|
|
ui.Box(c).Width(d.ControlHeight() * 4).Children(func() {
|
|
n := float64(rule.Count)
|
|
if NumberInput(c, &n, NumberInputOptions{Min: 1, Max: 999, Label: msg(c, "recurrence_editor.count"), Disabled: opts.Disabled}).Changed() && !math.IsNaN(n) {
|
|
rule.Count = int(n)
|
|
}
|
|
})
|
|
case RepeatUntil:
|
|
DatePicker(c, &rule.Until, DatePickerOptions{Label: msg(c, "recurrence_editor.until_date"), Min: opts.Start, Disabled: opts.Disabled})
|
|
}
|
|
})
|
|
r.problem = recurrenceProblem(c, *rule, opts.Start)
|
|
ui.Text(c, RecurrenceSummary(c, *rule)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus)
|
|
datetimeError(c, r.problem)
|
|
})
|
|
if after := recurrenceKey(*rule); after != before && !r.Element.IsDisabled() {
|
|
slog.Debug("mujica: recurrence_editor change", "rule", after)
|
|
r.changed = true
|
|
}
|
|
return r
|
|
}
|
|
|
|
var recurrenceUnitKeys = [...]string{"", "recurrence_editor.days", "recurrence_editor.weeks", "recurrence_editor.months", "recurrence_editor.years"}
|
|
|
|
func recurrenceKey(r Recurrence) string { return fmt.Sprintf("%v", r) }
|
|
|
|
// recurrenceCheck panics on rules no editor can show.
|
|
func recurrenceCheck(r Recurrence) {
|
|
if r.Unit < RepeatNone || r.Unit > RepeatYearly || r.End < RepeatForever || r.End > RepeatUntil || r.Interval < 0 || r.Count < 0 {
|
|
panic(fmt.Sprintf("mujica: Recurrence %+v is out of range", r))
|
|
}
|
|
if r.Unit != RepeatNone && r.Interval == 0 {
|
|
panic("mujica: Recurrence repeats with Interval 0")
|
|
}
|
|
datetimeCheckDate("Recurrence Until", r.Until)
|
|
}
|
|
|
|
// recurrenceProblem explains why a rule cannot be saved.
|
|
func recurrenceProblem(c *ui.Context, r Recurrence, start Date) string {
|
|
switch {
|
|
case r.Unit == RepeatNone:
|
|
return ""
|
|
case r.Unit == RepeatWeekly && len(r.Weekdays) == 0:
|
|
return msg(c, "recurrence_editor.no_days")
|
|
case r.End == RepeatCount && r.Count < 1:
|
|
return msg(c, "recurrence_editor.bad_count")
|
|
case r.End == RepeatUntil && (r.Until.IsZero() || !start.IsZero() && r.Until.Compare(start) < 0):
|
|
return msg(c, "recurrence_editor.bad_until")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// RecurrenceSummary describes a rule in the window's language.
|
|
func RecurrenceSummary(c *ui.Context, r Recurrence) string {
|
|
if r.Unit == RepeatNone {
|
|
return msg(c, "recurrence_editor.never")
|
|
}
|
|
s := fmt.Sprintf(msg(c, "recurrence_editor.every_n"), r.Interval, msg(c, recurrenceUnitKeys[r.Unit]))
|
|
if r.Interval == 1 {
|
|
s = msg(c, []string{"", "recurrence_editor.daily", "recurrence_editor.weekly", "recurrence_editor.monthly", "recurrence_editor.yearly"}[r.Unit])
|
|
}
|
|
if r.Unit == RepeatWeekly && len(r.Weekdays) > 0 {
|
|
var days []string
|
|
for _, wd := range r.Weekdays {
|
|
days = append(days, msg(c, fmt.Sprintf("calendar.weekday_long_%d", wd)))
|
|
}
|
|
s += fmt.Sprintf(msg(c, "recurrence_editor.on"), strings.Join(days, msg(c, "recurrence_editor.list_sep")))
|
|
}
|
|
switch r.End {
|
|
case RepeatCount:
|
|
s += calendarCount(c, "recurrence_editor.times", r.Count)
|
|
case RepeatUntil:
|
|
s += fmt.Sprintf(msg(c, "recurrence_editor.until_text"), r.Until)
|
|
}
|
|
return s
|
|
}
|