using Avalonia.Collections; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using DynamicData; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Models; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.Utilities.AppLibrary; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.ViewModels { public partial class DownloadableContentManagerViewModel : BaseModel { private readonly ApplicationLibrary _applicationLibrary; private AvaloniaList _downloadableContents = []; [ObservableProperty] private AvaloniaList _selectedDownloadableContents = []; [ObservableProperty] private AvaloniaList _views = []; [ObservableProperty] private bool _showBundledContentNotice = false; private string _search; private readonly ApplicationData _applicationData; private readonly IStorageProvider _storageProvider; public AvaloniaList DownloadableContents { get => _downloadableContents; set { _downloadableContents = value; OnPropertyChanged(); OnPropertyChanged(nameof(UpdateCount)); Sort(); } } public string Search { get => _search; set { _search = value; OnPropertyChanged(); Sort(); } } public string UpdateCount { get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count); } public DownloadableContentManagerViewModel(ApplicationLibrary applicationLibrary, ApplicationData applicationData) { _applicationLibrary = applicationLibrary; _applicationData = applicationData; _storageProvider = RyujinxApp.MainWindow.StorageProvider; LoadDownloadableContents(); } private void LoadDownloadableContents() { (DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id); bool hasBundledContent = false; foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs) { DownloadableContents.Add(dlc); hasBundledContent = hasBundledContent || dlc.IsBundled; if (isEnabled) { SelectedDownloadableContents.Add(dlc); } OnPropertyChanged(nameof(UpdateCount)); } ShowBundledContentNotice = hasBundledContent; Sort(); } public void Sort() { DownloadableContents // Sort bundled last .OrderBy(it => it.IsBundled ? 0 : 1) .ThenBy(it => it.TitleId) .AsObservableChangeSet() .Filter(Filter) .Bind(out ReadOnlyObservableCollection view).AsObservableList(); // NOTE(jpr): this works around a bug where calling _views.Clear also clears SelectedDownloadableContents for // some reason. so we save the items here and add them back after DownloadableContentModel[] items = SelectedDownloadableContents.ToArray(); Views.Clear(); Views.AddRange(view); foreach (DownloadableContentModel item in items) { SelectedDownloadableContents.ReplaceOrAdd(item, item); } OnPropertyChanged(nameof(Views)); } private bool Filter(T arg) { if (arg is DownloadableContentModel content) { return string.IsNullOrWhiteSpace(_search) || content.FileName.ToLower().Contains(_search.ToLower()) || content.TitleIdStr.ToLower().Contains(_search.ToLower()); } return false; } public async void Add() { IReadOnlyList result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle], AllowMultiple = true, FileTypeFilter = new List { new("NSP") { Patterns = ["*.nsp"], AppleUniformTypeIdentifiers = ["com.ryujinx.nsp"], MimeTypes = ["application/x-nx-nsp"], }, }, }); int totalDlcAdded = 0; foreach (IStorageFile file in result) { if (!AddDownloadableContent(file.Path.LocalPath, out int newDlcAdded)) { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]); } totalDlcAdded += newDlcAdded; } if (totalDlcAdded > 0) { await ShowNewDlcAddedDialog(totalDlcAdded); } } private bool AddDownloadableContent(string path, out int numDlcAdded) { numDlcAdded = 0; if (!File.Exists(path)) { return false; } if (!_applicationLibrary.TryGetDownloadableContentFromFile(path, out List dlcs) || dlcs.Count == 0) { return false; } List dlcsForThisGame = dlcs.Where(it => it.TitleIdBase == _applicationData.IdBase).ToList(); if (dlcsForThisGame.Count == 0) { return false; } foreach (DownloadableContentModel dlc in dlcsForThisGame) { if (!DownloadableContents.Contains(dlc)) { DownloadableContents.Add(dlc); SelectedDownloadableContents.ReplaceOrAdd(dlc, dlc); numDlcAdded++; } } if (numDlcAdded > 0) { OnPropertyChanged(nameof(UpdateCount)); Sort(); } return true; } public void Remove(DownloadableContentModel model) { SelectedDownloadableContents.Remove(model); if (!model.IsBundled) { DownloadableContents.Remove(model); OnPropertyChanged(nameof(UpdateCount)); Sort(); } } public void RemoveAll() { SelectedDownloadableContents.Clear(); DownloadableContents.RemoveMany(DownloadableContents.Where(it => !it.IsBundled)); OnPropertyChanged(nameof(UpdateCount)); Sort(); } public void EnableAll() { SelectedDownloadableContents.Clear(); SelectedDownloadableContents.AddRange(DownloadableContents); } public void DisableAll() { SelectedDownloadableContents.Clear(); } public void Enable(DownloadableContentModel model) { SelectedDownloadableContents.ReplaceOrAdd(model, model); } public void Disable(DownloadableContentModel model) { SelectedDownloadableContents.Remove(model); } public void Save() { List<(DownloadableContentModel it, bool)> dlcs = DownloadableContents.Select(it => (it, SelectedDownloadableContents.Contains(it))).ToList(); _applicationLibrary.SaveDownloadableContentsForGame(_applicationData, dlcs); } private Task ShowNewDlcAddedDialog(int numAdded) { string msg = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowDlcAddedMessage], numAdded); return Dispatcher.UIThread.InvokeAsync(async () => { await ContentDialogHelper.ShowTextDialog( LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle], msg, string.Empty, string.Empty, string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], (int)Symbol.Checkmark); }); } } }