Add WinUI and core source
build-winui / winui (push) Has been cancelled
build-winui / winui (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.System;
|
||||
using YMhut.Box.Core.DevEnvironments;
|
||||
using YMhut.Box.Core.Downloads;
|
||||
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 DevEnvironmentConfigToolViewModel(IToolModule module) : AdaptiveToolViewModel(module);
|
||||
|
||||
public sealed class DevEnvironmentConfigToolPage : ToolPageBase
|
||||
{
|
||||
private readonly IToolModule _module;
|
||||
private readonly Action? _goBack;
|
||||
private readonly IDevEnvironmentDetectionService _detection = AppServices.GetRequiredService<IDevEnvironmentDetectionService>();
|
||||
private readonly IDevEnvironmentCatalogService _catalog = AppServices.GetRequiredService<IDevEnvironmentCatalogService>();
|
||||
private readonly IDownloadManagerService _downloads = AppServices.GetRequiredService<IDownloadManagerService>();
|
||||
private readonly StackPanel _environmentList = new() { Spacing = 12 };
|
||||
private readonly TextBlock _statusText = ModernUi.Text(AppLocalizer.T("准备检测开发环境。", "Ready to detect development environments."), 14, foreground: ModernUi.TextSecondary);
|
||||
private readonly Dictionary<string, ComboBox> _versionBoxes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, ComboBox> _sourceBoxes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Button> _quickButtons = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Button> _sourceButtons = new(StringComparer.OrdinalIgnoreCase);
|
||||
private IReadOnlyDictionary<string, DetectedDevEnvironment> _detected = new Dictionary<string, DetectedDevEnvironment>();
|
||||
private IReadOnlyDictionary<string, IReadOnlyList<DevEnvironmentVersion>> _versions = new Dictionary<string, IReadOnlyList<DevEnvironmentVersion>>();
|
||||
|
||||
public DevEnvironmentConfigToolPage(IToolModule module, DevEnvironmentConfigToolViewModel viewModel, Action? goBack = null)
|
||||
{
|
||||
_module = module;
|
||||
_goBack = goBack;
|
||||
BindModule(module);
|
||||
Background = ModernUi.AppBackground;
|
||||
Content = BuildContent();
|
||||
Loaded += async (_, _) => await ReloadAsync();
|
||||
}
|
||||
|
||||
private UIElement BuildContent()
|
||||
{
|
||||
var root = new StackPanel
|
||||
{
|
||||
Padding = new Thickness(28, 22, 28, 28),
|
||||
Spacing = 16
|
||||
};
|
||||
|
||||
var back = ModernUi.IconButton("\uE72B", AppLocalizer.T("返回工具箱", "Back to toolbox"), () => _goBack?.Invoke());
|
||||
var reload = ModernUi.PillButton(AppLocalizer.T("重新检测", "Detect again"), "\uE895", async () => await ReloadAsync(), primary: true);
|
||||
var downloads = ModernUi.PillButton(AppLocalizer.T("打开下载管理", "Open downloads"), "\uE896", OpenDownloadManager);
|
||||
|
||||
var header = new Grid { ColumnSpacing = 14 };
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
header.Children.Add(back);
|
||||
var titlePanel = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(AppLocalizer.T("开发环境配置", "Development Environment Setup"), 28, FontWeights.SemiBold),
|
||||
ModernUi.Text(AppLocalizer.T("检测本机 Go、Python、Java、Docker、MySQL、Node.js、.NET、Git、Rust 和 CMake,并从官方来源下载版本。", "Detect local Go, Python, Java, Docker, MySQL, Node.js, .NET, Git, Rust, and CMake, then download versions from official sources."), 14, foreground: ModernUi.TextSecondary, maxLines: 2),
|
||||
_statusText
|
||||
}
|
||||
};
|
||||
Grid.SetColumn(titlePanel, 1);
|
||||
header.Children.Add(titlePanel);
|
||||
|
||||
var actionRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { reload, downloads } };
|
||||
Grid.SetColumn(actionRow, 2);
|
||||
header.Children.Add(actionRow);
|
||||
|
||||
root.Children.Add(ModernUi.Card(header, new Thickness(18), radius: 8));
|
||||
root.Children.Add(_environmentList);
|
||||
|
||||
return new ScrollViewer
|
||||
{
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Content = root
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
_statusText.Text = AppLocalizer.T("正在检测本机环境并读取官方版本信息...", "Detecting local environments and reading official version metadata...");
|
||||
_environmentList.Children.Clear();
|
||||
_environmentList.Children.Add(ModernUi.Card(ModernUi.Text(AppLocalizer.T("检测中...", "Detecting..."), 14, foreground: ModernUi.TextSecondary), new Thickness(14), radius: 8));
|
||||
|
||||
try
|
||||
{
|
||||
var detected = await _detection.DetectAsync();
|
||||
_detected = detected.ToDictionary(item => item.Id, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var versionPairs = await Task.WhenAll(_catalog.Definitions.Select(async definition =>
|
||||
{
|
||||
var versions = await _catalog.GetVersionsAsync(definition.Id);
|
||||
return (definition.Id, Versions: versions);
|
||||
}));
|
||||
_versions = versionPairs.ToDictionary(item => item.Id, item => item.Versions, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Render();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_statusText.Text = AppLocalizer.T("开发环境检测失败。", "Development environment detection failed.");
|
||||
_environmentList.Children.Clear();
|
||||
_environmentList.Children.Add(ModernUi.Card(
|
||||
ModernUi.Text(AppLocalizer.SanitizeSensitiveText(exception.Message, 180), 14, foreground: ModernUi.Danger),
|
||||
new Thickness(14),
|
||||
radius: 8));
|
||||
}
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
_versionBoxes.Clear();
|
||||
_sourceBoxes.Clear();
|
||||
_quickButtons.Clear();
|
||||
_sourceButtons.Clear();
|
||||
_environmentList.Children.Clear();
|
||||
var installed = _detected.Values.Count(item => item.IsInstalled);
|
||||
_statusText.Text = AppLocalizer.T($"已检测到 {installed} 个开发环境。", $"{installed} development environments detected.");
|
||||
|
||||
foreach (var definition in _catalog.Definitions)
|
||||
{
|
||||
_environmentList.Children.Add(BuildEnvironmentCard(definition));
|
||||
}
|
||||
}
|
||||
|
||||
private Border BuildEnvironmentCard(DevEnvironmentDefinition definition)
|
||||
{
|
||||
_detected.TryGetValue(definition.Id, out var detected);
|
||||
_versions.TryGetValue(definition.Id, out var versions);
|
||||
versions ??= [];
|
||||
|
||||
var box = new ComboBox
|
||||
{
|
||||
MinWidth = 190,
|
||||
PlaceholderText = AppLocalizer.T("选择版本", "Select version"),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||||
};
|
||||
foreach (var version in versions.Take(30))
|
||||
{
|
||||
box.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Content = VersionLabel(version),
|
||||
Tag = version
|
||||
});
|
||||
}
|
||||
|
||||
if (box.Items.Count > 0)
|
||||
{
|
||||
box.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
_versionBoxes[definition.Id] = box;
|
||||
var sourceBox = new ComboBox
|
||||
{
|
||||
MinWidth = 220,
|
||||
PlaceholderText = AppLocalizer.T("选择下载源", "Select source"),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||||
};
|
||||
_sourceBoxes[definition.Id] = sourceBox;
|
||||
|
||||
var quick = ModernUi.PillButton(AppLocalizer.T("快速安装", "Quick install"), "\uE896", async () => await QueueDownloadAsync(definition, DevEnvironmentInstallMode.QuickInstall), primary: true);
|
||||
var source = ModernUi.PillButton(AppLocalizer.T("源码/编译", "Source build"), "\uE756", async () => await QueueDownloadAsync(definition, DevEnvironmentInstallMode.SourceBuild));
|
||||
var official = ModernUi.IconButton("\uE8A7", AppLocalizer.T("打开官网", "Open official site"), async () => await Launcher.LaunchUriAsync(new Uri(definition.OfficialHome)));
|
||||
_quickButtons[definition.Id] = quick;
|
||||
_sourceButtons[definition.Id] = source;
|
||||
|
||||
var top = new Grid { ColumnSpacing = 14 };
|
||||
top.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
top.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
top.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
top.Children.Add(ModernUi.IconTile("\uE943", 42, ModernUi.AccentSoft, ModernUi.Accent, 18));
|
||||
var titlePanel = new StackPanel
|
||||
{
|
||||
Spacing = 3,
|
||||
Children =
|
||||
{
|
||||
ModernUi.Text(definition.Name, 18, FontWeights.SemiBold, maxLines: 1),
|
||||
ModernUi.Text(DetectionLine(detected), 13, foreground: detected?.IsInstalled == true ? ModernUi.Success : ModernUi.TextSecondary, maxLines: 2)
|
||||
}
|
||||
};
|
||||
Grid.SetColumn(titlePanel, 1);
|
||||
top.Children.Add(titlePanel);
|
||||
var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { official } };
|
||||
Grid.SetColumn(actions, 2);
|
||||
top.Children.Add(actions);
|
||||
|
||||
var controls = new Grid { ColumnSpacing = 10, RowSpacing = 10 };
|
||||
controls.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
controls.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
controls.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
controls.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
controls.Children.Add(box);
|
||||
Grid.SetColumn(sourceBox, 1);
|
||||
controls.Children.Add(sourceBox);
|
||||
Grid.SetColumn(quick, 2);
|
||||
Grid.SetColumn(source, 3);
|
||||
controls.Children.Add(quick);
|
||||
controls.Children.Add(source);
|
||||
box.SelectionChanged += (_, _) => RefreshSourceChoices(definition.Id);
|
||||
sourceBox.SelectionChanged += (_, _) => RefreshActionState(definition.Id);
|
||||
RefreshSourceChoices(definition.Id);
|
||||
|
||||
return ModernUi.Card(new StackPanel
|
||||
{
|
||||
Spacing = 12,
|
||||
Children =
|
||||
{
|
||||
top,
|
||||
ModernUi.Text(AppLocalizer.T($"版本来源:{definition.VersionSource}", $"Version source: {definition.VersionSource}"), 12, foreground: ModernUi.TextSecondary, maxLines: 1),
|
||||
controls
|
||||
}
|
||||
}, new Thickness(16), radius: 8);
|
||||
}
|
||||
|
||||
private async Task QueueDownloadAsync(DevEnvironmentDefinition definition, DevEnvironmentInstallMode mode)
|
||||
{
|
||||
var version = SelectedVersion(definition.Id);
|
||||
if (version is null)
|
||||
{
|
||||
ToastService.Show(AppLocalizer.T("没有可下载的版本。", "No downloadable version is available."), ToastKind.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var candidate = SelectedCandidate(definition.Id, mode);
|
||||
if (candidate is null)
|
||||
{
|
||||
ToastService.Show(AppLocalizer.T("请选择可用的下载源。", "Select an available download source."), ToastKind.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(candidate.Source.Url) ||
|
||||
!Uri.TryCreate(candidate.Source.Url, UriKind.Absolute, out var uri) ||
|
||||
uri.Scheme is not ("http" or "https"))
|
||||
{
|
||||
await Launcher.LaunchUriAsync(new Uri(definition.OfficialHome));
|
||||
ToastService.Show(AppLocalizer.T("该来源需要在官网手动选择下载。", "This source requires manual selection on the official site."), ToastKind.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var plan = _catalog.CreateInstallPlan(definition.Id, version, candidate);
|
||||
var item = await _downloads.EnqueueAsync(
|
||||
candidate.Source,
|
||||
new DownloadOptions(
|
||||
InstallCommand: mode == DevEnvironmentInstallMode.QuickInstall ? "installer" : null,
|
||||
InstallArguments: plan.InstallArguments,
|
||||
IsInstaller: mode == DevEnvironmentInstallMode.QuickInstall,
|
||||
DeleteAfterInstall: mode == DevEnvironmentInstallMode.QuickInstall));
|
||||
|
||||
ToastService.Show(AppLocalizer.T("已加入下载管理。", "Added to Download Manager."), ToastKind.Success);
|
||||
if (mode == DevEnvironmentInstallMode.SourceBuild)
|
||||
{
|
||||
OpenBuildTerminal(plan, item);
|
||||
}
|
||||
}
|
||||
|
||||
private DevEnvironmentVersion? SelectedVersion(string id)
|
||||
{
|
||||
if (!_versionBoxes.TryGetValue(id, out var box) ||
|
||||
box.SelectedItem is not ComboBoxItem item ||
|
||||
item.Tag is not DevEnvironmentVersion version)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private DevEnvironmentDownloadCandidate? SelectedCandidate(string id, DevEnvironmentInstallMode mode)
|
||||
{
|
||||
if (!_sourceBoxes.TryGetValue(id, out var box) ||
|
||||
box.SelectedItem is not ComboBoxItem item ||
|
||||
item.Tag is not DevEnvironmentDownloadCandidate candidate)
|
||||
{
|
||||
return SelectedVersion(id)?.AllCandidates.FirstOrDefault(candidate => candidate.Mode == mode);
|
||||
}
|
||||
|
||||
if (candidate.Mode == mode)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return SelectedVersion(id)?.AllCandidates.FirstOrDefault(candidate => candidate.Mode == mode);
|
||||
}
|
||||
|
||||
private void RefreshSourceChoices(string id)
|
||||
{
|
||||
if (!_sourceBoxes.TryGetValue(id, out var box))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
box.Items.Clear();
|
||||
var version = SelectedVersion(id);
|
||||
if (version is not null)
|
||||
{
|
||||
foreach (var candidate in version.AllCandidates)
|
||||
{
|
||||
box.Items.Add(new ComboBoxItem
|
||||
{
|
||||
Content = CandidateLabel(candidate),
|
||||
Tag = candidate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (box.Items.Count > 0)
|
||||
{
|
||||
box.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
RefreshActionState(id);
|
||||
}
|
||||
|
||||
private void RefreshActionState(string id)
|
||||
{
|
||||
var version = SelectedVersion(id);
|
||||
var hasQuick = version?.AllCandidates.Any(candidate => candidate.Mode == DevEnvironmentInstallMode.QuickInstall) == true;
|
||||
var hasSource = version?.AllCandidates.Any(candidate => candidate.Mode == DevEnvironmentInstallMode.SourceBuild) == true &&
|
||||
!string.Equals(id, "docker", StringComparison.OrdinalIgnoreCase);
|
||||
if (_quickButtons.TryGetValue(id, out var quick))
|
||||
{
|
||||
quick.IsEnabled = hasQuick;
|
||||
}
|
||||
if (_sourceButtons.TryGetValue(id, out var source))
|
||||
{
|
||||
source.Visibility = hasSource ? Visibility.Visible : Visibility.Collapsed;
|
||||
source.IsEnabled = hasSource;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenBuildTerminal(DevEnvironmentInstallPlan plan, DownloadItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
var script = Path.Combine(Path.GetTempPath(), $"ymhut-build-{plan.EnvironmentId}-{Guid.NewGuid():N}.cmd");
|
||||
File.WriteAllLines(script, [
|
||||
"@echo off",
|
||||
"chcp 65001 > nul",
|
||||
$"echo {plan.EnvironmentName} {plan.Version.Version}",
|
||||
$"echo Source download target: {item.TargetPath}",
|
||||
"echo.",
|
||||
"if not exist \"%~dp0\" mkdir \"%~dp0\"",
|
||||
$"set \"ARCHIVE={item.TargetPath}\"",
|
||||
$"set \"WORKDIR=%USERPROFILE%\\YMhutBuilds\\{plan.EnvironmentId}-{plan.Version.Version}\"",
|
||||
"echo Waiting for source archive...",
|
||||
":wait_download",
|
||||
"if not exist \"%ARCHIVE%\" (timeout /t 2 > nul & goto wait_download)",
|
||||
"mkdir \"%WORKDIR%\" 2>nul",
|
||||
"cd /d \"%WORKDIR%\"",
|
||||
"echo.",
|
||||
"echo Build recipe:",
|
||||
$"echo {plan.BuildRecipe.Replace(Environment.NewLine, " & echo ")}",
|
||||
"echo.",
|
||||
"echo Extract the archive here, review prerequisites, then run the vendor build commands above.",
|
||||
"echo.",
|
||||
"pause"
|
||||
]);
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/k \"{script}\"",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ToastService.Show(AppLocalizer.SanitizeSensitiveText(exception.Message, 140), ToastKind.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DetectionLine(DetectedDevEnvironment? detected)
|
||||
{
|
||||
if (detected is null || !detected.IsInstalled)
|
||||
{
|
||||
return AppLocalizer.T("未检测到,可选择官方版本下载。", "Not detected. Choose an official version to download.");
|
||||
}
|
||||
|
||||
var path = string.IsNullOrWhiteSpace(detected.Path) ? detected.Source : detected.Path;
|
||||
return string.IsNullOrWhiteSpace(path)
|
||||
? AppLocalizer.T($"已安装:{detected.Version}", $"Installed: {detected.Version}")
|
||||
: AppLocalizer.T($"已安装:{detected.Version} · {path}", $"Installed: {detected.Version} · {path}");
|
||||
}
|
||||
|
||||
private static string VersionLabel(DevEnvironmentVersion version)
|
||||
{
|
||||
return version.PublishedAt is DateTimeOffset date
|
||||
? $"{version.Version} · {date:yyyy-MM-dd}"
|
||||
: version.Version;
|
||||
}
|
||||
|
||||
private static string CandidateLabel(DevEnvironmentDownloadCandidate candidate)
|
||||
{
|
||||
var size = candidate.Source.SizeBytes is long bytes ? $" · {DownloadFormat.FormatBytes(bytes)}" : string.Empty;
|
||||
var region = string.IsNullOrWhiteSpace(candidate.Region) ? string.Empty : $" · {candidate.Region}";
|
||||
var mode = candidate.Mode == DevEnvironmentInstallMode.SourceBuild
|
||||
? AppLocalizer.T("源码", "Source")
|
||||
: AppLocalizer.T("安装包", "Installer");
|
||||
return $"{mode} · {candidate.SourceType}{region}{size}";
|
||||
}
|
||||
|
||||
private static void OpenDownloadManager()
|
||||
{
|
||||
if (App.CurrentWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.ShowDownloadManagerPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user