完善音乐下载、下载管理和开发工具

This commit is contained in:
2026-08-17 01:58:54 +08:00
parent c9fa6f7a88
commit 8b2dd89d2f
27 changed files with 4052 additions and 748 deletions
+4
View File
@@ -73,6 +73,10 @@ public static class AppServices
provider.GetRequiredService<IDownloadHostProcessService>(),
provider.GetService<ILogService>()));
services.AddSingleton<IDevEnvironmentDetectionService>(provider => new DevEnvironmentDetectionService(provider.GetService<ILogService>()));
services.AddSingleton<IDevTerminalPlatform>(provider => new WindowsDevTerminalPlatform(provider.GetRequiredService<AppPaths>()));
services.AddSingleton<IDevTerminalSetupService>(provider => new DevTerminalSetupService(
provider.GetRequiredService<IDevTerminalPlatform>(),
provider.GetService<ILogService>()));
services.AddSingleton<IDevEnvironmentCatalogService>(provider => new DevEnvironmentCatalogService(
provider.GetRequiredService<AppPaths>(),
provider.GetRequiredService<IHttpService>(),
@@ -0,0 +1,52 @@
using System.Diagnostics;
using Windows.Storage;
using Windows.System;
using YMhut.Box.Core.Downloads;
namespace YMhut.Box.WinUI.Services;
public sealed record DownloadLaunchResult(bool Succeeded, Process? Process = null, string Error = "");
public static class DownloadItemLauncher
{
public static async Task<DownloadLaunchResult> LaunchAsync(DownloadItem item)
{
try
{
var normalized = DownloadOpenPolicy.Normalize(item);
switch (normalized.OpenKind)
{
case DownloadOpenKind.Installer:
var process = DownloadManagerService.LaunchInstaller(normalized);
return process is null
? new DownloadLaunchResult(false, Error: "The installer file no longer exists.")
: new DownloadLaunchResult(true, process);
case DownloadOpenKind.ExternalUri:
if (!Uri.TryCreate(normalized.Source.Url, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https"))
{
return new DownloadLaunchResult(false, Error: "The download page address is invalid.");
}
return await Launcher.LaunchUriAsync(uri)
? new DownloadLaunchResult(true)
: new DownloadLaunchResult(false, Error: "Windows could not open the download page.");
case DownloadOpenKind.File:
if (!File.Exists(normalized.TargetPath))
{
return new DownloadLaunchResult(false, Error: "The downloaded file no longer exists.");
}
var file = await StorageFile.GetFileFromPathAsync(normalized.TargetPath);
return await Launcher.LaunchFileAsync(file)
? new DownloadLaunchResult(true)
: new DownloadLaunchResult(false, Error: "Windows has no application associated with this file.");
default:
return new DownloadLaunchResult(false, Error: "This download item cannot be opened.");
}
}
catch (Exception exception)
{
return new DownloadLaunchResult(false, Error: AppLocalizer.SanitizeSensitiveText(exception.Message, 220));
}
}
}
@@ -115,7 +115,8 @@ public sealed class DownloadManagerService : IDownloadManagerService, IDisposabl
options.InstallCommand,
options.InstallArguments,
options.IsInstaller,
options.DeleteAfterInstall);
options.DeleteAfterInstall,
options.OpenKind);
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
@@ -333,6 +334,13 @@ public sealed class DownloadManagerService : IDownloadManagerService, IDisposabl
return null;
}
item = DownloadOpenPolicy.Normalize(item);
if (item.OpenKind != DownloadOpenKind.Installer ||
!DownloadOpenPolicy.IsSupportedInstaller(item.TargetPath, item.InstallCommand))
{
throw new InvalidOperationException("The downloaded item is not a supported installer.");
}
var command = item.InstallCommand;
var fileName = string.IsNullOrWhiteSpace(command) || string.Equals(command, "installer", StringComparison.OrdinalIgnoreCase)
? item.TargetPath
@@ -601,9 +609,10 @@ public sealed class DownloadManagerService : IDownloadManagerService, IDisposabl
private static DownloadItem NormalizeLoadedItem(DownloadItem item)
{
var withPartialPath = string.IsNullOrWhiteSpace(item.PartialPath)
? item with { PartialPath = item.TargetPath + ".partial" }
: item;
var normalized = DownloadOpenPolicy.Normalize(item);
var withPartialPath = string.IsNullOrWhiteSpace(normalized.PartialPath)
? normalized with { PartialPath = normalized.TargetPath + ".partial" }
: normalized;
var partialBytes = FileLength(withPartialPath.EffectivePartialPath);
return withPartialPath.State == DownloadState.Running
? withPartialPath.WithProgress(DownloadState.Queued, partialBytes, withPartialPath.TotalBytes ?? withPartialPath.ContentLength, 0)
@@ -0,0 +1,147 @@
using System.IO.Ports;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.WinUI.Services;
public sealed class SerialPortTransport : ISerialPortTransport
{
private readonly object _gate = new();
private SerialPort? _port;
public bool IsOpen
{
get
{
lock (_gate)
{
return _port?.IsOpen == true;
}
}
}
public event EventHandler<YMhut.Box.Core.Tools.SerialDataReceivedEventArgs>? DataReceived;
public IReadOnlyList<string> GetPortNames()
=> SerialPort.GetPortNames().Order(StringComparer.OrdinalIgnoreCase).ToArray();
public Task OpenAsync(SerialConnectionOptions options, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
lock (_gate)
{
CloseCore();
var port = new SerialPort(
options.PortName,
options.BaudRate,
MapParity(options.Parity),
options.DataBits,
MapStopBits(options.StopBits))
{
Encoding = System.Text.Encoding.UTF8,
ReadTimeout = 1000,
WriteTimeout = 2000
};
port.DataReceived += Port_DataReceived;
port.Open();
_port = port;
}
return Task.CompletedTask;
}
public Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
lock (_gate)
{
if (_port?.IsOpen != true)
{
throw new InvalidOperationException("The serial port is not connected.");
}
_port.BaseStream.Write(data.Span);
_port.BaseStream.Flush();
}
return Task.CompletedTask;
}
public Task CloseAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
lock (_gate)
{
CloseCore();
}
return Task.CompletedTask;
}
public void Dispose()
{
lock (_gate)
{
CloseCore();
}
}
private void Port_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs args)
{
try
{
byte[] data;
lock (_gate)
{
if (_port?.IsOpen != true)
{
return;
}
var count = _port.BytesToRead;
if (count <= 0)
{
return;
}
data = new byte[count];
_ = _port.Read(data, 0, data.Length);
}
DataReceived?.Invoke(this, new YMhut.Box.Core.Tools.SerialDataReceivedEventArgs(data));
}
catch
{
}
}
private void CloseCore()
{
var port = _port;
_port = null;
if (port is null)
{
return;
}
port.DataReceived -= Port_DataReceived;
try
{
if (port.IsOpen)
{
port.Close();
}
}
finally
{
port.Dispose();
}
}
private static Parity MapParity(SerialParityMode value) => value switch
{
SerialParityMode.Odd => Parity.Odd,
SerialParityMode.Even => Parity.Even,
SerialParityMode.Mark => Parity.Mark,
SerialParityMode.Space => Parity.Space,
_ => Parity.None
};
private static StopBits MapStopBits(SerialStopBitsMode value) => value switch
{
SerialStopBitsMode.OnePointFive => StopBits.OnePointFive,
SerialStopBitsMode.Two => StopBits.Two,
_ => StopBits.One
};
}