diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs index ae5a914a4..24ca94c93 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs @@ -16,6 +16,9 @@ namespace UniGetUI.Avalonia.ViewModels; public partial class PackageDetailsViewModel : ObservableObject { public event EventHandler? CloseRequested; + /// Raised on the UI thread after details have been loaded so the view + /// can (re)populate the inline rich-text blocks. + public event EventHandler? DetailsLoaded; public readonly IPackage Package; public readonly OperationType OperationRole; @@ -46,14 +49,12 @@ public partial class PackageDetailsViewModel : ObservableObject [ObservableProperty] private string _description = CoreTools.Translate("Loading..."); - // ── Basic info ───────────────────────────────────────────────────────────── + // ── Basic info (raw values exposed; the view builds the inline rich text) ── [ObservableProperty] private string _versionDisplay = ""; [ObservableProperty] - private string _homepageText = CoreTools.Translate("Loading..."); - [ObservableProperty] - private bool _hasHomepageUrl; + private Uri? _homepageUrl; [ObservableProperty] private string _author = CoreTools.Translate("Loading..."); @@ -61,9 +62,9 @@ public partial class PackageDetailsViewModel : ObservableObject private string _publisher = CoreTools.Translate("Loading..."); [ObservableProperty] - private string _licenseText = CoreTools.Translate("Loading..."); + private string? _licenseName; [ObservableProperty] - private bool _hasLicenseUrl; + private Uri? _licenseUrl; // ── Actions ──────────────────────────────────────────────────────────────── public string MainActionLabel { get; } @@ -78,9 +79,7 @@ public partial class PackageDetailsViewModel : ObservableObject public string PackageId { get; } [ObservableProperty] - private string _manifestText = CoreTools.Translate("Loading..."); - [ObservableProperty] - private bool _hasManifestUrl; + private Uri? _manifestUrl; [ObservableProperty] private string _installerHashLabel = CoreTools.Translate("Installer SHA256") + ":"; @@ -89,9 +88,7 @@ public partial class PackageDetailsViewModel : ObservableObject [ObservableProperty] private string _installerType = CoreTools.Translate("Loading..."); [ObservableProperty] - private string _installerUrlText = CoreTools.Translate("Loading..."); - [ObservableProperty] - private bool _hasInstallerUrl; + private Uri? _installerUrl; [ObservableProperty] private string _installerSize = ""; @@ -118,60 +115,43 @@ public partial class PackageDetailsViewModel : ObservableObject [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasScreenshots))] - [NotifyPropertyChangedFor(nameof(SelectedScreenshot))] - [NotifyPropertyChangedFor(nameof(ScreenshotPageLabel))] - [NotifyPropertyChangedFor(nameof(CanGoNextScreenshot))] private int _screenshotCount; public bool HasScreenshots => ScreenshotCount > 0; [ObservableProperty] - [NotifyPropertyChangedFor(nameof(SelectedScreenshot))] - [NotifyPropertyChangedFor(nameof(ScreenshotPageLabel))] - [NotifyPropertyChangedFor(nameof(CanGoPrevScreenshot))] - [NotifyPropertyChangedFor(nameof(CanGoNextScreenshot))] private int _selectedScreenshotIndex; - public Bitmap? SelectedScreenshot => - ScreenshotCount > 0 && SelectedScreenshotIndex < Screenshots.Count - ? Screenshots[SelectedScreenshotIndex] - : null; - - public string ScreenshotPageLabel => - ScreenshotCount > 0 ? $"{SelectedScreenshotIndex + 1} / {ScreenshotCount}" : ""; - - public bool CanGoPrevScreenshot => SelectedScreenshotIndex > 0; - public bool CanGoNextScreenshot => SelectedScreenshotIndex < ScreenshotCount - 1; - // ── Release notes ────────────────────────────────────────────────────────── [ObservableProperty] private string _releaseNotes = CoreTools.Translate("Loading..."); [ObservableProperty] - private string _releaseNotesUrlText = CoreTools.Translate("Loading..."); - [ObservableProperty] - private bool _hasReleaseNotesUrl; + private Uri? _releaseNotesUrl; // ── Translated labels ────────────────────────────────────────────────────── public string LabelVersion { get; } - public string LabelHomepage { get; } = CoreTools.Translate("Homepage") + ":"; - public string LabelAuthor { get; } = CoreTools.Translate("Author") + ":"; - public string LabelPublisher { get; } = CoreTools.Translate("Publisher") + ":"; - public string LabelLicense { get; } = CoreTools.Translate("License") + ":"; - public string LabelPackageId { get; } = CoreTools.Translate("Package ID") + ":"; - public string LabelManifest { get; } = CoreTools.Translate("Manifest") + ":"; - public string LabelInstallerType { get; } = CoreTools.Translate("Installer Type") + ":"; - public string LabelInstallerSize { get; } = CoreTools.Translate("Size") + ":"; - public string LabelInstallerUrl { get; } = CoreTools.Translate("Installer URL") + ":"; - public string LabelUpdateDate { get; } = CoreTools.Translate("Last updated:"); - public string LabelReleaseNotesUrl { get; } = CoreTools.Translate("Release notes URL") + ":"; - public string LabelOpen { get; } = CoreTools.Translate("Open"); - public string LabelClose { get; } = CoreTools.Translate("Close"); - public string HeaderDetails { get; } = CoreTools.Translate("Package details"); - public string HeaderDeps { get; } = CoreTools.Translate("Dependencies:"); - public string HeaderReleaseNotes { get; } = CoreTools.Translate("Release notes"); - public string HeaderScreenshots { get; } = CoreTools.Translate("Screenshots"); - public string LabelScreenshotContribute { get; } = CoreTools.Translate( + public string LabelHomepage { get; } = CoreTools.Translate("Homepage"); + public string LabelAuthor { get; } = CoreTools.Translate("Author"); + public string LabelPublisher { get; } = CoreTools.Translate("Publisher"); + public string LabelLicense { get; } = CoreTools.Translate("License"); + public string LabelSource { get; } = CoreTools.Translate("Package Manager"); + public string LabelPackageId { get; } = CoreTools.Translate("Package ID"); + public string LabelManifest { get; } = CoreTools.Translate("Manifest"); + public string LabelInstallerType { get; } = CoreTools.Translate("Installer Type"); + public string LabelInstallerUrl { get; } = CoreTools.Translate("Installer URL"); + public string LabelUpdateDate { get; } = CoreTools.Translate("Last updated"); + public string LabelDependencies { get; } = CoreTools.Translate("Dependencies"); + public string LabelReleaseNotes { get; } = CoreTools.Translate("Release notes"); + public string LabelReleaseNotesUrl { get; } = CoreTools.Translate("Release notes URL"); + public string LabelDownloadInstaller { get; } = CoreTools.Translate("Download installer"); + public string LabelInstallerNotAvailable { get; } = CoreTools.Translate("Installer not available"); + public string LabelNotAvailable { get; } = CoreTools.Translate("Not available"); + public string LabelNoDependencies { get; } = CoreTools.Translate("No dependencies specified"); + public string LabelInstallationOptions { get; } = CoreTools.Translate("Installation options"); + public string LabelSave { get; } = CoreTools.Translate("Save"); + public string LabelContributorBanner { get; } = CoreTools.Translate( "This package has no screenshots or is missing the icon? Contribute to UniGetUI by adding the missing icons and screenshots to our open, public database."); + public string LabelContribute { get; } = CoreTools.Translate("Become a contributor"); public PackageDetailsViewModel(IPackage package, OperationType role) { @@ -197,7 +177,7 @@ public PackageDetailsViewModel(IPackage package, OperationType role) if (role == OperationType.Install) { MainActionLabel = CoreTools.Translate("Install"); - LabelVersion = CoreTools.Translate("Version") + ":"; + LabelVersion = CoreTools.Translate("Version"); VersionDisplay = available?.VersionString ?? package.VersionString; AsAdminLabel = CoreTools.Translate("Install as administrator"); InteractiveLabel = CoreTools.Translate("Interactive installation"); @@ -208,10 +188,10 @@ public PackageDetailsViewModel(IPackage package, OperationType role) { MainActionLabel = CoreTools.Translate( "Update to version {0}", upgradable?.NewVersionString ?? package.NewVersionString); - LabelVersion = CoreTools.Translate("Installed Version") + ":"; + LabelVersion = CoreTools.Translate("Installed Version"); VersionDisplay = (upgradable?.VersionString ?? package.VersionString) - + " \u27a4 " - + (upgradable?.NewVersionString ?? package.NewVersionString); + + " ➤ " + + (upgradable?.NewVersionString ?? package.NewVersionString); AsAdminLabel = CoreTools.Translate("Update as administrator"); InteractiveLabel = CoreTools.Translate("Interactive update"); SkipHashOrRemoveDataLabel = CoreTools.Translate("Skip hash check"); @@ -220,7 +200,7 @@ public PackageDetailsViewModel(IPackage package, OperationType role) else { MainActionLabel = CoreTools.Translate("Uninstall"); - LabelVersion = CoreTools.Translate("Installed Version") + ":"; + LabelVersion = CoreTools.Translate("Installed Version"); VersionDisplay = installed?.VersionString ?? package.VersionString; AsAdminLabel = CoreTools.Translate("Uninstall as administrator"); InteractiveLabel = CoreTools.Translate("Interactive uninstall"); @@ -229,20 +209,18 @@ public PackageDetailsViewModel(IPackage package, OperationType role) } } - [RelayCommand(CanExecute = nameof(CanGoPrevScreenshot))] + [RelayCommand] private void PreviousScreenshot() { - SelectedScreenshotIndex = Math.Max(0, SelectedScreenshotIndex - 1); - PreviousScreenshotCommand.NotifyCanExecuteChanged(); - NextScreenshotCommand.NotifyCanExecuteChanged(); + if (SelectedScreenshotIndex > 0) + SelectedScreenshotIndex--; } - [RelayCommand(CanExecute = nameof(CanGoNextScreenshot))] + [RelayCommand] private void NextScreenshot() { - SelectedScreenshotIndex = Math.Min(ScreenshotCount - 1, SelectedScreenshotIndex + 1); - PreviousScreenshotCommand.NotifyCanExecuteChanged(); - NextScreenshotCommand.NotifyCanExecuteChanged(); + if (SelectedScreenshotIndex < ScreenshotCount - 1) + SelectedScreenshotIndex++; } public async Task LoadDetailsAsync() @@ -257,39 +235,28 @@ public async Task LoadDetailsAsync() IsLoading = false; Description = details.Description ?? CoreTools.Translate("Not available"); - HomepageText = details.HomepageUrl?.ToString() ?? CoreTools.Translate("Not available"); - HasHomepageUrl = details.HomepageUrl is not null; + HomepageUrl = details.HomepageUrl; Author = details.Author ?? CoreTools.Translate("Not available"); Publisher = details.Publisher ?? CoreTools.Translate("Not available"); - if (details.License is not null && details.LicenseUrl is not null) - LicenseText = $"{details.License} ({details.LicenseUrl})"; - else if (details.License is not null) - LicenseText = details.License; - else if (details.LicenseUrl is not null) - LicenseText = details.LicenseUrl.ToString(); - else - LicenseText = CoreTools.Translate("Not available"); - HasLicenseUrl = details.LicenseUrl is not null; + LicenseName = details.License; + LicenseUrl = details.LicenseUrl; - ManifestText = details.ManifestUrl?.ToString() ?? CoreTools.Translate("Not available"); - HasManifestUrl = details.ManifestUrl is not null; + ManifestUrl = details.ManifestUrl; if (Package.Manager.Properties.Name.Equals("chocolatey", StringComparison.OrdinalIgnoreCase)) InstallerHashLabel = CoreTools.Translate("Installer SHA512") + ":"; InstallerHash = details.InstallerHash ?? CoreTools.Translate("Not available"); InstallerType = details.InstallerType ?? CoreTools.Translate("Not available"); - InstallerUrlText = details.InstallerUrl?.ToString() ?? CoreTools.Translate("Not available"); - HasInstallerUrl = details.InstallerUrl is not null; + InstallerUrl = details.InstallerUrl; InstallerSize = details.InstallerSize > 0 ? CoreTools.FormatAsSize(details.InstallerSize, 2) : CoreTools.Translate("Unknown size"); UpdateDate = details.UpdateDate ?? CoreTools.Translate("Not available"); ReleaseNotes = details.ReleaseNotes ?? CoreTools.Translate("Not available"); - ReleaseNotesUrlText = details.ReleaseNotesUrl?.ToString() ?? CoreTools.Translate("Not available"); - HasReleaseNotesUrl = details.ReleaseNotesUrl is not null; + ReleaseNotesUrl = details.ReleaseNotesUrl; if (!CanListDependencies) { @@ -313,23 +280,41 @@ public async Task LoadDetailsAsync() foreach (var tag in details.Tags) Tags.Add(tag); TagCount = Tags.Count; + + DetailsLoaded?.Invoke(this, EventArgs.Empty); } private async Task LoadIconAsync() { try { - var iconUrl = await Task.Run(Package.GetIconUrl); - if (iconUrl is not null) + var uri = await Task.Run(Package.GetIconUrlIfAny); + if (uri is not null) { - using var http = new HttpClient(CoreTools.GenericHttpClientParameters); - var bytes = await http.GetByteArrayAsync(iconUrl); - using var ms = new MemoryStream(bytes); - PackageIcon = new Bitmap(ms); - return; + Bitmap? bitmap = null; + if (uri.IsFile) + { + bitmap = await Task.Run(() => new Bitmap(uri.LocalPath)); + } + else if (uri.Scheme is "http" or "https") + { + using var http = new HttpClient(CoreTools.GenericHttpClientParameters); + var bytes = await http.GetByteArrayAsync(uri); + using var ms = new MemoryStream(bytes); + bitmap = new Bitmap(ms); + } + + if (bitmap is not null) + { + PackageIcon = bitmap; + return; + } } } - catch { /* icon is optional */ } + catch (Exception ex) + { + Logger.Warn($"[PackageDetailsViewModel] Failed to load icon: {ex.Message}"); + } try { @@ -359,8 +344,6 @@ await Dispatcher.UIThread.InvokeAsync(() => { Screenshots.Add(bmp); ScreenshotCount = Screenshots.Count; - PreviousScreenshotCommand.NotifyCanExecuteChanged(); - NextScreenshotCommand.NotifyCanExecuteChanged(); }); } catch { /* skip failed screenshots */ } @@ -373,7 +356,7 @@ await Dispatcher.UIThread.InvokeAsync(() => } [RelayCommand] - private static void OpenUrl(string? url) + public static void OpenUrl(string? url) { if (string.IsNullOrEmpty(url) || !url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return; @@ -391,7 +374,7 @@ public class DependencyViewModel public DependencyViewModel(IPackageDetails.Dependency dep) { - var text = $" \u2022 {dep.Name}"; + var text = $" • {dep.Name}"; if (!string.IsNullOrEmpty(dep.Version)) text += $" v{dep.Version}"; text += dep.Mandatory diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml index f0490c25b..91db051f4 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml @@ -6,8 +6,8 @@ xmlns:t="using:UniGetUI.Avalonia.MarkupExtensions" x:Class="UniGetUI.Avalonia.Views.PackageDetailsWindow" x:DataType="vm:PackageDetailsViewModel" - Width="900" MinWidth="600" - Height="680" MinHeight="400" + Width="1000" MinWidth="640" + Height="720" MinHeight="420" CanResize="True" ShowInTaskbar="False" Background="{DynamicResource AppDialogBackground}" @@ -18,441 +18,321 @@ - - + + + + + + + + + + + + + + + + + - - + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - + + + + + + + + + + + + - + + + + - - - - ; the realized container is the Button itself. + if (container is Button btn && btn.Content is Ellipse ellipse) + ellipse.Classes.Set("active", i == active); + i++; + } + } + + // ── Responsive layout ──────────────────────────────────────────────────── + + private void ApplyLayoutForCurrentSize() + { + var wide = Bounds.Width >= WideThreshold; + var mode = wide ? LayoutMode.Wide : LayoutMode.Normal; + if (mode == _layoutMode) return; + _layoutMode = mode; + + if (mode == LayoutMode.Wide) { - Header = _vm.InteractiveLabel, - IsEnabled = _vm.CanRunInteractively, - }; - var skipOrRemove = new MenuItem + // Ensure two columns and the right-column panels live in RightPanel. + EnsureChild(RightPanel, ScreenshotsPanel, 0); + EnsureChild(RightPanel, DetailsPanel, 1); + RightPanel.IsVisible = true; + MainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star); + + ScreenshotsBorder.Height = _vm.HasScreenshots ? 320 : 150; + InstallOptionsExpander.IsExpanded = true; + } + else { - Header = _vm.SkipHashOrRemoveDataLabel, - IsEnabled = _vm.CanSkipHashOrRemoveData, - }; + // Move screenshots + details into LeftPanel (after install options) and collapse the right column. + EnsureChild(LeftPanel, ScreenshotsPanel, LeftPanel.Children.Count); + EnsureChild(LeftPanel, DetailsPanel, LeftPanel.Children.Count); + RightPanel.IsVisible = false; + MainGrid.ColumnDefinitions[1].Width = new GridLength(0); + + ScreenshotsBorder.Height = _vm.HasScreenshots ? 225 : 130; + InstallOptionsExpander.IsExpanded = false; + } + } + + /// Move to at the given index, removing from its old parent first. + private static void EnsureChild(Panel target, Control child, int index) + { + if (child.Parent is Panel current && current != target) + current.Children.Remove(child); + if (!target.Children.Contains(child)) + target.Children.Insert(Math.Min(index, target.Children.Count), child); + } + + // ── Per-row layout builders (inline flow like WinUI's RichTextBlock) ───── + + private void BuildBasicInfoInlines() + { + BasicInfoPanel.Children.Clear(); + + if (!string.IsNullOrWhiteSpace(_vm.Description)) + { + BasicInfoPanel.Children.Add(new SelectableTextBlock + { + Text = _vm.Description, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 8), + }); + } + + AddInlineRow(BasicInfoPanel, _vm.LabelHomepage, _vm.HomepageUrl); + AddInlineRow(BasicInfoPanel, _vm.LabelPublisher, _vm.Publisher); + AddInlineRow(BasicInfoPanel, _vm.LabelAuthor, _vm.Author); + AddLicenseRow(BasicInfoPanel); + AddInlineRow(BasicInfoPanel, _vm.LabelUpdateDate, _vm.UpdateDate); + AddInlineRow(BasicInfoPanel, _vm.LabelSource, _vm.SourceDisplay); + } + + private void BuildDetailsInlines() + { + DetailsPanel.Children.Clear(); + + AddInlineRow(DetailsPanel, _vm.LabelPackageId, _vm.PackageId); + AddInlineRow(DetailsPanel, _vm.LabelManifest, _vm.ManifestUrl); + AddInlineRow(DetailsPanel, _vm.LabelVersion, _vm.VersionDisplay); + + AddSpacer(DetailsPanel); + + AddInlineRow(DetailsPanel, _vm.LabelInstallerType, _vm.InstallerType); + AddInlineRow(DetailsPanel, _vm.LabelInstallerUrl, _vm.InstallerUrl); + AddInlineRow(DetailsPanel, _vm.InstallerHashLabel.TrimEnd(':'), _vm.InstallerHash); + + AddDownloadRow(DetailsPanel); + + AddSpacer(DetailsPanel); + + // Dependencies header + list + DetailsPanel.Children.Add(new SelectableTextBlock + { + Text = _vm.LabelDependencies + ":", + FontWeight = FontWeight.Bold, + Margin = new Thickness(0, 0, 0, 4), + }); + if (_vm.HasDependencyNote) + { + DetailsPanel.Children.Add(new SelectableTextBlock + { + Text = " " + _vm.DependencyNote, + Foreground = NotAvailableBrush, + Margin = new Thickness(0, 0, 0, 4), + }); + } + else + { + foreach (var dep in _vm.Dependencies) + { + DetailsPanel.Children.Add(new SelectableTextBlock + { + Text = dep.DisplayText, + TextWrapping = TextWrapping.Wrap, + }); + } + } + + AddSpacer(DetailsPanel); + + // Release notes + DetailsPanel.Children.Add(new SelectableTextBlock + { + Text = _vm.LabelReleaseNotes + ":", + FontWeight = FontWeight.Bold, + Margin = new Thickness(0, 0, 0, 4), + }); + DetailsPanel.Children.Add(new SelectableTextBlock + { + Text = _vm.ReleaseNotes, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 8), + }); + AddInlineRow(DetailsPanel, _vm.LabelReleaseNotesUrl, _vm.ReleaseNotesUrl); + } + + private static readonly IBrush NotAvailableBrush = + new SolidColorBrush(Color.FromArgb(255, 127, 127, 127)); + + private static void AddSpacer(StackPanel host) => + host.Children.Add(new Border { Height = 10 }); + + /// + /// Builds a single wrap-able row with "Label: value" all on one line, + /// matching the WinUI RichTextBlock paragraph layout. + /// + private void AddInlineRow(StackPanel host, string label, string value) + { + var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; + var inlines = tb.Inlines ??= new InlineCollection(); + inlines.Add(new Run(label + ": ") { FontWeight = FontWeight.Bold }); + if (string.IsNullOrWhiteSpace(value) || value == _vm.LabelNotAvailable) + inlines.Add(new Run(_vm.LabelNotAvailable) + { + Foreground = NotAvailableBrush, + FontStyle = FontStyle.Italic, + }); + else + inlines.Add(new Run(value)); + host.Children.Add(tb); + } + + private void AddInlineRow(StackPanel host, string label, Uri? url) + { + var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; + var inlines = tb.Inlines ??= new InlineCollection(); + inlines.Add(new Run(label + ": ") { FontWeight = FontWeight.Bold }); + + if (url is null) + { + inlines.Add(new Run(_vm.LabelNotAvailable) + { + Foreground = NotAvailableBrush, + FontStyle = FontStyle.Italic, + }); + } + else + { + inlines.Add(new Run(url.ToString()) + { + Foreground = LinkBrush, + TextDecorations = TextDecorations.Underline, + }); + tb.Cursor = new Cursor(StandardCursorType.Hand); + tb.PointerPressed += (_, e) => + { + if (e.GetCurrentPoint(tb).Properties.IsLeftButtonPressed) + OpenUrl(url.ToString()); + }; + } + host.Children.Add(tb); + } + + private void AddLicenseRow(StackPanel host) + { + var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; + var inlines = tb.Inlines ??= new InlineCollection(); + inlines.Add(new Run(_vm.LabelLicense + ": ") { FontWeight = FontWeight.Bold }); + + bool hasName = !string.IsNullOrEmpty(_vm.LicenseName); + bool hasUrl = _vm.LicenseUrl is not null; + + if (hasName) + inlines.Add(new Run(_vm.LicenseName!)); + + if (hasUrl) + { + if (hasName) inlines.Add(new Run(" ")); + var url = _vm.LicenseUrl!.ToString(); + inlines.Add(new Run(url) + { + Foreground = LinkBrush, + TextDecorations = TextDecorations.Underline, + }); + tb.Cursor = new Cursor(StandardCursorType.Hand); + tb.PointerPressed += (_, e) => + { + if (e.GetCurrentPoint(tb).Properties.IsLeftButtonPressed) + OpenUrl(url); + }; + } + else if (!hasName) + { + inlines.Add(new Run(_vm.LabelNotAvailable) + { + Foreground = NotAvailableBrush, + FontStyle = FontStyle.Italic, + }); + } + + host.Children.Add(tb); + } + + private void AddDownloadRow(StackPanel host) + { + var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap }; + var inlines = tb.Inlines ??= new InlineCollection(); + + if (_vm.CanDownloadInstaller && !_vm.IsLoading) + { + inlines.Add(new Run(_vm.LabelDownloadInstaller) + { + Foreground = LinkBrush, + TextDecorations = TextDecorations.Underline, + FontWeight = FontWeight.SemiBold, + }); + if (!string.IsNullOrEmpty(_vm.InstallerSize)) + inlines.Add(new Run($" ({_vm.InstallerSize})")); + tb.Cursor = new Cursor(StandardCursorType.Hand); + tb.PointerPressed += (_, e) => + { + if (e.GetCurrentPoint(tb).Properties.IsLeftButtonPressed) + DownloadInstaller(); + }; + } + else + { + inlines.Add(new Run(_vm.LabelInstallerNotAvailable) + { + Foreground = NotAvailableBrush, + FontStyle = FontStyle.Italic, + }); + } + host.Children.Add(tb); + } + + private static IBrush LinkBrush => + Application.Current?.Resources["AccentTextFillColorPrimaryBrush"] as IBrush + ?? new SolidColorBrush(Color.FromArgb(255, 0, 120, 212)); + + private static void OpenUrl(string url) + { + if (string.IsNullOrEmpty(url) || !url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + return; + try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } + catch { } + } + + private void DownloadInstaller() + { + if (!_vm.Package.Manager.Capabilities.CanDownloadInstaller) return; + Close(); + // Hook into the platform's download path if/when wired up. For now, fall back to opening + // the installer URL so the user can still grab the file. + if (_vm.InstallerUrl is not null) + OpenUrl(_vm.InstallerUrl.ToString()); + } + + // ── Action flyout ──────────────────────────────────────────────────────── + + private MenuFlyout BuildActionFlyout() + { + var flyout = new MenuFlyout(); + var asAdmin = new MenuItem { Header = _vm.AsAdminLabel, IsEnabled = _vm.CanRunAsAdmin }; + var interactive = new MenuItem { Header = _vm.InteractiveLabel, IsEnabled = _vm.CanRunInteractively }; + var skipOrRemove = new MenuItem { Header = _vm.SkipHashOrRemoveDataLabel, IsEnabled = _vm.CanSkipHashOrRemoveData }; var role = _vm.OperationRole; if (role is OperationType.Uninstall) @@ -99,8 +445,6 @@ private MenuFlyout BuildActionFlyout() return flyout; } - // ── Action handlers ──────────────────────────────────────────────────────── - private void OnMainAction() { ShouldProceedWithOperation = true;