diff --git a/assets/locales.json b/assets/locales.json index 2c4b4ec7a..07be6e7b4 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -3222,6 +3222,31 @@ "zh_TW": "正在修剪 XCI 檔案 '{0}'" } }, + { + "ID": "StatusBarXCIFileScanning", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Scanning XCI Files...", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "LinuxVmMaxMapCountDialogTitle", "Translations": { @@ -21297,6 +21322,31 @@ "zh_TW": "節省的儲存空間" } }, + { + "ID": "XCITrimmerSortStatus", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Status", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "XCITrimmerTrim", "Translations": { diff --git a/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs index 5b233d1e0..536415263 100644 --- a/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs +++ b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs @@ -142,7 +142,6 @@ namespace Ryujinx.Common.Utilities { Log = log; Filename = path; - ReadHeader(); } public void CheckFreeSpace(CancellationToken? cancelToken = null) @@ -435,7 +434,6 @@ namespace Ryujinx.Common.Utilities _binaryReader.Close(); _binaryReader = null; _fileStream = null; - GC.Collect(); } private void ReadHeader() diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index e6648fd7e..68e9961c2 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -47,10 +47,18 @@ namespace Ryujinx.Ava.Common.Locale private void Load() { - string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ? - ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_'); - - LoadLanguage(localeLanguageCode); + try + { + string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) + ? ConfigurationState.Instance.UI.LanguageCode.Value + : CultureInfo.CurrentCulture.Name.Replace('-', '_'); + + LoadLanguage(localeLanguageCode); + } + catch + { + LoadLanguage(DefaultLanguageCode); + } // Save whatever we ended up with. if (Program.PreviewerDetached) diff --git a/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileStatusConverter.cs b/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileStatusConverter.cs index c3fb1fe95..9d99edd8b 100644 --- a/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileStatusConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/XCITrimmerFileStatusConverter.cs @@ -25,19 +25,26 @@ namespace Ryujinx.Ava.UI.Helpers return null; } - if (value is not XCITrimmerFileModel app) + if (value is not XCITrimmerFileModel model) { return null; } - return app.PercentageProgress != null ? String.Empty : - app.ProcessingOutcome != OperationOutcome.Successful && app.ProcessingOutcome != OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] : - app.Trimmable & app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] : - app.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] : - app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] : + return model.PercentageProgress != null ? String.Empty : + model.ProcessingOutcome != OperationOutcome.Successful && model.ProcessingOutcome != OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] : + model.Trimmable & model.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] : + model.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] : + model.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] : String.Empty; } + public static string From(XCITrimmerFileModel model) + { + if (model == null) return String.Empty; + + return (string)Instance.Convert(model, typeof(string), null, CultureInfo.CurrentUICulture) ?? String.Empty; + } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); diff --git a/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs b/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs index 1965fee69..404c77f34 100644 --- a/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/CompatibilityViewModel.cs @@ -4,12 +4,15 @@ using Ryujinx.Ava.Systems.AppLibrary; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Ryujinx.Ava.UI.ViewModels { public class CompatibilityViewModel : BaseModel, IDisposable { private readonly ApplicationLibrary _appLibrary; + private string _search; + private Timer _searchTimer; private IEnumerable _currentEntries = CompatibilityDatabase.Entries; private string[] _ownedGameTitleIds = []; @@ -54,7 +57,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public void Search(string searchTerm) + private void UpdateSearch(string searchTerm) { if (string.IsNullOrEmpty(searchTerm)) { @@ -64,6 +67,9 @@ namespace Ryujinx.Ava.UI.ViewModels SetEntries(CompatibilityDatabase.Entries.Where(x => x.GameName.ContainsIgnoreCase(searchTerm) + || x.FormattedIssueLabels.ContainsIgnoreCase(searchTerm) + || x.LocalizedStatus.ContainsIgnoreCase(searchTerm) + || x.LocalizedStatusDescription.ContainsIgnoreCase(searchTerm) || x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm)))); } @@ -74,5 +80,21 @@ namespace Ryujinx.Ava.UI.ViewModels #pragma warning restore MVVMTK0034 OnPropertyChanged(nameof(CurrentEntries)); } + + public string Search + { + get => _search; + set + { + _search = value; + _searchTimer?.Dispose(); + _searchTimer = new Timer(_ => + { + UpdateSearch(_search); + _searchTimer.Dispose(); + _searchTimer = null; + }, null, 250, 0); + } + } } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 020d96794..1e193a156 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1873,7 +1873,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async void ProcessTrimResult(String filename, XCIFileTrimmer.OperationOutcome operationOutcome) + public async Task ProcessTrimResult(String filename, XCIFileTrimmer.OperationOutcome operationOutcome) { string notifyUser = operationOutcome.ToLocalisedText(); diff --git a/src/Ryujinx/UI/ViewModels/XciTrimmerViewModel.cs b/src/Ryujinx/UI/ViewModels/XciTrimmerViewModel.cs index 2085ffe26..99c4e0bfd 100644 --- a/src/Ryujinx/UI/ViewModels/XciTrimmerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/XciTrimmerViewModel.cs @@ -8,6 +8,7 @@ using Ryujinx.Ava.Common.Models; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.Systems.AppLibrary; using Ryujinx.Common.Utilities; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -28,6 +29,7 @@ namespace Ryujinx.Ava.UI.ViewModels public enum SortField { Name, + Status, Saved } @@ -42,6 +44,7 @@ namespace Ryujinx.Ava.UI.ViewModels private MainWindowViewModel _mainWindowViewModel; private CancellationTokenSource _cancellationTokenSource; private string _search; + private Timer _searchTimer; private ProcessingMode _processingMode; private SortField _sortField = SortField.Name; private bool _sortAscending = true; @@ -55,11 +58,39 @@ namespace Ryujinx.Ava.UI.ViewModels private void LoadXCIApplications() { - IEnumerable apps = ApplicationLibrary.Applications.Items - .Where(app => app.FileExtension == _FileExtXCI); + try + { + _mainWindowViewModel.StatusBarProgressStatusText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileScanning); + _mainWindowViewModel.StatusBarProgressStatusVisible = true; + _mainWindowViewModel.StatusBarProgressMaximum = 1; + _mainWindowViewModel.StatusBarProgressValue = 0; + _mainWindowViewModel.StatusBarVisible = true; - foreach (ApplicationData xciApp in apps) - AddOrUpdateXCITrimmerFile(CreateXCITrimmerFile(xciApp.Path)); + IEnumerable apps = ApplicationLibrary.Applications.Items + .Where(app => app.FileExtension == _FileExtXCI).ToArray(); + + _mainWindowViewModel.StatusBarProgressMaximum = apps.Count(); + _mainWindowViewModel.StatusBarProgressValue = 0; + + int appsProcessed = 0; + foreach (ApplicationData xciApp in apps) + { + AddOrUpdateXCITrimmerFile(CreateXCITrimmerFile(xciApp.Path)); + + if (appsProcessed % 50 == 0) + { + _mainWindowViewModel.StatusBarProgressValue = appsProcessed; + Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Render).Wait(); + } + appsProcessed++; + } + } + finally + { + _mainWindowViewModel.StatusBarProgressStatusVisible = false; + _mainWindowViewModel.StatusBarProgressStatusText = string.Empty; + _mainWindowViewModel.StatusBarVisible = false; + } ApplicationsChanged(); } @@ -245,8 +276,9 @@ namespace Ryujinx.Ava.UI.ViewModels if (arg is XCITrimmerFileModel content) { return string.IsNullOrWhiteSpace(_search) - || content.Name.ToLower().Contains(_search.ToLower()) - || content.Path.ToLower().Contains(_search.ToLower()); + || content.Name.ContainsIgnoreCase(_search) + || XCITrimmerFileStatusConverter.From(content).ContainsIgnoreCase(_search) + || content.Path.ContainsIgnoreCase(_search); } return false; @@ -268,18 +300,24 @@ namespace Ryujinx.Ava.UI.ViewModels switch (_viewModel.SortingField) { case SortField.Name: - result = x.Name.CompareTo(y.Name); + result = String.Compare(x?.Name ?? String.Empty, y?.Name ?? String.Empty, StringComparison.OrdinalIgnoreCase); + break; + case SortField.Status: + result = String.Compare(XCITrimmerFileStatusConverter.From(x), XCITrimmerFileStatusConverter.From(y), StringComparison.OrdinalIgnoreCase); break; case SortField.Saved: result = x.PotentialSavingsB.CompareTo(y.PotentialSavingsB); break; } - if (!_viewModel.SortingAscending) - result = -result; + if (result == 0) + result = String.Compare(x?.Path ?? String.Empty, y?.Path ?? String.Empty, StringComparison.OrdinalIgnoreCase); if (result == 0) - result = x.Path.CompareTo(y.Path); + result = String.Compare(x?.Name ?? String.Empty, y?.Name ?? String.Empty, StringComparison.OrdinalIgnoreCase); + + if (!_viewModel.SortingAscending) + result = -result; return result; } @@ -446,7 +484,13 @@ namespace Ryujinx.Ava.UI.ViewModels set { _search = value; - FilteringChanged(); + _searchTimer?.Dispose(); + _searchTimer = new Timer(_ => + { + FilteringChanged(); + _searchTimer.Dispose(); + _searchTimer = null; + }, null, 250, 0); } } @@ -467,6 +511,7 @@ namespace Ryujinx.Ava.UI.ViewModels return SortingField switch { SortField.Name => LocaleManager.Instance[LocaleKeys.XCITrimmerSortName], + SortField.Status => LocaleManager.Instance[LocaleKeys.XCITrimmerSortStatus], SortField.Saved => LocaleManager.Instance[LocaleKeys.XCITrimmerSortSaved], _ => string.Empty, }; @@ -487,6 +532,11 @@ namespace Ryujinx.Ava.UI.ViewModels get => _sortField == SortField.Name; } + public bool IsSortedByStatus + { + get => _sortField == SortField.Status; + } + public bool IsSortedBySaved { get => _sortField == SortField.Saved; diff --git a/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml b/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml index d149d246c..aff6098fd 100644 --- a/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml +++ b/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml @@ -45,13 +45,19 @@ Orientation="Vertical"> + diff --git a/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml.cs b/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml.cs index 8fb8c151c..a9dec1c46 100644 --- a/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml.cs +++ b/src/Ryujinx/UI/Views/Dialog/XciTrimmerView.axaml.cs @@ -62,14 +62,28 @@ namespace Ryujinx.Ava.UI.Views.Dialog public void Sort_Checked(object sender, RoutedEventArgs args) { - if (sender is RadioButton { Tag: string sortField }) + if (sender is not RadioButton { Tag: string sortField, IsChecked: { } isChecked }) + { + return; + } + + if (isChecked) + { ViewModel.SortingField = Enum.Parse(sortField); + } } public void Order_Checked(object sender, RoutedEventArgs args) { - if (sender is RadioButton { Tag: string sortOrder }) + if (sender is not RadioButton { Tag: string sortOrder, IsChecked: { } isChecked }) + { + return; + } + + if (isChecked) + { ViewModel.SortingAscending = sortOrder is "Ascending"; + } } private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 3e9086a79..d4561a63a 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -86,7 +86,7 @@ Classes="withCheckbox" Padding="0" Icon="{ext:Icon fa-solid fa-expand}" - InputGesture="F11"> + InputGesture="{OnPlatform Default='F11', macOS='ctrl+cmd+F'}"> - + - + diff --git a/src/Ryujinx/UI/Windows/CompatibilityListWindow.axaml.cs b/src/Ryujinx/UI/Windows/CompatibilityListWindow.axaml.cs index a2b98f8f8..f4e78fb09 100644 --- a/src/Ryujinx/UI/Windows/CompatibilityListWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/CompatibilityListWindow.axaml.cs @@ -29,18 +29,5 @@ namespace Ryujinx.Ava.UI.Windows FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI; NormalControls.IsVisible = ConfigurationState.Instance.ShowOldUI; } - - // ReSharper disable once UnusedMember.Local - // its referenced in the axaml but rider keeps yelling at me that its unused so - private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e) - { - if (DataContext is not CompatibilityViewModel cvm) - return; - - if (sender is not TextBox searchBox) - return; - - cvm.Search(searchBox.Text); - } } }