Files
ClashWidget/MainWindow.xaml.cs
2026-05-08 11:59:26 +08:00

215 lines
6.7 KiB
C#

using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using ClashWidget.Helpers;
using ClashWidget.ViewModels;
namespace ClashWidget;
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
private Polyline? _downloadLine;
private Polyline? _uploadLine;
private bool _isMaximized;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainViewModel();
DataContext = _viewModel;
Opacity = 0;
Loaded += OnLoaded;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
_viewModel.SpeedHistory.CollectionChanged += OnSpeedHistoryChanged;
_viewModel.DropdownToggled += OnDropdownToggled;
CreateSparklines();
UpdateStatusDot();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
try
{
var hwnd = new WindowInteropHelper(this).Handle;
if (hwnd != IntPtr.Zero)
{
var source = HwndSource.FromHwnd(hwnd);
if (source?.CompositionTarget != null)
source.CompositionTarget.BackgroundColor = Colors.Transparent;
}
NativeMethods.ApplyAcrylic(this);
NativeMethods.SetTopmost(this);
NativeMethods.HideFromTaskbar(this);
}
catch { }
var workArea = SystemParameters.WorkArea;
Left = workArea.Right - Width - 20;
Top = workArea.Top + 20;
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(250))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
BeginAnimation(OpacityProperty, fadeIn);
_viewModel.Start();
}
private void CreateSparklines()
{
_downloadLine = new Polyline
{
Stroke = new SolidColorBrush(Color.FromRgb(0x3B, 0x82, 0xF6)),
StrokeThickness = 1.5, StrokeLineJoin = PenLineJoin.Round
};
_uploadLine = new Polyline
{
Stroke = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)),
StrokeThickness = 1, StrokeLineJoin = PenLineJoin.Round
};
SparklineCanvas.Children.Add(_downloadLine);
SparklineCanvas.Children.Add(_uploadLine);
}
private void OnSpeedHistoryChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(DrawSparklines));
}
private void DrawSparklines()
{
if (_downloadLine == null || _uploadLine == null) return;
var history = _viewModel.SpeedHistory.ToList();
if (history.Count < 2) return;
double cw = SparklineCanvas.ActualWidth, ch = SparklineCanvas.ActualHeight;
if (cw < 10 || ch < 10) return;
double maxVal = Math.Max(history.Max(p => p.DownloadBps), history.Max(p => p.UploadBps));
if (maxVal < 1) maxVal = 1;
double xStep = cw / (history.Count - 1);
var dl = new PointCollection();
var ul = new PointCollection();
for (int i = 0; i < history.Count; i++)
{
double x = i * xStep;
dl.Add(new Point(x, ch - (history[i].DownloadBps / maxVal * ch)));
ul.Add(new Point(x, ch - (history[i].UploadBps / maxVal * ch)));
}
_downloadLine.Points = dl;
_uploadLine.Points = ul;
}
private void OnWindowPreviewClick(object sender, MouseButtonEventArgs e)
{
if (NodePopup.IsOpen)
{
// Close popup if click is outside the popup
var pos = e.GetPosition(PopupBorder);
if (pos.X < 0 || pos.Y < 0 || pos.X > PopupBorder.ActualWidth || pos.Y > PopupBorder.ActualHeight)
{
NodePopup.IsOpen = false;
}
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
try { DragMove(); } catch { }
}
private void OnCloseClick(object sender, RoutedEventArgs e) => FadeOutAndClose();
private void OnMinimizeClick(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private void OnMaximizeClick(object sender, RoutedEventArgs e)
{
_isMaximized = !_isMaximized;
WindowState = _isMaximized ? WindowState.Maximized : WindowState.Normal;
}
private void OnNodeAreaClick(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
NodePopup.IsOpen = true;
}
private void OnPopupOpened(object? sender, EventArgs e)
{
_viewModel.IsDropdownOpen = true;
}
private void OnPopupClosed(object? sender, EventArgs e)
{
_viewModel.IsDropdownOpen = false;
}
private void OnDropdownToggled(bool open)
{
Dispatcher.BeginInvoke(new Action(() => NodePopup.IsOpen = open));
}
private void OnPopupBorderClick(object sender, MouseButtonEventArgs e)
{
e.Handled = true; // prevent bubbling to window drag
}
private async void OnNodeItemClick(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (NodeListBox.SelectedItem is string nodeName && !string.IsNullOrEmpty(nodeName))
{
await _viewModel.SwitchNodeAsync(nodeName);
NodePopup.IsOpen = false;
}
}
private void OnSettingsClick(object sender, RoutedEventArgs e)
{
var settings = new SettingsWindow(this);
settings.Owner = this;
settings.ShowDialog();
if (settings.Saved) _viewModel.Reconnect();
}
private async void OnRefreshLatencyClick(object sender, RoutedEventArgs e)
{
BtnRefreshLatency.IsEnabled = false;
try { await _viewModel.RefreshLatencyAsync(); } finally { BtnRefreshLatency.IsEnabled = true; }
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsConnected)) UpdateStatusDot();
}
private void UpdateStatusDot()
{
StatusDot.Fill = new SolidColorBrush(_viewModel.IsConnected ? Colors.LimeGreen : Colors.Tomato);
}
private void FadeOutAndClose()
{
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(150))
{
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
};
fadeOut.Completed += (_, _) => Close();
BeginAnimation(OpacityProperty, fadeOut);
}
protected override void OnClosed(EventArgs e)
{
_viewModel.Dispose();
base.OnClosed(e);
}
}