TitleUpdateViewModel.cs 7.1 KB

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