using System.Windows; using System.Windows.Controls; using Button = System.Windows.Controls.Button; using ComboBox = System.Windows.Controls.ComboBox; namespace OmniScheduler; public partial class TaskEditorWindow : Window { private readonly ScheduledTask _task; private readonly AppSettings _settings; private readonly NotifyClient _client; private readonly List> _hours = Enumerable.Range(0, 24).Select(v => new Option(v, v.ToString("00"))).ToList(); private readonly List> _minutesAndSeconds = Enumerable.Range(0, 60).Select(v => new Option(v, v.ToString("00"))).ToList(); private readonly List> _delayUnits = [ new(DelayUnit.Minutes, "分钟"), new(DelayUnit.Hours, "小时"), new(DelayUnit.Days, "天") ]; private bool _loading; private readonly List> _triggerKinds = [ new(TriggerKind.OneTime, "单次执行"), new(TriggerKind.Interval, "固定间隔"), new(TriggerKind.Daily, "每日定时"), new(TriggerKind.Weekly, "每周定时"), new(TriggerKind.Monthly, "每月定时"), new(TriggerKind.Cron, "Cron 表达式") ]; private readonly List> _intervalUnits = [ new(IntervalUnit.Seconds, "秒"), new(IntervalUnit.Minutes, "分钟"), new(IntervalUnit.Hours, "小时"), new(IntervalUnit.Days, "天") ]; private readonly List> _misfireStrategies = [ new(MisfireStrategy.DoNothing, "忽略,等待下一次"), new(MisfireStrategy.FireOnceNow, "立即补偿一次") ]; private TaskEditorWindow(Window owner, ScheduledTask task, AppSettings settings, NotifyClient client) { InitializeComponent(); Owner = owner; _task = task; _settings = settings; _client = client; KindBox.ItemsSource = _triggerKinds; IntervalUnitBox.ItemsSource = _intervalUnits; OneTimeDelayUnitBox.ItemsSource = _delayUnits; OneTimeDelayUnitBox.SelectedItem = _delayUnits.First(o => o.Value == DelayUnit.Minutes); MisfireBox.ItemsSource = _misfireStrategies; ChannelBox.ItemsSource = owner is MainWindow main ? main.State.Tasks.Select(t => t.Channel).Where(c => !string.IsNullOrWhiteSpace(c)).Distinct().ToList() : null; InitializeTimeSelectors(); LoadTask(); } public static bool Edit(Window owner, ScheduledTask task, AppSettings settings, NotifyClient client) { var workingCopy = task.Clone(); workingCopy.Id = task.Id; workingCopy.Name = task.Name; var dialog = new TaskEditorWindow(owner, workingCopy, settings, client); if (dialog.ShowDialog() == true) { task.CopyFrom(workingCopy); return true; } return false; } private void InitializeTimeSelectors() { SetTimeItems(OneTimeHourBox, OneTimeMinuteBox, OneTimeSecondBox); SetTimeItems(DailyHourBox, DailyMinuteBox, DailySecondBox); SetTimeItems(WeeklyHourBox, WeeklyMinuteBox, WeeklySecondBox); SetTimeItems(MonthlyHourBox, MonthlyMinuteBox, MonthlySecondBox); SetTimeItems(StartsAtHourBox, StartsAtMinuteBox, StartsAtSecondBox); SetTimeItems(EndsAtHourBox, EndsAtMinuteBox, EndsAtSecondBox); } private void LoadTask() { NameBox.Text = _task.Name; DescriptionBox.Text = _task.Description; EnabledBox.IsChecked = _task.IsEnabled; TargetUrlBox.Text = _settings.OmniNotifyUrl; ChannelBox.Text = _task.Channel; TitleBox.Text = _task.Title; BodyBox.Text = _task.Body; MisfireBox.SelectedItem = _misfireStrategies.First(o => o.Value == _task.MisfireStrategy); TriggersList.ItemsSource = _task.Triggers; TriggersList.SelectedIndex = _task.Triggers.Count > 0 ? 0 : -1; UpdateEditorAvailability(); } private void LoadTrigger(TaskTrigger? trigger) { _loading = true; try { if (trigger is null) { UpdateEditorAvailability(); return; } KindBox.SelectedItem = _triggerKinds.First(o => o.Value == trigger.Kind); SetDateTime(OneTimeDatePicker, OneTimeHourBox, OneTimeMinuteBox, OneTimeSecondBox, trigger.OneTimeAt); IntervalValueBox.Text = trigger.IntervalValue.ToString(); IntervalUnitBox.SelectedItem = _intervalUnits.First(o => o.Value == trigger.IntervalUnit); UseStartsAtBox.IsChecked = trigger.StartsAt.HasValue; SetOptionalDateTime(StartsAtDatePicker, StartsAtHourBox, StartsAtMinuteBox, StartsAtSecondBox, trigger.StartsAt); UseEndsAtBox.IsChecked = trigger.EndsAt.HasValue; SetOptionalDateTime(EndsAtDatePicker, EndsAtHourBox, EndsAtMinuteBox, EndsAtSecondBox, trigger.EndsAt); SetTimeOnly(DailyHourBox, DailyMinuteBox, DailySecondBox, trigger.TimeOfDay); SetTimeOnly(WeeklyHourBox, WeeklyMinuteBox, WeeklySecondBox, trigger.TimeOfDay); SetTimeOnly(MonthlyHourBox, MonthlyMinuteBox, MonthlySecondBox, trigger.TimeOfDay); MonthDayBox.Text = trigger.MonthDay.ToString(); LastBusinessDayBox.IsChecked = trigger.LastBusinessDay; CronBox.Text = trigger.CronExpression; SunBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Sunday); MonBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Monday); TueBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Tuesday); WedBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Wednesday); ThuBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Thursday); FriBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Friday); SatBox.IsChecked = trigger.WeekDays.HasFlag(DayOfWeekFlags.Saturday); UpdateVisibleSections(trigger.Kind); UpdateWindowControlState(); UpdateEditorAvailability(); } finally { _loading = false; RefreshPreview(); } } private bool SaveTriggerFields() { if (_loading || TriggersList.SelectedItem is not TaskTrigger trigger) { return true; } trigger.Kind = KindBox.SelectedItem is Option kind ? kind.Value : TriggerKind.Interval; trigger.IntervalUnit = IntervalUnitBox.SelectedItem is Option unit ? unit.Value : IntervalUnit.Minutes; trigger.LastBusinessDay = LastBusinessDayBox.IsChecked == true; trigger.CronExpression = CronBox.Text.Trim(); if (int.TryParse(IntervalValueBox.Text, out var interval)) { trigger.IntervalValue = interval; } if (int.TryParse(MonthDayBox.Text, out var monthDay)) { trigger.MonthDay = Math.Clamp(monthDay, 1, 31); } if (GetDateTime(OneTimeDatePicker, OneTimeHourBox, OneTimeMinuteBox, OneTimeSecondBox) is { } oneTime) { trigger.OneTimeAt = oneTime; } trigger.StartsAt = UseStartsAtBox.IsChecked == true ? GetDateTime(StartsAtDatePicker, StartsAtHourBox, StartsAtMinuteBox, StartsAtSecondBox) : null; trigger.EndsAt = UseEndsAtBox.IsChecked == true ? GetDateTime(EndsAtDatePicker, EndsAtHourBox, EndsAtMinuteBox, EndsAtSecondBox) : null; trigger.TimeOfDay = trigger.Kind switch { TriggerKind.Weekly => GetTimeOnly(WeeklyHourBox, WeeklyMinuteBox, WeeklySecondBox), TriggerKind.Monthly => GetTimeOnly(MonthlyHourBox, MonthlyMinuteBox, MonthlySecondBox), _ => GetTimeOnly(DailyHourBox, DailyMinuteBox, DailySecondBox) }; var days = DayOfWeekFlags.None; if (SunBox.IsChecked == true) days |= DayOfWeekFlags.Sunday; if (MonBox.IsChecked == true) days |= DayOfWeekFlags.Monday; if (TueBox.IsChecked == true) days |= DayOfWeekFlags.Tuesday; if (WedBox.IsChecked == true) days |= DayOfWeekFlags.Wednesday; if (ThuBox.IsChecked == true) days |= DayOfWeekFlags.Thursday; if (FriBox.IsChecked == true) days |= DayOfWeekFlags.Friday; if (SatBox.IsChecked == true) days |= DayOfWeekFlags.Saturday; trigger.WeekDays = days; UpdateVisibleSections(trigger.Kind); UpdateWindowControlState(); trigger.Notify(nameof(TaskTrigger.Summary)); TriggersList.Items.Refresh(); RefreshPreview(); return true; } private bool SaveTaskFields() { ValidationText.Text = ""; SaveTriggerFields(); if (string.IsNullOrWhiteSpace(NameBox.Text)) { ValidationText.Text = "任务名称不能为空"; return false; } foreach (var trigger in _task.Triggers) { if (trigger.Kind == TriggerKind.Interval && trigger.IntervalValue < 1) { ValidationText.Text = "固定间隔不能小于 1"; return false; } if (trigger.Kind == TriggerKind.Weekly && trigger.WeekDays == DayOfWeekFlags.None) { ValidationText.Text = "每周定时至少选择一个星期"; return false; } if (trigger.StartsAt.HasValue && trigger.EndsAt.HasValue && trigger.StartsAt >= trigger.EndsAt) { ValidationText.Text = "结束失效时间必须晚于起始生效时间"; return false; } } _task.Name = NameBox.Text.Trim(); _task.Description = DescriptionBox.Text.Trim(); _task.IsEnabled = EnabledBox.IsChecked == true; _task.Channel = ChannelBox.Text.Trim(); _task.Title = TitleBox.Text.Trim(); _task.Body = BodyBox.Text; _task.MisfireStrategy = MisfireBox.SelectedItem is Option strategy ? strategy.Value : MisfireStrategy.DoNothing; return true; } private void TriggersList_SelectionChanged(object sender, SelectionChangedEventArgs e) => LoadTrigger(TriggersList.SelectedItem as TaskTrigger); private void AddTrigger_Click(object sender, RoutedEventArgs e) { var trigger = new TaskTrigger(); _task.Triggers.Add(trigger); TriggersList.SelectedItem = trigger; TriggersList.Items.Refresh(); UpdateEditorAvailability(); } private void RemoveTrigger_Click(object sender, RoutedEventArgs e) { if (TriggersList.SelectedItem is TaskTrigger trigger) { _task.Triggers.Remove(trigger); TriggersList.Items.Refresh(); UpdateEditorAvailability(); } } private void Field_Changed(object sender, RoutedEventArgs e) { if (!_loading) { SaveTriggerFields(); } } private void WindowOption_Changed(object sender, RoutedEventArgs e) { if (!_loading) { EnsureOptionalDateDefaults(); } UpdateWindowControlState(); Field_Changed(sender, e); } private void ApplyOneTimeDelay_Click(object sender, RoutedEventArgs e) { if (!int.TryParse(OneTimeDelayValueBox.Text, out var value) || value < 1) { OneTimeDelayValueBox.Text = "1"; value = 1; } var unit = OneTimeDelayUnitBox.SelectedItem is Option option ? option.Value : DelayUnit.Minutes; ApplyOneTimeDelay(value, unit); } private void QuickOneTimeDelay_Click(object sender, RoutedEventArgs e) { if (sender is Button { Tag: string minutesText } && int.TryParse(minutesText, out var minutes)) { ApplyOneTimeDelay(minutes, DelayUnit.Minutes); } } private void ApplyOneTimeDelay(int value, DelayUnit unit) { var delay = unit switch { DelayUnit.Hours => TimeSpan.FromHours(value), DelayUnit.Days => TimeSpan.FromDays(value), _ => TimeSpan.FromMinutes(value) }; SetDateTime(OneTimeDatePicker, OneTimeHourBox, OneTimeMinuteBox, OneTimeSecondBox, DateTime.Now.Add(delay)); SaveTriggerFields(); } private async void TestSend_Click(object sender, RoutedEventArgs e) { if (!SaveTaskFields()) { return; } TestResultText.Text = "发送中..."; var log = await _client.SendAsync(_task, "测试发送", _settings.OmniNotifyUrl); TestResultText.Text = log.Level == "INFO" ? "测试发送成功" : log.Message; TestResultText.Foreground = log.Level == "INFO" ? System.Windows.Media.Brushes.ForestGreen : System.Windows.Media.Brushes.Firebrick; } private void Save_Click(object sender, RoutedEventArgs e) { if (!SaveTaskFields()) { return; } DialogResult = true; } private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false; private void RefreshPreview() { if (TriggersList.SelectedItem is not TaskTrigger trigger) { PreviewBox.Text = ""; return; } var preview = NextRunCalculator.Preview(trigger, DateTime.Now); PreviewBox.Text = preview.Count == 0 ? "无法计算未来执行时间,请检查规则。" : string.Join(Environment.NewLine, preview.Select(d => d.ToString("yyyy-MM-dd HH:mm:ss"))); } private void UpdateEditorAvailability() { var hasTrigger = TriggersList.SelectedItem is TaskTrigger; TriggerEmptyText.Visibility = hasTrigger ? Visibility.Collapsed : Visibility.Visible; TriggerEditorPanel.Visibility = hasTrigger ? Visibility.Visible : Visibility.Collapsed; } private void UpdateVisibleSections(TriggerKind kind) { OneTimeSection.Visibility = kind == TriggerKind.OneTime ? Visibility.Visible : Visibility.Collapsed; IntervalSection.Visibility = kind == TriggerKind.Interval ? Visibility.Visible : Visibility.Collapsed; DailySection.Visibility = kind == TriggerKind.Daily ? Visibility.Visible : Visibility.Collapsed; WeeklySection.Visibility = kind == TriggerKind.Weekly ? Visibility.Visible : Visibility.Collapsed; MonthlySection.Visibility = kind == TriggerKind.Monthly ? Visibility.Visible : Visibility.Collapsed; CronSection.Visibility = kind == TriggerKind.Cron ? Visibility.Visible : Visibility.Collapsed; WindowSection.Visibility = kind is TriggerKind.Interval or TriggerKind.Daily or TriggerKind.Weekly or TriggerKind.Monthly or TriggerKind.Cron ? Visibility.Visible : Visibility.Collapsed; } private void UpdateWindowControlState() { StartsAtPanel.IsEnabled = UseStartsAtBox.IsChecked == true; EndsAtPanel.IsEnabled = UseEndsAtBox.IsChecked == true; } private void EnsureOptionalDateDefaults() { if (UseStartsAtBox.IsChecked == true && StartsAtDatePicker.SelectedDate is null) { SetDateTime(StartsAtDatePicker, StartsAtHourBox, StartsAtMinuteBox, StartsAtSecondBox, DateTime.Now); } if (UseEndsAtBox.IsChecked == true && EndsAtDatePicker.SelectedDate is null) { SetDateTime(EndsAtDatePicker, EndsAtHourBox, EndsAtMinuteBox, EndsAtSecondBox, DateTime.Now.AddDays(1)); } } private void SetTimeItems(ComboBox hourBox, ComboBox minuteBox, ComboBox secondBox) { hourBox.ItemsSource = _hours; minuteBox.ItemsSource = _minutesAndSeconds; secondBox.ItemsSource = _minutesAndSeconds; } private void SetDateTime(DatePicker datePicker, ComboBox hourBox, ComboBox minuteBox, ComboBox secondBox, DateTime value) { datePicker.SelectedDate = value.Date; SetTimeOnly(hourBox, minuteBox, secondBox, value.TimeOfDay); } private void SetOptionalDateTime(DatePicker datePicker, ComboBox hourBox, ComboBox minuteBox, ComboBox secondBox, DateTime? value) { if (value.HasValue) { SetDateTime(datePicker, hourBox, minuteBox, secondBox, value.Value); return; } datePicker.SelectedDate = null; SetTimeOnly(hourBox, minuteBox, secondBox, TimeSpan.Zero); } private void SetTimeOnly(ComboBox hourBox, ComboBox minuteBox, ComboBox secondBox, TimeSpan value) { hourBox.SelectedItem = _hours.First(o => o.Value == value.Hours); minuteBox.SelectedItem = _minutesAndSeconds.First(o => o.Value == value.Minutes); secondBox.SelectedItem = _minutesAndSeconds.First(o => o.Value == value.Seconds); } private DateTime? GetDateTime(DatePicker datePicker, ComboBox hourBox, ComboBox minuteBox, ComboBox secondBox) { if (datePicker.SelectedDate is not { } date) { return null; } return date.Date.Add(GetTimeOnly(hourBox, minuteBox, secondBox)); } private static TimeSpan GetTimeOnly(ComboBox hourBox, ComboBox minuteBox, ComboBox secondBox) { var hour = hourBox.SelectedItem is Option h ? h.Value : 0; var minute = minuteBox.SelectedItem is Option m ? m.Value : 0; var second = secondBox.SelectedItem is Option s ? s.Value : 0; return new TimeSpan(hour, minute, second); } } public sealed record Option(T Value, string Label); public enum DelayUnit { Minutes, Hours, Days }