- 根据触发器类型动态展示对应配置区域,减少无关字段干扰。 - 将单次、每日、每周、每月和生效时间范围改为日期选择器与时分秒下拉选择,避免手动输入时间格式。 - 为单次执行增加延后执行快捷设置,支持常用快捷按钮和自定义分钟、小时、天后执行。 - 移除开机自启设置、注册表写入逻辑和相关配置字段,降低对用户系统的影响。 - 同步优化部分任务状态、触发摘要和设置界面文案。
470 lines
17 KiB
C#
470 lines
17 KiB
C#
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<Option<int>> _hours = Enumerable.Range(0, 24).Select(v => new Option<int>(v, v.ToString("00"))).ToList();
|
|
private readonly List<Option<int>> _minutesAndSeconds = Enumerable.Range(0, 60).Select(v => new Option<int>(v, v.ToString("00"))).ToList();
|
|
private readonly List<Option<DelayUnit>> _delayUnits =
|
|
[
|
|
new(DelayUnit.Minutes, "分钟"),
|
|
new(DelayUnit.Hours, "小时"),
|
|
new(DelayUnit.Days, "天")
|
|
];
|
|
private bool _loading;
|
|
|
|
private readonly List<Option<TriggerKind>> _triggerKinds =
|
|
[
|
|
new(TriggerKind.OneTime, "单次执行"),
|
|
new(TriggerKind.Interval, "固定间隔"),
|
|
new(TriggerKind.Daily, "每日定时"),
|
|
new(TriggerKind.Weekly, "每周定时"),
|
|
new(TriggerKind.Monthly, "每月定时"),
|
|
new(TriggerKind.Cron, "Cron 表达式")
|
|
];
|
|
|
|
private readonly List<Option<IntervalUnit>> _intervalUnits =
|
|
[
|
|
new(IntervalUnit.Seconds, "秒"),
|
|
new(IntervalUnit.Minutes, "分钟"),
|
|
new(IntervalUnit.Hours, "小时"),
|
|
new(IntervalUnit.Days, "天")
|
|
];
|
|
|
|
private readonly List<Option<MisfireStrategy>> _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<TriggerKind> kind ? kind.Value : TriggerKind.Interval;
|
|
trigger.IntervalUnit = IntervalUnitBox.SelectedItem is Option<IntervalUnit> 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<MisfireStrategy> 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<DelayUnit> 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<int> h ? h.Value : 0;
|
|
var minute = minuteBox.SelectedItem is Option<int> m ? m.Value : 0;
|
|
var second = secondBox.SelectedItem is Option<int> s ? s.Value : 0;
|
|
return new TimeSpan(hour, minute, second);
|
|
}
|
|
}
|
|
|
|
public sealed record Option<T>(T Value, string Label);
|
|
|
|
public enum DelayUnit
|
|
{
|
|
Minutes,
|
|
Hours,
|
|
Days
|
|
}
|