diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index fa421ed..09ecfbb 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -19,13 +19,13 @@ jobs:
arch: [win10-x64, win10-arm64]
steps:
- - uses: actions/checkout@v3
- - uses: microsoft/setup-msbuild@v1.1
- - uses: actions/setup-dotnet@v2
+ - uses: actions/checkout@v4
+ - uses: microsoft/setup-msbuild@v2
+ - uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
- 7.0.x
+ 8.0.x
- name: Setup NuGet
run: dotnet nuget add source --username github --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/dragonfruitnetwork/index.json"
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index db22077..03fa81e 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -9,21 +9,21 @@ jobs:
quality:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-dotnet@v2
+ - uses: actions/checkout@v4
+ - uses: actions/setup-dotnet@v4
with:
- dotnet-version: |
- "6.0.x"
- "7.0.x"
+ dotnet-version: '8.0.x'
- name: Restore Project/Tools
run: |
dotnet restore
- dotnet tool install --global NVika
dotnet tool install --global JetBrains.ReSharper.GlobalTools
- name: InspectCode
- run: jb inspectcode DragonFruit.Kaplan.sln --output=inspectcodereport.xml --verbosity=WARN --no-build
+ run: jb inspectcode DragonFruit.Kaplan.sln --output=inspectcode.sarif --severity=WARNING --properties:Configuration=Release
- - name: Vika
- run: nvika parsereport "${{github.workspace}}/inspectcodereport.xml"
+ - name: Upload SARIF file
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: inspectcode.sarif
+ category: InspectCode
diff --git a/DragonFruit.Kaplan.sln.DotSettings b/DragonFruit.Kaplan.sln.DotSettings
index e290b33..eba44ac 100644
--- a/DragonFruit.Kaplan.sln.DotSettings
+++ b/DragonFruit.Kaplan.sln.DotSettings
@@ -263,6 +263,7 @@ Licensed under Apache-2. Refer to the LICENSE file for more info
WARNING
WARNING
CA
+ True
True
True
True
diff --git a/DragonFruit.Kaplan/App.axaml.cs b/DragonFruit.Kaplan/App.axaml.cs
index 9ebc9d8..b93bd34 100644
--- a/DragonFruit.Kaplan/App.axaml.cs
+++ b/DragonFruit.Kaplan/App.axaml.cs
@@ -43,7 +43,7 @@ public override void Initialize()
s.MaxBreadcrumbs = 200;
s.MinimumEventLevel = LogLevel.Warning;
- s.SetBeforeSend(e => BugReportingEnabled ? e : null);
+ s.SetBeforeSend(e => BugReportingEnabled && typeof(Program).Assembly.GetName().Version?.Major > 1 ? e : null);
});
});
diff --git a/DragonFruit.Kaplan/DragonFruit.Kaplan.csproj b/DragonFruit.Kaplan/DragonFruit.Kaplan.csproj
index 2715bfe..d2da0f4 100644
--- a/DragonFruit.Kaplan/DragonFruit.Kaplan.csproj
+++ b/DragonFruit.Kaplan/DragonFruit.Kaplan.csproj
@@ -5,7 +5,7 @@
Assets\icon.ico
Debug;Release;DryRun
app.manifest
- net7.0-windows10.0.19041
+ net8.0-windows10.0.19041
true
$(Constants);DRY_RUN
@@ -35,17 +35,16 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
diff --git a/DragonFruit.Kaplan/PackageRemover.cs b/DragonFruit.Kaplan/PackageRemover.cs
new file mode 100644
index 0000000..094f3b4
--- /dev/null
+++ b/DragonFruit.Kaplan/PackageRemover.cs
@@ -0,0 +1,159 @@
+// Kaplan Copyright (c) DragonFruit Network
+// Licensed under Apache-2. Refer to the LICENSE file for more info
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.ApplicationModel;
+using Windows.Management.Deployment;
+using DragonFruit.Kaplan.ViewModels.Enums;
+
+namespace DragonFruit.Kaplan
+{
+ public class PackageRemover
+ {
+ private readonly RemovalOptions _mode;
+ private readonly PackageManager _manager;
+ private readonly IReadOnlyList _packages;
+
+ private Package _currentPackage;
+ private DeploymentProgress _currentPackageRemovalProgress;
+
+ private OperationState _state;
+ private Task _currentRemovalTask;
+
+ public PackageRemover(PackageInstallationMode mode, PackageManager manager, IReadOnlyList packages)
+ {
+ _manager = manager;
+ _packages = packages;
+
+ _mode = mode switch
+ {
+ PackageInstallationMode.Machine => RemovalOptions.RemoveForAllUsers,
+ PackageInstallationMode.User => RemovalOptions.None,
+
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ ///
+ /// The total number of packages to be removed
+ ///
+ public int TotalPackages => _packages.Count;
+
+ ///
+ /// The index of the package currently being removed
+ ///
+ public int CurrentIndex { get; private set; }
+
+ public OperationState State
+ {
+ get => _state;
+ private set
+ {
+ if (_state == value) return;
+
+ _state = value;
+ StateChanged?.Invoke(this, value);
+ }
+ }
+
+ public Package CurrentPackage
+ {
+ get => _currentPackage;
+ private set
+ {
+ _currentPackage = value;
+ CurrentPackageChanged?.Invoke(this, value);
+ }
+ }
+
+ public DeploymentProgress CurrentPackageRemovalProgress
+ {
+ get => _currentPackageRemovalProgress;
+ private set
+ {
+ _currentPackageRemovalProgress = value;
+ CurrentPackageRemovalProgressChanged?.Invoke(this, value);
+ }
+ }
+
+ public event EventHandler StateChanged;
+ public event EventHandler CurrentPackageChanged;
+ public event EventHandler CurrentPackageRemovalProgressChanged;
+
+ ///
+ /// Iterates through the provided packages, removing them from the system.
+ /// If previously cancelled, will continue from the last package.
+ ///
+ public Task RemovePackagesAsync(CancellationToken cancellation = default)
+ {
+ // prevent multiple removal tasks from running at once
+ if (_currentRemovalTask?.Status is TaskStatus.WaitingForActivation or TaskStatus.WaitingToRun or TaskStatus.Running or TaskStatus.Created)
+ {
+ return _currentRemovalTask;
+ }
+
+ return _currentRemovalTask = RemovePackagesAsyncImpl(cancellation);
+ }
+
+ private async Task RemovePackagesAsyncImpl(CancellationToken cancellation)
+ {
+ var removed = 0;
+ State = OperationState.Pending;
+
+ for (int i = CurrentIndex; i < _packages.Count; i++)
+ {
+ if (cancellation.IsCancellationRequested)
+ {
+ State = OperationState.Canceled;
+ break;
+ }
+
+ CurrentIndex = i;
+ CurrentPackage = _packages[i];
+ CurrentPackageRemovalProgress = default;
+
+ try
+ {
+ State = OperationState.Running;
+#if !DRY_RUN
+ var progress = new Progress(p => CurrentPackageRemovalProgress = p);
+ await _manager.RemovePackageAsync(_packages[i].Id.FullName, _mode).AsTask(cancellation, progress).ConfigureAwait(false);
+#else
+ // dummy removal progress
+ for (uint j = 0; j < 50; j++)
+ {
+ await Task.Delay(50, cancellation);
+ CurrentPackageRemovalProgress = new DeploymentProgress(DeploymentProgressState.Processing, j * 2);
+ }
+#endif
+ removed++;
+ }
+ catch (OperationCanceledException)
+ {
+ State = OperationState.Canceled;
+ return removed;
+ }
+ catch
+ {
+ State = OperationState.Errored;
+ return removed;
+ }
+ }
+
+ State = OperationState.Completed;
+ return removed;
+ }
+
+ public enum OperationState
+ {
+ Pending,
+ Running,
+ Errored,
+ Completed,
+ Canceled
+ }
+ }
+}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ReactiveAppWindow.cs b/DragonFruit.Kaplan/ReactiveAppWindow.cs
new file mode 100644
index 0000000..c667cf4
--- /dev/null
+++ b/DragonFruit.Kaplan/ReactiveAppWindow.cs
@@ -0,0 +1,76 @@
+// Kaplan Copyright (c) DragonFruit Network
+// Licensed under Apache-2. Refer to the LICENSE file for more info
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.ReactiveUI;
+using FluentAvalonia.UI.Windowing;
+using ReactiveUI;
+
+#nullable enable
+
+namespace DragonFruit.Kaplan
+{
+ ///
+ /// A ReactiveUI that implements the interface and will
+ /// activate your ViewModel automatically if the view model implements . When
+ /// the DataContext property changes, this class will update the ViewModel property with the new DataContext value,
+ /// and vice versa.
+ ///
+ /// ViewModel type.
+ ///
+ /// This is a version of the ReactiveUI class modified to support .
+ /// See https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.ReactiveUI/ReactiveWindow.cs for the original implementation.
+ ///
+ public class ReactiveAppWindow : AppWindow, IViewFor where TViewModel : class
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")]
+ public static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register, TViewModel?>(nameof(ViewModel));
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ReactiveAppWindow()
+ {
+ // This WhenActivated block calls ViewModel's WhenActivated
+ // block if the ViewModel implements IActivatableViewModel.
+ this.WhenActivated(disposables => { });
+ }
+
+ ///
+ /// The ViewModel.
+ ///
+ public TViewModel? ViewModel
+ {
+ get => GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ object? IViewFor.ViewModel
+ {
+ get => ViewModel;
+ set => ViewModel = (TViewModel?)value;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == DataContextProperty)
+ {
+ if (ReferenceEquals(change.OldValue, ViewModel)
+ && change.NewValue is null or TViewModel)
+ {
+ SetCurrentValue(ViewModelProperty, change.NewValue);
+ }
+ }
+ else if (change.Property == ViewModelProperty)
+ {
+ if (ReferenceEquals(change.OldValue, DataContext))
+ {
+ SetCurrentValue(DataContextProperty, change.NewValue);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ViewModels/MainWindowViewModel.cs b/DragonFruit.Kaplan/ViewModels/MainWindowViewModel.cs
index 590d36c..c8991ce 100644
--- a/DragonFruit.Kaplan/ViewModels/MainWindowViewModel.cs
+++ b/DragonFruit.Kaplan/ViewModels/MainWindowViewModel.cs
@@ -5,15 +5,17 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive;
using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
using System.Security.Principal;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Windows.ApplicationModel;
using Windows.Management.Deployment;
using Avalonia.Threading;
using DragonFruit.Kaplan.ViewModels.Enums;
-using DragonFruit.Kaplan.ViewModels.Messages;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.Logging;
@@ -26,7 +28,6 @@ public class MainWindowViewModel : ReactiveObject, IDisposable
private readonly ILogger _logger;
private readonly WindowsIdentity _currentUser;
private readonly PackageManager _packageManager;
- private readonly IDisposable _packageRefreshListener;
private readonly ObservableAsPropertyHelper> _displayedPackages;
@@ -38,20 +39,18 @@ public MainWindowViewModel()
{
_packageManager = new PackageManager();
_currentUser = WindowsIdentity.GetCurrent();
-
_logger = App.GetLogger();
AvailablePackageModes = _currentUser.User != null
? Enum.GetValues()
- : new[] {PackageInstallationMode.Machine};
+ : [PackageInstallationMode.Machine];
// create observables
var packagesSelected = SelectedPackages.ToObservableChangeSet()
.ToCollection()
.ObserveOn(RxApp.MainThreadScheduler)
- .Select(x => x.Any());
+ .Select(x => x.Count != 0);
- _packageRefreshListener = MessageBus.Current.Listen().ObserveOn(RxApp.TaskpoolScheduler).Subscribe(x => RefreshPackagesImpl());
_displayedPackages = this.WhenAnyValue(x => x.DiscoveredPackages, x => x.SearchQuery, x => x.SelectedPackages)
.ObserveOn(RxApp.TaskpoolScheduler)
.Select(q =>
@@ -66,7 +65,10 @@ public MainWindowViewModel()
RefreshPackages = ReactiveCommand.CreateFromTask(RefreshPackagesImpl);
RemovePackages = ReactiveCommand.Create(RemovePackagesImpl, packagesSelected);
ClearSelection = ReactiveCommand.Create(() => SelectedPackages.Clear(), packagesSelected);
- ShowAbout = ReactiveCommand.Create(() => MessageBus.Current.SendMessage(new ShowAboutWindowEventArgs()));
+ ShowAbout = ReactiveCommand.CreateFromTask(() => AboutPageInteraction.Handle(Unit.Default).ToTask());
+
+ AboutPageInteraction = new Interaction();
+ BeginRemovalInteraction = new Interaction();
// auto refresh the package list if the user package filter switch is changed
this.WhenValueChanged(x => x.PackageMode).ObserveOn(RxApp.TaskpoolScheduler).Subscribe(_ => RefreshPackages.Execute(null));
@@ -107,6 +109,9 @@ public string SearchQuery
public ICommand RemovePackages { get; }
public ICommand RefreshPackages { get; }
+ public Interaction AboutPageInteraction { get; }
+ public Interaction BeginRemovalInteraction { get; }
+
private async Task RefreshPackagesImpl()
{
IEnumerable packages;
@@ -147,20 +152,26 @@ await Dispatcher.UIThread.InvokeAsync(() =>
});
}
- private void RemovePackagesImpl()
+ private async Task RemovePackagesImpl()
{
- var packages = SelectedPackages.Select(x => x.Package).ToList();
- var args = new UninstallEventArgs(packages, PackageMode);
+ var remover = new PackageRemover(PackageMode, _packageManager, SelectedPackages.Select(x => x.Package).ToList());
+ var cts = new CancellationTokenSource();
- _logger.LogInformation("Starting removal of {x} packages", packages.Count);
+ using (var model = new RemovalProgressViewModel(remover, cts))
+ {
+ _logger.LogInformation("Starting removal of {x} packages", remover.TotalPackages);
+ _ = remover.RemovePackagesAsync(cts.Token);
+
+ await BeginRemovalInteraction.Handle(model);
+ }
- MessageBus.Current.SendMessage(args);
+ // reload packages after interaction ends
+ RefreshPackages.Execute(null);
}
public void Dispose()
{
_displayedPackages?.Dispose();
- _packageRefreshListener?.Dispose();
}
}
}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ViewModels/Messages/PackageRefreshEventArgs.cs b/DragonFruit.Kaplan/ViewModels/Messages/PackageRefreshEventArgs.cs
deleted file mode 100644
index 7b48bf0..0000000
--- a/DragonFruit.Kaplan/ViewModels/Messages/PackageRefreshEventArgs.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// Kaplan Copyright (c) DragonFruit Network
-// Licensed under Apache-2. Refer to the LICENSE file for more info
-
-namespace DragonFruit.Kaplan.ViewModels.Messages
-{
- public class PackageRefreshEventArgs
- {
- }
-}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ViewModels/Messages/ShowAboutWindowEventArgs.cs b/DragonFruit.Kaplan/ViewModels/Messages/ShowAboutWindowEventArgs.cs
deleted file mode 100644
index 53d2d58..0000000
--- a/DragonFruit.Kaplan/ViewModels/Messages/ShowAboutWindowEventArgs.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// Kaplan Copyright (c) DragonFruit Network
-// Licensed under Apache-2. Refer to the LICENSE file for more info
-
-namespace DragonFruit.Kaplan.ViewModels.Messages
-{
- public class ShowAboutWindowEventArgs
- {
- }
-}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ViewModels/Messages/UninstallEventArgs.cs b/DragonFruit.Kaplan/ViewModels/Messages/UninstallEventArgs.cs
deleted file mode 100644
index 3bbd91d..0000000
--- a/DragonFruit.Kaplan/ViewModels/Messages/UninstallEventArgs.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Kaplan Copyright (c) DragonFruit Network
-// Licensed under Apache-2. Refer to the LICENSE file for more info
-
-using System.Collections.Generic;
-using Windows.ApplicationModel;
-using DragonFruit.Kaplan.ViewModels.Enums;
-
-namespace DragonFruit.Kaplan.ViewModels.Messages
-{
- public class UninstallEventArgs
- {
- public UninstallEventArgs(IEnumerable packages, PackageInstallationMode mode)
- {
- Packages = packages;
- Mode = mode;
- }
-
- public IEnumerable Packages { get; }
- public PackageInstallationMode Mode { get; }
- }
-}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ViewModels/PackageRemovalTask.cs b/DragonFruit.Kaplan/ViewModels/PackageRemovalTask.cs
deleted file mode 100644
index 32b72fa..0000000
--- a/DragonFruit.Kaplan/ViewModels/PackageRemovalTask.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Kaplan Copyright (c) DragonFruit Network
-// Licensed under Apache-2. Refer to the LICENSE file for more info
-
-using System;
-using System.Reactive.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Windows.ApplicationModel;
-using Windows.Management.Deployment;
-using DragonFruit.Kaplan.ViewModels.Enums;
-using ReactiveUI;
-
-namespace DragonFruit.Kaplan.ViewModels
-{
- public class PackageRemovalTask : ReactiveObject
- {
- private readonly ObservableAsPropertyHelper _statusString;
- private readonly PackageInstallationMode _mode;
- private readonly PackageManager _manager;
-
- private DeploymentProgress? _progress;
-
- public PackageRemovalTask(PackageManager manager, Package package, PackageInstallationMode mode)
- {
- Package = new PackageViewModel(package);
-
- _mode = mode;
- _manager = manager;
- _statusString = this.WhenAnyValue(x => x.Progress)
- .Select(x => x?.state switch
- {
- DeploymentProgressState.Queued => $"Removing {Package.Name}: Pending",
- DeploymentProgressState.Processing when x.Value.percentage == 100 => $"Removing {Package.Name} Complete",
- DeploymentProgressState.Processing when x.Value.percentage > 0 => $"Removing {Package.Name}: {x.Value.percentage}% Complete",
-
- _ => $"Removing {Package.Name}"
- })
- .ToProperty(this, x => x.Status);
- }
-
- private DeploymentProgress? Progress
- {
- get => _progress;
- set => this.RaiseAndSetIfChanged(ref _progress, value);
- }
-
- public PackageViewModel Package { get; }
-
- public string Status => _statusString.Value;
-
- public async Task RemoveAsync(CancellationToken cancellation = default)
- {
- var progressCallback = new Progress(p => Progress = p);
- var options = _mode == PackageInstallationMode.Machine ? RemovalOptions.RemoveForAllUsers : RemovalOptions.None;
-
- await _manager.RemovePackageAsync(Package.Package.Id.FullName, options).AsTask(cancellation, progressCallback).ConfigureAwait(false);
- }
- }
-}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/ViewModels/RemovalProgressViewModel.cs b/DragonFruit.Kaplan/ViewModels/RemovalProgressViewModel.cs
index c884f0d..2ce6dc8 100644
--- a/DragonFruit.Kaplan/ViewModels/RemovalProgressViewModel.cs
+++ b/DragonFruit.Kaplan/ViewModels/RemovalProgressViewModel.cs
@@ -2,9 +2,8 @@
// Licensed under Apache-2. Refer to the LICENSE file for more info
using System;
-using System.Collections.Generic;
using System.ComponentModel;
-using System.Linq;
+using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -12,76 +11,95 @@
using Windows.ApplicationModel;
using Windows.Management.Deployment;
using Avalonia.Media;
-using DragonFruit.Kaplan.ViewModels.Enums;
-using DragonFruit.Kaplan.ViewModels.Messages;
-using DynamicData.Binding;
-using Microsoft.Extensions.Logging;
-using Nito.AsyncEx;
using ReactiveUI;
namespace DragonFruit.Kaplan.ViewModels
{
- public class RemovalProgressViewModel : ReactiveObject, IHandlesClosingEvent, IExecutesTaskPostLoad, ICanCloseWindow
+ public class RemovalProgressViewModel : ReactiveObject, IHandlesClosingEvent, IExecutesTaskPostLoad, ICanCloseWindow, IDisposable
{
- private readonly ILogger _logger = App.GetLogger();
- private readonly AsyncLock _lock = new();
- private readonly PackageInstallationMode _mode;
- private readonly CancellationTokenSource _cancellation = new();
+ private readonly PackageRemover _remover;
+ private readonly CancellationTokenSource _cancellation;
+
+ private readonly ObservableAsPropertyHelper _progressValue;
+ private readonly ObservableAsPropertyHelper _progressText;
private readonly ObservableAsPropertyHelper _progressColor;
- private OperationState _status;
- private int _currentPackageNumber;
- private PackageRemovalTask _current;
+ private readonly ObservableAsPropertyHelper _currentPackage;
+ private readonly ObservableAsPropertyHelper _currentState;
- public RemovalProgressViewModel(IEnumerable packages, PackageInstallationMode mode)
+ public RemovalProgressViewModel(PackageRemover remover, CancellationTokenSource cts = null)
{
- _mode = mode;
- _status = OperationState.Pending;
- _progressColor = this.WhenValueChanged(x => x.Status).Select(x => x switch
- {
- OperationState.Pending => Brushes.Gray,
- OperationState.Running => Brushes.DodgerBlue,
- OperationState.Errored => Brushes.Red,
- OperationState.Completed => Brushes.Green,
- OperationState.Canceled => Brushes.DarkGray,
+ _remover = remover;
+ _cancellation = cts ?? new CancellationTokenSource();
+
+ var currentPackage = Observable.FromEventPattern, Package>(h => remover.CurrentPackageChanged += h, h => remover.CurrentPackageChanged -= h)
+ .Select(static x => x.EventArgs);
- _ => throw new ArgumentOutOfRangeException(nameof(x), x, null)
- }).ToProperty(this, x => x.ProgressColor);
+ var currentPackageProgress = Observable.FromEventPattern, DeploymentProgress>(h => remover.CurrentPackageRemovalProgressChanged += h, h => remover.CurrentPackageRemovalProgressChanged -= h)
+ .StartWith(new EventPattern(null, remover.CurrentPackageRemovalProgress))
+ .Select(static x => x.EventArgs);
- var canCancelOperation = this.WhenAnyValue(x => x.CancellationRequested, x => x.Status)
+ _currentPackage = currentPackage
+ .Select(static x => new PackageViewModel(x))
.ObserveOn(RxApp.MainThreadScheduler)
- .Select(x => !x.Item1 && x.Item2 == OperationState.Running);
+ .ToProperty(this, x => x.Current);
- Packages = packages.ToList();
- RequestCancellation = ReactiveCommand.Create(CancelOperation, canCancelOperation);
- }
+ var state = Observable.FromEventPattern, PackageRemover.OperationState>(h => remover.StateChanged += h, h => remover.StateChanged -= h)
+ .StartWith(new EventPattern(null, remover.State))
+ .Select(static x => x.EventArgs);
- public event Action CloseRequested;
+ _currentState = state.ObserveOn(RxApp.MainThreadScheduler).ToProperty(this, x => x.CurrentState);
+ _progressValue = currentPackage.CombineLatest(currentPackageProgress)
+ // don't add to index, we only want processed packages up until this point
+ .Select(x =>
+ {
+ var singlePackagePercentage = 1f / remover.TotalPackages;
+ return remover.CurrentIndex * singlePackagePercentage + x.Second.percentage / 100f * singlePackagePercentage;
+ })
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .ToProperty(this, x => x.ProgressValue);
- public PackageRemovalTask Current
- {
- get => _current;
- private set => this.RaiseAndSetIfChanged(ref _current, value);
- }
+ _progressColor = state.Select(static x => x switch
+ {
+ PackageRemover.OperationState.Pending => Brushes.Gray,
+ PackageRemover.OperationState.Running => Brushes.DodgerBlue,
+ PackageRemover.OperationState.Errored => Brushes.Red,
+ PackageRemover.OperationState.Completed => Brushes.Green,
+ PackageRemover.OperationState.Canceled => Brushes.DarkGray,
+
+ _ => throw new ArgumentOutOfRangeException(nameof(x), x, null)
+ })
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .ToProperty(this, x => x.ProgressColor);
- public int CurrentPackageNumber
- {
- get => _currentPackageNumber;
- private set => this.RaiseAndSetIfChanged(ref _currentPackageNumber, value);
- }
+ _progressText = currentPackageProgress.CombineLatest(currentPackage).Select(static x => x.First.state switch
+ {
+ DeploymentProgressState.Queued => $"Removing {x.Second.DisplayName}: Pending",
+ DeploymentProgressState.Processing when x.First.percentage == 100 => $"Removing {x.Second.DisplayName} Complete",
+ DeploymentProgressState.Processing when x.First.percentage > 0 => $"Removing {x.Second.DisplayName} ({x.First.percentage}% Complete)",
- public OperationState Status
- {
- get => _status;
- private set => this.RaiseAndSetIfChanged(ref _status, value);
+ _ => $"Removing {x.Second.DisplayName}"
+ })
+ .ToProperty(this, x => x.ProgressText);
+
+ var canCancelOperation = this.WhenAnyValue(x => x.CancellationRequested)
+ .CombineLatest(state)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Select(static x => !x.Item1 && x.Item2 == PackageRemover.OperationState.Running);
+
+ RequestCancellation = ReactiveCommand.Create(CancelOperation, canCancelOperation);
}
- public bool CancellationRequested => _cancellation.IsCancellationRequested;
+ public event Action CloseRequested;
- public ISolidColorBrush ProgressColor => _progressColor.Value;
+ public PackageViewModel Current => _currentPackage.Value;
+ public PackageRemover.OperationState CurrentState => _currentState.Value;
- public IReadOnlyList Packages { get; }
+ public float ProgressValue => _progressValue.Value;
+ public string ProgressText => _progressText.Value;
+ public ISolidColorBrush ProgressColor => _progressColor.Value;
+ public bool CancellationRequested => _cancellation.IsCancellationRequested;
public ICommand RequestCancellation { get; }
private void CancelOperation()
@@ -92,70 +110,25 @@ private void CancelOperation()
void IHandlesClosingEvent.OnClose(CancelEventArgs args)
{
- args.Cancel = Status == OperationState.Running;
+ args.Cancel = _remover.State == PackageRemover.OperationState.Running;
}
async Task IExecutesTaskPostLoad.Perform()
{
- _logger.LogInformation("Removal process started");
- _logger.LogDebug("Waiting for lock access");
+ // waits for the process to end
+ await _remover.RemovePackagesAsync(_cancellation.Token);
- using (await _lock.LockAsync(_cancellation.Token).ConfigureAwait(false))
+ // auto close if completed and not cancelled
+ if (!_cancellation.IsCancellationRequested && _remover.State == PackageRemover.OperationState.Completed)
{
- Status = OperationState.Running;
-
- var manager = new PackageManager();
-
- for (var i = 0; i < Packages.Count; i++)
- {
- if (CancellationRequested)
- {
- break;
- }
-
- CurrentPackageNumber = i + 1;
- Current = new PackageRemovalTask(manager, Packages[i], _mode);
-
- try
- {
- _logger.LogInformation("Starting removal of {packageId}", Current.Package.Id);
-
-#if DRY_RUN
- await Task.Delay(1000, _cancellation.Token).ConfigureAwait(false);
-#else
- await Current.RemoveAsync(_cancellation.Token).ConfigureAwait(false);
-#endif
- }
- catch (OperationCanceledException)
- {
- _logger.LogInformation("Package removal cancelled by user (stopped at {packageId})", Current.Package.Id);
- }
- catch (Exception ex)
- {
- Status = OperationState.Errored;
- _logger.LogError(ex, "Package removal failed: {err}", ex.Message);
-
- break;
- }
- }
+ await Task.Delay(1000).ConfigureAwait(false);
+ CloseRequested?.Invoke();
}
-
- Status = CancellationRequested ? OperationState.Canceled : OperationState.Completed;
- MessageBus.Current.SendMessage(new PackageRefreshEventArgs());
-
- _logger.LogInformation("Package removal process ended: {state}", Status);
-
- await Task.Delay(1000).ConfigureAwait(false);
- CloseRequested?.Invoke();
}
- }
- public enum OperationState
- {
- Pending,
- Running,
- Errored,
- Completed,
- Canceled
+ public void Dispose()
+ {
+ _cancellation?.Dispose();
+ }
}
}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/Views/About.axaml b/DragonFruit.Kaplan/Views/About.axaml
index 4eb48f7..6640c6f 100644
--- a/DragonFruit.Kaplan/Views/About.axaml
+++ b/DragonFruit.Kaplan/Views/About.axaml
@@ -21,7 +21,7 @@
- Copyright © 2023 DragonFruit Network
+ Copyright © 2024 DragonFruit Network
// Licensed under Apache-2. Refer to the LICENSE file for more info
-using System;
-using System.Collections.Generic;
+using System.Reactive;
+using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using DragonFruit.Kaplan.ViewModels;
-using DragonFruit.Kaplan.ViewModels.Messages;
using FluentAvalonia.UI.Windowing;
using ReactiveUI;
namespace DragonFruit.Kaplan.Views
{
- public partial class MainWindow : AppWindow
+ public partial class MainWindow : ReactiveAppWindow
{
- private readonly IEnumerable _messageListeners;
-
public MainWindow()
{
InitializeComponent();
@@ -25,22 +22,26 @@ public MainWindow()
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
- _messageListeners = new[]
- {
- MessageBus.Current.Listen().Subscribe(OpenProgressDialog),
- MessageBus.Current.Listen().Subscribe(_ => new About().ShowDialog(this))
- };
+ this.WhenActivated(action => action(ViewModel!.AboutPageInteraction.RegisterHandler(OpenAboutPage)));
+ this.WhenActivated(action => action(ViewModel!.BeginRemovalInteraction.RegisterHandler(OpenProgressDialog)));
+ }
+
+ private async Task OpenAboutPage(InteractionContext ctx)
+ {
+ await new About().ShowDialog(this).ConfigureAwait(false);
+
+ ctx.SetOutput(Unit.Default);
}
- private async void OpenProgressDialog(UninstallEventArgs args)
+ private async Task OpenProgressDialog(InteractionContext ctx)
{
var window = new RemovalProgress
{
- DataContext = new RemovalProgressViewModel(args.Packages, args.Mode)
+ DataContext = ctx.Input
};
- await window.ShowDialog(this).ConfigureAwait(false);
- MessageBus.Current.SendMessage(new PackageRefreshEventArgs());
+ await window.ShowDialog(this);
+ ctx.SetOutput(ctx.Input.CurrentState);
}
private void PackageListPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
@@ -56,15 +57,5 @@ private void PackageListPropertyChanged(object sender, AvaloniaPropertyChangedEv
box.Scroll.Offset = Vector.Zero;
}
}
-
- protected override void OnClosed(EventArgs e)
- {
- base.OnClosed(e);
-
- foreach (var messageListener in _messageListeners)
- {
- messageListener.Dispose();
- }
- }
}
}
\ No newline at end of file
diff --git a/DragonFruit.Kaplan/Views/RemovalProgress.axaml b/DragonFruit.Kaplan/Views/RemovalProgress.axaml
index 8dd3c6a..052c894 100644
--- a/DragonFruit.Kaplan/Views/RemovalProgress.axaml
+++ b/DragonFruit.Kaplan/Views/RemovalProgress.axaml
@@ -6,7 +6,7 @@
mc:Ignorable="d"
x:Class="DragonFruit.Kaplan.Views.RemovalProgress"
x:DataType="vm:RemovalProgressViewModel"
- Width="500"
+ Width="650"
Height="100"
CanResize="False"
Title="Remove Packages"
@@ -27,22 +27,21 @@
-
+
-
+
+ Minimum="0" Maximum="1" Background="LightGray"
+ Value="{Binding ProgressValue}"
+ Foreground="{Binding ProgressColor}" />
+ TextTrimming="WordEllipsis" Text="{Binding ProgressText}" />
+ Content="Cancel" VerticalAlignment="Center" HorizontalAlignment="Stretch" Command="{Binding RequestCancellation}" />
\ No newline at end of file