Files
YMhut-box-C-/src/box-winUI/Views/AboutPage.cs
T
QWQLwToo ef25f8875c 更新UI
2026-07-12 09:12:12 +08:00

950 lines
38 KiB
C#

using System.Diagnostics;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Media.Imaging;
using YMhut.Box.Core.App;
using YMhut.Box.Core.DevEnvironments;
using YMhut.Box.Core.Settings;
using YMhut.Box.Core.Tools;
using YMhut.Box.Core.Updates;
using YMhut.Box.WinUI.Services;
namespace YMhut.Box.WinUI.Views;
public sealed class AboutPage : Page
{
private readonly IAppVersionService _versionService = AppServices.GetRequiredService<IAppVersionService>();
private readonly IAppInstallerUpdateService _updateService = AppServices.GetRequiredService<IAppInstallerUpdateService>();
private readonly IOpenSourceReferenceService _referenceService = AppServices.GetRequiredService<IOpenSourceReferenceService>();
private readonly ISettingsService _settingsService = AppServices.GetRequiredService<ISettingsService>();
private readonly IDevEnvironmentDetectionService _environmentDetection = AppServices.GetRequiredService<IDevEnvironmentDetectionService>();
private readonly ToolCatalog _catalog = AppServices.GetRequiredService<ToolCatalog>();
private readonly Action<string>? _openToolById;
private readonly StackPanel _referencePanel = new() { Spacing = 8 };
private readonly StackPanel _runtimeEnvironmentPanel = new() { Spacing = 8 };
private readonly TextBlock _runtimeSummary = ModernUi.Text(AppLocalizer.T("正在检测本机开发环境...", "Detecting installed development environments..."), 14, foreground: ModernUi.TextSecondary);
private readonly Grid _referencesOverlay = new()
{
Visibility = Visibility.Collapsed,
Opacity = 0
};
private readonly TranslateTransform _referencesTransform = new() { X = 430 };
private Border? _referencesPanelHost;
private readonly TextBlock _updateStatus = ModernUi.Text(AppLocalizer.T("可检查远程发布信息并获取最新版本。", "Check remote release information and get the latest version."), 14, foreground: ModernUi.TextSecondary);
private readonly Button _updateButton = new();
private readonly ProgressRing _updateProgress = new()
{
Width = 16,
Height = 16,
IsActive = false,
Visibility = Visibility.Collapsed,
VerticalAlignment = VerticalAlignment.Center
};
private readonly TextBlock _updateButtonText = ModernUi.Text(AppLocalizer.T("检查更新", "Check for updates"), 14, FontWeights.SemiBold);
private readonly ProgressBar _downloadProgressBar = new()
{
Minimum = 0,
Maximum = 100,
IsIndeterminate = true
};
private readonly TextBlock _downloadProgressText = ModernUi.Text(" ", 13, foreground: ModernUi.TextSecondary);
private readonly StackPanel _downloadPanel = new()
{
Spacing = 6,
Visibility = Visibility.Collapsed
};
private bool _checkingUpdate;
private bool _downloadingUpdate;
private CancellationTokenSource? _downloadCts;
private DateTimeOffset _lastDownloadProgressUpdate;
public AboutPage(Action<string>? openToolById = null)
{
_openToolById = openToolById;
Background = ModernUi.AppBackground;
_referencePanel.Children.Add(ModernUi.Text(AppLocalizer.T("正在加载引用来源...", "Loading source references..."), 13, foreground: ModernUi.TextSecondary));
Content = BuildContent();
Loaded += async (_, _) =>
{
await LoadReferencesAsync();
await LoadRuntimeEnvironmentsAsync();
};
}
private UIElement BuildContent()
{
var root = new StackPanel
{
Padding = new Thickness(24, 22, 24, 28),
Spacing = 18,
MaxWidth = 1120,
HorizontalAlignment = HorizontalAlignment.Center
};
root.Children.Add(BuildHero());
var applicationCard = BuildInfoSection(AppLocalizer.T("应用", "Application"), [
(AppLocalizer.T("应用名称", "Name"), "YMhut Box"),
(AppLocalizer.T("应用标识", "Identifier"), "cn.ymhut.box"),
(AppLocalizer.T("当前版本", "Current version"), _versionService.GetCurrent().Version),
(AppLocalizer.T("工具数量", "Tools"), AppLocalizer.ToolCount(_catalog.Modules.Count)),
(AppLocalizer.T("技术栈", "Stack"), "C# + .NET + WinUI 3")
], "\uE8A9");
var authorCard = BuildAuthorCard();
var updateCard = BuildUpdateCard();
var distributionCard = BuildDistributionCard();
var runtimeCard = BuildRuntimeEnvironmentCard();
var referencesCard = BuildReferencesEntryCard();
var primaryColumn = new StackPanel { Spacing = 16 };
var secondaryColumn = new StackPanel { Spacing = 16 };
var contentGrid = new Grid { ColumnSpacing = 16, RowSpacing = 16 };
void ArrangeAboutGrid(bool singleColumn)
{
primaryColumn.Children.Clear();
secondaryColumn.Children.Clear();
contentGrid.Children.Clear();
contentGrid.ColumnDefinitions.Clear();
contentGrid.RowDefinitions.Clear();
primaryColumn.Children.Add(applicationCard);
primaryColumn.Children.Add(updateCard);
primaryColumn.Children.Add(distributionCard);
secondaryColumn.Children.Add(authorCard);
secondaryColumn.Children.Add(runtimeCard);
secondaryColumn.Children.Add(referencesCard);
contentGrid.ColumnDefinitions.Add(new ColumnDefinition());
if (singleColumn)
{
contentGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
contentGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
Grid.SetColumn(secondaryColumn, 0);
Grid.SetRow(secondaryColumn, 1);
}
else
{
contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0.9, GridUnitType.Star) });
Grid.SetColumn(secondaryColumn, 1);
Grid.SetRow(secondaryColumn, 0);
}
Grid.SetColumn(primaryColumn, 0);
Grid.SetRow(primaryColumn, 0);
contentGrid.Children.Add(primaryColumn);
contentGrid.Children.Add(secondaryColumn);
}
ArrangeAboutGrid(singleColumn: false);
contentGrid.SizeChanged += (_, e) => ArrangeAboutGrid(e.NewSize.Width < 860);
root.Children.Add(contentGrid);
var scroll = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
Content = root
};
BuildReferencesOverlay();
return new Grid { Children = { scroll, _referencesOverlay } };
}
private Border BuildHero()
{
var version = _versionService.GetCurrent().Version;
var grid = new Grid { ColumnSpacing = 16, RowSpacing = 12 };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.Children.Add(new Border
{
Width = 72,
Height = 72,
CornerRadius = new CornerRadius(8),
Background = ModernUi.Surface,
BorderBrush = ModernUi.Stroke,
BorderThickness = new Thickness(1),
Child = new Image
{
Source = new BitmapImage(new Uri("ms-appx:///Assets/icons/app_icon.png")),
Width = 54,
Height = 54
}
});
var text = new StackPanel
{
Spacing = 6,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
ModernUi.Text("YMhut Box", 28, FontWeights.Bold),
ModernUi.Text(AppLocalizer.T($"版本 {version}", $"Version {version}"), 15, foreground: ModernUi.TextSecondary),
ModernUi.Text(AppLocalizer.T($"当前桌面工具箱包含 {_catalog.Modules.Count} 个工具能力,运行于 C#/.NET + WinUI 3。", $"This desktop toolbox contains {_catalog.Modules.Count} tool capabilities and runs on C#/.NET + WinUI 3."), 14, foreground: ModernUi.TextSecondary, maxLines: 2)
}
};
Grid.SetColumn(text, 1);
grid.Children.Add(text);
var badge = ModernUi.SmallBadge($"v{version}", ModernUi.Accent, ModernUi.AccentSoft);
badge.Padding = new Thickness(14, 8, 14, 8);
Grid.SetColumn(badge, 2);
grid.Children.Add(badge);
return ModernUi.Card(grid, new Thickness(18), radius: 8);
}
private Border BuildAuthorCard()
{
return ModernUi.Card(new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 14,
Children =
{
new Image
{
Source = new BitmapImage(new Uri("ms-appx:///Assets/images/author_avatar.png")),
Width = 48,
Height = 48
},
new StackPanel
{
VerticalAlignment = VerticalAlignment.Center,
Spacing = 2,
Children =
{
ModernUi.Text(AppLocalizer.T("开发者", "Developer"), 13, FontWeights.SemiBold, ModernUi.TextSecondary),
ModernUi.Text("YMhut", 20, FontWeights.SemiBold)
}
}
}
}, new Thickness(16), radius: 8);
}
private Border BuildUpdateCard()
{
_updateButton.HorizontalAlignment = HorizontalAlignment.Stretch;
_updateButton.Padding = new Thickness(14, 8, 14, 8);
_updateButton.CornerRadius = new CornerRadius(6);
_updateButton.BorderBrush = ModernUi.Stroke;
_updateButton.Background = ModernUi.SurfaceAlt;
_updateButton.Content = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 8,
HorizontalAlignment = HorizontalAlignment.Center,
Children =
{
_updateProgress,
new Image
{
Source = new SvgImageSource(new Uri("ms-appx:///Assets/download.svg")),
Width = 18,
Height = 18,
Stretch = Microsoft.UI.Xaml.Media.Stretch.Uniform
},
_updateButtonText
}
};
_downloadPanel.Children.Clear();
_downloadPanel.Children.Add(_downloadProgressBar);
_downloadPanel.Children.Add(_downloadProgressText);
_updateButton.Click += async (_, _) =>
{
if (_downloadingUpdate)
{
_downloadCts?.Cancel();
return;
}
await CheckUpdateAsync();
};
return ModernUi.Card(new StackPanel
{
Spacing = 12,
Children =
{
ModernUi.Text(AppLocalizer.T("版本更新", "Updates"), 18, FontWeights.SemiBold),
_updateStatus,
_downloadPanel,
_updateButton
}
}, new Thickness(16), radius: 8);
}
private Border BuildDistributionCard()
{
var current = _versionService.GetCurrent().Version;
return ModernUi.Card(new StackPanel
{
Spacing = 12,
Children =
{
new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
Children =
{
ModernUi.IconTile("\uE8B7", 34, ModernUi.SurfaceAlt, ModernUi.Accent, 16),
ModernUi.Text(AppLocalizer.T("更新分发", "Update distribution"), 18, FontWeights.SemiBold)
}
},
BuildUpdateLine(AppLocalizer.T("分发方式", "Delivery"), AppLocalizer.T("完整离线安装包 / MSIX", "Full offline installer / MSIX")),
BuildUpdateLine(AppLocalizer.T("当前版本", "Current"), current),
BuildUpdateLine(AppLocalizer.T("客户端策略", "Client policy"), AppLocalizer.T("仅解析完整安装包和 MSIX 清单", "Full installer and MSIX manifest only")),
ModernUi.Text(
AppLocalizer.T("当前更新通道只发布完整离线安装包和 MSIX,请按需要选择对应安装方式。", "The update channel now publishes the full offline installer and MSIX. Choose the package that fits your install flow."),
13,
foreground: ModernUi.TextSecondary,
maxLines: 3)
}
}, new Thickness(16), radius: 8);
}
private Border BuildReferencesEntryCard()
{
var openButton = ModernUi.PillButton(
AppLocalizer.T("查看引用来源", "View source references"),
"\uE8A5",
ShowReferencesDrawer,
primary: true);
openButton.HorizontalAlignment = HorizontalAlignment.Left;
openButton.VerticalAlignment = VerticalAlignment.Center;
Grid.SetColumn(openButton, 2);
return ModernUi.Card(new Grid
{
ColumnSpacing = 14,
ColumnDefinitions =
{
new ColumnDefinition { Width = GridLength.Auto },
new ColumnDefinition(),
new ColumnDefinition { Width = GridLength.Auto }
},
Children =
{
ModernUi.IconTile("\uE8A5", 44, ModernUi.SurfaceAlt, ModernUi.Accent, 19),
ReferenceEntryText(),
openButton
}
}, new Thickness(16), radius: 8);
}
private static StackPanel ReferenceEntryText()
{
var text = new StackPanel
{
Spacing = 3,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
ModernUi.Text(AppLocalizer.T("引用来源", "Source references"), 18, FontWeights.SemiBold),
ModernUi.Text(
AppLocalizer.T("来源、作者署名、许可证和第三方声明独立放在右侧抽屉中查看。", "Sources, attribution, licenses, and third-party notices open in a dedicated right drawer."),
13,
foreground: ModernUi.TextSecondary,
maxLines: 2)
}
};
Grid.SetColumn(text, 1);
return text;
}
private void BuildReferencesOverlay()
{
_referencesOverlay.Children.Clear();
_referencesOverlay.Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0));
var scrim = new Border { Background = ModernUi.Brush("#66000000") };
scrim.Tapped += (_, _) => HideReferencesDrawer();
var close = ModernUi.IconButton("\uE711", AppLocalizer.T("关闭", "Close"), HideReferencesDrawer);
close.Width = 38;
var header = new Grid { ColumnSpacing = 12 };
header.ColumnDefinitions.Add(new ColumnDefinition());
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.Children.Add(new StackPanel
{
Spacing = 4,
Children =
{
ModernUi.Text(AppLocalizer.T("引用来源", "Source references"), 24, FontWeights.SemiBold),
ModernUi.Text(
AppLocalizer.T("这里仅展示来源、署名、许可证和原始声明入口。", "Only sources, attribution, licenses, and original notices are shown here."),
13,
foreground: ModernUi.TextSecondary,
maxLines: 2)
}
});
Grid.SetColumn(close, 1);
header.Children.Add(close);
_referencesPanelHost = new Border
{
Width = 430,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Stretch,
Background = ModernUi.SidebarBackground,
BorderBrush = ModernUi.Stroke,
BorderThickness = new Thickness(1, 0, 0, 0),
RenderTransform = _referencesTransform,
Child = new ScrollViewer
{
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
Padding = new Thickness(0, 0, 8, 0),
Content = new StackPanel
{
Padding = new Thickness(22, 24, 14, 22),
Spacing = 16,
Children =
{
header,
_referencePanel
}
}
}
};
_referencesPanelHost.Tapped += (_, e) => e.Handled = true;
_referencesOverlay.Children.Add(scrim);
_referencesOverlay.Children.Add(_referencesPanelHost);
}
private void ShowReferencesDrawer()
{
var panelWidth = ClampReferencesPanelWidth();
if (_referencesPanelHost is not null)
{
_referencesPanelHost.Width = panelWidth;
}
AnimateReferencesDrawer(show: true, panelWidth);
}
private void HideReferencesDrawer()
{
AnimateReferencesDrawer(show: false, ClampReferencesPanelWidth());
}
private double ClampReferencesPanelWidth()
{
if (ActualWidth <= 0)
{
return 430;
}
var maxWidth = Math.Max(240, ActualWidth - 24);
return Math.Min(430, maxWidth);
}
private void AnimateReferencesDrawer(bool show, double panelWidth)
{
if (show)
{
_referencesOverlay.Visibility = Visibility.Visible;
}
if (!_settingsService.Current.AnimationsEnabled)
{
_referencesOverlay.Opacity = show ? 1 : 0;
_referencesTransform.X = show ? 0 : panelWidth;
_referencesOverlay.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
return;
}
var storyboard = new Storyboard();
var opacity = new DoubleAnimation
{
To = show ? 1 : 0,
Duration = TimeSpan.FromMilliseconds(180),
EnableDependentAnimation = true
};
Storyboard.SetTarget(opacity, _referencesOverlay);
Storyboard.SetTargetProperty(opacity, "Opacity");
storyboard.Children.Add(opacity);
var slide = new DoubleAnimation
{
To = show ? 0 : panelWidth,
Duration = TimeSpan.FromMilliseconds(220),
EnableDependentAnimation = true,
EasingFunction = new CubicEase { EasingMode = show ? EasingMode.EaseOut : EasingMode.EaseIn }
};
Storyboard.SetTarget(slide, _referencesTransform);
Storyboard.SetTargetProperty(slide, "X");
storyboard.Children.Add(slide);
if (!show)
{
storyboard.Completed += (_, _) => _referencesOverlay.Visibility = Visibility.Collapsed;
}
storyboard.Begin();
}
private async Task LoadReferencesAsync()
{
try
{
var references = await _referenceService.GetReferencesAsync();
_referencePanel.Children.Clear();
if (references.Count == 0)
{
_referencePanel.Children.Add(ModernUi.Text(AppLocalizer.T("暂无引用来源。", "No source references."), 13, foreground: ModernUi.TextSecondary));
return;
}
foreach (var group in references.GroupBy(item => item.Kind).OrderBy(group => ReferenceKindOrder(group.Key)))
{
_referencePanel.Children.Add(ModernUi.Text(ReferenceKindLabel(group.Key), 12, FontWeights.SemiBold, ModernUi.Accent, maxLines: 1));
foreach (var reference in group.Take(80))
{
_referencePanel.Children.Add(BuildReferenceRow(reference));
}
}
}
catch (Exception exception)
{
CrashLog.Write(exception);
_referencePanel.Children.Clear();
_referencePanel.Children.Add(ModernUi.Text(AppLocalizer.T("无法加载引用来源。", "Unable to load source references."), 13, FontWeights.SemiBold, ModernUi.TextSecondary));
}
}
private static UIElement BuildReferenceRow(OpenSourceReferenceItem reference)
{
var row = new Grid
{
ColumnSpacing = 10,
Padding = new Thickness(0, 8, 0, 8)
};
row.ColumnDefinitions.Add(new ColumnDefinition());
row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var nameLine = string.IsNullOrWhiteSpace(reference.Version)
? reference.Name
: $"{reference.Name} {reference.Version}";
var text = new StackPanel
{
Spacing = 2,
Children =
{
ModernUi.Text(nameLine, 13, FontWeights.SemiBold, maxLines: 2),
ModernUi.Text(AppLocalizer.T($"来源类型:{ReferenceKindLabel(reference.Kind)}", $"Source type: {ReferenceKindLabel(reference.Kind)}"), 12, foreground: ModernUi.TextSecondary, maxLines: 2),
ModernUi.Text(reference.Usage, 12, foreground: ModernUi.TextSecondary, maxLines: 3),
ModernUi.Text(reference.Attribution, 12, foreground: ModernUi.TextSecondary, maxLines: 3)
}
};
row.Children.Add(text);
if (!string.IsNullOrWhiteSpace(reference.Path) && File.Exists(reference.Path))
{
var button = ModernUi.IconButton("\uE8A7", AppLocalizer.T("打开原始声明文件", "Open original notice"), () => OpenReferenceFile(reference.Path));
Grid.SetColumn(button, 1);
row.Children.Add(button);
}
return row;
}
private static void OpenReferenceFile(string path)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = path,
WorkingDirectory = Path.GetDirectoryName(path) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
UseShellExecute = true
});
}
catch (Exception exception)
{
CrashLog.Write(exception);
ToastService.Show(AppLocalizer.T("无法打开原始声明文件。", "Unable to open notice file."), ToastKind.Error);
}
}
private static string ReferenceKindLabel(string kind)
{
return kind switch
{
"Application" => AppLocalizer.T("应用", "Application"),
"Reference project attribution" => AppLocalizer.T("参考项目来源", "Reference project source"),
"Runtime" => AppLocalizer.T("运行时", "Runtime"),
"NuGet package" => AppLocalizer.T("NuGet 包", "NuGet package"),
"Third-party tool notice" => AppLocalizer.T("第三方工具声明", "Third-party tool notice"),
_ => AppLocalizer.T("其他来源", "Other source")
};
}
private static int ReferenceKindOrder(string kind)
{
return kind switch
{
"Application" => 0,
"Reference project attribution" => 1,
"Runtime" => 2,
"NuGet package" => 3,
_ => 4
};
}
private Border BuildRuntimeEnvironmentCard()
{
_runtimeEnvironmentPanel.Children.Clear();
_runtimeEnvironmentPanel.Children.Add(ModernUi.Text(AppLocalizer.T("检测完成后仅展示已安装的开发环境。", "Only installed development environments are listed after detection."), 13, foreground: ModernUi.TextSecondary));
var openButton = ModernUi.PillButton(
AppLocalizer.T("开发环境配置", "Development setup"),
"\uE943",
() => _openToolById?.Invoke("dev_environment_config"),
primary: true);
openButton.HorizontalAlignment = HorizontalAlignment.Left;
ToolTipService.SetToolTip(openButton, AppLocalizer.T("打开工具箱中的开发环境配置工具", "Open the development environment setup tool in Toolbox"));
var header = new Grid { ColumnSpacing = 12, RowSpacing = 8 };
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.ColumnDefinitions.Add(new ColumnDefinition());
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
header.Children.Add(ModernUi.IconTile("\uE950", 34, ModernUi.SurfaceAlt, ModernUi.Accent, 16));
var text = new StackPanel
{
Spacing = 3,
Children =
{
ModernUi.Text(AppLocalizer.T("运行环境", "Runtime"), 18, FontWeights.SemiBold),
_runtimeSummary
}
};
Grid.SetColumn(text, 1);
header.Children.Add(text);
Grid.SetColumn(openButton, 2);
header.Children.Add(openButton);
return ModernUi.Card(new StackPanel
{
Spacing = 12,
Children =
{
header,
_runtimeEnvironmentPanel
}
}, new Thickness(16), radius: 8);
}
private async Task LoadRuntimeEnvironmentsAsync()
{
_runtimeSummary.Text = AppLocalizer.T("正在检测本机开发环境...", "Detecting installed development environments...");
_runtimeEnvironmentPanel.Children.Clear();
_runtimeEnvironmentPanel.Children.Add(ModernUi.Text(AppLocalizer.T("检测中...", "Detecting..."), 13, foreground: ModernUi.TextSecondary));
try
{
var detected = await _environmentDetection.DetectAsync();
RenderRuntimeEnvironments(detected);
}
catch (Exception exception)
{
CrashLog.Write(exception);
_runtimeSummary.Text = AppLocalizer.T("开发环境检测失败。", "Development environment detection failed.");
_runtimeEnvironmentPanel.Children.Clear();
_runtimeEnvironmentPanel.Children.Add(ModernUi.Text(AppLocalizer.SanitizeSensitiveText(exception.Message, 160), 13, foreground: ModernUi.Danger, maxLines: 3));
}
}
private void RenderRuntimeEnvironments(IReadOnlyList<DetectedDevEnvironment> detected)
{
var installed = detected
.Where(item => item.IsInstalled)
.OrderBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
_runtimeSummary.Text = AppLocalizer.T($"已安装 {installed.Length} 项开发环境。", $"{installed.Length} development environments installed.");
_runtimeEnvironmentPanel.Children.Clear();
if (installed.Length == 0)
{
_runtimeEnvironmentPanel.Children.Add(ModernUi.Text(AppLocalizer.T("未检测到常用开发环境,可进入配置工具选择官方版本下载。", "No common development environments were detected. Open setup to download official versions."), 13, foreground: ModernUi.TextSecondary, maxLines: 3));
return;
}
foreach (var item in installed.Take(6))
{
_runtimeEnvironmentPanel.Children.Add(BuildRuntimeEnvironmentRow(item));
}
if (installed.Length > 6)
{
_runtimeEnvironmentPanel.Children.Add(ModernUi.Text(AppLocalizer.T($"还有 {installed.Length - 6} 项已安装环境,可在配置工具中查看。", $"{installed.Length - 6} more installed environments can be viewed in setup."), 12, foreground: ModernUi.TextSecondary));
}
}
private static UIElement BuildRuntimeEnvironmentRow(DetectedDevEnvironment item)
{
var grid = new Grid { ColumnSpacing = 10, MinHeight = 34 };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.Children.Add(ModernUi.SmallBadge(item.Name, ModernUi.Accent, ModernUi.AccentSoft));
var path = string.IsNullOrWhiteSpace(item.Path) ? item.Source : item.Path;
var value = string.IsNullOrWhiteSpace(path)
? item.Version
: $"{item.Version} · {path}";
var text = ModernUi.Text(value, 13, foreground: ModernUi.TextSecondary, maxLines: 2);
Grid.SetColumn(text, 1);
grid.Children.Add(text);
return grid;
}
private Border BuildInfoSection(string title, IEnumerable<(string Label, string Value)> rows, string glyph)
{
var rowArray = rows.ToArray();
var stack = new StackPanel { Spacing = 10 };
stack.Children.Add(new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
Children =
{
ModernUi.IconTile(glyph, 34, ModernUi.SurfaceAlt, ModernUi.Accent, 16),
ModernUi.Text(title, 18, FontWeights.SemiBold)
}
});
foreach (var row in rowArray)
{
var grid = new Grid { ColumnSpacing = 12 };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(112) });
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.Children.Add(ModernUi.Text(row.Label, 13, FontWeights.SemiBold, ModernUi.TextSecondary));
var value = ModernUi.Text(row.Value, 13, maxLines: 2);
Grid.SetColumn(value, 1);
grid.Children.Add(value);
stack.Children.Add(grid);
}
return ModernUi.Card(stack, new Thickness(16), radius: 8);
}
private async Task CheckUpdateAsync()
{
if (_downloadingUpdate)
{
_downloadCts?.Cancel();
return;
}
if (_checkingUpdate)
{
return;
}
SetCheckingState(true);
_updateStatus.Text = AppLocalizer.T("正在检查更新...", "Checking for updates...");
try
{
var info = await _updateService.GetUpdateInfoAsync();
if (info is null)
{
_updateStatus.Text = AppLocalizer.T("暂时无法读取更新信息,请稍后重试。", "Could not read update information. Please try again later.");
ToastService.Show(AppLocalizer.T("检查更新失败", "Update check failed"), ToastKind.Warning);
return;
}
var current = _versionService.GetCurrent().Version;
_updateStatus.Text = AppLocalizer.T($"最新版本 {info.DisplayVersion} / 当前版本 {current}", $"Latest {info.DisplayVersion} / Current {current}");
if (!info.IsNewerThan(current))
{
ToastService.Show(AppLocalizer.T($"当前已是最新版本({current})。", $"You are already on the latest version ({current})."), ToastKind.Success);
return;
}
await ShowUpdateDialogAsync(info, current);
}
catch (Exception exception)
{
_updateStatus.Text = AppLocalizer.T("检查更新时发生错误。", "An error occurred while checking for updates.");
var message = AppLocalizer.SanitizeSensitiveText(exception.Message, 140);
ToastService.Show(AppLocalizer.T($"检查更新失败:{message}", $"Update check failed: {message}"), ToastKind.Error, TimeSpan.FromSeconds(3));
}
finally
{
SetCheckingState(false);
}
}
private async Task ShowUpdateDialogAsync(RemoteUpdateInfo info, string current)
{
var dialog = new ContentDialog
{
Title = AppLocalizer.T("软件更新", "Software update"),
Content = ModernUi.GutterScroll(UpdateNoticeRenderer.Render(info.Notice, UpdateNoticeRenderMode.UpdatePrompt, current), 620),
PrimaryButtonText = AppLocalizer.T("立即更新", "Update now"),
SecondaryButtonText = AppLocalizer.T("稍后", "Later"),
CloseButtonText = AppLocalizer.T("取消", "Cancel"),
XamlRoot = XamlRoot
};
var result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary)
{
return;
}
if (string.IsNullOrWhiteSpace(info.DownloadUrl))
{
var message = await _updateService.CheckForUpdatesAsync();
ToastService.Show(SanitizeUpdateMessage(message), ToastKind.Info, TimeSpan.FromSeconds(3));
return;
}
SetCheckingState(false);
await ShowDownloadDialogAsync(info);
}
private async Task ShowDownloadDialogAsync(RemoteUpdateInfo info)
{
await StartDownloadUpdateAsync(info);
}
private async Task StartDownloadUpdateAsync(RemoteUpdateInfo info)
{
if (_downloadingUpdate)
{
return;
}
_downloadingUpdate = true;
_downloadCts?.Dispose();
_downloadCts = new CancellationTokenSource();
_lastDownloadProgressUpdate = DateTimeOffset.MinValue;
_downloadPanel.Visibility = Visibility.Visible;
_downloadProgressBar.IsIndeterminate = true;
_downloadProgressBar.Value = 0;
_downloadProgressText.Text = AppLocalizer.T("准备下载更新包...", "Preparing update download...");
_updateStatus.Text = AppLocalizer.T("正在下载更新包,窗口仍可继续操作。", "Downloading update package. You can keep using the window.");
SetUpdateButtonState();
var progress = new Progress<UpdateDownloadProgress>(value =>
{
var now = DateTimeOffset.UtcNow;
if (value.Percentage is not >= 100 &&
now - _lastDownloadProgressUpdate < TimeSpan.FromMilliseconds(180))
{
return;
}
_lastDownloadProgressUpdate = now;
if (value.Percentage is double percentage)
{
_downloadProgressBar.IsIndeterminate = false;
_downloadProgressBar.Value = Math.Clamp(percentage, 0, 100);
}
_downloadProgressText.Text = $"{FormatBytes(value.BytesReceived)} / {(value.TotalBytes is long total ? FormatBytes(total) : "-")} {FormatBytes((long)value.BytesPerSecond)}/s";
});
try
{
var file = await _updateService.DownloadUpdateAsync(info, progress, _downloadCts.Token);
if (file is not null)
{
_downloadProgressBar.IsIndeterminate = false;
_downloadProgressBar.Value = 100;
_downloadProgressText.Text = AppLocalizer.T("更新包已下载,正在打开安装器。", "Update package downloaded. Opening installer.");
_updateStatus.Text = AppLocalizer.T("更新包已下载。", "Update package downloaded.");
ToastService.Show(AppLocalizer.T("更新包已下载", "Update package downloaded"), ToastKind.Success);
OpenDownloadedInstaller(file);
}
}
catch (OperationCanceledException)
{
_downloadProgressText.Text = AppLocalizer.T("下载已取消。", "Download canceled.");
_updateStatus.Text = AppLocalizer.T("更新下载已取消。", "Update download canceled.");
ToastService.Show(AppLocalizer.T("更新下载已取消", "Update download canceled"), ToastKind.Info, TimeSpan.FromSeconds(2));
}
catch (Exception exception)
{
var message = AppLocalizer.SanitizeSensitiveText(exception.Message, 140);
ToastService.Show(AppLocalizer.T($"下载失败:{message}", $"Download failed: {message}"), ToastKind.Error, TimeSpan.FromSeconds(3));
_updateStatus.Text = AppLocalizer.T("更新下载失败。", "Update download failed.");
_downloadProgressText.Text = AppLocalizer.T($"下载失败:{message}", $"Download failed: {message}");
}
finally
{
_downloadingUpdate = false;
_downloadCts?.Dispose();
_downloadCts = null;
SetUpdateButtonState();
}
}
private void SetCheckingState(bool checking)
{
_checkingUpdate = checking;
_updateProgress.IsActive = checking;
_updateProgress.Visibility = checking ? Visibility.Visible : Visibility.Collapsed;
SetUpdateButtonState();
}
private void SetUpdateButtonState()
{
_updateButton.IsEnabled = !_checkingUpdate || _downloadingUpdate;
_updateButtonText.Text = _downloadingUpdate
? AppLocalizer.T("取消下载", "Cancel download")
: _checkingUpdate
? AppLocalizer.T("正在检查...", "Checking...")
: AppLocalizer.T("检查更新", "Check for updates");
}
private static UIElement BuildUpdateLine(string label, string value)
{
var grid = new Grid { ColumnSpacing = 12 };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(96) });
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.Children.Add(ModernUi.Text(label, 13, FontWeights.SemiBold, ModernUi.TextSecondary));
var text = ModernUi.Text(value, 13);
Grid.SetColumn(text, 1);
grid.Children.Add(text);
return grid;
}
private static string SanitizeUpdateMessage(string message)
{
return AppLocalizer.SanitizeSensitiveText(message, 160)
.Replace("appinstaller", "App Installer", StringComparison.OrdinalIgnoreCase);
}
private static void OpenDownloadedInstaller(string file)
{
Process.Start(new ProcessStartInfo
{
FileName = file,
WorkingDirectory = Path.GetDirectoryName(file) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
UseShellExecute = true
});
}
private static string FormatBytes(long bytes)
{
string[] units = ["B", "KB", "MB", "GB"];
var value = (double)Math.Max(0, bytes);
var unit = 0;
while (value >= 1024 && unit < units.Length - 1)
{
value /= 1024;
unit++;
}
return $"{value:0.##} {units[unit]}";
}
}