Merge branch 'various-ui-fixes-and-improvements' into 'master'

Various UI Fixes and Improvements for XCI File Trimmer and Games Compatibility Window

See merge request [ryubing/ryujinx!35](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/35)
This commit is contained in:
TheToid 2025-05-29 22:46:08 -05:00
commit 89d51fae1d
12 changed files with 190 additions and 48 deletions

View file

@ -3222,6 +3222,31 @@
"zh_TW": "正在修剪 XCI 檔案 '{0}'" "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", "ID": "LinuxVmMaxMapCountDialogTitle",
"Translations": { "Translations": {
@ -21297,6 +21322,31 @@
"zh_TW": "節省的儲存空間" "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", "ID": "XCITrimmerTrim",
"Translations": { "Translations": {

View file

@ -142,7 +142,6 @@ namespace Ryujinx.Common.Utilities
{ {
Log = log; Log = log;
Filename = path; Filename = path;
ReadHeader();
} }
public void CheckFreeSpace(CancellationToken? cancelToken = null) public void CheckFreeSpace(CancellationToken? cancelToken = null)
@ -435,7 +434,6 @@ namespace Ryujinx.Common.Utilities
_binaryReader.Close(); _binaryReader.Close();
_binaryReader = null; _binaryReader = null;
_fileStream = null; _fileStream = null;
GC.Collect();
} }
private void ReadHeader() private void ReadHeader()

View file

@ -47,10 +47,18 @@ namespace Ryujinx.Ava.Common.Locale
private void Load() private void Load()
{ {
string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ? try
ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_'); {
string localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value)
LoadLanguage(localeLanguageCode); ? ConfigurationState.Instance.UI.LanguageCode.Value
: CultureInfo.CurrentCulture.Name.Replace('-', '_');
LoadLanguage(localeLanguageCode);
}
catch
{
LoadLanguage(DefaultLanguageCode);
}
// Save whatever we ended up with. // Save whatever we ended up with.
if (Program.PreviewerDetached) if (Program.PreviewerDetached)

View file

@ -25,19 +25,26 @@ namespace Ryujinx.Ava.UI.Helpers
return null; return null;
} }
if (value is not XCITrimmerFileModel app) if (value is not XCITrimmerFileModel model)
{ {
return null; return null;
} }
return app.PercentageProgress != null ? String.Empty : return model.PercentageProgress != null ? String.Empty :
app.ProcessingOutcome != OperationOutcome.Successful && app.ProcessingOutcome != OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] : model.ProcessingOutcome != OperationOutcome.Successful && model.ProcessingOutcome != OperationOutcome.Undetermined ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusFailedLabel] :
app.Trimmable & app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] : model.Trimmable & model.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusPartialLabel] :
app.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] : model.Trimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusTrimmableLabel] :
app.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] : model.Untrimmable ? LocaleManager.Instance[LocaleKeys.TitleXCIStatusUntrimmableLabel] :
String.Empty; 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) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ {
throw new NotSupportedException(); throw new NotSupportedException();

View file

@ -4,12 +4,15 @@ using Ryujinx.Ava.Systems.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
public class CompatibilityViewModel : BaseModel, IDisposable public class CompatibilityViewModel : BaseModel, IDisposable
{ {
private readonly ApplicationLibrary _appLibrary; private readonly ApplicationLibrary _appLibrary;
private string _search;
private Timer _searchTimer;
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityDatabase.Entries; private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityDatabase.Entries;
private string[] _ownedGameTitleIds = []; 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)) if (string.IsNullOrEmpty(searchTerm))
{ {
@ -64,6 +67,9 @@ namespace Ryujinx.Ava.UI.ViewModels
SetEntries(CompatibilityDatabase.Entries.Where(x => SetEntries(CompatibilityDatabase.Entries.Where(x =>
x.GameName.ContainsIgnoreCase(searchTerm) x.GameName.ContainsIgnoreCase(searchTerm)
|| x.FormattedIssueLabels.ContainsIgnoreCase(searchTerm)
|| x.LocalizedStatus.ContainsIgnoreCase(searchTerm)
|| x.LocalizedStatusDescription.ContainsIgnoreCase(searchTerm)
|| x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm)))); || x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm))));
} }
@ -74,5 +80,21 @@ namespace Ryujinx.Ava.UI.ViewModels
#pragma warning restore MVVMTK0034 #pragma warning restore MVVMTK0034
OnPropertyChanged(nameof(CurrentEntries)); 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);
}
}
} }
} }

View file

@ -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(); string notifyUser = operationOutcome.ToLocalisedText();

View file

