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 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ RenderOptions.BitmapInterpolationMode="HighQuality"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
-
-
-
-
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs
index 0a4690d75..353a8fabe 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs
@@ -1,8 +1,18 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Media;
using Avalonia.Threading;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.ViewModels;
using UniGetUI.Avalonia.Views.DialogPages;
+using UniGetUI.Core.Tools;
using UniGetUI.Interface.Telemetry;
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.Interfaces;
@@ -15,10 +25,13 @@ namespace UniGetUI.Avalonia.Views;
public partial class PackageDetailsWindow : Window
{
- ///
- /// True when the user confirmed the main action (install/update/uninstall) without extras.
- /// Callers check this after ShowDialog() to trigger the default operation.
- ///
+ private const double WideThreshold = 950;
+ private const string ContributeUrl = "https://github.com/Devolutions/UniGetUI";
+
+ private enum LayoutMode { Unset, Normal, Wide }
+ private LayoutMode _layoutMode = LayoutMode.Unset;
+
+ /// True when the user confirmed the main action (install/update/uninstall) without extras.
public bool ShouldProceedWithOperation { get; private set; }
private readonly PackageDetailsViewModel _vm;
@@ -32,23 +45,52 @@ public PackageDetailsWindow(IPackage package, OperationType operation)
InitializeComponent();
_vm.CloseRequested += (_, _) => Close();
+ _vm.DetailsLoaded += (_, _) =>
+ {
+ BuildBasicInfoInlines();
+ BuildDetailsInlines();
+ };
+ _vm.PropertyChanged += OnVmPropertyChanged;
+ _vm.Screenshots.CollectionChanged += (_, _) => Dispatcher.UIThread.Post(UpdatePips);
MainActionButton.Click += (_, _) => OnMainAction();
ActionVariantsButton.Flyout = BuildActionFlyout();
InstallOptionsSaveButton.Click += (_, _) => _ = SaveInstallOptionsAsync();
+ ContributeButton.Click += (_, _) => OpenUrl(ContributeUrl);
+ PrevScreenshotButton.Click += (_, _) =>
+ {
+ if (_vm.SelectedScreenshotIndex > 0)
+ _vm.SelectedScreenshotIndex--;
+ };
+ NextScreenshotButton.Click += (_, _) =>
+ {
+ if (_vm.SelectedScreenshotIndex < _vm.ScreenshotCount - 1)
+ _vm.SelectedScreenshotIndex++;
+ };
+ ScreenshotPips.AddHandler(Button.ClickEvent, OnPipClicked);
+
+ SizeChanged += (_, _) => ApplyLayoutForCurrentSize();
+
+ // Seed inline blocks with loading placeholders.
+ BuildBasicInfoInlines();
+ BuildDetailsInlines();
}
- protected override async void OnOpened(EventArgs e)
+ protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
+ ApplyLayoutForCurrentSize();
Dispatcher.UIThread.Post(() => MainActionButton.Focus(), DispatcherPriority.Background);
_ = _vm.LoadDetailsAsync();
TelemetryHandler.PackageDetails(_vm.Package, _vm.OperationRole.ToString());
+ _ = InitInstallOptionsAsync();
+ }
+ private async Task InitInstallOptionsAsync()
+ {
_installOpts = await InstallOptionsFactory.LoadForPackageAsync(_vm.Package);
_installVm = new InstallOptionsViewModel(_vm.Package, _vm.OperationRole, _installOpts);
- var embed = new InstallOptionsControl();
- embed.DataContext = _installVm;
+ var embed = new InstallOptionsControl { DataContext = _installVm };
InstallOptionsHolder.Content = embed;
}
@@ -59,25 +101,329 @@ private async Task SaveInstallOptionsAsync()
await InstallOptionsFactory.SaveForPackageAsync(_installOpts, _vm.Package);
}
- private MenuFlyout BuildActionFlyout()
+ private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- var flyout = new MenuFlyout();
+ if (e.PropertyName == nameof(PackageDetailsViewModel.SelectedScreenshotIndex)
+ || e.PropertyName == nameof(PackageDetailsViewModel.ScreenshotCount))
+ Dispatcher.UIThread.Post(UpdatePips);
+ }
+
+ private void OnPipClicked(object? sender, RoutedEventArgs e)
+ {
+ if (e.Source is not Control src) return;
+ // Walk up to find the pip Button container; ask the ItemsControl for its index.
+ Control? cursor = src;
+ while (cursor is not null && cursor.Parent is not ItemsControl)
+ cursor = cursor.Parent as Control;
+ if (cursor is null) return;
+ int idx = ScreenshotPips.IndexFromContainer(cursor);
+ if (idx >= 0 && idx < _vm.ScreenshotCount)
+ _vm.SelectedScreenshotIndex = idx;
+ }
- var asAdmin = new MenuItem
+ private void UpdatePips()
+ {
+ int active = _vm.SelectedScreenshotIndex;
+ int i = 0;
+ foreach (var container in ScreenshotPips.GetRealizedContainers())
{
- Header = _vm.AsAdminLabel,
- IsEnabled = _vm.CanRunAsAdmin,
- };
- var interactive = new MenuItem
+ // The pip template is ; 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;