TitleUpdateViewModel.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. using Avalonia.Collections;
  2. using Avalonia.Controls;
  3. using Avalonia.Controls.ApplicationLifetimes;
  4. using Avalonia.Threading;
  5. using LibHac.Common;
  6. using LibHac.Fs;
  7. using LibHac.Fs.Fsa;
  8. using LibHac.FsSystem;
  9. using LibHac.Ns;
  10. using LibHac.Tools.FsSystem;
  11. using LibHac.Tools.FsSystem.NcaUtils;
  12. using Ryujinx.Ava.Common.Locale;
  13. using Ryujinx.Ava.UI.Helpers;
  14. using Ryujinx.Ava.UI.Models;
  15. using Ryujinx.Common.Configuration;
  16. using Ryujinx.Common.Logging;
  17. using Ryujinx.Common.Utilities;
  18. using Ryujinx.HLE.FileSystem;
  19. using Ryujinx.HLE.HOS;
  20. using System;
  21. using System.Collections.Generic;
  22. using System.IO;
  23. using System.Linq;
  24. using SpanHelpers = LibHac.Common.SpanHelpers;
  25. using Path = System.IO.Path;
  26. namespace Ryujinx.Ava.UI.ViewModels;
  27. public class TitleUpdateViewModel : BaseModel
  28. {
  29. public TitleUpdateMetadata _titleUpdateWindowData;
  30. public readonly string _titleUpdateJsonPath;
  31. private VirtualFileSystem _virtualFileSystem { get; }
  32. private ulong _titleId { get; }
  33. private string _titleName { get; }
  34. private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
  35. private AvaloniaList<object> _views = new();
  36. private object _selectedUpdate;
  37. public AvaloniaList<TitleUpdateModel> TitleUpdates
  38. {
  39. get => _titleUpdates;
  40. set
  41. {
  42. _titleUpdates = value;
  43. OnPropertyChanged();
  44. }
  45. }
  46. public AvaloniaList<object> Views
  47. {
  48. get => _views;
  49. set
  50. {
  51. _views = value;
  52. OnPropertyChanged();
  53. }
  54. }
  55. public object SelectedUpdate
  56. {
  57. get => _selectedUpdate;
  58. set
  59. {
  60. _selectedUpdate = value;
  61. OnPropertyChanged();
  62. }
  63. }
  64. public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
  65. {
  66. _virtualFileSystem = virtualFileSystem;
  67. _titleId = titleId;
  68. _titleName = titleName;
  69. _titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
  70. try
  71. {
  72. _titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
  73. }
  74. catch
  75. {
  76. Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {_titleId} at {_titleUpdateJsonPath}");
  77. _titleUpdateWindowData = new TitleUpdateMetadata
  78. {
  79. Selected = "",
  80. Paths = new List<string>()
  81. };
  82. }
  83. LoadUpdates();
  84. }
  85. private void LoadUpdates()
  86. {
  87. foreach (string path in _titleUpdateWindowData.Paths)
  88. {
  89. AddUpdate(path);
  90. }
  91. TitleUpdateModel selected = TitleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected, null);
  92. SelectedUpdate = selected;
  93. SortUpdates();
  94. }
  95. public void SortUpdates()
  96. {
  97. var list = TitleUpdates.ToList();
  98. list.Sort((first, second) =>
  99. {
  100. if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
  101. {
  102. return -1;
  103. }
  104. else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
  105. {
  106. return 1;
  107. }
  108. return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
  109. });
  110. Views.Clear();
  111. Views.Add(new BaseModel());
  112. Views.AddRange(list);
  113. if (SelectedUpdate == null)
  114. {
  115. SelectedUpdate = Views[0];
  116. }
  117. else if (!TitleUpdates.Contains(SelectedUpdate))
  118. {
  119. if (Views.Count > 1)
  120. {
  121. SelectedUpdate = Views[1];
  122. }
  123. else
  124. {
  125. SelectedUpdate = Views[0];
  126. }
  127. }
  128. }
  129. private void AddUpdate(string path)
  130. {
  131. if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
  132. {
  133. using FileStream file = new(path, FileMode.Open, FileAccess.Read);
  134. try
  135. {
  136. (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
  137. if (controlNca != null && patchNca != null)
  138. {
  139. ApplicationControlProperty controlData = new();
  140. using UniqueRef<IFile> nacpFile = new();
  141. controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
  142. nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
  143. TitleUpdates.Add(new TitleUpdateModel(controlData, path));
  144. }
  145. else
  146. {
  147. Dispatcher.UIThread.Post(async () =>
  148. {
  149. await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
  150. });
  151. }
  152. }
  153. catch (Exception ex)
  154. {
  155. Dispatcher.UIThread.Post(async () =>
  156. {
  157. await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogDlcLoadNcaErrorMessage], ex.Message, path));
  158. });
  159. }
  160. }
  161. }
  162. public void RemoveUpdate(TitleUpdateModel update)
  163. {
  164. TitleUpdates.Remove(update);
  165. SortUpdates();
  166. }
  167. public async void Add()
  168. {
  169. OpenFileDialog dialog = new()
  170. {
  171. Title = LocaleManager.Instance[LocaleKeys.SelectUpdateDialogTitle],
  172. AllowMultiple = true
  173. };
  174. dialog.Filters.Add(new FileDialogFilter
  175. {
  176. Name = "NSP",
  177. Extensions = { "nsp" }
  178. });
  179. if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  180. {
  181. string[] files = await dialog.ShowAsync(desktop.MainWindow);
  182. if (files != null)
  183. {
  184. foreach (string file in files)
  185. {
  186. AddUpdate(file);
  187. }
  188. }
  189. }
  190. SortUpdates();
  191. }
  192. }