@@ -16,6 +16,9 @@ namespace UniGetUI.Avalonia.ViewModels;
1616public 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