Skip to content

Commit 5a3a505

Browse files
authored
Merge pull request #2858 from Nexus-Mods/feat/2662
"Remove Game"-overlay
2 parents d39d4b7 + fbe3136 commit 5a3a505

14 files changed

+898
-1434
lines changed

src/NexusMods.App.UI/NexusMods.App.UI.csproj.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=leftmenu_005Cempty/@EntryIndexedValue">True</s:Boolean>
1313
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=leftmenu_005Citems_005Capplycontrol/@EntryIndexedValue">True</s:Boolean>
1414
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=overlays_005Clogin_005Cstartupmessagebox/@EntryIndexedValue">True</s:Boolean>
15+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=overlays_005Cremovegame/@EntryIndexedValue">True</s:Boolean>
1516
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=overlays_005Cupgradetopremium/@EntryIndexedValue">True</s:Boolean>
1617
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pages_005Cdiagnostics_005Cdetails/@EntryIndexedValue">True</s:Boolean>
1718
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pages_005Cdiagnostics_005Clist/@EntryIndexedValue">True</s:Boolean>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using NexusMods.Paths;
2+
using R3;
3+
4+
namespace NexusMods.App.UI.Overlays;
5+
6+
public record struct RemoveGameOverlayResult(bool ShouldRemoveGame, bool ShouldDeleteDownloads)
7+
{
8+
public static readonly RemoveGameOverlayResult Cancel = new(ShouldRemoveGame: false, ShouldDeleteDownloads: false);
9+
}
10+
11+
public interface IRemoveGameOverlayViewModel : IOverlayViewModel<RemoveGameOverlayResult>
12+
{
13+
string GameName { get; }
14+
15+
int NumDownloads { get; }
16+
17+
Size SumDownloadsSize { get; }
18+
19+
int NumCollections { get; }
20+
21+
BindableReactiveProperty<bool> ShouldDeleteDownloads { get; }
22+
23+
ReactiveCommand<Unit> CommandCancel { get; }
24+
25+
ReactiveCommand<Unit> CommandRemove { get; }
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.GameLocators;
2+
using NexusMods.Paths;
3+
using R3;
4+
5+
namespace NexusMods.App.UI.Overlays;
6+
7+
public class RemoveGameOverlayDesignViewModel : AOverlayViewModel<IRemoveGameOverlayViewModel, RemoveGameOverlayResult>, IRemoveGameOverlayViewModel
8+
{
9+
public required string GameName { get; init; } = "Stardew Valley";
10+
public required int NumDownloads { get; init; } = 98;
11+
public required Size SumDownloadsSize { get; init; } = Size.From(3435678);
12+
public required int NumCollections { get; init; } = 3;
13+
public BindableReactiveProperty<bool> ShouldDeleteDownloads { get; } = new(value: false);
14+
public ReactiveCommand<Unit> CommandCancel { get; }
15+
public ReactiveCommand<Unit> CommandRemove { get; }
16+
17+
public RemoveGameOverlayDesignViewModel()
18+
{
19+
CommandCancel = new ReactiveCommand(_ => Complete(result: RemoveGameOverlayResult.Cancel));
20+
CommandRemove = new ReactiveCommand(_ => Complete(result: new RemoveGameOverlayResult(ShouldRemoveGame: true, ShouldDeleteDownloads: ShouldDeleteDownloads.Value)));
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<reactive:ReactiveUserControl
2+
x:TypeArguments="local:IRemoveGameOverlayViewModel"
3+
xmlns="https://github.com/avaloniaui"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
7+
xmlns:reactive="http://reactiveui.net"
8+
xmlns:local="clr-namespace:NexusMods.App.UI.Overlays"
9+
xmlns:base="clr-namespace:NexusMods.App.UI.Overlays.Generic.MessageBox.Base"
10+
xmlns:controls="clr-namespace:NexusMods.App.UI.Controls"
11+
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="350"
12+
x:Class="NexusMods.App.UI.Overlays.RemoveGameOverlayView">
13+
14+
<Design.DataContext>
15+
<local:RemoveGameOverlayDesignViewModel />
16+
</Design.DataContext>
17+
18+
<base:MessageBoxBackground MinWidth="576" MaxWidth="576">
19+
<base:MessageBoxBackground.TopContent>
20+
<Border Padding="24">
21+
<StackPanel Spacing="16">
22+
<TextBlock
23+
x:Name="TitleText"
24+
TextWrapping="Wrap"
25+
Theme="{StaticResource HeadingXSSemiTheme}" />
26+
<TextBlock
27+
x:Name="DescriptionText"
28+
TextWrapping="Wrap"
29+
Theme="{StaticResource BodyMDNormalTheme}" />
30+
31+
<Grid ColumnDefinitions="Auto, *">
32+
<ToggleSwitch x:Name="SwitchDeleteDownloads"
33+
Classes="ExtraSmall"
34+
OnContent="{x:Null}"
35+
OffContent="{x:Null}" />
36+
<TextBlock Grid.Column="1"
37+
x:Name="ToggleDescription"
38+
Margin="12,0,0,0"
39+
TextWrapping="Wrap"
40+
Theme="{StaticResource BodyMDNormalTheme}" />
41+
</Grid>
42+
</StackPanel>
43+
</Border>
44+
45+
</base:MessageBoxBackground.TopContent>
46+
47+
<base:MessageBoxBackground.BottomContent>
48+
<StackPanel Orientation="Horizontal"
49+
Margin="24"
50+
VerticalAlignment="Center"
51+
HorizontalAlignment="Right"
52+
Spacing="{StaticResource Spacing-2.5}">
53+
<controls:StandardButton x:Name="ButtonCancel" />
54+
<controls:StandardButton x:Name="ButtonRemove"
55+
Type="Secondary"
56+
Fill="None"
57+
Foreground="{StaticResource DangerStrongBrush}"/>
58+
</StackPanel>
59+
</base:MessageBoxBackground.BottomContent>
60+
</base:MessageBoxBackground>
61+
62+
</reactive:ReactiveUserControl>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Avalonia.ReactiveUI;
2+
using Humanizer;
3+
using Humanizer.Bytes;
4+
using NexusMods.App.UI.Resources;
5+
using R3;
6+
using ReactiveUI;
7+
8+
namespace NexusMods.App.UI.Overlays;
9+
10+
public partial class RemoveGameOverlayView : ReactiveUserControl<IRemoveGameOverlayViewModel>
11+
{
12+
public RemoveGameOverlayView()
13+
{
14+
InitializeComponent();
15+
16+
this.WhenActivated(disposables =>
17+
{
18+
this.WhenAnyValue(view => view.ViewModel)
19+
.WhereNotNull()
20+
.Subscribe(viewModel =>
21+
{
22+
TitleText.Text = string.Format(Language.RemoveGameOverlayView_Title, viewModel.GameName);
23+
DescriptionText.Text = string.Format(Language.RemoveGameOverlayView_Description, viewModel.GameName);
24+
ToggleDescription.Text = string.Format(Language.RemoveGameOverlayView_ToggleDescription, viewModel.NumDownloads.ToString("N0"), viewModel.NumCollections.ToString("N0"), viewModel.GameName);
25+
ButtonCancel.Text = Language.RemoveGameOverlayView_CancelButton;
26+
}).AddTo(disposables);
27+
28+
this.WhenAnyValue(
29+
view => view.ViewModel!.ShouldDeleteDownloads.Value,
30+
view => view.ViewModel!.SumDownloadsSize)
31+
.Subscribe(tuple =>
32+
{
33+
var (shouldDeleteDownloads, _) = tuple;
34+
35+
if (shouldDeleteDownloads)
36+
{
37+
ButtonRemove.Text = Language.RemoveGameOverlayView_RemoveButton_AlsoDelete;
38+
// ButtonRemove.Text = string.Format(Language.RemoveGameOverlayView_RemoveButton_AlsoDelete, ByteSize.FromBytes(sumDownloadsSize.Value).Humanize());
39+
}
40+
else
41+
{
42+
ButtonRemove.Text = Language.RemoveGameOverlayView_RemoveButton;
43+
}
44+
})
45+
.AddTo(disposables);
46+
47+
this.Bind(ViewModel, vm => vm.ShouldDeleteDownloads.Value, view => view.SwitchDeleteDownloads.IsChecked)
48+
.AddTo(disposables);
49+
50+
this.BindCommand(ViewModel, vm => vm.CommandCancel, view => view.ButtonCancel)
51+
.AddTo(disposables);
52+
53+
this.BindCommand(ViewModel, vm => vm.CommandRemove, view => view.ButtonRemove)
54+
.AddTo(disposables);
55+
});
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.GameLocators;
2+
using NexusMods.Paths;
3+
using R3;
4+
5+
namespace NexusMods.App.UI.Overlays;
6+
7+
public class RemoveGameOverlayViewModel : AOverlayViewModel<IRemoveGameOverlayViewModel, RemoveGameOverlayResult>, IRemoveGameOverlayViewModel
8+
{
9+
public required string GameName { get; init; }
10+
public required int NumDownloads { get; init; }
11+
public required Size SumDownloadsSize { get; init; }
12+
public required int NumCollections { get; init; }
13+
public BindableReactiveProperty<bool> ShouldDeleteDownloads { get; } = new(value: false);
14+
public ReactiveCommand<Unit> CommandCancel { get; }
15+
public ReactiveCommand<Unit> CommandRemove { get; }
16+
17+
public RemoveGameOverlayViewModel()
18+
{
19+
CommandCancel = new ReactiveCommand(_ => Complete(result: RemoveGameOverlayResult.Cancel));
20+
CommandRemove = new ReactiveCommand(_ => Complete(result: new RemoveGameOverlayResult(ShouldRemoveGame: true, ShouldDeleteDownloads: ShouldDeleteDownloads.Value)));
21+
}
22+
}

src/NexusMods.App.UI/Pages/ILibraryDataProvider.cs

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using NexusMods.Abstractions.GameLocators;
77
using NexusMods.Abstractions.Library.Models;
88
using NexusMods.Abstractions.Loadouts;
9+
using NexusMods.Abstractions.NexusWebApi.Types.V2;
910
using NexusMods.App.UI.Controls;
1011
using NexusMods.App.UI.Extensions;
1112
using NexusMods.App.UI.Pages.LibraryPage;
@@ -19,6 +20,11 @@ public interface ILibraryDataProvider
1920
IObservable<IChangeSet<CompositeItemModel<EntityId>, EntityId>> ObserveLibraryItems(LibraryFilter libraryFilter);
2021

2122
IObservable<int> CountLibraryItems(LibraryFilter libraryFilter);
23+
24+
/// <summary>
25+
/// Returns all library files for the given game.
26+
/// </summary>
27+
LibraryFile.ReadOnly[] GetAllFiles(GameId gameId, IDb? db = null);
2228
}
2329

2430
public record LibraryFilter(LoadoutId LoadoutId, ILocatableGame Game);

src/NexusMods.App.UI/Pages/LocalFileDataProvider.cs

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.DependencyInjection;
66
using NexusMods.Abstractions.Library.Models;
77
using NexusMods.Abstractions.Loadouts;
8+
using NexusMods.Abstractions.NexusWebApi.Types.V2;
89
using NexusMods.App.UI.Controls;
910
using NexusMods.App.UI.Pages.LibraryPage;
1011
using NexusMods.MnemonicDB.Abstractions;
@@ -23,6 +24,9 @@ public LocalFileDataProvider(IServiceProvider serviceProvider)
2324
_connection = serviceProvider.GetRequiredService<IConnection>();
2425
}
2526

27+
// TODO: update once we have game information on Local Files
28+
public LibraryFile.ReadOnly[] GetAllFiles(GameId gameId, IDb? db = null) => [];
29+
2630
public IObservable<IChangeSet<CompositeItemModel<EntityId>, EntityId>> ObserveLibraryItems(LibraryFilter libraryFilter)
2731
{
2832
return LocalFile

src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs

+37-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
using NexusMods.Abstractions.Loadouts.Synchronizers;
1515
using NexusMods.App.UI.Controls.GameWidget;
1616
using NexusMods.App.UI.Controls.MiniGameWidget;
17-
using NexusMods.App.UI.Pages.LoadoutPage;
1817
using NexusMods.App.UI.Resources;
1918
using NexusMods.App.UI.Windows;
2019
using NexusMods.App.UI.WorkspaceSystem;
@@ -26,12 +25,17 @@
2625
using System.Reactive;
2726
using System.Reactive.Linq;
2827
using DynamicData.Aggregation;
28+
using NexusMods.Abstractions.Library;
29+
using NexusMods.Abstractions.Library.Models;
30+
using NexusMods.Abstractions.NexusModsLibrary.Models;
2931
using NexusMods.Abstractions.Settings;
3032
using NexusMods.App.UI.Extensions;
3133
using NexusMods.App.UI.Overlays;
3234
using NexusMods.App.UI.Overlays.AlphaWarning;
3335
using NexusMods.App.UI.Pages.LibraryPage;
36+
using NexusMods.Collections;
3437
using NexusMods.CrossPlatform.Process;
38+
using NexusMods.Paths;
3539
using NexusMods.Telemetry;
3640

3741
namespace NexusMods.App.UI.Pages.MyGames;
@@ -41,6 +45,8 @@ public class MyGamesViewModel : APageViewModel<IMyGamesViewModel>, IMyGamesViewM
4145
{
4246
private const string TrelloPublicRoadmapUrl = "https://trello.com/b/gPzMuIr3/nexus-mods-app-roadmap";
4347

48+
private readonly ILibraryService _libraryService;
49+
private readonly CollectionDownloader _collectionDownloader;
4450
private readonly IWindowManager _windowManager;
4551
private readonly IJobMonitor _jobMonitor;
4652

@@ -65,6 +71,10 @@ public MyGamesViewModel(
6571
var settingsManager = serviceProvider.GetRequiredService<ISettingsManager>();
6672
var experimentalSettings = settingsManager.Get<ExperimentalSettings>();
6773

74+
var libraryDataProviders = serviceProvider.GetServices<ILibraryDataProvider>().ToArray();
75+
76+
_collectionDownloader = serviceProvider.GetRequiredService<CollectionDownloader>();
77+
_libraryService = serviceProvider.GetRequiredService<ILibraryService>();
6878
_jobMonitor = serviceProvider.GetRequiredService<IJobMonitor>();
6979

7080
TabTitle = Language.MyGames;
@@ -123,8 +133,24 @@ public MyGamesViewModel(
123133
{
124134
if (GetJobRunningForGameInstallation(installation).IsT2) return;
125135

136+
var filesToDelete = libraryDataProviders.SelectMany(dataProvider => dataProvider.GetAllFiles(gameId: installation.Game.GameId)).ToArray();
137+
var totalSize = filesToDelete.Sum(static Size (file) => file.Size);
138+
139+
var collections = CollectionDownloader.GetCollections(conn.Db, installation.Game.GameId);
140+
141+
var overlay = new RemoveGameOverlayViewModel
142+
{
143+
GameName = installation.Game.Name,
144+
NumDownloads = filesToDelete.Length,
145+
SumDownloadsSize = totalSize,
146+
NumCollections = collections.Length,
147+
};
148+
149+
var result = await overlayController.EnqueueAndWait(overlay);
150+
if (!result.ShouldRemoveGame) return;
151+
126152
vm.State = GameWidgetState.RemovingGame;
127-
await Task.Run(async () => await RemoveAllLoadouts(installation));
153+
await Task.Run(async () => await RemoveGame(installation, shouldDeleteDownloads: result.ShouldDeleteDownloads, filesToDelete, collections));
128154
vm.State = GameWidgetState.DetectedGame;
129155

130156
Tracking.AddEvent(Events.Game.RemoveGame, new EventMetadata(name: installation.Game.Name));
@@ -206,9 +232,17 @@ private OneOf<None, CreateLoadoutJob, UnmanageGameJob> GetJobRunningForGameInsta
206232
return OneOf<None, CreateLoadoutJob, UnmanageGameJob>.FromT0(new None());
207233
}
208234

209-
private async Task RemoveAllLoadouts(GameInstallation installation)
235+
private async Task RemoveGame(GameInstallation installation, bool shouldDeleteDownloads, LibraryFile.ReadOnly[] filesToDelete, CollectionMetadata.ReadOnly[] collections)
210236
{
211237
await installation.GetGame().Synchronizer.UnManage(installation);
238+
239+
if (!shouldDeleteDownloads) return;
240+
await _libraryService.RemoveItems(filesToDelete.Select(file => file.AsLibraryItem()));
241+
242+
foreach (var collection in collections)
243+
{
244+
await _collectionDownloader.DeleteCollection(collection);
245+
}
212246
}
213247

214248
private async Task ManageGame(GameInstallation installation)

src/NexusMods.App.UI/Pages/NexusModsDataProvider.cs

+24
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
using NexusMods.Abstractions.Library.Models;
88
using NexusMods.Abstractions.Loadouts;
99
using NexusMods.Abstractions.NexusModsLibrary;
10+
using NexusMods.Abstractions.NexusWebApi.Types.V2;
1011
using NexusMods.Abstractions.Resources;
1112
using NexusMods.App.UI.Controls;
1213
using NexusMods.App.UI.Extensions;
1314
using NexusMods.App.UI.Pages.LibraryPage;
1415
using NexusMods.MnemonicDB.Abstractions;
1516
using NexusMods.MnemonicDB.Abstractions.DatomIterators;
17+
using NexusMods.MnemonicDB.Abstractions.IndexSegments;
1618
using NexusMods.MnemonicDB.Abstractions.Query;
1719
using NexusMods.Networking.NexusWebApi;
1820
using NuGet.Versioning;
@@ -34,6 +36,28 @@ public NexusModsDataProvider(IServiceProvider serviceProvider)
3436
_thumbnailLoader = new Lazy<IResourceLoader<EntityId, Bitmap>>(() => ImagePipelines.GetModPageThumbnailPipeline(serviceProvider));
3537
}
3638

39+
public LibraryFile.ReadOnly[] GetAllFiles(GameId gameId, IDb? db = null)
40+
{
41+
db ??= _connection.Db;
42+
43+
var libraryItems = NexusModsLibraryItem
44+
.All(db)
45+
.Where(libraryItem => libraryItem.AsLibraryItem().TryGetAsLibraryFile(out _))
46+
.GroupBy(libraryItem => libraryItem.FileMetadataId)
47+
.ToDictionary(group => group.Key, group => group.ToArray());
48+
49+
var files = NexusModsFileMetadata
50+
.All(db)
51+
.Where(modPage => modPage.Uid.GameId == gameId)
52+
.Select(fileMetadata => libraryItems.GetValueOrDefault(fileMetadata))
53+
.Where(static arr => arr is not null)
54+
.SelectMany(static x => x!)
55+
.Select(libraryItem => new LibraryFile.ReadOnly(db, libraryItem))
56+
.ToArray();
57+
58+
return files;
59+
}
60+
3761
private IObservable<IChangeSet<NexusModsModPageMetadata.ReadOnly, EntityId>> FilterLibraryItems(LibraryFilter libraryFilter)
3862
{
3963
return NexusModsModPageMetadata

0 commit comments

Comments
 (0)