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 }