Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added: Requested Mod Library Refinements #1543

Merged
merged 58 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
f4449e8
Merge branch 'cleanup-toolbar' into mod-library-refinement-v2
Sewer56 May 29, 2024
91fd56a
Merge branch 'add-loadoutgrid-design-vm' into mod-library-refinement-v2
Sewer56 May 29, 2024
9cfdf5a
Rearrange FileOriginsPageButtons & Rename Labels
Sewer56 May 29, 2024
d8dd337
Changed: Add Button style to ListAdd
Sewer56 May 29, 2024
7f4ec3a
Added: 'Get Mods' Label to UI
Sewer56 May 29, 2024
ccf3e52
Added: 'From Drive' and 'From Nexus Mods' Labels
Sewer56 May 29, 2024
20d8642
Added: Nexus and Disk Icons to Mod Library
Sewer56 May 29, 2024
5428137
Moved: The 'Add Mod' Code over to FileOriginsPage
Sewer56 May 29, 2024
b0bd341
Renamed: Added -> Installed, and fixed Actions Column Size
Sewer56 May 29, 2024
7591e46
WIP: Loadout from Disk
Sewer56 May 30, 2024
cb5caf9
Moved: Setting `SuggestedName` into Analyze Step instead of Install M…
Sewer56 May 30, 2024
adcf4f4
Merge remote-tracking branch 'origin/main' into mod-library-refinemen…
Sewer56 Jun 3, 2024
1ba93d4
Fixed: Build around Spectre.Console
Sewer56 Jun 3, 2024
66186ba
Added: Support for 'Add' and 'Add Advanced' button
Sewer56 Jun 3, 2024
bd71d85
Fix: (Holy) Advanced Manual Installer Invoke
Sewer56 Jun 3, 2024
29bbb9b
Fix: Don't allow re-adding a mod that's already added
Sewer56 Jun 3, 2024
113c08d
Improved: Optimize case where mod was added to a loadout
Sewer56 Jun 3, 2024
d6ca002
Changed: Only Lockout 'Add' from Library
Sewer56 Jun 4, 2024
3b9f444
Merge remote-tracking branch 'erri120/main' into mod-library-refineme…
Sewer56 Jun 4, 2024
4ee5fc4
Added: View Mod from Library
Sewer56 Jun 4, 2024
83200c2
Removed: Unused Icon
Sewer56 Jun 4, 2024
d2f6014
Removed: Accidental Inclusion of Dependency by Location
Sewer56 Jun 4, 2024
4ec440f
Merge remote-tracking branch 'erri120/main' into mod-library-refineme…
Sewer56 Jun 4, 2024
c06b085
Minor Cleanup pre-PR
Sewer56 Jun 4, 2024
074d0ab
Added: A Missing Vertical Separator in LoadoutGrid that's Present in …
Sewer56 Jun 4, 2024
1b8a148
Removed Redundant Comment
Sewer56 Jun 4, 2024
2c5fb0d
Change: Regenerated the Designer File with VS
Sewer56 Jun 4, 2024
6bc99cd
Update src/NexusMods.App.UI/Resources/Language.pl.resx
Sewer56 Jun 4, 2024
e501c3f
Merge remote-tracking branch 'origin/main' into mod-library-refinemen…
Sewer56 Jun 4, 2024
ab6a24a
Clarified the issue with [Reactive] Attribute in SelectedModsObservable
Sewer56 Jun 5, 2024
b198db5
Removed: A redundant try/catch in FileOriginsPageViewModel.cs
Sewer56 Jun 5, 2024
39dfb8b
Removed: A Redundant case of UsedImplicitly
Sewer56 Jun 5, 2024
acb8e7e
Update src/NexusMods.App.UI/Resources/Language.pl.resx
Sewer56 Jun 5, 2024
75a07d0
Moved FileOriginPageView Commands to the ViewModel
Sewer56 Jun 5, 2024
12b67df
Added: Debug Assert for Null and Item Type on DataGridOnSelectionChanged
Sewer56 Jun 5, 2024
940513c
Simplify SelectedItemsToViewModelObservableChangeSetProperty to Selec…
Sewer56 Jun 5, 2024
6f8cdf7
Moved: Icon Styles to IconValues.cs
Sewer56 Jun 5, 2024
5c41e0a
Renamed 'ListAdd' to 'PlaylistAdd' to match Figma closer.
Sewer56 Jun 5, 2024
e8be7d6
Removed: Dead items from constructor of LoadoutGrid Page
Sewer56 Jun 5, 2024
6a25061
Renamed: Disk to HardDrive to match Figma Icon Name Better
Sewer56 Jun 5, 2024
a0bfe0b
Cleanup: Constructor of FileOriginsPage to use injected provider inst…
Sewer56 Jun 5, 2024
f25a9a8
Fixup: Propagate IdBundle to FileOriginsPageViewModel
Sewer56 Jun 5, 2024
4b4ef07
Changed: Installing via Installer no longer a Command since UI Doesn'…
Sewer56 Jun 5, 2024
a6d5bb9
Change: Lift out View Mod Command since it's same code for every mod …
Sewer56 Jun 5, 2024
a192e68
Changed: Buttons are now only executable when the command itself is
Sewer56 Jun 5, 2024
befd671
Changed: Now listens to 'my mods' and blocks 'add' again.
Sewer56 Jun 5, 2024
8f11425
Refactor: Use BindCommand in case Commands ever raise PropertyChanged
Sewer56 Jun 6, 2024
666f031
Removed: Unused Using(s)
Sewer56 Jun 6, 2024
0fab0ba
Cleaned up the `FileOriginsPageViewModel` Constructor a bit
Sewer56 Jun 6, 2024
4747ae7
Use SerialDisposable for JIT Disposal
Sewer56 Jun 6, 2024
1b89968
Added: A note regarding disposal.
Sewer56 Jun 6, 2024
11f2159
A Commit that allows Installing a Mod Multiple Times at any Time
Sewer56 Jun 6, 2024
5d41a56
Merge branch 'main' into mod-library-refinement-v2
Sewer56 Jun 6, 2024
9a8f58f
Moved the Implementation of Adding a Mod to FileOriginsPageViewModel
Sewer56 Jun 6, 2024
4316e8a
Merge remote-tracking branch 'origin/main' into mod-library-refinemen…
Sewer56 Jun 6, 2024
7fb7bd6
Added: Nexus App Referrer to Nexus Game Page
Sewer56 Jun 6, 2024
fa2d95f
Merge remote-tracking branch 'origin/main' into mod-library-refinemen…
Sewer56 Jun 10, 2024
20fccda
Adapt to new features in Main
Sewer56 Jun 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/NexusMods.App.Cli/ProtocolVerbs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using NexusMods.Abstractions.Cli;
using NexusMods.Abstractions.FileStore;
using NexusMods.Abstractions.FileStore.ArchiveMetadata;
using NexusMods.Abstractions.Games.Loadouts;
using NexusMods.Abstractions.HttpDownloader;
using NexusMods.Abstractions.Installers;
using NexusMods.Abstractions.Loadouts;
Expand Down
3 changes: 3 additions & 0 deletions src/NexusMods.App.UI/Assets/Icons/disk_20px.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/NexusMods.App.UI/Assets/nexus-logo-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions src/NexusMods.App.UI/Converters/BooleanInverterConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Globalization;
using Avalonia.Data.Converters;