@ -8,6 +8,7 @@ using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Systems.AppLibrary; using Ryujinx.Ava.Systems.AppLibrary;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
@ -28,6 +29,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public enum SortField public enum SortField
{ {
Name, Name,
Status,
Saved Saved
} }
@ -42,6 +44,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private MainWindowViewModel _mainWindowViewModel; private MainWindowViewModel _mainWindowViewModel;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
private string _search; private string _search;
private Timer _searchTimer;
private ProcessingMode _processingMode; private ProcessingMode _processingMode;
private SortField _sortField = SortField.Name; private SortField _sortField = SortField.Name;
private bool _sortAscending = true; private bool _sortAscending = true;
@ -55,11 +58,39 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadXCIApplications() private void LoadXCIApplications()
{ {
IEnumerable<ApplicationData> apps = ApplicationLibrary.Applications.Items try
.Where(app => app.FileExtension == _FileExtXCI); {
_mainWindowViewModel.StatusBarProgressStatusText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarXCIFileScanning);
_mainWindowViewModel.StatusBarProgressStatusVisible = true;
_mainWindowViewModel.StatusBarProgressMaximum = 1;
_mainWindowViewModel.StatusBarProgressValue = 0;
_mainWindowViewModel.StatusBarVisible = true;
foreach (ApplicationData xciApp in apps) IEnumerable<ApplicationData> apps = ApplicationLibrary.Applications.Items
AddOrUpdateXCITrimmerFile(CreateXCITrimmerFile(xciApp.Path)); .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(); ApplicationsChanged();
} }
@ -245,8 +276,9 @@ namespace Ryujinx.Ava.UI.ViewModels
if (arg is XCITrimmerFileModel content) if (arg is XCITrimmerFileModel content)
{ {
return string.IsNullOrWhiteSpace(_search) return string.IsNullOrWhiteSpace(_search)
|| content.Name.ToLower().Contains(_search.ToLower()) || content.Name.ContainsIgnoreCase(_search)
|| content.Path.ToLower().Contains(_search.ToLower()); || XCITrimmerFileStatusConverter.From(content).ContainsIgnoreCase(_search)
|| content.Path.ContainsIgnoreCase(_search);
} }
return false; return false;
@ -268,18 +300,24 @@ namespace Ryujinx.Ava.UI.ViewModels
switch (_viewModel.SortingField) switch (_viewModel.SortingField)
{ {
case SortField.Name: 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; break;
case SortField.Saved: case SortField.Saved:
result = x.PotentialSavingsB.CompareTo(y.PotentialSavingsB); result = x.PotentialSavingsB.CompareTo(y.PotentialSavingsB);
break; break;
} }
if (!_viewModel.SortingAscending) if (result == 0)
result = -result; result = String.Compare(x?.Path ?? String.Empty, y?.Path ?? String.Empty, StringComparison.OrdinalIgnoreCase);
if (result == 0) 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; return result;
} }
@ -446,7 +484,13 @@ namespace Ryujinx.Ava.UI.ViewModels
set set
{ {
_search = value; _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 return SortingField switch
{ {
SortField.Name => LocaleManager.Instance[LocaleKeys.XCITrimmerSortName], SortField.Name => LocaleManager.Instance[LocaleKeys.XCITrimmerSortName],
SortField.Status => LocaleManager.Instance[LocaleKeys.XCITrimmerSortStatus],
SortField.Saved => LocaleManager.Instance[LocaleKeys.XCITrimmerSortSaved], SortField.Saved => LocaleManager.Instance[LocaleKeys.XCITrimmerSortSaved],
_ => string.Empty, _ => string.Empty,
}; };
@ -487,6 +532,11 @@ namespace Ryujinx.Ava.UI.ViewModels
get => _sortField == SortField.Name; get => _sortField == SortField.Name;
} }
public bool IsSortedByStatus
{
get => _sortField == SortField.Status;
}
public bool IsSortedBySaved public bool IsSortedBySaved
{ {
get => _sortField == SortField.Saved; get => _sortField == SortField.Saved;

View file

@ -45,13 +45,19 @@
Orientation="Vertical"> Orientation="Vertical">
<StackPanel> <StackPanel>
<RadioButton <RadioButton
Checked="Sort_Checked" IsCheckedChanged="Sort_Checked"
Content="{ext:Locale XCITrimmerSortName}" Content="{ext:Locale XCITrimmerSortName}"
GroupName="Sort" GroupName="Sort"
IsChecked="{Binding IsSortedByName, Mode=OneTime}" IsChecked="{Binding IsSortedByName, Mode=OneTime}"
Tag="Name" /> Tag="Name" />
<RadioButton <RadioButton
Checked="Sort_Checked" IsCheckedChanged="Sort_Checked"
Content="{ext:Locale XCITrimmerSortStatus}"
GroupName="Sort"
IsChecked="{Binding IsSortedByStatus, Mode=OneTime}"
Tag="Status" />
<RadioButton
IsCheckedChanged="Sort_Checked"
Content="{ext:Locale XCITrimmerSortSaved}" Content="{ext:Locale XCITrimmerSortSaved}"
GroupName="Sort" GroupName="Sort"
IsChecked="{Binding IsSortedBySaved, Mode=OneTime}" IsChecked="{Binding IsSortedBySaved, Mode=OneTime}"
@ -67,13 +73,13 @@
<Separator Height="0" HorizontalAlignment="Stretch" /> <Separator Height="0" HorizontalAlignment="Stretch" />
</Border> </Border>
<RadioButton <RadioButton
Checked="Order_Checked" IsCheckedChanged="Order_Checked"
Content="{ext:Locale OrderAscending}" Content="{ext:Locale OrderAscending}"
GroupName="Order" GroupName="Order"
IsChecked="{Binding SortingAscending, Mode=OneTime}" IsChecked="{Binding SortingAscending, Mode=OneTime}"
Tag="Ascending" /> Tag="Ascending" />
<RadioButton <RadioButton
Checked="Order_Checked" IsCheckedChanged="Order_Checked"
Content="{ext:Locale OrderDescending}" Content="{ext:Locale OrderDescending}"
GroupName="Order" GroupName="Order"
IsChecked="{Binding !SortingAscending, Mode=OneTime}" IsChecked="{Binding !SortingAscending, Mode=OneTime}"
@ -151,7 +157,7 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
CornerRadius="5" CornerRadius="5"
IsVisible="{Binding $parent[UserControl].((viewModels:XciTrimmerViewModel)DataContext).Processing}" IsVisible="{Binding $parent[UserControl].((viewModels:XciTrimmerViewModel)DataContext).Processing, FallbackValue=False}"
Maximum="100" Maximum="100"
Minimum="0" Minimum="0"
Value="{Binding PercentageProgress}" /> Value="{Binding PercentageProgress}" />

View file

@ -62,14 +62,28 @@ namespace Ryujinx.Ava.UI.Views.Dialog
public void Sort_Checked(object sender, RoutedEventArgs args) 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<XciTrimmerViewModel.SortField>(sortField); ViewModel.SortingField = Enum.Parse<XciTrimmerViewModel.SortField>(sortField);
}
} }
public void Order_Checked(object sender, RoutedEventArgs args) 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"; ViewModel.SortingAscending = sortOrder is "Ascending";
}
} }
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)

