完善音乐下载、下载管理和开发工具
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
using System.Text;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
using YMhut.Box.Core.Tools;
|
||||
using YMhut.Box.WinUI.Services;
|
||||
using YMhut.Box.WinUI.ViewModels.Tools;
|
||||
|
||||
namespace YMhut.Box.WinUI.Views.Tools;
|
||||
|
||||
public sealed class SerialTerminalToolViewModel(IToolModule module) : AdaptiveToolViewModel(module);
|
||||
|
||||
public sealed class SerialTerminalToolPage : ToolPageBase
|
||||
{
|
||||
private readonly ISerialPortTransport _transport;
|
||||
private readonly Action? _goBack;
|
||||
private readonly ComboBox _portBox = new() { MinWidth = 130, PlaceholderText = "COM" };
|
||||
private readonly ComboBox _baudBox = new() { MinWidth = 120 };
|
||||
private readonly ComboBox _parityBox = new() { MinWidth = 110 };
|
||||
private readonly ComboBox _dataBitsBox = new() { MinWidth = 90 };
|
||||
private readonly ComboBox _stopBitsBox = new() { MinWidth = 100 };
|
||||
private readonly ToggleSwitch _hexSend = new();
|
||||
private readonly ToggleSwitch _hexReceive = new();
|
||||
private readonly ToggleSwitch _timedSend = new();
|
||||
private readonly NumberBox _interval = new() { Minimum = 100, Maximum = 60000, Value = 1000, SmallChange = 100, Width = 120 };
|
||||
private readonly TextBox _sendBox = new() { AcceptsReturn = true, MinHeight = 92, TextWrapping = TextWrapping.Wrap };
|
||||
private readonly TextBox _receiveBox = new()
|
||||
{
|
||||
AcceptsReturn = true,
|
||||
IsReadOnly = true,
|
||||
MinHeight = 260,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Cascadia Mono")
|
||||
};
|
||||
private readonly TextBlock _status = ModernUi.Text(string.Empty, 13, FontWeights.SemiBold, ModernUi.TextSecondary);
|
||||
private readonly DispatcherTimer _sendTimer = new();
|
||||
private Button? _connectButton;
|
||||
private Button? _sendButton;
|
||||
private bool _sending;
|
||||
private bool _disposed;
|
||||
|
||||
public SerialTerminalToolPage(
|
||||
IToolModule module,
|
||||
Action? goBack = null,
|
||||
ISerialPortTransport? transport = null)
|
||||
{
|
||||
_goBack = goBack;
|
||||
_transport = transport ?? new SerialPortTransport();
|
||||
BindModule(module);
|
||||
Background = ModernUi.AppBackground;
|
||||
ConfigureControls();
|
||||
Content = BuildContent(module);
|
||||
_transport.DataReceived += Transport_DataReceived;
|
||||
_sendTimer.Tick += SendTimer_Tick;
|
||||
Loaded += (_, _) => RefreshPorts();
|
||||
Unloaded += SerialTerminalToolPage_Unloaded;
|
||||
}
|
||||
|
||||
private void ConfigureControls()
|
||||
{
|
||||
foreach (var baud in new[] { 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400 })
|
||||
{
|
||||
_baudBox.Items.Add(baud);
|
||||
}
|
||||
_baudBox.SelectedItem = 115200;
|
||||
|
||||
foreach (var parity in Enum.GetValues<SerialParityMode>())
|
||||
{
|
||||
_parityBox.Items.Add(parity);
|
||||
}
|
||||
_parityBox.SelectedItem = SerialParityMode.None;
|
||||
|
||||
foreach (var dataBits in new[] { 5, 6, 7, 8 })
|
||||
{
|
||||
_dataBitsBox.Items.Add(dataBits);
|
||||
}
|
||||
_dataBitsBox.SelectedItem = 8;
|
||||
|
||||
foreach (var stopBits in Enum.GetValues<SerialStopBitsMode>())
|
||||
{
|
||||
_stopBitsBox.Items.Add(stopBits);
|
||||
}
|
||||
_stopBitsBox.SelectedItem = SerialStopBitsMode.One;
|
||||
|
||||
_hexSend.Header = AppLocalizer.T("HEX 发送", "HEX send");
|
||||
_hexReceive.Header = AppLocalizer.T("HEX 显示", "HEX display");
|
||||
_timedSend.Header = AppLocalizer.T("定时发送", "Timed send");
|
||||
_timedSend.Toggled += (_, _) => UpdateTimer();
|
||||
_interval.ValueChanged += (_, _) => UpdateTimer();
|
||||
_sendBox.PlaceholderText = AppLocalizer.T("输入要发送的文本或十六进制字节", "Enter text or hexadecimal bytes to send");
|
||||
_status.Text = AppLocalizer.T("未连接", "Disconnected");
|
||||
}
|
||||
|
||||
private UIElement BuildContent(IToolModule module)
|
||||
{
|
||||
var refresh = ModernUi.IconButton("\uE72C", AppLocalizer.T("刷新串口", "Refresh ports"), RefreshPorts);
|
||||
_connectButton = ModernUi.PillButton(AppLocalizer.T("连接", "Connect"), "\uE703", async () => await ToggleConnectionAsync(), primary: true);
|
||||
var connectionActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { _connectButton, refresh } };
|
||||
|
||||
var connectionGrid = new Grid { ColumnSpacing = 10, RowSpacing = 10 };
|
||||
for (var index = 0; index < 5; index++)
|
||||
{
|
||||
connectionGrid.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
}
|
||||
AddLabeledControl(connectionGrid, 0, AppLocalizer.T("端口", "Port"), _portBox);
|
||||
AddLabeledControl(connectionGrid, 1, AppLocalizer.T("波特率", "Baud"), _baudBox);
|
||||
AddLabeledControl(connectionGrid, 2, AppLocalizer.T("校验", "Parity"), _parityBox);
|
||||
AddLabeledControl(connectionGrid, 3, AppLocalizer.T("数据位", "Data bits"), _dataBitsBox);
|
||||
AddLabeledControl(connectionGrid, 4, AppLocalizer.T("停止位", "Stop bits"), _stopBitsBox);
|
||||
|
||||
_sendButton = ModernUi.PillButton(AppLocalizer.T("发送", "Send"), "\uE724", async () => await SendAsync(), primary: true);
|
||||
_sendButton.IsEnabled = false;
|
||||
var sendActions = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 10,
|
||||
Children =
|
||||
{
|
||||
_hexSend,
|
||||
_timedSend,
|
||||
new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 6,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(AppLocalizer.T("间隔 ms", "Interval ms"), 12, foreground: ModernUi.TextSecondary),
|
||||
_interval
|
||||
}
|
||||
},
|
||||
_sendButton
|
||||
}
|
||||
};
|
||||
|
||||
var clear = ModernUi.IconButton("\uE74D", AppLocalizer.T("清空接收日志", "Clear receive log"), () => _receiveBox.Text = string.Empty);
|
||||
var export = ModernUi.IconButton("\uE159", AppLocalizer.T("导出接收日志", "Export receive log"), async () => await ExportLogAsync());
|
||||
var receiveHeader = new Grid();
|
||||
receiveHeader.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
receiveHeader.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
receiveHeader.Children.Add(_hexReceive);
|
||||
var receiveActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6, Children = { clear, export } };
|
||||
Grid.SetColumn(receiveActions, 1);
|
||||
receiveHeader.Children.Add(receiveActions);
|
||||
|
||||
var root = new StackPanel { Padding = new Thickness(24, 20, 24, 28), Spacing = 16 };
|
||||
root.Children.Add(ModernUi.PageHeader(
|
||||
ToolText.Name(module),
|
||||
ToolText.Description(module),
|
||||
module.Metadata.IconGlyph,
|
||||
actions: connectionActions,
|
||||
back: _goBack,
|
||||
backTooltip: AppLocalizer.T("返回上一级", "Back"),
|
||||
meta: _status));
|
||||
root.Children.Add(ModernUi.Card(connectionGrid, new Thickness(14), radius: 8));
|
||||
root.Children.Add(ModernUi.Card(new StackPanel
|
||||
{
|
||||
Spacing = 10,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(AppLocalizer.T("发送", "Send"), 16, FontWeights.SemiBold),
|
||||
_sendBox,
|
||||
sendActions
|
||||
}
|
||||
}, new Thickness(14), radius: 8));
|
||||
root.Children.Add(ModernUi.Card(new StackPanel
|
||||
{
|
||||
Spacing = 10,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(AppLocalizer.T("接收日志", "Receive log"), 16, FontWeights.SemiBold),
|
||||
receiveHeader,
|
||||
_receiveBox
|
||||
}
|
||||
}, new Thickness(14), radius: 8));
|
||||
|
||||
return new ScrollViewer
|
||||
{
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Content = root
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddLabeledControl(Grid grid, int column, string label, Control control)
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Spacing = 5,
|
||||
Children = { ModernUi.Text(label, 12, FontWeights.SemiBold, ModernUi.TextSecondary), control }
|
||||
};
|
||||
Grid.SetColumn(panel, column);
|
||||
grid.Children.Add(panel);
|
||||
}
|
||||
|
||||
private void RefreshPorts()
|
||||
{
|
||||
var selected = _portBox.SelectedItem?.ToString();
|
||||
_portBox.Items.Clear();
|
||||
foreach (var name in _transport.GetPortNames())
|
||||
{
|
||||
_portBox.Items.Add(name);
|
||||
}
|
||||
_portBox.SelectedItem = !string.IsNullOrWhiteSpace(selected) && _portBox.Items.Contains(selected)
|
||||
? selected
|
||||
: _portBox.Items.FirstOrDefault();
|
||||
if (_portBox.Items.Count == 0)
|
||||
{
|
||||
_status.Text = AppLocalizer.T("未发现串口设备", "No serial ports found");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleConnectionAsync()
|
||||
{
|
||||
if (_transport.IsOpen)
|
||||
{
|
||||
await DisconnectAsync();
|
||||
return;
|
||||
}
|
||||
if (_portBox.SelectedItem is not string portName)
|
||||
{
|
||||
ToastService.Show(AppLocalizer.T("请选择串口。", "Select a serial port."), ToastKind.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = new SerialConnectionOptions(
|
||||
portName,
|
||||
_baudBox.SelectedItem is int baud ? baud : 115200,
|
||||
_dataBitsBox.SelectedItem is int dataBits ? dataBits : 8,
|
||||
_parityBox.SelectedItem is SerialParityMode parity ? parity : SerialParityMode.None,
|
||||
_stopBitsBox.SelectedItem is SerialStopBitsMode stopBits ? stopBits : SerialStopBitsMode.One);
|
||||
await _transport.OpenAsync(options);
|
||||
_status.Text = AppLocalizer.T($"已连接 {portName} · {options.BaudRate}", $"Connected to {portName} · {options.BaudRate}");
|
||||
_connectButton!.Content = AppLocalizer.T("断开", "Disconnect");
|
||||
_sendButton!.IsEnabled = true;
|
||||
SetConnectionControlsEnabled(false);
|
||||
UpdateTimer();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_status.Text = AppLocalizer.T("连接失败", "Connection failed");
|
||||
ToastService.Show(AppLocalizer.SanitizeSensitiveText(exception.Message, 160), ToastKind.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DisconnectAsync()
|
||||
{
|
||||
_sendTimer.Stop();
|
||||
await _transport.CloseAsync();
|
||||
_status.Text = AppLocalizer.T("未连接", "Disconnected");
|
||||
if (_connectButton is not null)
|
||||
{
|
||||
_connectButton.Content = AppLocalizer.T("连接", "Connect");
|
||||
}
|
||||
if (_sendButton is not null)
|
||||
{
|
||||
_sendButton.IsEnabled = false;
|
||||
}
|
||||
SetConnectionControlsEnabled(true);
|
||||
}
|
||||
|
||||
private async Task SendAsync()
|
||||
{
|
||||
if (_sending || !_transport.IsOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_sending = true;
|
||||
try
|
||||
{
|
||||
var data = SerialPayloadCodec.Parse(_sendBox.Text, _hexSend.IsOn);
|
||||
if (data.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _transport.WriteAsync(data);
|
||||
AppendLog("TX", SerialPayloadCodec.Format(data, _hexSend.IsOn));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ToastService.Show(AppLocalizer.SanitizeSensitiveText(exception.Message, 160), ToastKind.Warning);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sending = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendTimer_Tick(object? sender, object e)
|
||||
=> await SendAsync();
|
||||
|
||||
private void UpdateTimer()
|
||||
{
|
||||
_sendTimer.Stop();
|
||||
if (_timedSend.IsOn && _transport.IsOpen)
|
||||
{
|
||||
_sendTimer.Interval = TimeSpan.FromMilliseconds(Math.Clamp(_interval.Value, 100, 60000));
|
||||
_sendTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void Transport_DataReceived(object? sender, YMhut.Box.Core.Tools.SerialDataReceivedEventArgs e)
|
||||
{
|
||||
var data = e.Data.ToArray();
|
||||
DispatcherQueue.TryEnqueue(() => AppendLog("RX", SerialPayloadCodec.Format(data, _hexReceive.IsOn)));
|
||||
}
|
||||
|
||||
private void AppendLog(string direction, string value)
|
||||
{
|
||||
var next = $"[{DateTime.Now:HH:mm:ss.fff}] {direction} {value}";
|
||||
_receiveBox.Text = string.IsNullOrEmpty(_receiveBox.Text) ? next : _receiveBox.Text + Environment.NewLine + next;
|
||||
if (_receiveBox.Text.Length > 250_000)
|
||||
{
|
||||
_receiveBox.Text = _receiveBox.Text[^200_000..];
|
||||
}
|
||||
_receiveBox.SelectionStart = _receiveBox.Text.Length;
|
||||
}
|
||||
|
||||
private async Task ExportLogAsync()
|
||||
{
|
||||
if (App.CurrentWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var picker = new FileSavePicker { SuggestedFileName = $"serial-{DateTime.Now:yyyyMMdd-HHmmss}" };
|
||||
picker.FileTypeChoices.Add(AppLocalizer.T("文本日志", "Text log"), [".txt"]);
|
||||
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(App.CurrentWindow));
|
||||
var file = await picker.PickSaveFileAsync();
|
||||
if (file is not null)
|
||||
{
|
||||
await FileIO.WriteTextAsync(file, _receiveBox.Text);
|
||||
ToastService.Show(AppLocalizer.T("串口日志已导出。", "Serial log exported."), ToastKind.Success);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetConnectionControlsEnabled(bool enabled)
|
||||
{
|
||||
_portBox.IsEnabled = enabled;
|
||||
_baudBox.IsEnabled = enabled;
|
||||
_parityBox.IsEnabled = enabled;
|
||||
_dataBitsBox.IsEnabled = enabled;
|
||||
_stopBitsBox.IsEnabled = enabled;
|
||||
}
|
||||
|
||||
private async void SerialTerminalToolPage_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
_sendTimer.Stop();
|
||||
_sendTimer.Tick -= SendTimer_Tick;
|
||||
_transport.DataReceived -= Transport_DataReceived;
|
||||
try
|
||||
{
|
||||
await _transport.CloseAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
_transport.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user