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