View file

@ -86,7 +86,7 @@
Classes="withCheckbox" Classes="withCheckbox"
Padding="0" Padding="0"
Icon="{ext:Icon fa-solid fa-expand}" Icon="{ext:Icon fa-solid fa-expand}"
InputGesture="F11"> InputGesture="{OnPlatform Default='F11', macOS='ctrl+cmd+F'}">
</MenuItem> </MenuItem>
<MenuItem <MenuItem
Padding="0" Padding="0"

View file

@ -23,12 +23,12 @@
Grid.Column="0" Grid.Column="0"
Margin="15, 0, 7, 0" Margin="15, 0, 7, 0"
ToolTip.Tip="{ext:WindowTitle CompatibilityListTitle, False}"/> ToolTip.Tip="{ext:WindowTitle CompatibilityListTitle, False}"/>
<TextBox Name="SearchBoxFlush" Grid.Column="1" Margin="0, 5, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermarkWithCount}" TextChanged="TextBox_OnTextChanged" /> <TextBox Name="SearchBoxFlush" Grid.Column="1" Margin="0, 5, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermarkWithCount}" Text="{Binding Search}" />
<CheckBox Grid.Column="2" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" /> <CheckBox Grid.Column="2" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="3" Padding="0, 0, 138, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" /> <TextBlock Grid.Column="3" Padding="0, 0, 138, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid> </Grid>
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Name="NormalControls"> <Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Name="NormalControls">
<TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="15, 0, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" /> <TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="15, 0, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" Text="{Binding Search}" />
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" /> <CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="2" Padding="0, 0, 1, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" /> <TextBlock Grid.Column="2" Padding="0, 0, 1, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid> </Grid>

View file

@ -29,18 +29,5 @@ namespace Ryujinx.Ava.UI.Windows
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI; FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
NormalControls.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);
}
} }
} }