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>
This commit is contained in:
@@ -54,8 +54,6 @@
|
||||
"GameListContextMenuManageTitleUpdatesToolTip": "Opens the Title Update management window",
|
||||
"GameListContextMenuManageDlc": "Manage DLC",
|
||||
"GameListContextMenuManageDlcToolTip": "Opens the DLC management window",
|
||||
"GameListContextMenuOpenModsDirectory": "Open Mods Directory",
|
||||
"GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods",
|
||||
"GameListContextMenuCacheManagement": "Cache Management",
|
||||
"GameListContextMenuCacheManagementPurgePptc": "Queue PPTC Rebuild",
|
||||
"GameListContextMenuCacheManagementPurgePptcToolTip": "Trigger PPTC to rebuild at boot time on the next game launch",
|
||||
@@ -383,7 +381,8 @@
|
||||
"DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?",
|
||||
"DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.",
|
||||
"DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?",
|
||||
"DialogLoadNcaErrorMessage": "{0}. Errored File: {1}",
|
||||
"DialogLoadFileErrorMessage": "{0}. Errored File: {1}",
|
||||
"DialogModAlreadyExistsMessage": "Mod already exists",
|
||||
"DialogDlcNoDlcErrorMessage": "The specified file does not contain a DLC for the selected title!",
|
||||
"DialogPerformanceCheckLoggingEnabledMessage": "You have trace logging enabled, which is designed to be used by developers only.",
|
||||
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?",
|
||||
@@ -394,6 +393,8 @@
|
||||
"DialogUpdateAddUpdateErrorMessage": "The specified file does not contain an update for the selected title!",
|
||||
"DialogSettingsBackendThreadingWarningTitle": "Warning - Backend Threading",
|
||||
"DialogSettingsBackendThreadingWarningMessage": "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.",
|
||||
"DialogModManagerDeletionWarningMessage": "You are about to delete the mod: {0}\n\nAre you sure you want to proceed?",
|
||||
"DialogModManagerDeletionAllWarningMessage": "You are about to delete all mods for this title.\n\nAre you sure you want to proceed?",
|
||||
"SettingsTabGraphicsFeaturesOptions": "Features",
|
||||
"SettingsTabGraphicsBackendMultithreading": "Graphics Backend Multithreading:",
|
||||
"CommonAuto": "Auto",
|
||||
@@ -428,6 +429,7 @@
|
||||
"DlcManagerRemoveAllButton": "Remove All",
|
||||
"DlcManagerEnableAllButton": "Enable All",
|
||||
"DlcManagerDisableAllButton": "Disable All",
|
||||
"ModManagerDeleteAllButton": "Delete All",
|
||||
"MenuBarOptionsChangeLanguage": "Change Language",
|
||||
"MenuBarShowFileTypes": "Show File Types",
|
||||
"CommonSort": "Sort",
|
||||
@@ -502,6 +504,8 @@
|
||||
"EnableInternetAccessTooltip": "Allows the emulated application to connect to the Internet.\n\nGames with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well.\n\nDoes NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet.\n\nLeave OFF if unsure.",
|
||||
"GameListContextMenuManageCheatToolTip": "Manage Cheats",
|
||||
"GameListContextMenuManageCheat": "Manage Cheats",
|
||||
"GameListContextMenuManageModToolTip": "Manage Mods",
|
||||
"GameListContextMenuManageMod": "Manage Mods",
|
||||
"ControllerSettingsStickRange": "Range:",
|
||||
"DialogStopEmulationTitle": "Ryujinx - Stop Emulation",
|
||||
"DialogStopEmulationMessage": "Are you sure you want to stop emulation?",
|
||||
@@ -513,8 +517,6 @@
|
||||
"SettingsTabCpuMemory": "CPU Mode",
|
||||
"DialogUpdaterFlatpakNotSupportedMessage": "Please update Ryujinx via FlatHub.",
|
||||
"UpdaterDisabledWarningTitle": "Updater Disabled!",
|
||||
"GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory",
|
||||
"GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.",
|
||||
"ControllerSettingsRotate90": "Rotate 90° Clockwise",
|
||||
"IconSize": "Icon Size",
|
||||
"IconSizeTooltip": "Change the size of game icons",
|
||||
@@ -586,6 +588,7 @@
|
||||
"Writable": "Writable",
|
||||
"SelectDlcDialogTitle": "Select DLC files",
|
||||
"SelectUpdateDialogTitle": "Select update files",
|
||||
"SelectModDialogTitle": "Select mod directory",
|
||||
"UserProfileWindowTitle": "User Profiles Manager",
|
||||
"CheatWindowTitle": "Cheats Manager",
|
||||
"DlcWindowTitle": "Manage Downloadable Content for {0} ({1})",
|
||||
@@ -593,6 +596,7 @@
|
||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||
"BuildId": "BuildId:",
|
||||
"DlcWindowHeading": "{0} Downloadable Content(s)",
|
||||
"ModWindowHeading": "{0} Mod(s)",
|
||||
"UserProfilesEditProfile": "Edit Selected",
|
||||
"Cancel": "Cancel",
|
||||
"Save": "Save",
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
30
src/Ryujinx.Ava/UI/Models/ModModel.cs
Normal file
30
src/Ryujinx.Ava/UI/Models/ModModel.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
260
src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs
Normal file
260
src/Ryujinx.Ava/UI/ViewModels/ModManagerViewModel.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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");
|
||||
|
179
src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml
Normal file
179
src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml
Normal file
@@ -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>
|
139
src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs
Normal file
139
src/Ryujinx.Ava/UI/Windows/ModManagerWindow.axaml.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user