Skip to content

Commit

Permalink
Manual installer UI Layout and Styling (#673)
Browse files Browse the repository at this point in the history
* Setup view stub for body and get Preview working

* Added AdvancedInstaller Footer View and styling

* Styling and layout for Body of Manual installer

* Added stubs for ModContent and Results views

* Fix codebehind not using reactive usercontrol

* Use Grid for base:MessageBox overlay since StackPanel breaks ScrollViews

* Add Overlay view and file structure

* Register views in the DI

* Fix preview size

* Correct center sections colors

* Add overlay header translation

* Overlay view layout and styling

* Add translations for body view

* Remove nullable from fixed child vms

* Added translations for ModContent heading

* Add ModContent header section

* Add TreeDataGrid dependency

* Add TreeDataGrid to App.UI project to be able to style it.

* WIP testing of TreeDataGrid stuff

* Add file structure for TreeEntry view

* Add File and Folder icons

* Add new Accent Pill style. Disable pointerover if it's just a label

* Rename old Rounded button

* Add Label version for Pill buttons (that don't react to mouseover)

* Add Pill style Standard

* Add WidgetRounded Button style

* Add "AddCircle" icon and update GameWidget Style

* Remove old rounded button

* Rename WidgetRounded to Rounded

* Added Close icon

* Add check icon

* Added more Tree entry elements

* Added Bytton.Rounded.Secondary style

* Change button to new style

* Register TreeEntryView to DI

* Added Active, SemiActive, Accent and SemiActiveAccent rounded button styles

* Add plus Icon

* Add create Folder button

* Add some minimal temporary styling to TreeDataGrid

* Add Delete-Outline icon

* Add Cancel, Save, Delete buttons for Folder Creation

* Add disabled versions of Rounded button styles

* Update preview for rounded buttons

* Hide most buttons and align filename text

* Add Translations for TreeItemView

* Add Empty Preview view and add dependencies between view models and interfaces

* Add empty right preview view

* Translate Preview Text

* Fix background for right side

* Organize Preview ViewModels

* Separate ViewModels for Preview section again, this should make switching simpler.

* Formalize Border Styles into classes

* Add file structure for SelectLocation view for Preview section

* Add some border styles

* Implement Layout for Select Folder view for preview section

* Fix background colors

* Moved: Advanced Installer Content from Real ViewModel to Design ViewModel

* Add translations for SelectLocation View

* Add file structure for SuggestedEntry View

* Fix resizing issues with Tree Entry

* Implement layout of SuggestedEntry view

* Temporary: Create TreeEntryView from FuncDataTemplate

* Add preview for Suggested Entries

* Some tree styling

* Remove Selection Styling from TreeDataGrid and add hover styling

* Added: FileTreeNode to TreeDataGrid Conversion & Cleanup ModContentView

* Added Install Location Preview view layout, styling and translations.

* Add Unsuppoerted Mod Overlay view

* Add Alert-Outline icon

* Add Standard SecondaryOutlined button style

* Unsupported mod Overlay layout and styling

* Update translations for Unsupported Overlay

* Hide Header from TreeDataGrid without causing bug with Star columns

* Added: Mechanism for 'Selecting' a file

* Changed: Split Results Section into 3 Namespaces

* Rename TreeDataGridSourceFileNode to ModContentFileNode

* Refactor: Move Page Specific Nodes to Relevant Page(s)

* Changed: Reworked Namespaces Throughout UI Project

* Changed: UI Project Visual Components are now Internal

* Changed: Re-marked views as public because Avalonia previewer can't handle internal

* WIP: SuggestedEntryNode

* Renamed: IModContentFileNode to ModContentNode

* Split Location Target Nodes

* - Rename ModContentEntryView to TreeEntryView
- ITreeEntryVM interface temp implementation wip.
- Add TextBox to TreeItemView for CreateFolder text input field.

* Added: Some incomplete boilerplate for Preview and SuggestedEntry Nodes

* Changed: DeploymentData.RemoveMapping should only unmap from output if it has item (in case GamePath is changed to class)

* Added: The ability to force remap items in DeploymentData

* Added: Ability to force unmap items [tests included]. And Source node integration with it.

* Added: Getters for pill flags in IPreviewEntryNode

* Added: Extra clarification in docs.

* Removed: A Redundant TODO

* Added: Small cleanup of PreviewEntryNode

* Fixed: Invalid doc element

* Update documentation element for ModContentNode

* Hide all elements from the TreeEntryView by default

* Update project structure

* Add Node property on TreeEntryVM, can be one of three types

* Add ModContentNode binding to TreeEntryView

* Update translations

* Remove leftover stuff from ItreeEntryVM

* [WIP] Node Linking

* Fix Missing directoryname for link target

* Create temporary constructor without parameters to allow previews to work

* Added IsTopLevel handling in UI for ModContentNode

* Switch ModContentNode.Children to be ITreeItemViewModel to allow view to bind to it.

* Fix missing dispose

* Added: Remainder of Node Linking

* Fix casts due to Children type change

* Added: Support for linking/unlinking files w/ Tests

* Added: Test for unlinking from Right Hand Side (via UnlinkableItem)

* Fix crash in preview

* Added: Some work towards Results Section node unlinkability

* [Untested - Pushing only for API Availability] Added: PreviewEntryNode Creation

* Setup TreeEntryView for PreviewNodes

* Added: Results Tree Creation (w/ Tests)

* Fixed: Accidental Class Rename in last Commit

* Change IPreviewEntryNode.Children to be ITreeEntryVM

* Added: Miscellaneous Fixes

* Added: Basic (non-recursive) preview linking test

* Attempt to show a preview tree on right content

* Fix build error

* Added: Test for Recursive Linking

* Fixed: Borked test for recursive bind

* Fixed: GetChild due to merge

* Add stub SelectableDirectoryNode implementation

* Changed: Linking from ModContentNode now recursively links all children one by one.

* Added: Test for recursive unlinking.

* Basic view wiring for SelectableDirectoryNode

* Added: Test to ensure Child node is deleted in CanUnlinkFolders

* Bunch of fixes to fix crash in preview.
Crash is fixed but items don't show as wanted still

* Changed: AddChild to AddChildren in LocationPreviewVM

* Fix IBodyViewModel not extending IViewModel
- Avoid tree freeze by showing empty preview on the right for now.

* Fix preview trees being broken

* Fix Preview tree entry generation and display

* [WIP] Added: Tree Creation in SelectableDirectoryNode

* Infrastructure for select tree

* Added: Tree Creation in SelectableDirectoryNode 2/2

* Changed: Locale is now updated via changing CurrentUICulture

* Reuse Shared Components in AdvancedInstaller Projects (fixes #703)

* Various fixes for Select location tree view preview.

* Fix black expanding caret on select tree view

* Add other statuses types to Select tree element preview

* Fix placement of remove created folder button

* Remove Unneeded Dependency from AdvancedInstaller

* Added: Basic [Untested] AdvancedInstaller UI Bootstrapper

* Some textbox fixes

* Wire up Data for AdvancedInstaller UI Bootstrapping

* Fixed: Borked DesignViewModels from Last Commit

* Fixed build errors due to merge conflicts

* Wire up UnsupportedModOverlay & Remove Unused Using(s)

* Removed: Unused using(s) post merge

* Added: Misc cleanup of .xaml files tied to ModContentView

* Fixed: ModContentView referencing wrong ViewModel

* [1/3] [ModContent] Separate out TreeView(s)

* Added: Clarification on 'X Select Location'

* [2/3] [Results] Separate out TreeView(s)

* Optimized TreeEntryView UI XAML

* [3/3] [SuggestedEntry] Separate out TreeView(s)

* Changed: Rename SelectableDirectoryEntryView as TreeEntryView

* Added: Additional Preview for SelectableDirectory TreeEntry

* Added: Bugfix for Empty Folder in LocationPreviewTreeView

+ small cleanup

* Removed: Redundant Parenthesis

* Added: Wire up SelectLocationViewModel at Runtime

* Changed: Excluded Views from Code Coverage for AdvancedInstaller.UI

* Removed: Dead Method in SelectableDirectory TreeEntry

* Test Some Remaining ViewModels & Exclude Design ViewModels

* Add Cancel and Install settable commands to the Advanced installer footer.

* Bind the top right close button to decline command

* Added: General Tests for Actually Running AdvancedInstaller

* Changed: Apply R# refactoring guidance

* Add and Bind commands for ModContent TreeEntry

* Add CreateFolder entries in the Select tree

* Added: Ability for MainBody to receive notifications from sub-controls

* Use Encapsulating IAdvancedInstallerCoordinator

* Fixed: Remaining Test

* Added: Tests for BodyViewModel, Fix bug in SelectableDirectoryNode + Some Cleanup

* Changed: UnlinkableItems should be null by default

* Changed: OnDirectorySelected Should Check for Regular, not Created

* Added: Tests for MultiSelect Support for Binding

* Added: Support for re-linking via unlinking last item

* Added: Support for unlinking from ModContent

* Fixed: Bug where right hand side empty folder was leftover.

* - Move coordinator stuff from the View to the VMs of the TreeEntries
- Use Subjects and RX to subscribe to changes deeper in the Trees.
- Fixed tests.

* Fix build after Merge with main

* Fix exception because OverlayController wasn't called from UI thread.

* Add temporary way to invoke AdvancedInstaller when installing mods for SSE or SLE.

* Cleanup Advanced installer ui code structure a bit

* Implement Cancel buttons functionality and abort install
Bind Install button and make it disabled depending on CanInstall property

* Implement full select and deselect functionality for ModContent tree.
- Removed previous code that didn't satisfy requirements.

* Populate SuggestedEntries list on SelectPrview view

* Fix default overlay dimensions

* Pass LoadoutId to ModInstaller.GetModsAsync to allow querying loadout
Do some workaround to avoid circular dependency

* Show mod Name in Advanced installer UI

* Dispose of Subjects in AdvancedInstaller BodyVM

* WIP: Getting Linking working. This binds all the commands, functionality is broken.

* Fix Preview target generation to actually create and link to the item nodes rather than the parent nodes

* Fix mapping All Files to Root location creating extra folder

* Various fixes for unlinking:
- Remove all empty anchestors (root removal is still WIP)
- Cleanup with naming

* Wip fixes

* Wip cleanup

* Properly cleanup Root items from Preview view when left empty

* Fix always linking subfolders if they were excluded

* Show correct linked location name on Mod Content linked items

* Code cleanup

* Implement unlinking from Preview side as well

* - Unlink parent folder when all children are unlinked
- Deselect parent folder when all children are deselected

* Enable Install button when there is something to install and disable otherwise

* WIP: switch to IViewModelInterface from IViewModel

* Implement Create Folder functionality:
- Editing
- Validation
- Creation
- Removal

* General warning and code cleanup

* Fix all the AdvancedInstaller Tests

* Disable Manual Installer from bethesda games as it breaks Network and CLI tests

* Use IViewModelInterface instead of just IViewModel

* Fix crash caused by Game adding a file as a LocationId

* Remove min size from overlay

* Use 1024x576 to better accommodate smaller screen sizes

* Localize fallback mod name

---------

Co-authored-by: sewer56 <[email protected]>
Co-authored-by: Sewer <[email protected]>
  • Loading branch information
3 people authored Nov 7, 2023
1 parent 9182f89 commit 4486ef2
Show file tree
Hide file tree
Showing 149 changed files with 6,733 additions and 164 deletions.
6 changes: 4 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
<PackageVersion Include="NexusMods.Hashing.xxHash64" Version="1.0.1" />
<PackageVersion Include="NexusMods.Paths" Version="0.1.8" />
<PackageVersion Include="NexusMods.Archives.Nx" Version="0.3.7" />
<PackageVersion Include="NexusMods.Paths.TestingHelpers" Version="0.1.8" />
<PackageVersion Include="NexusMods.Telemetry.OpenTelemetry" Version="1.0.0" />
<PackageVersion Include="FomodInstaller.Interface" Version="1.2.0" />
<PackageVersion Include="FomodInstaller.Interface" Version="1.2.0" />
<PackageVersion Include="FomodInstaller.Scripting.XmlScript" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
Expand All @@ -17,6 +18,7 @@
<!-- Avalonia -->
<PackageVersion Include="Avalonia" Version="11.0.5" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.5" />
<PackageVersion Include="Avalonia.Controls.TreeDataGrid" Version="11.0.1" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.5" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.5" />
<PackageVersion Include="Avalonia.Headless" Version="11.0.5" />
Expand Down Expand Up @@ -98,4 +100,4 @@
<PackageVersion Include="Splat.Microsoft.Extensions.Logging" Version="14.6.37" />
<PackageVersion Include="Vogen" Version="3.0.20" />
</ItemGroup>
</Project>
</Project>
14 changes: 14 additions & 0 deletions NexusMods.App.sln
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.AdvancedIns
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.AdvancedInstaller", "src\Games\NexusMods.Games.AdvancedInstaller\NexusMods.Games.AdvancedInstaller.csproj", "{E7E68E07-6732-4FA4-BC4D-6E4249B592FD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.AdvancedInstaller.UI", "src\Games\NexusMods.Games.AdvancedInstaller.UI\NexusMods.Games.AdvancedInstaller.UI.csproj", "{07B8ACA6-CE4B-496D-B183-63A57C5F08E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.AdvancedInstaller.UI.Tests", "tests\Games\NexusMods.Games.AdvancedInstaller.UI.Tests\NexusMods.Games.AdvancedInstaller.UI.Tests.csproj", "{2BFAAE53-AFFF-4F0B-AD76-67918665F298}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -313,6 +317,14 @@ Global
{E7E68E07-6732-4FA4-BC4D-6E4249B592FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7E68E07-6732-4FA4-BC4D-6E4249B592FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7E68E07-6732-4FA4-BC4D-6E4249B592FD}.Release|Any CPU.Build.0 = Release|Any CPU
{07B8ACA6-CE4B-496D-B183-63A57C5F08E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07B8ACA6-CE4B-496D-B183-63A57C5F08E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07B8ACA6-CE4B-496D-B183-63A57C5F08E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07B8ACA6-CE4B-496D-B183-63A57C5F08E1}.Release|Any CPU.Build.0 = Release|Any CPU
{2BFAAE53-AFFF-4F0B-AD76-67918665F298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BFAAE53-AFFF-4F0B-AD76-67918665F298}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BFAAE53-AFFF-4F0B-AD76-67918665F298}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BFAAE53-AFFF-4F0B-AD76-67918665F298}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -368,6 +380,8 @@ Global
{500CE772-93C3-4DA9-9AB3-E9E9EC0A9429} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
{63BC2EE7-18E5-4887-913A-4843DBBE2C8D} = {05B06AC1-7F2B-492F-983E-5BC63CDBF20D}
{E7E68E07-6732-4FA4-BC4D-6E4249B592FD} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
{07B8ACA6-CE4B-496D-B183-63A57C5F08E1} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
{2BFAAE53-AFFF-4F0B-AD76-67918665F298} = {05B06AC1-7F2B-492F-983E-5BC63CDBF20D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}
Expand Down
2 changes: 1 addition & 1 deletion docs/LocalizationAndTranslation.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ This file is in the `NexusMods.App` project at time of writing and is copied to
Language can be switched at runtime with the following code:

```
Language.Culture = new CultureInfo(/* locale */);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(/* locale */);
```
Where `locale` is a string representing the locale, e.g. `it` for Italian.

Expand Down
169 changes: 169 additions & 0 deletions src/Games/NexusMods.Games.AdvancedInstaller.UI/AdvancedInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System.Reactive.Disposables;
using Avalonia.ReactiveUI;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.App.UI.Overlays;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.Loadouts.Mods;
using NexusMods.DataModel.ModInstallers;
using NexusMods.Games.AdvancedInstaller.UI.Resources;
using NexusMods.Paths;
using NexusMods.Paths.FileTree;

namespace NexusMods.Games.AdvancedInstaller.UI;

// Temporary.
#pragma warning disable CS1998

/// <summary>
/// Provides the implementation of the 'Advanced Installer' functionality.
/// </summary>
/// <typeparam name="TUnsupportedOverlayFactory">Use <see cref="UnsupportedModOverlayViewModelFactory"/>, or alternative for testing.</typeparam>
/// <typeparam name="TAdvancedInstallerOverlayViewModelFactory">Use <see cref="AdvancedInstallerOverlayViewModelFactory"/>, or alternative for testing.</typeparam>
public class AdvancedInstaller<TUnsupportedOverlayFactory, TAdvancedInstallerOverlayViewModelFactory> : IModInstaller
where TUnsupportedOverlayFactory : IUnsupportedModOverlayViewModelFactory
where TAdvancedInstallerOverlayViewModelFactory : IAdvancedInstallerOverlayViewModelFactory
{
private readonly IOverlayController _overlayController;
private readonly Lazy<LoadoutRegistry> _loadoutRegistry;
private readonly IServiceProvider _provider;

public static AdvancedInstaller<UnsupportedModOverlayViewModelFactory, AdvancedInstallerOverlayViewModelFactory>
Create(IServiceProvider provider) =>
new(provider.GetRequiredService<IOverlayController>(), provider);

public AdvancedInstaller(IOverlayController overlayController, IServiceProvider provider)
{
_overlayController = overlayController;
_provider = provider;
// Delay to avoid circular dependency.
_loadoutRegistry = new Lazy<LoadoutRegistry>(provider.GetRequiredService<LoadoutRegistry>);
}

public async ValueTask<IEnumerable<ModInstallerResult>> GetModsAsync(
GameInstallation gameInstallation,
LoadoutId loadoutId,
ModId baseModId,
FileTreeNode<RelativePath, ModSourceFileEntry> archiveFiles,
CancellationToken cancellationToken = default)
{
// Get default name of the mod for UI purposes.
Loadout? loadout = null;
Mod? mod = null;
if (loadoutId != LoadoutId.Null)
{
loadout = _loadoutRegistry.Value.Get(loadoutId);
loadout?.Mods.TryGetValue(baseModId, out mod);
}

var modName = mod?.Name ?? Language.AdvancedInstaller_Manual_Mod;


// Note: This code is effectively a stub.
var (shouldInstall, deploymentData) = await GetDeploymentDataAsync(gameInstallation, loadout, modName,
baseModId, archiveFiles, cancellationToken);

if (!shouldInstall)
return Array.Empty<ModInstallerResult>();

return new[]
{
new ModInstallerResult
{
Id = baseModId,
Files = deploymentData.EmitOperations(archiveFiles)
}
};
}

private async Task<(bool shouldInstall, DeploymentData data)> GetDeploymentDataAsync(
GameInstallation gameInstallation, Loadout? loadout, string modName, ModId baseModId,
FileTreeNode<RelativePath, ModSourceFileEntry> archiveFiles, CancellationToken cancellationToken)
{
var showInstaller = await ShowUnsupportedModOverlay(modName);

// TODO: Abort this somehow so if user closes dialog, the installed data does not change in db.
if (!showInstaller)
return (false, new DeploymentData());

// This is a stub, until we implement some UI logic to pull this data
return await ShowAdvancedInstallerOverlay(modName, archiveFiles, gameInstallation.LocationsRegister,
gameInstallation.Game.Name);
}

private async Task<bool> ShowUnsupportedModOverlay(string modName, object? referenceItem = null)
{
var tcs = new TaskCompletionSource<bool>();
var vm = TUnsupportedOverlayFactory.Create(modName);
OnUi(_overlayController,
controller => { controller.SetOverlayContent(new SetOverlayItem(vm, referenceItem), tcs); });
await tcs.Task;
return vm.ShouldAdvancedInstall;
}

private async Task<(bool shouldInstall, DeploymentData data)> ShowAdvancedInstallerOverlay(string modName,
FileTreeNode<RelativePath, ModSourceFileEntry> archiveFiles, GameLocationsRegister register,
string gameName = "", object? referenceItem = null)
{
var tcs = new TaskCompletionSource<bool>();
var vm = TAdvancedInstallerOverlayViewModelFactory.Create(archiveFiles, register, gameName, modName);
OnUi(_overlayController,
controller => { _overlayController.SetOverlayContent(new SetOverlayItem(vm, referenceItem), tcs); });
await tcs.Task;
return (!vm.WasCancelled, vm.BodyViewModel.Data);
}

private static void OnUi<TState>(TState state, Action<TState> action)
{
// NOTE: AvaloniaScheduler has to be used to do work on the UI thread
AvaloniaScheduler.Instance.Schedule(
(action, state),
AvaloniaScheduler.Instance.Now,
(_, tuple) =>
{
var (innerAction, innerState) = tuple;
innerAction(innerState);
return Disposable.Empty;
});
}
}

/// <summary>
/// Factory for creating instances of <see cref="IUnsupportedModOverlayViewModel" />.
/// </summary>
public interface IUnsupportedModOverlayViewModelFactory
{
static abstract IUnsupportedModOverlayViewModel Create(string modName);
}

public class UnsupportedModOverlayViewModelFactory : IUnsupportedModOverlayViewModelFactory
{
public static IUnsupportedModOverlayViewModel Create(string modName)
{
var overlay = new UnsupportedModOverlayViewModel(modName);
return overlay;
}
}

/// <summary>
/// Factory for creating instances of <see cref="IAdvancedInstallerOverlayViewModel" />.
/// </summary>
public interface IAdvancedInstallerOverlayViewModelFactory
{
static abstract IAdvancedInstallerOverlayViewModel Create(
FileTreeNode<RelativePath, ModSourceFileEntry> archiveFiles, GameLocationsRegister register,
string gameName = "",
string modName = ""
);
}

public class AdvancedInstallerOverlayViewModelFactory : IAdvancedInstallerOverlayViewModelFactory
{
public static IAdvancedInstallerOverlayViewModel Create(FileTreeNode<RelativePath, ModSourceFileEntry> archiveFiles,
GameLocationsRegister register,
string gameName = "", string modName = "")
{
var overlay = new AdvancedInstallerOverlayViewModel(modName, archiveFiles, register, gameName);
return overlay;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NexusMods.Paths;

namespace NexusMods.Games.AdvancedInstaller.UI;

/// <summary>
/// An interface that informs the node adding process whether an item has previously existed.
/// </summary>
public interface ICheckIfItemAlreadyExists
{
/// <summary>
/// Returns true if the given path already exist
/// </summary>
/// <param name="path">The path to validate if it already exists in the game folder.</param>
/// <returns>True if this path already exists, else false.</returns>
bool AlreadyExists(GamePath path);
}

/// <summary>
/// A checker that always returns false.
/// </summary>
internal struct AlwaysFalseChecker : ICheckIfItemAlreadyExists
{
public bool AlreadyExists(GamePath path) => true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using NexusMods.Games.AdvancedInstaller.UI.Content.Right.Results.SelectLocation;
using NexusMods.Paths;

namespace NexusMods.Games.AdvancedInstaller.UI;

/// <summary>
/// An interface for an item which can be bound to a source file/folder within a game mod archive.
/// </summary>
/// <remarks>
/// This is part of <see cref="ISuggestedEntryNode" /> but separated for easier testing.
/// </remarks>
public interface IModContentBindingTarget : IUnlinkableItem
{
/// <summary>
/// Returns the child of this target, i.e. child node in target's tree.
/// </summary>
/// <param name="name">The name of the child.</param>
/// <param name="isDirectory">If this child does not exist, it will be created as directory if this is true.</param>
/// <remarks>
/// If the child does not exist, it may be created.
/// </remarks>
IModContentBindingTarget GetOrCreateChild(string name, bool isDirectory);

/// <summary>
/// Binds to the path represented by this target.
/// </summary>
/// <param name="unlinkable">You can use this item for unlinking.</param>
/// <param name="data">The deployment data to update, in case an existing target requires to be unbound.</param>
/// <param name="previouslyExisted">This location previously existed (on FileSystem, or as result of another mod binding into that folder).</param>
GamePath Bind(IUnlinkableItem unlinkable, DeploymentData data, bool previouslyExisted);

/// <summary>
/// Name of the directory where the file was linked (the parent of the linked file).
/// </summary>
string DirectoryName { get; }

/// <summary>
/// The file name of the linked location
/// </summary>
string FileName { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace NexusMods.Games.AdvancedInstaller.UI;

/// <summary>
/// Represents an item that can be unlinked from the deployment data.
/// </summary>
public interface IUnlinkableItem
{
/// <summary>
/// Removes itself and all of its children recursively from the deployment data.
/// </summary>
/// <param name="data">The deployment data.</param>
/// <param name="isCalledFromDoubleLinkedItem">
/// If this is true, the <see cref="Unlink"/> method was called from another <see cref="IUnlinkableItem"/> which
/// is being unlinked (it's doubly linked to this item).
///
/// If this is true, do not call `unlink` on the other item.
/// </param>
public void Unlink(bool isCalledFromDoubleLinkedItem);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using NexusMods.Games.AdvancedInstaller.UI.Content.Left;
using NexusMods.Games.AdvancedInstaller.UI.Content.Right.Results.EmptyPreview;
using NexusMods.Games.AdvancedInstaller.UI.Content.Right.Results.PreviewView;
using NexusMods.Games.AdvancedInstaller.UI.Content.Right.Results.SelectLocation;
using ReactiveUI.Fody.Helpers;

namespace NexusMods.Games.AdvancedInstaller.UI.Content;

[ExcludeFromCodeCoverage]
// ReSharper disable once UnusedType.Global
internal class BodyDesignViewModel : AViewModel<IBodyViewModel>,
IBodyViewModel
{
public string ModName { get; set; } = "Design Mod Name";
public IModContentViewModel ModContentViewModel { get; } = new ModContentDesignViewModel();
public IPreviewViewModel PreviewViewModel { get; } = new PreviewDesignViewModel();

public IEmptyPreviewViewModel EmptyPreviewViewModel { get; } =
new EmptyPreviewDesignViewModel();

public ISelectLocationViewModel SelectLocationViewModel { get; } = new SelectLocationDesignViewModel();
[Reactive] public bool CanInstall { get; set; } = false;

public IViewModelInterface CurrentPreviewViewModel => PreviewViewModel;
public DeploymentData Data { get; } = new();
}
Loading

0 comments on commit 4486ef2

Please sign in to comment.