TitleUpdateViewModel.cs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. using Avalonia.Collections;
  2. using Avalonia.Controls.ApplicationLifetimes;
  3. using Avalonia.Platform.Storage;
  4. using Avalonia.Threading;
  5. using FluentAvalonia.UI.Controls;
  6. using Ryujinx.Ava.Common.Locale;
  7. using Ryujinx.Ava.UI.Helpers;
  8. using Ryujinx.HLE.FileSystem;
  9. using Ryujinx.UI.App.Common;
  10. using Ryujinx.UI.Common.Models;
  11. using System.Collections.Generic;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Threading.Tasks;
  15. using Application = Avalonia.Application;
  16. namespace Ryujinx.Ava.UI.ViewModels
  17. {
  18. public record TitleUpdateViewNoUpdateSentinal();
  19. public class TitleUpdateViewModel : BaseModel
  20. {
  21. private ApplicationLibrary ApplicationLibrary { get; }
  22. private ApplicationData ApplicationData { get; }
  23. private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
  24. private AvaloniaList<object> _views = new();
  25. private object _selectedUpdate = new TitleUpdateViewNoUpdateSentinal();
  26. private bool _showBundledContentNotice = false;
  27. public AvaloniaList<TitleUpdateModel> TitleUpdates
  28. {
  29. get => _titleUpdates;
  30. set
  31. {
  32. _titleUpdates = value;
  33. OnPropertyChanged();
  34. }
  35. }
  36. public AvaloniaList<object> Views
  37. {
  38. get => _views;
  39. set
  40. {
  41. _views = value;
  42. OnPropertyChanged();
  43. }
  44. }
  45. public object SelectedUpdate
  46. {
  47. get => _selectedUpdate;
  48. set
  49. {
  50. _selectedUpdate = value;
  51. OnPropertyChanged();
  52. }
  53. }
  54. public bool ShowBundledContentNotice
  55. {
  56. get => _showBundledContentNotice;
  57. set
  58. {
  59. _showBundledContentNotice = value;
  60. OnPropertyChanged();
  61. }
  62. }
  63. public IStorageProvider StorageProvider;
  64. public TitleUpdateViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData)
  65. {
  66. ApplicationLibrary = applicationLibrary;
  67. ApplicationData = applicationData;
  68. if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  69. {
  70. StorageProvider = desktop.MainWindow.StorageProvider;
  71. }
  72. LoadUpdates();
  73. }
  74. private void LoadUpdates()
  75. {
  76. var updates = ApplicationLibrary.TitleUpdates.Items
  77. .Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
  78. bool hasBundledContent = false;
  79. SelectedUpdate = new TitleUpdateViewNoUpdateSentinal();
  80. foreach ((TitleUpdateModel update, bool isSelected) in updates)
  81. {
  82. TitleUpdates.Add(update);
  83. hasBundledContent = hasBundledContent || update.IsBundled;
  84. if (isSelected)
  85. {
  86. SelectedUpdate = update;
  87. }
  88. }
  89. ShowBundledContentNotice = hasBundledContent;
  90. SortUpdates();
  91. }
  92. public void SortUpdates()
  93. {
  94. var sortedUpdates = TitleUpdates.OrderByDescending(update => update.Version);
  95. // NOTE(jpr): this works around a bug where calling Views.Clear also clears SelectedUpdate for
  96. // some reason. so we save the item here and restore it after
  97. var selected = SelectedUpdate;
  98. Views.Clear();
  99. Views.Add(new TitleUpdateViewNoUpdateSentinal());
  100. Views.AddRange(sortedUpdates);
  101. SelectedUpdate = selected;
  102. if (SelectedUpdate is TitleUpdateViewNoUpdateSentinal)
  103. {
  104. SelectedUpdate = Views[0];
  105. }
  106. // this is mainly to handle a scenario where the user removes the selected update
  107. else if (!TitleUpdates.Contains((TitleUpdateModel)SelectedUpdate))
  108. {
  109. SelectedUpdate = Views.Count > 1 ? Views[1] : Views[0];
  110. }
  111. }
  112. private bool AddUpdate(string path, out int numUpdatesAdded)
  113. {
  114. numUpdatesAdded = 0;
  115. if (!File.Exists(path))
  116. {
  117. return false;
  118. }
  119. if (!ApplicationLibrary.TryGetTitleUpdatesFromFile(path, out var updates))
  120. {
  121. return false;
  122. }
  123. var updatesForThisGame = updates.Where(it => it.TitleIdBase == ApplicationData.Id).ToList();
  124. if (updatesForThisGame.Count == 0)
  125. {
  126. return false;
  127. }
  128. foreach (var update in updatesForThisGame)
  129. {
  130. if (!TitleUpdates.Contains(update))
  131. {
  132. TitleUpdates.Add(update);
  133. SelectedUpdate = update;
  134. numUpdatesAdded++;
  135. }
  136. }
  137. if (numUpdatesAdded > 0)
  138. {
  139. SortUpdates();
  140. }
  141. return true;
  142. }
  143. public void RemoveUpdate(TitleUpdateModel update)
  144. {
  145. if (!update.IsBundled)
  146. {
  147. TitleUpdates.Remove(update);
  148. }
  149. else if (update == SelectedUpdate as TitleUpdateModel)
  150. {
  151. SelectedUpdate = new TitleUpdateViewNoUpdateSentinal();
  152. }
  153. SortUpdates();
  154. }
  155. public async Task Add()
  156. {
  157. var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
  158. {
  159. AllowMultiple = true,
  160. FileTypeFilter = new List<FilePickerFileType>
  161. {
  162. new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
  163. {
  164. Patterns = new[] { "*.nsp" },
  165. AppleUniformTypeIdentifiers = new[] { "com.ryujinx.nsp" },
  166. MimeTypes = new[] { "application/x-nx-nsp" },
  167. },
  168. },
  169. });
  170. var totalUpdatesAdded = 0;
  171. foreach (var file in result)
  172. {
  173. if (!AddUpdate(file.Path.LocalPath, out var newUpdatesAdded))
  174. {
  175. await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]);
  176. }
  177. totalUpdatesAdded += newUpdatesAdded;
  178. }
  179. if (totalUpdatesAdded > 0)
  180. {
  181. await ShowNewUpdatesAddedDialog(totalUpdatesAdded);
  182. }
  183. }
  184. public void Save()
  185. {
  186. var updates = TitleUpdates.Select(it => (it, it == SelectedUpdate as TitleUpdateModel)).ToList();
  187. ApplicationLibrary.SaveTitleUpdatesForGame(ApplicationData, updates);
  188. }
  189. private Task ShowNewUpdatesAddedDialog(int numAdded)
  190. {
  191. var msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded);
  192. return Dispatcher.UIThread.InvokeAsync(async () =>
  193. {
  194. await ContentDialogHelper.ShowTextDialog(LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, "", "", "", LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark);
  195. });
  196. }
  197. }
  198. }