ModManagerViewModel.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. using Avalonia;
  2. using Avalonia.Collections;
  3. using Avalonia.Controls.ApplicationLifetimes;
  4. using Avalonia.Platform.Storage;
  5. using Avalonia.Threading;
  6. using DynamicData;
  7. using Gommon;
  8. using Ryujinx.Ava.Common.Locale;
  9. using Ryujinx.Ava.UI.Helpers;
  10. using Ryujinx.Ava.UI.Models;
  11. using Ryujinx.Common.Configuration;
  12. using Ryujinx.Common.Logging;
  13. using Ryujinx.Common.Utilities;
  14. using Ryujinx.HLE.HOS;
  15. using System;
  16. using System.IO;
  17. using System.Linq;
  18. namespace Ryujinx.Ava.UI.ViewModels
  19. {
  20. public class ModManagerViewModel : BaseModel
  21. {
  22. private readonly string _modJsonPath;
  23. private AvaloniaList<ModModel> _mods = new();
  24. private AvaloniaList<ModModel> _views = new();
  25. private AvaloniaList<ModModel> _selectedMods = new();
  26. private string _search;
  27. private readonly ulong _applicationId;
  28. private readonly IStorageProvider _storageProvider;
  29. private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
  30. public AvaloniaList<ModModel> Mods
  31. {
  32. get => _mods;
  33. set
  34. {
  35. _mods = value;
  36. OnPropertyChanged();
  37. OnPropertyChanged(nameof(ModCount));
  38. Sort();
  39. }
  40. }
  41. public AvaloniaList<ModModel> Views
  42. {
  43. get => _views;
  44. set
  45. {
  46. _views = value;
  47. OnPropertyChanged();
  48. }
  49. }
  50. public AvaloniaList<ModModel> SelectedMods
  51. {
  52. get => _selectedMods;
  53. set
  54. {
  55. _selectedMods = value;
  56. OnPropertyChanged();
  57. }
  58. }
  59. public string Search
  60. {
  61. get => _search;
  62. set
  63. {
  64. _search = value;
  65. OnPropertyChanged();
  66. Sort();
  67. }
  68. }
  69. public string ModCount
  70. {
  71. get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count);
  72. }
  73. public ModManagerViewModel(ulong applicationId)
  74. {
  75. _applicationId = applicationId;
  76. _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationId.ToString("x16"), "mods.json");
  77. if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  78. {
  79. _storageProvider = desktop.MainWindow.StorageProvider;
  80. }
  81. LoadMods(applicationId);
  82. }
  83. private void LoadMods(ulong applicationId)
  84. {
  85. Mods.Clear();
  86. SelectedMods.Clear();
  87. string[] modsBasePaths = [ModLoader.GetSdModsBasePath(), ModLoader.GetModsBasePath()];
  88. foreach (var path in modsBasePaths)
  89. {
  90. var inSd = path == ModLoader.GetSdModsBasePath();
  91. var modCache = new ModLoader.ModCache();
  92. ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(path, "contents")), applicationId);
  93. foreach (var mod in modCache.RomfsDirs)
  94. {
  95. var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled, inSd);
  96. if (Mods.All(x => x.Path != mod.Path.Parent.FullName))
  97. {
  98. Mods.Add(modModel);
  99. }
  100. }
  101. foreach (var mod in modCache.RomfsContainers)
  102. {
  103. Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled, inSd));
  104. }
  105. foreach (var mod in modCache.ExefsDirs)
  106. {
  107. var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled, inSd);
  108. if (Mods.All(x => x.Path != mod.Path.Parent.FullName))
  109. {
  110. Mods.Add(modModel);
  111. }
  112. }
  113. foreach (var mod in modCache.ExefsContainers)
  114. {
  115. Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled, inSd));
  116. }
  117. }
  118. Sort();
  119. }
  120. public void Sort()
  121. {
  122. Mods.AsObservableChangeSet()
  123. .Filter(Filter)
  124. .Bind(out var view).AsObservableList();
  125. _views.Clear();
  126. _views.AddRange(view);
  127. SelectedMods = new(Views.Where(x => x.Enabled));
  128. OnPropertyChanged(nameof(ModCount));
  129. OnPropertyChanged(nameof(Views));
  130. OnPropertyChanged(nameof(SelectedMods));
  131. }
  132. private bool Filter(object arg)
  133. {
  134. if (arg is ModModel content)
  135. {
  136. return string.IsNullOrWhiteSpace(_search) || content.Name.ToLower().Contains(_search.ToLower());
  137. }
  138. return false;
  139. }
  140. public void Save()
  141. {
  142. ModMetadata modData = new();
  143. foreach (ModModel mod in Mods)
  144. {
  145. modData.Mods.Add(new Mod
  146. {
  147. Name = mod.Name,
  148. Path = mod.Path,
  149. Enabled = SelectedMods.Contains(mod),
  150. });
  151. }
  152. JsonHelper.SerializeToFile(_modJsonPath, modData, _serializerContext.ModMetadata);
  153. }
  154. public void Delete(ModModel model, bool removeFromList = true)
  155. {
  156. var isSubdir = true;
  157. var pathToDelete = model.Path;
  158. var basePath = model.InSd ? ModLoader.GetSdModsBasePath() : ModLoader.GetModsBasePath();
  159. var modsDir = ModLoader.GetApplicationDir(basePath, _applicationId.ToString("x16"));
  160. if (new DirectoryInfo(model.Path).Parent?.FullName == modsDir)
  161. {
  162. isSubdir = false;
  163. }
  164. if (isSubdir)
  165. {
  166. var parentDir = String.Empty;
  167. foreach (var dir in Directory.GetDirectories(modsDir, "*", SearchOption.TopDirectoryOnly))
  168. {
  169. if (Directory.GetDirectories(dir, "*", SearchOption.AllDirectories).Contains(model.Path))
  170. {
  171. parentDir = dir;
  172. break;
  173. }
  174. }
  175. if (parentDir == String.Empty)
  176. {
  177. Dispatcher.UIThread.Post(async () =>
  178. {
  179. await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(
  180. LocaleKeys.DialogModDeleteNoParentMessage,
  181. model.Path));
  182. });
  183. return;
  184. }
  185. }
  186. Logger.Info?.Print(LogClass.Application, $"Deleting mod at \"{pathToDelete}\"");
  187. Directory.Delete(pathToDelete, true);
  188. if (removeFromList)
  189. {
  190. Mods.Remove(model);
  191. OnPropertyChanged(nameof(ModCount));
  192. }
  193. Sort();
  194. }
  195. private void AddMod(DirectoryInfo directory)
  196. {
  197. string[] directories;
  198. try
  199. {
  200. directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories);
  201. }
  202. catch (Exception exception)
  203. {
  204. Dispatcher.UIThread.Post(async () =>
  205. {
  206. await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(
  207. LocaleKeys.DialogLoadFileErrorMessage,
  208. exception.ToString(),
  209. directory));
  210. });
  211. return;
  212. }
  213. var destinationDir = ModLoader.GetApplicationDir(ModLoader.GetSdModsBasePath(), _applicationId.ToString("x16"));
  214. // TODO: More robust checking for valid mod folders
  215. var isDirectoryValid = true;
  216. if (directories.Length == 0)
  217. {
  218. isDirectoryValid = false;
  219. }
  220. if (!isDirectoryValid)
  221. {
  222. Dispatcher.UIThread.Post(async () =>
  223. {
  224. await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogModInvalidMessage]);
  225. });
  226. return;
  227. }
  228. foreach (var dir in directories)
  229. {
  230. string dirToCreate = dir.Replace(directory.Parent.ToString(), destinationDir);
  231. // Mod already exists
  232. if (Directory.Exists(dirToCreate))
  233. {
  234. Dispatcher.UIThread.Post(async () =>
  235. {
  236. await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(
  237. LocaleKeys.DialogLoadFileErrorMessage,
  238. LocaleManager.Instance[LocaleKeys.DialogModAlreadyExistsMessage],
  239. dirToCreate));
  240. });
  241. return;
  242. }
  243. Directory.CreateDirectory(dirToCreate);
  244. }
  245. var files = Directory.GetFiles(directory.ToString(), "*", SearchOption.AllDirectories);
  246. foreach (var file in files)
  247. {
  248. File.Copy(file, file.Replace(directory.Parent.ToString(), destinationDir), true);
  249. }
  250. LoadMods(_applicationId);
  251. }
  252. public async void Add()
  253. {
  254. var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
  255. {
  256. Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle],
  257. AllowMultiple = true,
  258. });
  259. foreach (var folder in result)
  260. {
  261. AddMod(new DirectoryInfo(folder.Path.LocalPath));
  262. }
  263. }
  264. public void DeleteAll()
  265. {
  266. Mods.ForEach(it => Delete(it, false));
  267. Mods.Clear();
  268. OnPropertyChanged(nameof(ModCount));
  269. Sort();
  270. }
  271. public void EnableAll()
  272. {
  273. SelectedMods = new(Mods);
  274. }
  275. public void DisableAll()
  276. {
  277. SelectedMods.Clear();
  278. }
  279. }
  280. }