namespace NexusMods.App.UI.Converters;

public class BooleanInverterConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool boolValue)
return !boolValue;

return false;
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool boolValue)
return !boolValue;

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using Avalonia.Controls;
using DynamicData;

namespace NexusMods.App.UI.Helpers;

/// <summary>
/// A helper class that provides an observable collection of selected items for a DataGrid using DynamicData and Observables.
/// Also see <see cref="DataGridSelectedItemsAsObservableHelperExtensions"/> before using.
/// </summary>
/// <typeparam name="T">The type of the items in the DataGrid.</typeparam>
/// <example>
/// Usage example:
/// <code>
/// public YourViewModel()
/// {
/// this.WhenActivated(disposables =&gt;
/// {
/// var helper = new DataGridSelectedItemsAsObservableHelper&lt;YourItemType&gt;(MyDataGrid);
/// .DisposeWith(disposables);
///
/// // Use helper.SelectedItems
///
/// // That said, it is recommended you use DataGridSelectedItemsAsObservableHelperExtensions
/// });
/// }
/// </code>
/// </example>
public class DataGridSelectedItemsAsObservableHelper<T> : IDisposable where T : notnull
{
private readonly DataGrid _dataGrid;
private readonly SourceCache<T, T> _selectedItemsCache;

/// <summary>
/// Contains the currently selected items as an observable.
/// </summary>
public IObservable<IChangeSet<T, T>> SelectedItems { get; init; }

public DataGridSelectedItemsAsObservableHelper(DataGrid dataGrid)
{
_dataGrid = dataGrid;
_selectedItemsCache = new SourceCache<T, T>(x => x);
SelectedItems = _selectedItemsCache.Connect();
_dataGrid.SelectionChanged += DataGridOnSelectionChanged;
}

/// <inheritdoc/>
public void Dispose()
{
_dataGrid.SelectionChanged -= DataGridOnSelectionChanged;
_selectedItemsCache.Dispose();
}

private void DataGridOnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
// Note(sewer) It's important we use Edit here because the event should be atomic
// If we do an `WhenValueChanged` without an Edit, the ReadOnlyObservableCollection
// derived from the SourceCache will have a brief period of time where it shows
// an item which should just have been removed.
_selectedItemsCache.Edit(updater =>
{
foreach (var removedItem in e.RemovedItems)
{
Debug.Assert(removedItem is T, "Removed item is not of the correct type");
Debug.Assert(removedItem != null, "Removed item is null");
updater.RemoveKey((T)removedItem);
}

foreach (var addedItem in e.AddedItems)
{
Debug.Assert(addedItem is T, "Added item is not of the correct type");
Debug.Assert(addedItem != null, "Added item is null");
updater.AddOrUpdate((T)addedItem);
}
});
}
}

