using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using Clipboard = System.Windows.Clipboard; using MessageBox = System.Windows.MessageBox; namespace OmniNotify; public partial class MainWindow : Window { private const string AllChannelsLabel = "全部频道"; private const string AllStatusesLabel = "全部状态"; private readonly App _app; private readonly AppState _state; private readonly ICollectionView _historyView; private bool _loading; public MainWindow() { InitializeComponent(); _app = (App)System.Windows.Application.Current; _state = _app.State; AnchorBox.ItemsSource = LabeledValues( (ScreenAnchor.TopLeft, "左上"), (ScreenAnchor.TopCenter, "顶部居中"), (ScreenAnchor.TopRight, "右上"), (ScreenAnchor.MiddleLeft, "左侧居中"), (ScreenAnchor.Center, "屏幕中央"), (ScreenAnchor.MiddleRight, "右侧居中"), (ScreenAnchor.BottomLeft, "左下"), (ScreenAnchor.BottomCenter, "底部居中"), (ScreenAnchor.BottomRight, "右下")); EnterAnimationBox.ItemsSource = LabeledValues( (PopupAnimation.Fade, "淡入淡出"), (PopupAnimation.Slide, "滑入滑出"), (PopupAnimation.Zoom, "缩放")); ExitAnimationBox.ItemsSource = EnterAnimationBox.ItemsSource; StackModeBox.ItemsSource = LabeledValues( (StackMode.Queue, "排队显示"), (StackMode.Push, "并排堆叠"), (StackMode.Replace, "替换旧弹窗")); OverflowModeBox.ItemsSource = LabeledValues( (OverflowMode.Truncate, "截断省略"), (OverflowMode.VerticalScroll, "纵向滚动"), (OverflowMode.Split, "分割显示")); ChannelList.ItemsSource = _state.Channels; HistoryGrid.ItemsSource = _state.History; _historyView = CollectionViewSource.GetDefaultView(_state.History); _historyView.Filter = FilterHistory; _app.Router.StateChanged += RefreshAll; Loaded += (_, _) => RefreshAll(); Closing += MainWindow_Closing; } public void FocusSettingsTab() { MainTabs.SelectedIndex = 2; } private void RefreshAll() { Dispatcher.Invoke(() => { _loading = true; ServerStatusText.Text = string.IsNullOrWhiteSpace(_app.ServerError) ? $"本地接收地址:{_app.ListenUrl}notify" : $"本地监听失败:{_app.ServerError}"; RefreshHistoryFilters(); LoadSettings(); LoadSelectedChannel(); ResetBreakerButton.IsEnabled = _state.Settings.CircuitBreakerOpen; _historyView.Refresh(); _loading = false; }); } private void MainWindow_Closing(object? sender, CancelEventArgs e) { e.Cancel = true; Hide(); } private void AddChannel_Click(object sender, RoutedEventArgs e) { SaveSelectedChannel(); var baseName = "channel"; var index = 1; while (_state.Channels.Any(channel => channel.Name == $"{baseName}-{index}")) { index++; } var item = new Channel { Name = $"{baseName}-{index}" }; _state.Channels.Add(item); ChannelList.SelectedItem = item; _app.SaveState(); RefreshHistoryFilters(); } private void DeleteChannel_Click(object sender, RoutedEventArgs e) { if (ChannelList.SelectedItem is not Channel channel) { return; } if (MessageBox.Show($"确认删除频道 {channel.Name}?", "删除频道", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) { return; } _state.Channels.Remove(channel); ChannelList.SelectedIndex = _state.Channels.Count > 0 ? 0 : -1; _app.SaveState(); RefreshHistoryFilters(); } private void ChannelList_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_loading) { return; } LoadSelectedChannel(); } private void Save_Click(object sender, RoutedEventArgs e) { SaveSelectedChannel(); SaveSettings(); _app.SaveState(); MessageBox.Show("配置已保存。", "Omni-Notify", MessageBoxButton.OK, MessageBoxImage.Information); } private void SendTest_Click(object sender, RoutedEventArgs e) { SaveSelectedChannel(); var channel = ChannelList.SelectedItem as Channel ?? _state.Channels.FirstOrDefault(); if (channel is null) { MessageBox.Show("请先创建频道。", "Omni-Notify", MessageBoxButton.OK, MessageBoxImage.Information); return; } _app.Router.Receive(new IncomingMessage { Channel = channel.Name, Title = "Omni-Notify 测试消息", Body = "这是一条按当前频道样式展示的纯文本弹窗,用于验证位置、尺寸、透明度、动画、历史记录,以及在内容较长时的截断、纵向滚动或分割显示效果。" }); } private void LoadSelectedChannel() { _loading = true; if (ChannelList.SelectedItem is null && _state.Channels.Count > 0) { ChannelList.SelectedIndex = 0; } if (ChannelList.SelectedItem is Channel channel) { var profile = channel.Profile; ChannelNameBox.Text = channel.Name; ScreenIndexBox.Text = profile.ScreenIndex.ToString(); AnchorBox.SelectedValue = profile.Anchor; MarginXBox.Text = profile.MarginX.ToString("0.##"); MarginYBox.Text = profile.MarginY.ToString("0.##"); WidthBox.Text = profile.Width.ToString("0.##"); MaxHeightBox.Text = profile.MaxHeight.ToString("0.##"); PaddingBox.Text = profile.Padding.ToString("0.##"); FontFamilyBox.Text = profile.FontFamily; TitleFontSizeBox.Text = profile.TitleFontSize.ToString("0.##"); BodyFontSizeBox.Text = profile.BodyFontSize.ToString("0.##"); TextColorBox.Text = profile.TextColor; BackgroundColorBox.Text = profile.BackgroundColor; BackgroundOpacityBox.Text = profile.BackgroundOpacity.ToString("0.##"); BorderColorBox.Text = profile.BorderColor; BorderOpacityBox.Text = profile.BorderOpacity.ToString("0.##"); OverallOpacityBox.Text = profile.OverallOpacity.ToString("0.##"); LifetimeBox.Text = profile.LifetimeSeconds.ToString("0.##"); EnterAnimationBox.SelectedValue = profile.EnterAnimation; ExitAnimationBox.SelectedValue = profile.ExitAnimation; StackModeBox.SelectedValue = profile.StackMode; OverflowModeBox.SelectedValue = profile.OverflowMode; SplitIntervalBox.Text = profile.SplitIntervalSeconds.ToString("0.##"); VerticalScrollHoldBox.Text = profile.VerticalScrollHoldSeconds.ToString("0.##"); VerticalScrollSpeedBox.Text = profile.VerticalScrollSpeed.ToString("0.##"); } _loading = false; } private void SaveSelectedChannel() { if (ChannelList.SelectedItem is not Channel channel) { return; } var newName = ChannelNameBox.Text.Trim(); if (string.IsNullOrWhiteSpace(newName)) { newName = channel.Name; } if (_state.Channels.Any(candidate => candidate != channel && candidate.Name == newName)) { MessageBox.Show("频道名必须唯一。", "Omni-Notify", MessageBoxButton.OK, MessageBoxImage.Warning); return; } channel.Name = newName; var profile = channel.Profile; profile.ScreenIndex = ReadInt(ScreenIndexBox, profile.ScreenIndex); profile.Anchor = AnchorBox.SelectedValue as ScreenAnchor? ?? profile.Anchor; profile.MarginX = ReadDouble(MarginXBox, profile.MarginX); profile.MarginY = ReadDouble(MarginYBox, profile.MarginY); profile.Width = ReadDouble(WidthBox, profile.Width); profile.MaxHeight = ReadDouble(MaxHeightBox, profile.MaxHeight); profile.Padding = ReadDouble(PaddingBox, profile.Padding); profile.FontFamily = FontFamilyBox.Text.Trim(); profile.TitleFontSize = ReadDouble(TitleFontSizeBox, profile.TitleFontSize); profile.BodyFontSize = ReadDouble(BodyFontSizeBox, profile.BodyFontSize); profile.TextColor = TextColorBox.Text.Trim(); profile.BackgroundColor = BackgroundColorBox.Text.Trim(); profile.BackgroundOpacity = ReadDouble(BackgroundOpacityBox, profile.BackgroundOpacity); profile.BorderColor = BorderColorBox.Text.Trim(); profile.BorderOpacity = ReadDouble(BorderOpacityBox, profile.BorderOpacity); profile.OverallOpacity = ReadDouble(OverallOpacityBox, profile.OverallOpacity); profile.LifetimeSeconds = ReadDouble(LifetimeBox, profile.LifetimeSeconds); profile.EnterAnimation = EnterAnimationBox.SelectedValue as PopupAnimation? ?? profile.EnterAnimation; profile.ExitAnimation = ExitAnimationBox.SelectedValue as PopupAnimation? ?? profile.ExitAnimation; profile.StackMode = StackModeBox.SelectedValue as StackMode? ?? profile.StackMode; profile.OverflowMode = OverflowModeBox.SelectedValue as OverflowMode? ?? profile.OverflowMode; profile.SplitIntervalSeconds = ReadDouble(SplitIntervalBox, profile.SplitIntervalSeconds); profile.VerticalScrollHoldSeconds = ReadDouble(VerticalScrollHoldBox, profile.VerticalScrollHoldSeconds); profile.VerticalScrollSpeed = ReadDouble(VerticalScrollSpeedBox, profile.VerticalScrollSpeed); ChannelList.Items.Refresh(); } private void LoadSettings() { StartWithWindowsBox.IsChecked = _state.Settings.StartWithWindows; DndBox.IsChecked = _state.Settings.DndEnabled; RateLimitBox.Text = _state.Settings.MaxMessagesPerSecond.ToString(); RetainDaysBox.Text = _state.Settings.RetainDays.ToString(); RetainCountBox.Text = _state.Settings.RetainCount.ToString(); PortBox.Text = _state.Settings.LocalPort.ToString(); } private void SaveSettings() { _state.Settings.StartWithWindows = StartWithWindowsBox.IsChecked == true; _state.Settings.DndEnabled = DndBox.IsChecked == true; _state.Settings.MaxMessagesPerSecond = ReadInt(RateLimitBox, _state.Settings.MaxMessagesPerSecond); _state.Settings.RetainDays = ReadInt(RetainDaysBox, _state.Settings.RetainDays); _state.Settings.RetainCount = ReadInt(RetainCountBox, _state.Settings.RetainCount); _state.Settings.LocalPort = ReadInt(PortBox, _state.Settings.LocalPort); StartupManager.Apply(_state.Settings.StartWithWindows); } private void RefreshHistoryFilters() { var selectedChannel = HistoryChannelFilter.SelectedItem as string ?? AllChannelsLabel; HistoryChannelFilter.ItemsSource = new[] { AllChannelsLabel }.Concat(_state.Channels.Select(channel => channel.Name)).ToList(); HistoryChannelFilter.SelectedItem = HistoryChannelFilter.Items.Contains(selectedChannel) ? selectedChannel : AllChannelsLabel; var selectedStatus = HistoryStatusFilter.SelectedItem as string ?? AllStatusesLabel; HistoryStatusFilter.ItemsSource = new[] { AllStatusesLabel }.Concat(Enum.GetValues().Select(StatusLabel)).ToList(); HistoryStatusFilter.SelectedItem = HistoryStatusFilter.Items.Contains(selectedStatus) ? selectedStatus : AllStatusesLabel; } private bool FilterHistory(object item) { if (item is not HistoryItem history) { return false; } var keyword = SearchBox.Text.Trim(); var channel = HistoryChannelFilter.SelectedItem as string; var status = HistoryStatusFilter.SelectedItem as string; if (!string.IsNullOrWhiteSpace(keyword) && !history.Title.Contains(keyword, StringComparison.OrdinalIgnoreCase) && !history.Body.Contains(keyword, StringComparison.OrdinalIgnoreCase) && !history.Channel.Contains(keyword, StringComparison.OrdinalIgnoreCase)) { return false; } if (!string.IsNullOrWhiteSpace(channel) && channel != AllChannelsLabel && history.Channel != channel) { return false; } return string.IsNullOrWhiteSpace(status) || status == AllStatusesLabel || StatusLabel(history.Status) == status; } private void HistoryFilter_Changed(object sender, RoutedEventArgs e) { if (!_loading) { _historyView.Refresh(); } } private void ClearHistory_Click(object sender, RoutedEventArgs e) { if (MessageBox.Show("确认清空所有历史记录?", "清空历史", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) { return; } _state.History.Clear(); _app.SaveState(); } private void CopyHistory_Click(object sender, RoutedEventArgs e) { if (HistoryGrid.SelectedItem is HistoryItem item) { Clipboard.SetText($"{item.Title}{Environment.NewLine}{item.Body}"); } } private void ReplayHistory_Click(object sender, RoutedEventArgs e) { if (HistoryGrid.SelectedItem is HistoryItem item) { _app.Router.Replay(item); } } private void ResetBreaker_Click(object sender, RoutedEventArgs e) { _app.Router.ResetCircuitBreaker(); } private static int ReadInt(System.Windows.Controls.TextBox box, int fallback) { return int.TryParse(box.Text, out var value) ? Math.Max(0, value) : fallback; } private static double ReadDouble(System.Windows.Controls.TextBox box, double fallback) { return double.TryParse(box.Text, out var value) ? Math.Max(0, value) : fallback; } private static IReadOnlyList> LabeledValues(params (T Value, string Label)[] items) where T : struct, Enum { return items.Select(item => new OptionItem(item.Value, item.Label)).ToList(); } private static string StatusLabel(NotificationStatus status) => status switch { NotificationStatus.Displayed => "已显示", NotificationStatus.DndMuted => "免打扰", NotificationStatus.IllegalChannel => "无效频道", NotificationStatus.System => "系统", _ => status.ToString() }; private sealed record OptionItem(T Value, string Label) where T : struct, Enum; }