Skip to content

Commit a83a9b8

Browse files
committed
reworked package Details view
1 parent f243ad7 commit a83a9b8

3 files changed

Lines changed: 743 additions & 536 deletions

File tree

src/UniGetUI.Avalonia/ViewModels/DialogPages/PackageDetailsViewModel.cs

Lines changed: 76 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ namespace UniGetUI.Avalonia.ViewModels;
1616
public partial class PackageDetailsViewModel : ObservableObject
1717
{
1818
public event EventHandler? CloseRequested;
19+
/// <summary>Raised on the UI thread after details have been loaded so the view
20+
/// can (re)populate the inline rich-text blocks.</summary>
21+
public event EventHandler? DetailsLoaded;
1922

2023
public readonly IPackage Package;
2124
public readonly OperationType OperationRole;
@@ -46,24 +49,22 @@ public partial class PackageDetailsViewModel : ObservableObject
4649
[ObservableProperty]
4750
private string _description = CoreTools.Translate("Loading...");
4851

49-
// ── Basic info ─────────────────────────────────────────────────────────────
52+
// ── Basic info (raw values exposed; the view builds the inline rich text) ──
5053
[ObservableProperty]
5154
private string _versionDisplay = "";
5255

5356
[ObservableProperty]
54-
private string _homepageText = CoreTools.Translate("Loading...");
55-
[ObservableProperty]
56-
private bool _hasHomepageUrl;
57+
private Uri? _homepageUrl;
5758

5859
[ObservableProperty]
5960
private string _author = CoreTools.Translate("Loading...");
6061
[ObservableProperty]
6162
private string _publisher = CoreTools.Translate("Loading...");
6263

6364
[ObservableProperty]
64-
private string _licenseText = CoreTools.Translate("Loading...");
65+
private string? _licenseName;
6566
[ObservableProperty]
66-
private bool _hasLicenseUrl;
67+
private Uri? _licenseUrl;
6768

6869
// ── Actions ────────────────────────────────────────────────────────────────
6970
public string MainActionLabel { get; }
@@ -78,9 +79,7 @@ public partial class PackageDetailsViewModel : ObservableObject
7879
public string PackageId { get; }
7980

8081
[ObservableProperty]
81-
private string _manifestText = CoreTools.Translate("Loading...");
82-
[ObservableProperty]
83-
private bool _hasManifestUrl;
82+
private Uri? _manifestUrl;
8483

8584
[ObservableProperty]
8685
private string _installerHashLabel = CoreTools.Translate("Installer SHA256") + ":";
@@ -89,9 +88,7 @@ public partial class PackageDetailsViewModel : ObservableObject
8988
[ObservableProperty]
9089
private string _installerType = CoreTools.Translate("Loading...");
9190
[ObservableProperty]
92-
private string _installerUrlText = CoreTools.Translate("Loading...");
93-
[ObservableProperty]
94-
private bool _hasInstallerUrl;
91+
private Uri? _installerUrl;
9592
[ObservableProperty]
9693
private string _installerSize = "";
9794

@@ -118,60 +115,43 @@ public partial class PackageDetailsViewModel : ObservableObject
118115

119116
[ObservableProperty]
120117
[NotifyPropertyChangedFor(nameof(HasScreenshots))]
121-
[NotifyPropertyChangedFor(nameof(SelectedScreenshot))]
122-
[NotifyPropertyChangedFor(nameof(ScreenshotPageLabel))]
123-
[NotifyPropertyChangedFor(nameof(CanGoNextScreenshot))]
124118
private int _screenshotCount;
125119

126120
public bool HasScreenshots => ScreenshotCount > 0;
127121

128122
[ObservableProperty]
129-
[NotifyPropertyChangedFor(nameof(SelectedScreenshot))]
130-
[NotifyPropertyChangedFor(nameof(ScreenshotPageLabel))]
131-
[NotifyPropertyChangedFor(nameof(CanGoPrevScreenshot))]
132-
[NotifyPropertyChangedFor(nameof(CanGoNextScreenshot))]
133123
private int _selectedScreenshotIndex;
134124

135-
public Bitmap? SelectedScreenshot =>
136-
ScreenshotCount > 0 && SelectedScreenshotIndex < Screenshots.Count
137-
? Screenshots[SelectedScreenshotIndex]
138-
: null;
139-
140-
public string ScreenshotPageLabel =>
141-
ScreenshotCount > 0 ? $"{SelectedScreenshotIndex + 1} / {ScreenshotCount}" : "";
142-
143-
public bool CanGoPrevScreenshot => SelectedScreenshotIndex > 0;
144-
public bool CanGoNextScreenshot => SelectedScreenshotIndex < ScreenshotCount - 1;
145-
146125
// ── Release notes ──────────────────────────────────────────────────────────
147126
[ObservableProperty]
148127
private string _releaseNotes = CoreTools.Translate("Loading...");
149128
[ObservableProperty]
150-
private string _releaseNotesUrlText = CoreTools.Translate("Loading...");
151-
[ObservableProperty]
152-
private bool _hasReleaseNotesUrl;
129+
private Uri? _releaseNotesUrl;
153130

154131
// ── Translated labels ──────────────────────────────────────────────────────
155132
public string LabelVersion { get; }
156-
public string LabelHomepage { get; } = CoreTools.Translate("Homepage") + ":";
157-
public string LabelAuthor { get; } = CoreTools.Translate("Author") + ":";
158-
public string LabelPublisher { get; } = CoreTools.Translate("Publisher") + ":";
159-
public string LabelLicense { get; } = CoreTools.Translate("License") + ":";
160-
public string LabelPackageId { get; } = CoreTools.Translate("Package ID") + ":";
161-
public string LabelManifest { get; } = CoreTools.Translate("Manifest") + ":";
162-
public string LabelInstallerType { get; } = CoreTools.Translate("Installer Type") + ":";
163-
public string LabelInstallerSize { get; } = CoreTools.Translate("Size") + ":";
164-
public string LabelInstallerUrl { get; } = CoreTools.Translate("Installer URL") + ":";
165-
public string LabelUpdateDate { get; } = CoreTools.Translate("Last updated:");
166-
public string LabelReleaseNotesUrl { get; } = CoreTools.Translate("Release notes URL") + ":";
167-
public string LabelOpen { get; } = CoreTools.Translate("Open");
168-
public string LabelClose { get; } = CoreTools.Translate("Close");
169-
public string HeaderDetails { get; } = CoreTools.Translate("Package details");
170-
public string HeaderDeps { get; } = CoreTools.Translate("Dependencies:");
171-
public string HeaderReleaseNotes { get; } = CoreTools.Translate("Release notes");
172-
public string HeaderScreenshots { get; } = CoreTools.Translate("Screenshots");
173-
public string LabelScreenshotContribute { get; } = CoreTools.Translate(
133+
public string LabelHomepage { get; } = CoreTools.Translate("Homepage");
134+
public string LabelAuthor { get; } = CoreTools.Translate("Author");
135+
public string LabelPublisher { get; } = CoreTools.Translate("Publisher");
136+
public string LabelLicense { get; } = CoreTools.Translate("License");
137+
public string LabelSource { get; } = CoreTools.Translate("Package Manager");
138+
public string LabelPackageId { get; } = CoreTools.Translate("Package ID");
139+
public string LabelManifest { get; } = CoreTools.Translate("Manifest");
140+
public string LabelInstallerType { get; } = CoreTools.Translate("Installer Type");
141+
public string LabelInstallerUrl { get; } = CoreTools.Translate("Installer URL");
142+
public string LabelUpdateDate { get; } = CoreTools.Translate("Last updated");
143+
public string LabelDependencies { get; } = CoreTools.Translate("Dependencies");
144+
public string LabelReleaseNotes { get; } = CoreTools.Translate("Release notes");
145+
public string LabelReleaseNotesUrl { get; } = CoreTools.Translate("Release notes URL");
146+
public string LabelDownloadInstaller { get; } = CoreTools.Translate("Download installer");
147+
public string LabelInstallerNotAvailable { get; } = CoreTools.Translate("Installer not available");
148+
public string LabelNotAvailable { get; } = CoreTools.Translate("Not available");
149+
public string LabelNoDependencies { get; } = CoreTools.Translate("No dependencies specified");
150+
public string LabelInstallationOptions { get; } = CoreTools.Translate("Installation options");
151+
public string LabelSave { get; } = CoreTools.Translate("Save");
152+
public string LabelContributorBanner { get; } = CoreTools.Translate(
174153
"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.");
154+
public string LabelContribute { get; } = CoreTools.Translate("Become a contributor");
175155

176156
public PackageDetailsViewModel(IPackage package, OperationType role)
177157
{
@@ -197,7 +177,7 @@ public PackageDetailsViewModel(IPackage package, OperationType role)
197177
if (role == OperationType.Install)
198178
{
199179
MainActionLabel = CoreTools.Translate("Install");
200-
LabelVersion = CoreTools.Translate("Version") + ":";
180+
LabelVersion = CoreTools.Translate("Version");
201181
VersionDisplay = available?.VersionString ?? package.VersionString;
202182
AsAdminLabel = CoreTools.Translate("Install as administrator");
203183
InteractiveLabel = CoreTools.Translate("Interactive installation");
@@ -208,10 +188,10 @@ public PackageDetailsViewModel(IPackage package, OperationType role)
208188
{
209189
MainActionLabel = CoreTools.Translate(
210190
"Update to version {0}", upgradable?.NewVersionString ?? package.NewVersionString);
211-
LabelVersion = CoreTools.Translate("Installed Version") + ":";
191+
LabelVersion = CoreTools.Translate("Installed Version");
212192
VersionDisplay = (upgradable?.VersionString ?? package.VersionString)
213-
+ " \u27a4 "
214-
+ (upgradable?.NewVersionString ?? package.NewVersionString);
193+
+ " "
194+
+ (upgradable?.NewVersionString ?? package.NewVersionString);
215195
AsAdminLabel = CoreTools.Translate("Update as administrator");
216196
InteractiveLabel = CoreTools.Translate("Interactive update");
217197
SkipHashOrRemoveDataLabel = CoreTools.Translate("Skip hash check");
@@ -220,7 +200,7 @@ public PackageDetailsViewModel(IPackage package, OperationType role)
220200
else
221201
{
222202
MainActionLabel = CoreTools.Translate("Uninstall");
223-
LabelVersion = CoreTools.Translate("Installed Version") + ":";
203+
LabelVersion = CoreTools.Translate("Installed Version");
224204
VersionDisplay = installed?.VersionString ?? package.VersionString;
225205
AsAdminLabel = CoreTools.Translate("Uninstall as administrator");
226206
InteractiveLabel = CoreTools.Translate("Interactive uninstall");
@@ -229,20 +209,18 @@ public PackageDetailsViewModel(IPackage package, OperationType role)
229209
}
230210
}
231211

232-
[RelayCommand(CanExecute = nameof(CanGoPrevScreenshot))]
212+
[RelayCommand]
233213
private void PreviousScreenshot()
234214
{
235-
SelectedScreenshotIndex = Math.Max(0, SelectedScreenshotIndex - 1);
236-
PreviousScreenshotCommand.NotifyCanExecuteChanged();
237-
NextScreenshotCommand.NotifyCanExecuteChanged();
215+
if (SelectedScreenshotIndex > 0)
216+
SelectedScreenshotIndex--;
238217
}
239218

240-
[RelayCommand(CanExecute = nameof(CanGoNextScreenshot))]
219+
[RelayCommand]
241220
private void NextScreenshot()
242221
{
243-
SelectedScreenshotIndex = Math.Min(ScreenshotCount - 1, SelectedScreenshotIndex + 1);
244-
PreviousScreenshotCommand.NotifyCanExecuteChanged();
245-
NextScreenshotCommand.NotifyCanExecuteChanged();
222+
if (SelectedScreenshotIndex < ScreenshotCount - 1)
223+
SelectedScreenshotIndex++;
246224
}
247225

248226
public async Task LoadDetailsAsync()
@@ -257,39 +235,28 @@ public async Task LoadDetailsAsync()
257235
IsLoading = false;
258236

259237
Description = details.Description ?? CoreTools.Translate("Not available");
260-
HomepageText = details.HomepageUrl?.ToString() ?? CoreTools.Translate("Not available");
261-
HasHomepageUrl = details.HomepageUrl is not null;
238+
HomepageUrl = details.HomepageUrl;
262239
Author = details.Author ?? CoreTools.Translate("Not available");
263240
Publisher = details.Publisher ?? CoreTools.Translate("Not available");
264241

265-
if (details.License is not null && details.LicenseUrl is not null)
266-
LicenseText = $"{details.License} ({details.LicenseUrl})";
267-
else if (details.License is not null)
268-
LicenseText = details.License;
269-
else if (details.LicenseUrl is not null)
270-
LicenseText = details.LicenseUrl.ToString();
271-
else
272-
LicenseText = CoreTools.Translate("Not available");
273-
HasLicenseUrl = details.LicenseUrl is not null;
242+
LicenseName = details.License;
243+
LicenseUrl = details.LicenseUrl;
274244

275-
ManifestText = details.ManifestUrl?.ToString() ?? CoreTools.Translate("Not available");
276-
HasManifestUrl = details.ManifestUrl is not null;
245+
ManifestUrl = details.ManifestUrl;
277246

278247
if (Package.Manager.Properties.Name.Equals("chocolatey", StringComparison.OrdinalIgnoreCase))
279248
InstallerHashLabel = CoreTools.Translate("Installer SHA512") + ":";
280249

281250
InstallerHash = details.InstallerHash ?? CoreTools.Translate("Not available");
282251
InstallerType = details.InstallerType ?? CoreTools.Translate("Not available");
283-
InstallerUrlText = details.InstallerUrl?.ToString() ?? CoreTools.Translate("Not available");
284-
HasInstallerUrl = details.InstallerUrl is not null;
252+
InstallerUrl = details.InstallerUrl;
285253
InstallerSize = details.InstallerSize > 0
286254
? CoreTools.FormatAsSize(details.InstallerSize, 2)
287255
: CoreTools.Translate("Unknown size");
288256
UpdateDate = details.UpdateDate ?? CoreTools.Translate("Not available");
289257

290258
ReleaseNotes = details.ReleaseNotes ?? CoreTools.Translate("Not available");
291-
ReleaseNotesUrlText = details.ReleaseNotesUrl?.ToString() ?? CoreTools.Translate("Not available");
292-
HasReleaseNotesUrl = details.ReleaseNotesUrl is not null;
259+
ReleaseNotesUrl = details.ReleaseNotesUrl;
293260

294261
if (!CanListDependencies)
295262
{
@@ -313,23 +280,41 @@ public async Task LoadDetailsAsync()
313280
foreach (var tag in details.Tags)
314281
Tags.Add(tag);
315282
TagCount = Tags.Count;
283+
284+
DetailsLoaded?.Invoke(this, EventArgs.Empty);
316285
}
317286

318287
private async Task LoadIconAsync()
319288
{
320289
try
321290
{
322-
var iconUrl = await Task.Run(Package.GetIconUrl);
323-
if (iconUrl is not null)
291+
var uri = await Task.Run(Package.GetIconUrlIfAny);
292+
if (uri is not null)
324293
{
325-
using var http = new HttpClient(CoreTools.GenericHttpClientParameters);
326-
var bytes = await http.GetByteArrayAsync(iconUrl);
327-
using var ms = new MemoryStream(bytes);
328-
PackageIcon = new Bitmap(ms);
329-
return;
294+
Bitmap? bitmap = null;
295+
if (uri.IsFile)
296+
{
297+
bitmap = await Task.Run(() => new Bitmap(uri.LocalPath));
298+
}
299+
else if (uri.Scheme is "http" or "https")
300+
{
301+
using var http = new HttpClient(CoreTools.GenericHttpClientParameters);
302+
var bytes = await http.GetByteArrayAsync(uri);
303+
using var ms = new MemoryStream(bytes);
304+
bitmap = new Bitmap(ms);
305+
}
306+
307+
if (bitmap is not null)
308+
{
309+
PackageIcon = bitmap;
310+
return;
311+
}
330312
}
331313
}
332-
catch { /* icon is optional */ }
314+
catch (Exception ex)
315+
{
316+
Logger.Warn($"[PackageDetailsViewModel] Failed to load icon: {ex.Message}");
317+
}
333318

334319
try
335320
{
@@ -359,8 +344,6 @@ await Dispatcher.UIThread.InvokeAsync(() =>
359344
{
360345
Screenshots.Add(bmp);
361346
ScreenshotCount = Screenshots.Count;
362-
PreviousScreenshotCommand.NotifyCanExecuteChanged();
363-
NextScreenshotCommand.NotifyCanExecuteChanged();
364347
});
365348
}
366349
catch { /* skip failed screenshots */ }
@@ -373,7 +356,7 @@ await Dispatcher.UIThread.InvokeAsync(() =>
373356
}
374357

375358
[RelayCommand]
376-
private static void OpenUrl(string? url)
359+
public static void OpenUrl(string? url)
377360
{
378361
if (string.IsNullOrEmpty(url) || !url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
379362
return;
@@ -391,7 +374,7 @@ public class DependencyViewModel
391374

392375
public DependencyViewModel(IPackageDetails.Dependency dep)
393376
{
394-
var text = $" \u2022 {dep.Name}";
377+
var text = $" {dep.Name}";
395378
if (!string.IsNullOrEmpty(dep.Version))
396379
text += $" v{dep.Version}";
397380
text += dep.Mandatory

0 commit comments

Comments
 (0)