/// <summary>
/// Extension methods for synchronizing the selected items of a DataGrid with an existing property on a view model using the DataGridSelectedItemsAsObservableHelper.
/// </summary>
public static class DataGridSelectedItemsAsObservableHelperExtensions
{
/// <summary>
/// Synchronizes the selected items of the current DataGrid with an existing
/// property on a view model using the DataGridSelectedItemsAsObservableHelper.
///
/// This gets us a DynamicData observable for the selected items of a DataGrid.
/// </summary>
/// <typeparam name="T">The type of the items in the DataGrid.</typeparam>
/// <typeparam name="TVM">Type of ViewModel used.</typeparam>
/// <param name="dataGrid">The current DataGrid instance.</param>
/// <param name="viewModel">The view model instance.</param>
/// <param name="selectedItemsProperty">The property on the view model to synchronize with.</param>
/// <example>
/// Usage example:
/// <code>
/// public class YourView : ReactiveUserControl&lt;YourViewModel&gt;
/// {
/// public YourView()
/// {
/// this.WhenActivated(d =>
/// {
/// // Synchronize the selected items with the SelectedItems property on the view model using the helper
/// MyDataGrid.SelectedItemsToProperty(ViewModel, vm => vm.SelectedItems)
/// .DisposeWith(d);
/// });
/// }
/// }
///
/// public class YourViewModel : ReactiveObject, IActivatableViewModel
/// {
/// public IObservable&lt;IChangeSet&lt;YourItemType&gt;&gt; SelectedItems { get; private set; }
///
/// public YourViewModel()
/// {
/// // Initialize other properties
/// }
/// }
/// </code>
/// </example>
public static IDisposable SelectedItemsToProperty<T, TVM>(this DataGrid dataGrid, TVM viewModel, Expression<Func<TVM, IObservable<IChangeSet<T, T>>>> selectedItemsProperty) where T : notnull
{
var propertyInfo = ((MemberExpression)selectedItemsProperty.Body).Member as PropertyInfo;
if (propertyInfo == null)
throw new ArgumentException("The selectedItemsProperty must be a property.");

var selectedItemsHelper = new DataGridSelectedItemsAsObservableHelper<T>(dataGrid);
propertyInfo.SetValue(viewModel, selectedItemsHelper.SelectedItems);
return selectedItemsHelper;
}
}
14 changes: 0 additions & 14 deletions src/NexusMods.App.UI/Pages/LoadoutGrid/ILoadoutGridViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,6 @@ public interface ILoadoutGridViewModel : IPageViewModelInterface

public ReactiveCommand<NavigationInformation, Unit> ViewModContentsCommand { get; }

/// <summary>
/// Add a mod to the loadout using the standard installer.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public Task AddMod(string path);

