Ionite
1 year ago
80 changed files with 1495 additions and 759 deletions
@ -0,0 +1,12 @@
|
||||
using System; |
||||
using FluentAvalonia.UI.Media.Animation; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Animations; |
||||
|
||||
public abstract class BaseTransitionInfo : NavigationTransitionInfo |
||||
{ |
||||
/// <summary> |
||||
/// The duration of the animation at 1x animation scale |
||||
/// </summary> |
||||
public abstract TimeSpan Duration { get; set; } |
||||
} |
@ -0,0 +1,59 @@
|
||||
using System; |
||||
using System.Threading; |
||||
using Avalonia; |
||||
using Avalonia.Animation; |
||||
using Avalonia.Animation.Easings; |
||||
using Avalonia.Media; |
||||
using Avalonia.Styling; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Animations; |
||||
|
||||
public class BetterDrillInNavigationTransition : BaseTransitionInfo |
||||
{ |
||||
/// <summary> |
||||
/// Gets or sets whether the animation should drill in (false) or drill out (true) |
||||
/// </summary> |
||||
public bool IsReversed { get; set; } = false; //Zoom out if true |
||||
|
||||
public override TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(400); |
||||
|
||||
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken) |
||||
{ |
||||
var animation = new Animation |
||||
{ |
||||
Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), |
||||
Children = |
||||
{ |
||||
new KeyFrame |
||||
{ |
||||
Setters = |
||||
{ |
||||
new Setter(Visual.OpacityProperty, 0.0), |
||||
new Setter(ScaleTransform.ScaleXProperty, IsReversed ? 1.5 : 0.0), |
||||
new Setter(ScaleTransform.ScaleYProperty, IsReversed ? 1.5 : 0.0) |
||||
}, |
||||
Cue = new Cue(0d) |
||||
}, |
||||
new KeyFrame |
||||
{ |
||||
Setters = |
||||
{ |
||||
new Setter(Visual.OpacityProperty, 1.0), |
||||
new Setter(ScaleTransform.ScaleXProperty, IsReversed ? 1.0 : 1.0), |
||||
new Setter(ScaleTransform.ScaleYProperty, IsReversed ? 1.0 : 1.0) |
||||
}, |
||||
Cue = new Cue(1d) |
||||
} |
||||
}, |
||||
Duration = Duration, |
||||
FillMode = FillMode.Forward |
||||
}; |
||||
|
||||
await animation.RunAsync(ctrl, cancellationToken); |
||||
|
||||
if (ctrl is Visual visualCtrl) |
||||
{ |
||||
visualCtrl.Opacity = 1; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,66 @@
|
||||
using System; |
||||
using System.Threading; |
||||
using AsyncAwaitBestPractices; |
||||
using Avalonia; |
||||
using Avalonia.Animation; |
||||
using Avalonia.Animation.Easings; |
||||
using Avalonia.Media; |
||||
using Avalonia.Styling; |
||||
using FluentAvalonia.UI.Media.Animation; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Animations; |
||||
|
||||
public class BetterEntranceNavigationTransition : BaseTransitionInfo |
||||
{ |
||||
public override TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(500); |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the Horizontal Offset used when animating |
||||
/// </summary> |
||||
public double FromHorizontalOffset { get; set; } = 0; |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the Vertical Offset used when animating |
||||
/// </summary> |
||||
public double FromVerticalOffset { get; set; } = 100; |
||||
|
||||
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken) |
||||
{ |
||||
var animation = new Animation |
||||
{ |
||||
Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), |
||||
Children = |
||||
{ |
||||
new KeyFrame |
||||
{ |
||||
Setters = |
||||
{ |
||||
new Setter(Visual.OpacityProperty, 0.0), |
||||
new Setter(TranslateTransform.XProperty,FromHorizontalOffset), |
||||
new Setter(TranslateTransform.YProperty, FromVerticalOffset) |
||||
}, |
||||
Cue = new Cue(0d) |
||||
}, |
||||
new KeyFrame |
||||
{ |
||||
Setters = |
||||
{ |
||||
new Setter(Visual.OpacityProperty, 1d), |
||||
new Setter(TranslateTransform.XProperty,0.0), |
||||
new Setter(TranslateTransform.YProperty, 0.0) |
||||
}, |
||||
Cue = new Cue(1d) |
||||
} |
||||
}, |
||||
Duration = Duration, |
||||
FillMode = FillMode.Forward |
||||
}; |
||||
|
||||
await animation.RunAsync(ctrl, cancellationToken); |
||||
|
||||
if (ctrl is Visual visualCtrl) |
||||
{ |
||||
visualCtrl.Opacity = 1; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,96 @@
|
||||
using System; |
||||
using System.Threading; |
||||
using Avalonia; |
||||
using Avalonia.Animation; |
||||
using Avalonia.Animation.Easings; |
||||
using Avalonia.Media; |
||||
using Avalonia.Styling; |
||||
using FluentAvalonia.UI.Media.Animation; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Animations; |
||||
|
||||
public class BetterSlideNavigationTransition : BaseTransitionInfo |
||||
{ |
||||
public override TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(167); |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the type of animation effect to play during the slide transition. |
||||
/// </summary> |
||||
public SlideNavigationTransitionEffect Effect { get; set; } = SlideNavigationTransitionEffect.FromRight; |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the HorizontalOffset used when animating from the Left or Right |
||||
/// </summary> |
||||
public double FromHorizontalOffset { get; set; } = 56; |
||||
|
||||
/// <summary> |
||||
/// Gets or sets the VerticalOffset used when animating from the Top or Bottom |
||||
/// </summary> |
||||
public double FromVerticalOffset { get; set; } = 56; |
||||
|
||||
public override async void RunAnimation(Animatable ctrl, CancellationToken cancellationToken) |
||||
{ |
||||
double length = 0; |
||||
bool isVertical = false; |
||||
switch (Effect) |
||||
{ |
||||
case SlideNavigationTransitionEffect.FromLeft: |
||||
length = -FromHorizontalOffset; |
||||
break; |
||||
case SlideNavigationTransitionEffect.FromRight: |
||||
length = FromHorizontalOffset; |
||||
break; |
||||
case SlideNavigationTransitionEffect.FromTop: |
||||
length = -FromVerticalOffset; |
||||
isVertical = true; |
||||
break; |
||||
case SlideNavigationTransitionEffect.FromBottom: |
||||
length = FromVerticalOffset; |
||||
isVertical = true; |
||||
break; |
||||
} |
||||
|
||||
var animation = new Animation |
||||
{ |
||||
Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0), |
||||
Children = |
||||
{ |
||||
new KeyFrame |
||||
{ |
||||
Setters = |
||||
{ |
||||
new Setter(isVertical ? TranslateTransform.YProperty : TranslateTransform.XProperty, length), |
||||
new Setter(Visual.OpacityProperty, 0d) |
||||
}, |
||||
Cue = new Cue(0d) |
||||
}, |
||||
new KeyFrame |
||||
{ |
||||
Setters= |
||||
{ |
||||
new Setter(Visual.OpacityProperty, 1d) |
||||
}, |
||||
Cue = new Cue(0.05d) |
||||
}, |
||||
new KeyFrame |
||||
{ |
||||
Setters = |
||||
{ |
||||
new Setter(Visual.OpacityProperty, 1d), |
||||
new Setter(isVertical ? TranslateTransform.YProperty : TranslateTransform.XProperty, 0.0) |
||||
}, |
||||
Cue = new Cue(1d) |
||||
} |
||||
}, |
||||
Duration = Duration, |
||||
FillMode = FillMode.Forward |
||||
}; |
||||
|
||||
await animation.RunAsync(ctrl, cancellationToken); |
||||
|
||||
if (ctrl is Visual visual) |
||||
{ |
||||
visual.Opacity = 1; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
using FluentAvalonia.UI.Controls; |
||||
using FluentAvalonia.UI.Media.Animation; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Services; |
||||
|
||||
public interface INavigationService |
||||
{ |
||||
/// <summary> |
||||
/// Set the frame to use for navigation. |
||||
/// </summary> |
||||
void SetFrame(Frame frame); |
||||
|
||||
/// <summary> |
||||
/// Navigate to the view of the given view model type. |
||||
/// </summary> |
||||
void NavigateTo<TViewModel>(NavigationTransitionInfo? transitionInfo = null) where TViewModel : ViewModelBase; |
||||
|
||||
/// <summary> |
||||
/// Navigate to the view of the given view model. |
||||
/// </summary> |
||||
void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null); |
||||
} |
@ -0,0 +1,99 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Diagnostics; |
||||
using System.Linq; |
||||
using FluentAvalonia.UI.Controls; |
||||
using FluentAvalonia.UI.Media.Animation; |
||||
using FluentAvalonia.UI.Navigation; |
||||
using StabilityMatrix.Avalonia.Animations; |
||||
using StabilityMatrix.Avalonia.ViewModels; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.Services; |
||||
|
||||
public class NavigationService : INavigationService |
||||
{ |
||||
private Frame? _frame; |
||||
|
||||
/// <inheritdoc /> |
||||
public void SetFrame(Frame frame) |
||||
{ |
||||
_frame = frame; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void NavigateTo<TViewModel>(NavigationTransitionInfo? transitionInfo = null) where TViewModel : ViewModelBase |
||||
{ |
||||
if (_frame is null) |
||||
{ |
||||
throw new InvalidOperationException("SetFrame was not called before NavigateTo."); |
||||
} |
||||
|
||||
|
||||
if (App.Services.GetService(typeof(ISettingsManager)) is ISettingsManager settingsManager) |
||||
{ |
||||
// Handle animation scale |
||||
switch (transitionInfo) |
||||
{ |
||||
// If the transition info is null or animation scale is 0, suppress the transition |
||||
case null: |
||||
case BaseTransitionInfo when settingsManager.Settings.AnimationScale == 0f: |
||||
transitionInfo = new SuppressNavigationTransitionInfo(); |
||||
break; |
||||
case BaseTransitionInfo baseTransitionInfo: |
||||
baseTransitionInfo.Duration *= settingsManager.Settings.AnimationScale; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
_frame.NavigateToType(typeof(TViewModel), |
||||
null, |
||||
new FrameNavigationOptions |
||||
{ |
||||
IsNavigationStackEnabled = true, |
||||
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo() |
||||
}); |
||||
|
||||
if (!typeof(TViewModel).IsAssignableTo(typeof(PageViewModelBase))) |
||||
return; |
||||
|
||||
if (App.Services.GetService(typeof(MainWindowViewModel)) is MainWindowViewModel mainViewModel) |
||||
{ |
||||
mainViewModel.SelectedCategory = |
||||
mainViewModel.Pages.FirstOrDefault(x => x.GetType() == typeof(TViewModel)); |
||||
} |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null) |
||||
{ |
||||
if (_frame is null) |
||||
{ |
||||
throw new InvalidOperationException("SetFrame was not called before NavigateTo."); |
||||
} |
||||
|
||||
if (App.Services.GetService(typeof(ISettingsManager)) is ISettingsManager settingsManager) |
||||
{ |
||||
// Handle animation scale |
||||
switch (transitionInfo) |
||||
{ |
||||
// If the transition info is null or animation scale is 0, suppress the transition |
||||
case null: |
||||
case BaseTransitionInfo when settingsManager.Settings.AnimationScale == 0f: |
||||
transitionInfo = new SuppressNavigationTransitionInfo(); |
||||
break; |
||||
case BaseTransitionInfo baseTransitionInfo: |
||||
baseTransitionInfo.Duration *= settingsManager.Settings.AnimationScale; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
_frame.NavigateFromObject(viewModel, |
||||
new FrameNavigationOptions |
||||
{ |
||||
IsNavigationStackEnabled = true, |
||||
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo() |
||||
}); |
||||
} |
||||
} |
@ -1,47 +1,81 @@
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using Avalonia.Controls; |
||||
using Avalonia.Controls.Templates; |
||||
using StabilityMatrix.Avalonia.ViewModels; |
||||
using FluentAvalonia.UI.Controls; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Attributes; |
||||
|
||||
namespace StabilityMatrix.Avalonia; |
||||
|
||||
public class ViewLocator : IDataTemplate |
||||
public class ViewLocator : IDataTemplate, INavigationPageFactory |
||||
{ |
||||
/// <inheritdoc /> |
||||
public Control Build(object? data) |
||||
{ |
||||
if (data is null) throw new ArgumentNullException(nameof(data)); |
||||
|
||||
var type = data.GetType(); |
||||
|
||||
if (type is null) throw new InvalidOperationException("Type is null"); |
||||
|
||||
if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr) |
||||
{ |
||||
var viewType = viewAttr.GetViewType(); |
||||
|
||||
#pragma warning disable IL2072 |
||||
// In design mode, just create a new instance of the view |
||||
if (Design.IsDesignMode) |
||||
{ |
||||
return (Control) Activator.CreateInstance(viewType)!; |
||||
} |
||||
#pragma warning restore IL2072 |
||||
// Otherwise get from the service provider |
||||
if (App.Services.GetService(viewType) is Control view) |
||||
{ |
||||
return view; |
||||
} |
||||
return GetView(viewType); |
||||
} |
||||
|
||||
return new TextBlock |
||||
{ |
||||
Text = "Not Found: " + data.GetType().FullName |
||||
Text = "View Model Not Found: " + data.GetType().FullName |
||||
}; |
||||
} |
||||
|
||||
private Control GetView(Type viewType) |
||||
{ |
||||
// Otherwise get from the service provider |
||||
if (App.Services.GetService(viewType) is Control view) |
||||
{ |
||||
return view; |
||||
} |
||||
|
||||
return new TextBlock |
||||
{ |
||||
Text = "View Not Found: " + viewType.FullName |
||||
}; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public bool Match(object? data) |
||||
{ |
||||
return data is ViewModelBase; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public Control? GetPage(Type srcType) |
||||
{ |
||||
if (Attribute.GetCustomAttribute(srcType, typeof(ViewAttribute)) is not ViewAttribute |
||||
viewAttr) |
||||
{ |
||||
throw new InvalidOperationException("View not found for " + srcType.FullName); |
||||
} |
||||
|
||||
var viewType = viewAttr.GetViewType(); |
||||
var view = GetView(viewType); |
||||
view.DataContext ??= App.Services.GetService(srcType); |
||||
return view; |
||||
} |
||||
|
||||
/// <inheritdoc /> |
||||
public Control GetPageFromObject(object target) |
||||
{ |
||||
if (Attribute.GetCustomAttribute(target.GetType(), typeof(ViewAttribute)) is not |
||||
ViewAttribute viewAttr) |
||||
{ |
||||
throw new InvalidOperationException("View not found for " + target.GetType().FullName); |
||||
} |
||||
|
||||
var viewType = viewAttr.GetViewType(); |
||||
var view = GetView(viewType); |
||||
view.DataContext ??= target; |
||||
return view; |
||||
} |
||||
} |
||||
|
@ -1,6 +1,6 @@
|
||||
using FluentAvalonia.UI.Controls; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
/// <summary> |
||||
/// An abstract class for enabling page navigation. |
@ -1,11 +1,11 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels; |
||||
namespace StabilityMatrix.Avalonia.ViewModels.Base; |
||||
|
||||
/// <summary> |
||||
/// Generic view model for progress reporting. |
||||
/// </summary> |
||||
public partial class ProgressViewModel : ObservableObject |
||||
public partial class ProgressViewModel : ViewModelBase |
||||
{ |
||||
[ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))] |
||||
private string? text; |
@ -0,0 +1,309 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using Avalonia.Controls.Notifications; |
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
using FluentAvalonia.UI.Controls; |
||||
using NLog; |
||||
using Polly; |
||||
using StabilityMatrix.Avalonia.Animations; |
||||
using StabilityMatrix.Avalonia.Services; |
||||
using StabilityMatrix.Avalonia.ViewModels.Base; |
||||
using StabilityMatrix.Core.Extensions; |
||||
using StabilityMatrix.Core.Helper; |
||||
using StabilityMatrix.Core.Helper.Factory; |
||||
using StabilityMatrix.Core.Models; |
||||
using StabilityMatrix.Core.Models.Progress; |
||||
using StabilityMatrix.Core.Processes; |
||||
using StabilityMatrix.Core.Services; |
||||
|
||||
namespace StabilityMatrix.Avalonia.ViewModels.PackageManager; |
||||
|
||||
public partial class PackageCardViewModel : ProgressViewModel |
||||
{ |
||||
private readonly IPackageFactory packageFactory; |
||||
private readonly INotificationService notificationService; |
||||
private readonly ISettingsManager settingsManager; |
||||
private readonly INavigationService navigationService; |
||||
private readonly Logger logger = LogManager.GetCurrentClassLogger(); |
||||
|
||||
[ObservableProperty] private InstalledPackage? package; |
||||
[ObservableProperty] private Uri cardImage; |
||||
[ObservableProperty] private bool isUpdateAvailable; |
||||
[ObservableProperty] private string installedVersion; |
||||
|
||||
public PackageCardViewModel( |
||||
IPackageFactory packageFactory, |
||||
INotificationService notificationService, |
||||
ISettingsManager settingsManager, |
||||
INavigationService navigationService) |
||||
{ |
||||
this.packageFactory = packageFactory; |
||||
this.notificationService = notificationService; |
||||
this.settingsManager = settingsManager; |
||||
this.navigationService = navigationService; |
||||
} |
||||
|
||||
partial void OnPackageChanged(InstalledPackage? value) |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(value?.PackageName)) |
||||
return; |
||||
|
||||
var basePackage = packageFactory[value.PackageName]; |
||||
CardImage = basePackage?.PreviewImageUri ?? Assets.NoImage; |
||||
InstalledVersion = value.DisplayVersion ?? "Unknown"; |
||||
} |
||||
|
||||
public override async Task OnLoadedAsync() |
||||
{ |
||||
IsUpdateAvailable = await HasUpdate(); |
||||
} |
||||
|
||||
public void Launch() |
||||
{ |
||||
if (Package == null) |
||||
return; |
||||
|
||||
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id); |
||||
|
||||
navigationService.NavigateTo<LaunchPageViewModel>(new BetterDrillInNavigationTransition()); |
||||
EventManager.Instance.OnPackageLaunchRequested(Package.Id); |
||||
} |
||||
|
||||
public async Task Uninstall() |
||||
{ |
||||
if (Package?.LibraryPath == null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var dialog = new ContentDialog |
||||
{ |
||||
Title = "Are you sure?", |
||||
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.", |
||||
PrimaryButtonText = "Yes, delete it", |
||||
CloseButtonText = "No, keep it", |
||||
DefaultButton = ContentDialogButton.Primary |
||||
}; |
||||
var result = await dialog.ShowAsync(); |
||||
|
||||
if (result == ContentDialogResult.Primary) |
||||
{ |
||||
Text = "Uninstalling..."; |
||||
IsIndeterminate = true; |
||||
Value = -1; |
||||
|
||||
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir, |
||||
Package.LibraryPath)); |
||||
var taskResult = await notificationService.TryAsync(deleteTask, |
||||
"Some files could not be deleted. Please close any open files in the package directory and try again."); |
||||
if (taskResult.IsSuccessful) |
||||
{ |
||||
notificationService.Show(new Notification("Success", |
||||
$"Package {Package.DisplayName} uninstalled", |
||||
NotificationType.Success)); |
||||
|
||||
settingsManager.Transaction(settings => |
||||
{ |
||||
settings.RemoveInstalledPackageAndUpdateActive(Package); |
||||
}); |
||||
|
||||
EventManager.Instance.OnInstalledPackagesChanged(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public async Task Update() |
||||
{ |
||||
if (Package == null) return; |
||||
|
||||
var basePackage = packageFactory[Package.PackageName!]; |
||||
if (basePackage == null) |
||||
{ |
||||
logger.Warn("Could not find package {SelectedPackagePackageName}", |
||||
Package.PackageName); |
||||
notificationService.Show("Invalid Package type", |
||||
$"Package {Package.PackageName.ToRepr()} is not a valid package type", |
||||
NotificationType.Error); |
||||
return; |
||||
} |
||||
|
||||
Text = $"Updating {Package.DisplayName}"; |
||||
IsIndeterminate = true; |
||||
|
||||
var progressId = Guid.NewGuid(); |
||||
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, |
||||
Package.DisplayName, |
||||
new ProgressReport(0f, isIndeterminate: true, type: ProgressType.Update))); |
||||
|
||||
try |
||||
{ |
||||
basePackage.InstallLocation = Package.FullPath!; |
||||
|
||||
var progress = new Progress<ProgressReport>(progress => |
||||
{ |
||||
var percent = Convert.ToInt32(progress.Percentage); |
||||
|
||||
Value = percent; |
||||
IsIndeterminate = progress.IsIndeterminate; |
||||
Text = $"Updating {Package.DisplayName}"; |
||||
|
||||
EventManager.Instance.OnGlobalProgressChanged(percent); |
||||
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, |
||||
Package.DisplayName, progress)); |
||||
}); |
||||
|
||||
var updateResult = await basePackage.Update(Package, progress); |
||||
|
||||
settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult); |
||||
notificationService.Show("Update complete", |
||||
$"{Package.DisplayName} has been updated to the latest version.", |
||||
NotificationType.Success); |
||||
|
||||
await using (settingsManager.BeginTransaction()) |
||||
{ |
||||
Package.UpdateAvailable = false; |
||||
} |
||||
IsUpdateAvailable = false; |
||||
InstalledVersion = Package.DisplayVersion ?? "Unknown"; |
||||
|
||||
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, |
||||
Package.DisplayName, |
||||
new ProgressReport(1f, "Update complete", type: ProgressType.Update))); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
logger.Error(e, "Error Updating Package ({PackageName})", basePackage.Name); |
||||
notificationService.ShowPersistent($"Error Updating {Package.DisplayName}", e.Message, NotificationType.Error); |
||||
EventManager.Instance.OnProgressChanged(new ProgressItem(progressId, |
||||
Package.DisplayName, |
||||
new ProgressReport(0f, "Update failed", type: ProgressType.Update), Failed: true)); |
||||
} |
||||
finally |
||||
{ |
||||
IsIndeterminate = false; |
||||
Value = 0; |
||||
Text = ""; |
||||
} |
||||
} |
||||
|
||||
public async Task OpenFolder() |
||||
{ |
||||
if (string.IsNullOrWhiteSpace(Package?.FullPath)) |
||||
return; |
||||
|
||||
await ProcessRunner.OpenFolderBrowser(Package.FullPath); |
||||
} |
||||
|
||||
private async Task<bool> HasUpdate() |
||||
{ |
||||
if (Package == null) |
||||
return false; |
||||
|
||||
var basePackage = packageFactory[Package.PackageName!]; |
||||
if (basePackage == null) |
||||
return false; |
||||
|
||||
var canCheckUpdate = Package.LastUpdateCheck == null || |
||||
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15); |
||||
|
||||
if (!canCheckUpdate) |
||||
{ |
||||
return Package.UpdateAvailable; |
||||
} |
||||
|
||||
try |
||||
{ |
||||
var hasUpdate = await basePackage.CheckForUpdates(Package); |
||||
Package.UpdateAvailable = hasUpdate; |
||||
Package.LastUpdateCheck = DateTimeOffset.Now; |
||||
settingsManager.SetLastUpdateCheck(Package); |
||||
return hasUpdate; |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
logger.Error(e, $"Error checking {Package.PackageName} for updates"); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Deletes a directory and all of its contents recursively. |
||||
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff. |
||||
/// </summary> |
||||
/// <param name="targetDirectory"></param> |
||||
private Task DeleteDirectoryAsync(string targetDirectory) |
||||
{ |
||||
var policy = Policy.Handle<IOException>() |
||||
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)), |
||||
onRetry: (exception, calculatedWaitDuration) => |
||||
{ |
||||
logger.Warn( |
||||
exception, |
||||
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}", |
||||
targetDirectory, calculatedWaitDuration); |
||||
}); |
||||
|
||||
return policy.ExecuteAsync(async () => |
||||
{ |
||||
await Task.Run(() => |
||||
{ |
||||
DeleteDirectory(targetDirectory); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
private void DeleteDirectory(string targetDirectory) |
||||
{ |
||||
// Skip if directory does not exist |
||||
if (!Directory.Exists(targetDirectory)) |
||||
{ |
||||
return; |
||||
} |
||||
// For junction points, delete with recursive false |
||||
if (new DirectoryInfo(targetDirectory).LinkTarget != null) |
||||
{ |
||||
logger.Info("Removing junction point {TargetDirectory}", targetDirectory); |
||||
try |
||||
{ |
||||
Directory.Delete(targetDirectory, false); |
||||
return; |
||||
} |
||||
catch (IOException ex) |
||||
{ |
||||
throw new IOException($"Failed to delete junction point {targetDirectory}", ex); |
||||
} |
||||
} |
||||
// Recursively delete all subdirectories |
||||
var subdirectoryEntries = Directory.GetDirectories(targetDirectory); |
||||
foreach (var subdirectoryPath in subdirectoryEntries) |
||||
{ |
||||
DeleteDirectory(subdirectoryPath); |
||||
} |
||||
// Delete all files in the directory |
||||
var fileEntries = Directory.GetFiles(targetDirectory); |
||||
foreach (var filePath in fileEntries) |
||||
{ |
||||
try |
||||
{ |
||||
File.SetAttributes(filePath, FileAttributes.Normal); |
||||
File.Delete(filePath); |
||||
} |
||||
catch (IOException ex) |
||||
{ |
||||
throw new IOException($"Failed to delete file {filePath}", ex); |
||||
} |
||||
} |
||||
// Delete the target directory itself |
||||
try |
||||
{ |
||||
Directory.Delete(targetDirectory, false); |
||||
} |
||||
catch (IOException ex) |
||||
{ |
||||
throw new IOException($"Failed to delete directory {targetDirectory}", ex); |
||||
} |
||||
} |
||||
} |
@ -1,7 +1,7 @@
|
||||
namespace StabilityMatrix.Core.Models; |
||||
|
||||
public class PackageVersion |
||||
public record PackageVersion |
||||
{ |
||||
public string TagName { get; set; } |
||||
public string ReleaseNotesMarkdown { get; set; } |
||||
public required string TagName { get; set; } |
||||
public string? ReleaseNotesMarkdown { get; set; } |
||||
} |
||||
|
Loading…
Reference in new issue