950 lines
38 KiB
C#
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]}";
|
|
}
|
|
|
|
}
|
|
|