/// <summary>
/// Add a mod to the loadout using the advanced installer.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public Task AddModAdvanced(string path);

/// <summary>
/// Delete the mods from the loadout.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,5 @@ public class LoadoutGridDesignViewModel(IWindowManager windowManager) : APageVie

public LoadoutGridDesignViewModel() : this(new DesignWindowManager()) { }

public Task AddMod(string path) => throw new NotImplementedException();
public Task AddModAdvanced(string path) => throw new NotImplementedException();
public Task DeleteMods(IEnumerable<ModId> modsToDelete, string commitMessage) => throw new NotImplementedException();
}
22 changes: 5 additions & 17 deletions src/NexusMods.App.UI/Pages/LoadoutGrid/LoadoutGridView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,25 @@
xmlns:resources="clr-namespace:NexusMods.App.UI.Resources"
xmlns:loadoutGrid="clr-namespace:NexusMods.App.UI.Pages.LoadoutGrid"
xmlns:navigation="clr-namespace:NexusMods.App.UI.Controls.Navigation"
xmlns:ui="clr-namespace:NexusMods.App.UI"
xmlns:markdownRenderer="clr-namespace:NexusMods.App.UI.Controls.MarkdownRenderer">
<Design.DataContext>
<loadoutGrid:LoadoutGridDesignViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto, *">
<Border Grid.Row="0" Classes="Toolbar">
<StackPanel>
<Button x:Name="DeleteModsButton">
<StackPanel>
<icons:UnifiedIcon Classes="Close" />
<TextBlock Text="{x:Static resources:Language.LoadoutGridView__Remove}"/>
</StackPanel>
</Button>
<Line/>

<navigation:NavigationControl x:Name="ViewModFilesButton">
<StackPanel>
<icons:UnifiedIcon Classes="FolderOutline" />
<TextBlock Text="{x:Static resources:Language.LoadoutGridView__View_Files}"/>
</StackPanel>
</navigation:NavigationControl>
<Line/>
<Button x:Name="AddModButton">
<StackPanel>
<icons:UnifiedIcon Classes="PlusCircleOutline" />
<TextBlock Text="{x:Static resources:Language.LoadoutGridView__Add_Mod}"/>
</StackPanel>
</Button>
<Button x:Name="AddModAdvancedButton">
<Button x:Name="DeleteModsButton">
<StackPanel>
<icons:UnifiedIcon Classes="PlusCircleOutline" />
<TextBlock Text="{x:Static resources:Language.LoadoutGridView__Add_Mod_Advanced}"/>
<icons:UnifiedIcon Classes="Close" />
<TextBlock Text="{x:Static resources:Language.LoadoutGridView__Remove}"/>
</StackPanel>
</Button>
</StackPanel>
Expand Down
37 changes: 0 additions & 37 deletions src/NexusMods.App.UI/Pages/LoadoutGrid/LoadoutGridView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,47 +51,10 @@ public LoadoutGridView()
view => view.ModsDataGrid.SelectedIndex,
(selectedIndex) => selectedIndex >= 0);

AddModButton.Command = ReactiveCommand.CreateFromTask(AddMod);

AddModAdvancedButton.Command = ReactiveCommand.CreateFromTask(AddModAdvanced);

DeleteModsButton.Command = ReactiveCommand.CreateFromTask(DeleteSelectedMods, isItemSelected);
});
}

private async Task AddMod()
{
foreach (var file in await PickModFiles())
{
await ViewModel!.AddMod(file.Path.LocalPath);
}
}

private async Task AddModAdvanced()
{
foreach (var file in await PickModFiles())
{
await ViewModel!.AddModAdvanced(file.Path.LocalPath);
}
}

private async Task<IEnumerable<IStorageFile>> PickModFiles()
{
var provider = TopLevel.GetTopLevel(this)!.StorageProvider;
var options =
new FilePickerOpenOptions
{
Title = Language.LoadoutGridView_AddMod_FilePicker_Title,
AllowMultiple = true,
FileTypeFilter = new[]
{
new FilePickerFileType(Language.LoadoutGridView_AddMod_FileType_Archive) {Patterns = new [] {"*.zip", "*.7z", "*.rar"}},
}
};

return await provider.OpenFilePickerAsync(options);
}

private async Task DeleteSelectedMods()
{
var toDelete = new List<ModId>();
Expand Down
Loading
Loading