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(); private readonly IDevEnvironmentCatalogService _catalog = AppServices.GetRequiredService(); private readonly IDevTerminalSetupService _terminalSetup = AppServices.GetRequiredService(); private readonly IDownloadManagerService _downloads = AppServices.GetRequiredService(); 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 _versionBoxes = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _sourceBoxes = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _quickButtons = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _sourceButtons = new(StringComparer.OrdinalIgnoreCase); private readonly TextBlock _terminalStatus = ModernUi.Text(AppLocalizer.T("正在检测终端环境...", "Checking terminal environment..."), 14, FontWeights.SemiBold); private readonly TextBlock _terminalDetail = ModernUi.Text(string.Empty, 12, foreground: ModernUi.TextSecondary, maxLines: 3); private readonly ProgressBar _terminalProgress = new() { Minimum = 0, Maximum = 100, Visibility = Visibility.Collapsed }; private Button? _terminalInstallButton; private Button? _terminalOpenButton; private DevTerminalSnapshot? _terminalSnapshot; private IReadOnlyDictionary _detected = new Dictionary(); private IReadOnlyDictionary> _versions = new Dictionary>(); 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(24, 20, 24, 28), Spacing = 18 }; 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 actionRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { reload, downloads } }; root.Children.Add(ModernUi.PageHeader( AppLocalizer.T("开发环境配置", "Development Environment Setup"), AppLocalizer.T("检测常用开发环境,并从官方来源选择版本和下载源。", "Detect common development environments and select versions from official sources."), "\uE943", actions: actionRow, back: _goBack, backTooltip: AppLocalizer.T("返回上一级", "Back"), meta: _statusText)); root.Children.Add(BuildTerminalSetupCard()); root.Children.Add(_environmentList); return new ScrollViewer { HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Content = root }; } private Border BuildTerminalSetupCard() { _terminalInstallButton = ModernUi.PillButton( AppLocalizer.T("安装 / 修复", "Install / repair"), "\uE896", async () => await InstallTerminalAsync(), primary: true); _terminalOpenButton = ModernUi.PillButton( AppLocalizer.T("打开终端", "Open terminal"), "\uE756", async () => await OpenTerminalAsync()); var refresh = ModernUi.IconButton("\uE72C", AppLocalizer.T("重新检测终端", "Check terminal again"), async () => await RefreshTerminalAsync()); var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Children = { _terminalInstallButton, _terminalOpenButton, refresh } }; 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("\uE756", 44, ModernUi.AccentSoft, ModernUi.Accent, 19)); var copy = new StackPanel { Spacing = 3, Children = { ModernUi.Text(AppLocalizer.T("Windows 终端环境", "Windows terminal environment"), 18, FontWeights.SemiBold), _terminalStatus, _terminalDetail } }; Grid.SetColumn(copy, 1); top.Children.Add(copy); Grid.SetColumn(actions, 2); top.Children.Add(actions); return ModernUi.Card(new StackPanel { Spacing = 12, Children = { top, _terminalProgress, ModernUi.Text( AppLocalizer.T( "安装 Windows Terminal 与 PowerShell 7;系统支持时自动设置默认终端和默认 PowerShell 配置。", "Install Windows Terminal and PowerShell 7, then configure supported default terminal settings."), 12, foreground: ModernUi.TextSecondary, maxLines: 2) } }, new Thickness(16), radius: 8); } 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 terminalTask = _terminalSetup.DetectAsync(); var detectedTask = _detection.DetectAsync(); var detected = await detectedTask; _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); ApplyTerminalSnapshot(await terminalTask); 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 async Task RefreshTerminalAsync() { SetTerminalBusy(true, AppLocalizer.T("正在检测终端环境...", "Checking terminal environment...")); try { ApplyTerminalSnapshot(await _terminalSetup.DetectAsync()); } catch (Exception exception) { _terminalStatus.Text = AppLocalizer.T("终端检测失败", "Terminal check failed"); _terminalDetail.Text = AppLocalizer.SanitizeSensitiveText(exception.Message, 180); } finally { SetTerminalBusy(false); } } private async Task InstallTerminalAsync() { if (XamlRoot is null) { return; } var dialog = new ContentDialog { XamlRoot = XamlRoot, Title = AppLocalizer.T("安装终端套件?", "Install terminal suite?"), Content = ModernUi.Text( AppLocalizer.T( "将安装或升级 Windows Terminal 与 PowerShell 7,并在系统支持时更新当前用户的 PATH 和默认终端设置。安装包仅从微软官方来源获取。", "Windows Terminal and PowerShell 7 will be installed or upgraded from official Microsoft sources. Supported PATH and default-terminal settings will be updated for the current user."), 13, foreground: ModernUi.TextSecondary, maxLines: 5), PrimaryButtonText = AppLocalizer.T("继续", "Continue"), CloseButtonText = AppLocalizer.T("取消", "Cancel"), DefaultButton = ContentDialogButton.Primary }; if (await dialog.ShowAsync() != ContentDialogResult.Primary) { return; } SetTerminalBusy(true, AppLocalizer.T("正在准备终端安装...", "Preparing terminal setup...")); try { var progress = new Progress(item => { _terminalProgress.Value = item.Percent; _terminalStatus.Text = AppLocalizer.T(item.Message switch { "Checking the current terminal environment..." => "正在检查当前终端环境...", "Installing or upgrading Windows Terminal..." => "正在安装或升级 Windows Terminal...", "Installing or upgrading PowerShell 7..." => "正在安装或升级 PowerShell 7...", "Refreshing PATH and verifying terminal commands..." => "正在刷新 PATH 并验证终端命令...", "Configuring Windows Terminal defaults..." => "正在配置默认终端...", "Terminal setup completed." => "终端配置已完成。", _ => item.Message }, item.Message); }); var result = await _terminalSetup.InstallOrRepairAsync(progress); ApplyTerminalSnapshot(result.Snapshot); if (result.Succeeded) { ToastService.Show(AppLocalizer.T("终端套件已配置。", "Terminal suite configured."), ToastKind.Success); } else { _terminalDetail.Text = string.Join(" ", result.Messages.Append(AppLocalizer.T("请查看状态后重试失败的组件。", "Review the status and retry the failed component."))); ToastService.Show(AppLocalizer.T("终端套件仅完成了部分配置。", "Terminal setup completed partially."), ToastKind.Warning); } } catch (Exception exception) { _terminalStatus.Text = AppLocalizer.T("终端安装失败", "Terminal setup failed"); _terminalDetail.Text = AppLocalizer.SanitizeSensitiveText(exception.Message, 200); ToastService.Show(AppLocalizer.T("终端安装失败,请查看详细状态。", "Terminal setup failed. Review the status details."), ToastKind.Warning); } finally { SetTerminalBusy(false); } } private async Task OpenTerminalAsync() { if (!await _terminalSetup.OpenTerminalAsync()) { ToastService.Show(AppLocalizer.T("无法启动终端。", "Could not start a terminal."), ToastKind.Warning); } } private void ApplyTerminalSnapshot(DevTerminalSnapshot snapshot) { _terminalSnapshot = snapshot; var terminal = snapshot.WindowsTerminalInstalled ? $"Windows Terminal {ValueOrDash(snapshot.WindowsTerminalVersion)}" : AppLocalizer.T("Windows Terminal 未安装", "Windows Terminal not installed"); var powershell = snapshot.PowerShellInstalled ? $"PowerShell {ValueOrDash(snapshot.PowerShellVersion)}" : AppLocalizer.T("PowerShell 7 未安装", "PowerShell 7 not installed"); _terminalStatus.Text = $"{terminal} · {powershell}"; var defaultState = !snapshot.SupportsDefaultTerminal ? AppLocalizer.T($"Windows {snapshot.WindowsBuild} 不支持系统默认终端切换", $"Windows {snapshot.WindowsBuild} does not support changing the system default terminal") : snapshot.IsWindowsTerminalDefault ? AppLocalizer.T("Windows Terminal 已设为默认", "Windows Terminal is the default") : AppLocalizer.T("尚未设为默认终端", "Not yet the default terminal"); var profileState = snapshot.IsPowerShellDefaultProfile ? AppLocalizer.T("PowerShell 7 默认配置已启用", "PowerShell 7 default profile enabled") : AppLocalizer.T("PowerShell 7 默认配置待设置", "PowerShell 7 default profile pending"); _terminalDetail.Text = $"{defaultState} · {profileState} · {snapshot.Architecture}"; if (_terminalOpenButton is not null) { _terminalOpenButton.IsEnabled = snapshot.PowerShellInstalled || snapshot.WindowsTerminalInstalled; } } private void SetTerminalBusy(bool busy, string? message = null) { if (_terminalInstallButton is not null) { _terminalInstallButton.IsEnabled = !busy; } if (_terminalOpenButton is not null) { _terminalOpenButton.IsEnabled = !busy && (_terminalSnapshot?.IsReady == true || _terminalSnapshot?.PowerShellInstalled == true); } _terminalProgress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed; _terminalProgress.IsIndeterminate = busy && _terminalProgress.Value <= 0; if (!string.IsNullOrWhiteSpace(message)) { _terminalStatus.Text = message; } } private static string ValueOrDash(string value) => string.IsNullOrWhiteSpace(value) ? "--" : value; 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; } if (string.Equals(candidate.Source.SourceKind, "Manual", StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(candidate.Source.FileName), ".url", StringComparison.OrdinalIgnoreCase)) { await Launcher.LaunchUriAsync(uri); ToastService.Show(AppLocalizer.T("已打开官方下载安装页面。", "Opened the official download page."), ToastKind.Success); 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, OpenKind: mode == DevEnvironmentInstallMode.QuickInstall ? DownloadOpenKind.Installer : DownloadOpenKind.File)); 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(); } } }