aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Ava/UI
diff options
context:
space:
mode:
authorIsaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>2024-01-26 01:02:28 +0000
committerGitHub <noreply@github.com>2024-01-26 02:02:28 +0100
commit35fb409e85ef07b8e1c3a582cdc6615e6da71429 (patch)
tree1696d9498a73fe94d228a6bbf3ed06ff92f88a0c /src/Ryujinx.Ava/UI
parentd7ec4308b45d4ecb8d77cdc8d98ee618944292ed (diff)
Ava UI: Mod Manager (#4390)
* Let’s start again * Read folders and such * Remove Open Mod Folder menu items * Fix folder opening, Selecting/deselecting * She works * Fix GTK * AddMod * Delete * Fix duplicate entries * Fix file check * Avalonia 11 * Style fixes * Final style fixes * Might be too general * Remove unnecessary using * Enable new mods by default * More cleanup * Fix saving metadata * Dont deseralise ModMetadata several times * Avalonia I hate you * Confirmation dialgoues * Allow selecting multiple folders * Add back secondary folder * Search both paths * Fix formatting * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * Rename Title to Application * Generic locale key * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * Locale Updates * GDK Feedback * Fix --------- Co-authored-by: Ac_K <Acoustik666@gmail.com>
Diffstat (limited to 'src/Ryujinx.Ava/UI')
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml10
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs20
-rw-r--r--src/Ryujinx.Ava/UI/Models/ModModel.cs30
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs9
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs260
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs2
-rw-r--r--src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs2
-rw-r--r--src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml179
-rw-r--r--src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs139
9 files changed, 619 insertions, 32 deletions
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
index b8fe7e76..7f786cf3 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
@@ -47,13 +47,9 @@
Header="{locale:Locale GameListContextMenuManageCheat}"
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
<MenuItem
- Click="OpenModsDirectory_Click"
- Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
- ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
- <MenuItem
- Click="OpenSdModsDirectory_Click"
- Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
- ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
+ Click="OpenModManager_Click"
+ Header="{locale:Locale GameListContextMenuManageMod}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageModToolTip}" />
<Separator />
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
<MenuItem
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
index 0f007106..01d97709 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
@@ -126,29 +126,13 @@ namespace Ryujinx.Ava.UI.Controls
}
}
- public void OpenModsDirectory_Click(object sender, RoutedEventArgs args)
+ public async void OpenModManager_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
- string modsBasePath = ModLoader.GetModsBasePath();
- string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId);
-
- OpenHelper.OpenFolder(titleModsPath);
- }
- }
-
- public void OpenSdModsDirectory_Click(object sender, RoutedEventArgs args)
- {
- var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
-
- if (viewModel?.SelectedApplication != null)
- {
- string sdModsBasePath = ModLoader.GetSdModsBasePath();
- string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
-
- OpenHelper.OpenFolder(titleModsPath);
+ await ModManagerWindow.Show(ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
}
}
diff --git a/src/Ryujinx.Ava/UI/Models/ModModel.cs b/src/Ryujinx.Ava/UI/Models/ModModel.cs
new file mode 100644
index 00000000..f68e1593
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Models/ModModel.cs
@@ -0,0 +1,30 @@
+using Ryujinx.Ava.UI.ViewModels;
+using System.IO;
+
+namespace Ryujinx.Ava.UI.Models
+{
+ public class ModModel : BaseModel
+ {
+ private bool _enabled;
+
+ public bool Enabled
+ {
+ get => _enabled;
+ set
+ {
+ _enabled = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public string Path { get; }
+ public string Name { get; }
+
+ public ModModel(string path, string name, bool enabled)
+ {
+ Path = path;
+ Name = name;
+ Enabled = enabled;
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
index cdecae77..2cd714f4 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -39,6 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private string _search;
private readonly ulong _titleId;
+ private readonly IStorageProvider _storageProvider;
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@@ -90,8 +91,6 @@ namespace Ryujinx.Ava.UI.ViewModels
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
}
- public IStorageProvider StorageProvider;
-
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
{
_virtualFileSystem = virtualFileSystem;
@@ -100,7 +99,7 @@ namespace Ryujinx.Ava.UI.ViewModels
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- StorageProvider = desktop.MainWindow.StorageProvider;
+ _storageProvider = desktop.MainWindow.StorageProvider;
}
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
@@ -194,7 +193,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
- await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadNcaErrorMessage], ex.Message, containerPath));
+ await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance[LocaleKeys.DialogLoadFileErrorMessage], ex.Message, containerPath));
});
}
@@ -203,7 +202,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void Add()
{
- var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
AllowMultiple = true,
diff --git a/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs
new file mode 100644
index 00000000..7340873a
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs
@@ -0,0 +1,260 @@
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
+using Avalonia.Threading;
+using DynamicData;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Ava.UI.ViewModels
+{
+ public class ModManagerViewModel : BaseModel
+ {
+ private readonly string _modJsonPath;
+
+ private AvaloniaList<ModModel> _mods = new();
+ private AvaloniaList<ModModel> _views = new();
+ private AvaloniaList<ModModel> _selectedMods = new();
+
+ private string _search;
+ private readonly ulong _applicationId;
+ private readonly IStorageProvider _storageProvider;
+
+ private static readonly ModMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ public AvaloniaList<ModModel> Mods
+ {
+ get => _mods;
+ set
+ {
+ _mods = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ModCount));
+ Sort();
+ }
+ }
+
+ public AvaloniaList<ModModel> Views
+ {
+ get => _views;
+ set
+ {
+ _views = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public AvaloniaList<ModModel> SelectedMods
+ {
+ get => _selectedMods;
+ set
+ {
+ _selectedMods = value;
+ OnPropertyChanged();
+ }
+ }
+
+ public string Search
+ {
+ get => _search;
+ set
+ {
+ _search = value;
+ OnPropertyChanged();
+ Sort();
+ }
+ }
+
+ public string ModCount
+ {
+ get => string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], Mods.Count);
+ }
+
+ public ModManagerViewModel(ulong applicationId)
+ {
+ _applicationId = applicationId;
+
+ _modJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationId.ToString("x16"), "mods.json");
+
+ if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ _storageProvider = desktop.MainWindow.StorageProvider;
+ }
+
+ LoadMods(applicationId);
+ }
+
+ private void LoadMods(ulong applicationId)
+ {
+ Mods.Clear();
+ SelectedMods.Clear();
+
+ string[] modsBasePaths = [ModLoader.GetSdModsBasePath(), ModLoader.GetModsBasePath()];
+
+ foreach (var path in modsBasePaths)
+ {
+ var modCache = new ModLoader.ModCache();
+
+ ModLoader.QueryContentsDir(modCache, new DirectoryInfo(Path.Combine(path, "contents")), applicationId);
+
+ foreach (var mod in modCache.RomfsDirs)
+ {
+ var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled);
+ if (Mods.All(x => x.Path != mod.Path.Parent.FullName))
+ {
+ Mods.Add(modModel);
+ }
+ }
+
+ foreach (var mod in modCache.RomfsContainers)
+ {
+ Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled));
+ }
+
+ foreach (var mod in modCache.ExefsDirs)
+ {
+ var modModel = new ModModel(mod.Path.Parent.FullName, mod.Name, mod.Enabled);
+ if (Mods.All(x => x.Path != mod.Path.Parent.FullName))
+ {
+ Mods.Add(modModel);
+ }
+ }
+
+ foreach (var mod in modCache.ExefsContainers)
+ {
+ Mods.Add(new ModModel(mod.Path.FullName, mod.Name, mod.Enabled));
+ }
+ }
+
+ Sort();
+ }
+
+ public void Sort()
+ {
+ Mods.AsObservableChangeSet()
+ .Filter(Filter)
+ .Bind(out var view).AsObservableList();
+
+ _views.Clear();
+ _views.AddRange(view);
+
+ SelectedMods = new(Views.Where(x => x.Enabled));
+
+ OnPropertyChanged(nameof(ModCount));
+ OnPropertyChanged(nameof(Views));
+ OnPropertyChanged(nameof(SelectedMods));
+ }
+
+ private bool Filter(object arg)
+ {
+ if (arg is ModModel content)
+ {
+ return string.IsNullOrWhiteSpace(_search) || content.Name.ToLower().Contains(_search.ToLower());
+ }
+
+ return false;
+ }
+
+ public void Save()
+ {
+ ModMetadata modData = new();
+
+ foreach (ModModel mod in Mods)
+ {
+ modData.Mods.Add(new Mod
+ {
+ Name = mod.Name,
+ Path = mod.Path,
+ Enabled = SelectedMods.Contains(mod),
+ });
+ }
+
+ JsonHelper.SerializeToFile(_modJsonPath, modData, _serializerContext.ModMetadata);
+ }
+
+ public void Delete(ModModel model)
+ {
+ Directory.Delete(model.Path, true);
+
+ Mods.Remove(model);
+ OnPropertyChanged(nameof(ModCount));
+ Sort();
+ }
+
+ private void AddMod(DirectoryInfo directory)
+ {
+ var directories = Directory.GetDirectories(directory.ToString(), "*", SearchOption.AllDirectories);
+ var destinationDir = ModLoader.GetApplicationDir(ModLoader.GetSdModsBasePath(), _applicationId.ToString("x16"));
+
+ foreach (var dir in directories)
+ {
+ string dirToCreate = dir.Replace(directory.Parent.ToString(), destinationDir);
+
+ // Mod already exists
+ if (Directory.Exists(dirToCreate))
+ {
+ Dispatcher.UIThread.Post(async () =>
+ {
+ await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, LocaleKeys.DialogModAlreadyExistsMessage, dirToCreate));
+ });
+
+ return;
+ }
+
+ Directory.CreateDirectory(dirToCreate);
+ }
+
+ var files = Directory.GetFiles(directory.ToString(), "*", SearchOption.AllDirectories);
+
+ foreach (var file in files)
+ {
+ File.Copy(file, file.Replace(directory.Parent.ToString(), destinationDir), true);
+ }
+
+ LoadMods(_applicationId);
+ }
+
+ public async void Add()
+ {
+ var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
+ {
+ Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle],
+ AllowMultiple = true,
+ });
+
+ foreach (var folder in result)
+ {
+ AddMod(new DirectoryInfo(folder.Path.LocalPath));
+ }
+ }
+
+ public void DeleteAll()
+ {
+ foreach (var mod in Mods)
+ {
+ Directory.Delete(mod.Path, true);
+ }
+
+ Mods.Clear();
+ OnPropertyChanged(nameof(ModCount));
+ Sort();
+ }
+
+ public void EnableAll()
+ {
+ SelectedMods = new(Mods);
+ }
+
+ public void DisableAll()
+ {
+ SelectedMods.Clear();
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
index 5090a8c7..8a287eca 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
@@ -192,7 +192,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
catch (Exception ex)
{
- Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadNcaErrorMessage, ex.Message, path)));
+ Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogLoadFileErrorMessage, ex.Message, path)));
}
}
}
diff --git a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
index c2de67ab..46441faa 100644
--- a/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
@@ -41,7 +41,7 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
string modsBasePath = ModLoader.GetModsBasePath();
- string titleModsPath = ModLoader.GetTitleDir(modsBasePath, titleId);
+ string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, titleId);
ulong titleIdValue = ulong.Parse(titleId, NumberStyles.HexNumber);
_enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml
new file mode 100644
index 00000000..d9f58640
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml
@@ -0,0 +1,179 @@
+<UserControl
+ xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
+ xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ Width="500"
+ Height="380"
+ mc:Ignorable="d"
+ x:Class="Ryujinx.Ava.UI.Windows.ModManagerWindow"
+ x:CompileBindings="True"
+ x:DataType="viewModels:ModManagerViewModel"
+ Focusable="True">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Panel
+ Margin="0 0 0 10"
+ Grid.Row="0">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition Width="*" />
+ </Grid.ColumnDefinitions>
+ <TextBlock
+ Grid.Column="0"
+ Text="{Binding ModCount}" />
+ <StackPanel
+ Margin="10 0"
+ Grid.Column="1"
+ Orientation="Horizontal">
+ <Button
+ Name="EnableAllButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{ReflectionBinding EnableAll}">
+ <TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
+ </Button>
+ <Button
+ Name="DisableAllButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{ReflectionBinding DisableAll}">
+ <TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
+ </Button>
+ </StackPanel>
+ <TextBox
+ Grid.Column="2"
+ MinHeight="27"
+ MaxHeight="27"
+ HorizontalAlignment="Stretch"
+ Watermark="{locale:Locale Search}"
+ Text="{Binding Search}" />
+ </Grid>
+ </Panel>
+ <Border
+ Grid.Row="1"
+ Margin="0 0 0 24"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Stretch"
+ BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
+ BorderThickness="1"
+ CornerRadius="5"
+ Padding="2.5">
+ <ListBox
+ AutoScrollToSelectedItem="False"
+ SelectionMode="Multiple, Toggle"
+ Background="Transparent"
+ SelectionChanged="OnSelectionChanged"
+ SelectedItems="{Binding SelectedMods, Mode=TwoWay}"
+ ItemsSource="{Binding Views}">
+ <ListBox.DataTemplates>
+ <DataTemplate
+ DataType="models:ModModel">
+ <Panel Margin="10">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*" />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
+ <TextBlock
+ HorizontalAlignment="Left"
+ VerticalAlignment="Center"
+ MaxLines="2"
+ TextWrapping="Wrap"
+ TextTrimming="CharacterEllipsis"
+ Text="{Binding Name}" />
+ <StackPanel
+ Grid.Column="1"
+ Spacing="10"
+ Orientation="Horizontal"
+ HorizontalAlignment="Right">
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Click="OpenLocation">
+ <ui:SymbolIcon
+ Symbol="OpenFolder"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ <Button
+ VerticalAlignment="Center"
+ HorizontalAlignment="Right"
+ Padding="10"
+ MinWidth="0"
+ MinHeight="0"
+ Click="DeleteMod">
+ <ui:SymbolIcon
+ Symbol="Cancel"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center" />
+ </Button>
+ </StackPanel>
+ </Grid>
+ </Panel>
+ </DataTemplate>
+ </ListBox.DataTemplates>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Background" Value="Transparent" />
+ </Style>
+ </ListBox.Styles>
+ </ListBox>
+ </Border>
+ <Panel
+ Grid.Row="2"
+ HorizontalAlignment="Stretch">
+ <StackPanel
+ Orientation="Horizontal"
+ Spacing="10"
+ HorizontalAlignment="Left">
+ <Button
+ Name="AddButton"
+ MinWidth="90"
+ Margin="5"
+ Command="{Binding Add}">
+ <TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
+ </Button>
+ <Button
+ Name="RemoveAllButton"
+ MinWidth="90"
+ Margin="5"
+ Click="DeleteAll">
+ <TextBlock Text="{locale:Locale ModManagerDeleteAllButton}" />
+ </Button>
+ </StackPanel>
+ <StackPanel
+ Orientation="Horizontal"
+ Spacing="10"
+ HorizontalAlignment="Right">
+ <Button
+ Name="SaveButton"
+ MinWidth="90"
+ Margin="5"
+ Click="SaveAndClose">
+ <TextBlock Text="{locale:Locale SettingsButtonSave}" />
+ </Button>
+ <Button
+ Name="CancelButton"
+ MinWidth="90"
+ Margin="5"
+ Click="Close">
+ <TextBlock Text="{locale:Locale InputDialogCancel}" />
+ </Button>
+ </StackPanel>
+ </Panel>
+ </Grid>
+</UserControl>
diff --git a/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs
new file mode 100644
index 00000000..5de09ba0
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs
@@ -0,0 +1,139 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Styling;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Helpers;
+using Ryujinx.Ava.UI.Models;
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ui.Common.Helper;
+using System.Threading.Tasks;
+using Button = Avalonia.Controls.Button;
+
+namespace Ryujinx.Ava.UI.Windows
+{
+ public partial class ModManagerWindow : UserControl
+ {
+ public ModManagerViewModel ViewModel;
+
+ public ModManagerWindow()
+ {
+ DataContext = this;
+
+ InitializeComponent();
+ }
+
+ public ModManagerWindow(ulong titleId)
+ {
+ DataContext = ViewModel = new ModManagerViewModel(titleId);
+
+ InitializeComponent();
+ }
+
+ public static async Task Show(ulong titleId, string titleName)
+ {
+ ContentDialog contentDialog = new()
+ {
+ PrimaryButtonText = "",
+ SecondaryButtonText = "",
+ CloseButtonText = "",
+ Content = new ModManagerWindow(titleId),
+ Title = string.Format(LocaleManager.Instance[LocaleKeys.ModWindowHeading], titleName, titleId.ToString("X16")),
+ };
+
+ Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
+ bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
+
+ contentDialog.Styles.Add(bottomBorder);
+
+ await contentDialog.ShowAsync();
+ }
+
+ private void SaveAndClose(object sender, RoutedEventArgs e)
+ {
+ ViewModel.Save();
+ ((ContentDialog)Parent).Hide();
+ }
+
+ private void Close(object sender, RoutedEventArgs e)
+ {
+ ((ContentDialog)Parent).Hide();
+ }
+
+ private async void DeleteMod(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button)
+ {
+ if (button.DataContext is ModModel model)
+ {
+ var result = await ContentDialogHelper.CreateConfirmationDialog(
+ LocaleManager.Instance[LocaleKeys.DialogWarning],
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogModManagerDeletionWarningMessage, model.Name),
+ LocaleManager.Instance[LocaleKeys.InputDialogYes],
+ LocaleManager.Instance[LocaleKeys.InputDialogNo],
+ LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
+
+ if (result == UserResult.Yes)
+ {
+ ViewModel.Delete(model);
+ }
+ }
+ }
+ }
+
+ private async void DeleteAll(object sender, RoutedEventArgs e)
+ {
+ var result = await ContentDialogHelper.CreateConfirmationDialog(
+ LocaleManager.Instance[LocaleKeys.DialogWarning],
+ LocaleManager.Instance[LocaleKeys.DialogModManagerDeletionAllWarningMessage],
+ LocaleManager.Instance[LocaleKeys.InputDialogYes],
+ LocaleManager.Instance[LocaleKeys.InputDialogNo],
+ LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
+
+ if (result == UserResult.Yes)
+ {
+ ViewModel.DeleteAll();
+ }
+ }
+
+ private void OpenLocation(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button)
+ {
+ if (button.DataContext is ModModel model)
+ {
+ OpenHelper.OpenFolder(model.Path);
+ }
+ }
+ }
+
+ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ foreach (var content in e.AddedItems)
+ {
+ if (content is ModModel model)
+ {
+ var index = ViewModel.Mods.IndexOf(model);
+
+ if (index != -1)
+ {
+ ViewModel.Mods[index].Enabled = true;
+ }
+ }
+ }
+
+ foreach (var content in e.RemovedItems)
+ {
+ if (content is ModModel model)
+ {
+ var index = ViewModel.Mods.IndexOf(model);
+
+ if (index != -1)
+ {
+ ViewModel.Mods[index].Enabled = false;
+ }
+ }
+ }
+ }
+ }
+}