TitleUpdateViewModel.cs 7.1 KB

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