변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
339 lines
14 KiB
C#
339 lines
14 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
using AxCopilot.Models;
|
|
using AxCopilot.Services;
|
|
using Microsoft.Win32;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ScheduleEditorWindow : Window
|
|
{
|
|
private readonly SettingsService _settings;
|
|
private readonly ScheduleEntry? _editing; // null = 새 스케줄
|
|
|
|
private string _triggerType = "daily";
|
|
private string _actionType = "app";
|
|
private bool _enabled = true;
|
|
private bool _conditionMustRun = true;
|
|
|
|
// 요일 버튼 → Border 참조
|
|
private Border[] _dayBtns = null!;
|
|
|
|
public ScheduleEditorWindow(ScheduleEntry? entry, SettingsService settings)
|
|
{
|
|
InitializeComponent();
|
|
_settings = settings;
|
|
_editing = entry;
|
|
|
|
_dayBtns = new[] { BtnSun, BtnMon, BtnTue, BtnWed, BtnThu, BtnFri, BtnSat };
|
|
|
|
Loaded += OnLoaded;
|
|
}
|
|
|
|
// ─── 초기화 ─────────────────────────────────────────────────────────────
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// 다크 테마 색상
|
|
var dimBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x25, 0x26, 0x37));
|
|
var accent = TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var border = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2E, 0x2F, 0x4A));
|
|
|
|
// 요일 버튼 기본 색
|
|
foreach (var b in _dayBtns)
|
|
{
|
|
b.Background = dimBg;
|
|
b.BorderBrush = border;
|
|
b.BorderThickness = new Thickness(1);
|
|
if (b.Child is TextBlock tb)
|
|
tb.Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
}
|
|
|
|
if (_editing != null)
|
|
LoadFromEntry(_editing);
|
|
else
|
|
SetTriggerUi("daily");
|
|
|
|
SetActionUi(_actionType);
|
|
UpdateToggleUi(_enabled);
|
|
SetConditionModeUi(_conditionMustRun);
|
|
}
|
|
|
|
private void LoadFromEntry(ScheduleEntry e)
|
|
{
|
|
NameBox.Text = e.Name;
|
|
TimeBox.Text = e.TriggerTime;
|
|
_enabled = e.Enabled;
|
|
_triggerType = e.TriggerType;
|
|
_actionType = e.ActionType;
|
|
|
|
if (e.TriggerDate != null)
|
|
DateBox.Text = e.TriggerDate;
|
|
|
|
SetTriggerUi(e.TriggerType);
|
|
|
|
// 요일 복원
|
|
foreach (var b in _dayBtns)
|
|
{
|
|
if (int.TryParse(b.Tag?.ToString(), out var day) && e.WeekDays.Contains(day))
|
|
SetDaySelected(b, true);
|
|
}
|
|
|
|
if (e.ActionType == "app")
|
|
{
|
|
AppPathBox.Text = e.ActionTarget;
|
|
AppArgsBox.Text = e.ActionArgs ?? "";
|
|
}
|
|
else
|
|
{
|
|
NotifMsgBox.Text = e.ActionTarget;
|
|
}
|
|
|
|
// 조건 복원
|
|
ConditionProcessBox.Text = e.ConditionProcess ?? "";
|
|
_conditionMustRun = e.ConditionProcessMustRun;
|
|
SetConditionModeUi(_conditionMustRun);
|
|
}
|
|
|
|
// ─── 트리거 유형 ─────────────────────────────────────────────────────────
|
|
private void TriggerType_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is Border b && b.Tag is string tag)
|
|
SetTriggerUi(tag);
|
|
}
|
|
|
|
private void SetTriggerUi(string type)
|
|
{
|
|
_triggerType = type;
|
|
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
|
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
|
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var white = Brushes.White;
|
|
|
|
// 버튼 배경·텍스트 색 초기화
|
|
void SetBtn(Border btn, TextBlock txt, bool active)
|
|
{
|
|
btn.Background = active ? accent : dimBg;
|
|
txt.Foreground = active ? white : secFg;
|
|
}
|
|
|
|
SetBtn(BtnDaily, TxtDaily, type == "daily");
|
|
SetBtn(BtnWeekdays, TxtWeekdays, type == "weekdays");
|
|
SetBtn(BtnWeekly, TxtWeekly, type == "weekly");
|
|
SetBtn(BtnOnce, TxtOnce, type == "once");
|
|
|
|
// 요일 패널 / 날짜 패널 표시
|
|
WeekDaysPanel.Visibility = type == "weekly" ? Visibility.Visible : Visibility.Collapsed;
|
|
DatePanel.Visibility = type == "once" ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
// once 기본값
|
|
if (type == "once" && string.IsNullOrWhiteSpace(DateBox.Text))
|
|
DateBox.Text = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd");
|
|
}
|
|
|
|
// ─── 요일 선택 ──────────────────────────────────────────────────────────
|
|
private void WeekDay_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is not Border btn) return;
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
|
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
|
bool current = btn.Background == accent;
|
|
SetDaySelected(btn, !current);
|
|
}
|
|
|
|
private void SetDaySelected(Border btn, bool selected)
|
|
{
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
|
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
|
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
|
|
btn.Background = selected ? accent : dimBg;
|
|
if (btn.Child is TextBlock tb)
|
|
tb.Foreground = selected ? Brushes.White : secFg;
|
|
}
|
|
|
|
private List<int> GetSelectedDays()
|
|
{
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
|
var list = new List<int>();
|
|
foreach (var b in _dayBtns)
|
|
{
|
|
if (b.Background == accent && int.TryParse(b.Tag?.ToString(), out var day))
|
|
list.Add(day);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
// ─── 액션 유형 ──────────────────────────────────────────────────────────
|
|
private void ActionType_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is Border b && b.Tag is string tag)
|
|
SetActionUi(tag);
|
|
}
|
|
|
|
private void SetActionUi(string type)
|
|
{
|
|
_actionType = type;
|
|
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
|
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
|
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
var white = Brushes.White;
|
|
|
|
bool isApp = type == "app";
|
|
|
|
BtnActionApp.Background = isApp ? accent : dimBg;
|
|
BtnActionNotif.Background = !isApp ? accent : dimBg;
|
|
|
|
TxtActionApp.Foreground = isApp ? white : secFg;
|
|
TxtActionNotif.Foreground = !isApp ? white : secFg;
|
|
|
|
// 아이콘 TextBlock은 StackPanel의 첫 번째 자식
|
|
if (BtnActionApp.Child is StackPanel spApp && spApp.Children.Count > 0)
|
|
((TextBlock)spApp.Children[0]).Foreground = isApp ? white : secFg;
|
|
if (BtnActionNotif.Child is StackPanel spNotif && spNotif.Children.Count > 0)
|
|
((TextBlock)spNotif.Children[0]).Foreground = !isApp ? white : secFg;
|
|
|
|
AppPathPanel.Visibility = isApp ? Visibility.Visible : Visibility.Collapsed;
|
|
NotifPanel.Visibility = !isApp ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
// ─── 앱 찾아보기 ─────────────────────────────────────────────────────────
|
|
private void BtnBrowseApp_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var dlg = new OpenFileDialog
|
|
{
|
|
Title = "실행 파일 선택",
|
|
Filter = "실행 파일|*.exe;*.bat;*.cmd;*.lnk;*.ps1|모든 파일|*.*"
|
|
};
|
|
if (dlg.ShowDialog(this) == true)
|
|
AppPathBox.Text = dlg.FileName;
|
|
}
|
|
|
|
// ─── 조건 모드 (L6-4) ───────────────────────────────────────────────────
|
|
private void ConditionMode_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (sender is Border b && b.Tag is string tag)
|
|
SetConditionModeUi(tag == "run");
|
|
}
|
|
|
|
private void SetConditionModeUi(bool mustRun)
|
|
{
|
|
_conditionMustRun = mustRun;
|
|
|
|
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
|
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
|
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
|
|
|
BtnCondRun.Background = mustRun ? accent : dimBg;
|
|
BtnCondNotRun.Background = !mustRun ? accent : dimBg;
|
|
TxtCondRun.Foreground = mustRun ? Brushes.White : secFg;
|
|
TxtCondNotRun.Foreground = !mustRun ? Brushes.White : secFg;
|
|
}
|
|
|
|
// ─── 활성화 토글 ─────────────────────────────────────────────────────────
|
|
private void EnabledToggle_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_enabled = !_enabled;
|
|
UpdateToggleUi(_enabled);
|
|
}
|
|
|
|
private void UpdateToggleUi(bool enabled)
|
|
{
|
|
var accent = TryFindResource("AccentColor") as Brush
|
|
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
|
var off = new SolidColorBrush(Color.FromRgb(0x3A, 0x3B, 0x5A));
|
|
|
|
EnabledToggle.Background = enabled ? accent : off;
|
|
|
|
// 썸 위치 애니메이션
|
|
var da = new DoubleAnimation(
|
|
enabled ? 1.0 : -1.0, // 실제 HorizontalAlignment·Margin으로 처리
|
|
TimeSpan.FromMilliseconds(150));
|
|
|
|
EnabledThumb.HorizontalAlignment = enabled ? HorizontalAlignment.Right : HorizontalAlignment.Left;
|
|
EnabledThumb.Margin = enabled ? new Thickness(0, 0, 2, 0) : new Thickness(2, 0, 0, 0);
|
|
}
|
|
|
|
// ─── 저장 ────────────────────────────────────────────────────────────────
|
|
private void BtnSave_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var name = NameBox.Text.Trim();
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
MessageBox.Show("스케줄 이름을 입력하세요.", "저장 오류",
|
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
var timeStr = TimeBox.Text.Trim();
|
|
if (!TimeSpan.TryParseExact(timeStr, new[] { @"hh\:mm", @"h\:mm" },
|
|
System.Globalization.CultureInfo.InvariantCulture, out _))
|
|
{
|
|
MessageBox.Show("실행 시각을 HH:mm 형식으로 입력하세요. (예: 09:00)",
|
|
"저장 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
if (_triggerType == "once")
|
|
{
|
|
var dateStr = DateBox.Text.Trim();
|
|
if (!DateTime.TryParse(dateStr, out _))
|
|
{
|
|
MessageBox.Show("실행 날짜를 yyyy-MM-dd 형식으로 입력하세요.",
|
|
"저장 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_actionType == "app" && string.IsNullOrWhiteSpace(AppPathBox.Text))
|
|
{
|
|
MessageBox.Show("실행할 앱 경로를 입력하세요.", "저장 오류",
|
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
// 기존 항목 편집 or 신규 생성
|
|
var entry = _editing ?? new ScheduleEntry();
|
|
|
|
entry.Name = name;
|
|
entry.Enabled = _enabled;
|
|
entry.TriggerType = _triggerType;
|
|
entry.TriggerTime = timeStr;
|
|
entry.WeekDays = _triggerType == "weekly" ? GetSelectedDays() : new List<int>();
|
|
entry.TriggerDate = _triggerType == "once" ? DateBox.Text.Trim() : null;
|
|
entry.ActionType = _actionType;
|
|
entry.ActionTarget = _actionType == "app"
|
|
? AppPathBox.Text.Trim()
|
|
: NotifMsgBox.Text.Trim();
|
|
entry.ActionArgs = _actionType == "app" ? AppArgsBox.Text.Trim() : "";
|
|
|
|
// 조건 저장 (L6-4)
|
|
entry.ConditionProcess = ConditionProcessBox.Text.Trim();
|
|
entry.ConditionProcessMustRun = _conditionMustRun;
|
|
|
|
var schedules = _settings.Settings.Schedules;
|
|
|
|
if (_editing == null)
|
|
schedules.Add(entry);
|
|
// 편집 모드: 이미 리스트 내 참조이므로 별도 추가 불필요
|
|
|
|
_settings.Save();
|
|
NotificationService.Notify("AX Copilot", $"스케줄 '{entry.Name}' 저장됨");
|
|
Close();
|
|
}
|
|
|
|
// ─── 윈도우 컨트롤 ──────────────────────────────────────────────────────
|
|
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.LeftButton == MouseButtonState.Pressed)
|
|
DragMove();
|
|
}
|
|
|
|
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
|
|
private void BtnCancel_Click(object sender, MouseButtonEventArgs e) => Close();
|
|
}
|