diff --git a/StabilityMatrix.Avalonia/Animations/BaseTransitionInfo.cs b/StabilityMatrix.Avalonia/Animations/BaseTransitionInfo.cs
new file mode 100644
index 00000000..85c36d03
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Animations/BaseTransitionInfo.cs
@@ -0,0 +1,12 @@
+using System;
+using FluentAvalonia.UI.Media.Animation;
+
+namespace StabilityMatrix.Avalonia.Animations;
+
+public abstract class BaseTransitionInfo : NavigationTransitionInfo
+{
+ ///
+ /// The duration of the animation at 1x animation scale
+ ///
+ public abstract TimeSpan Duration { get; set; }
+}
diff --git a/StabilityMatrix.Avalonia/Animations/BetterDrillInNavigationTransition.cs b/StabilityMatrix.Avalonia/Animations/BetterDrillInNavigationTransition.cs
new file mode 100644
index 00000000..0f127395
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Animations/BetterDrillInNavigationTransition.cs
@@ -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
+{
+ ///
+ /// Gets or sets whether the animation should drill in (false) or drill out (true)
+ ///
+ 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;
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs b/StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs
new file mode 100644
index 00000000..fef4f8db
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Animations/BetterEntranceNavigationTransition.cs
@@ -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);
+
+ ///
+ /// Gets or sets the Horizontal Offset used when animating
+ ///
+ public double FromHorizontalOffset { get; set; } = 0;
+
+ ///
+ /// Gets or sets the Vertical Offset used when animating
+ ///
+ 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;
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Animations/BetterSlideNavigationTransition.cs b/StabilityMatrix.Avalonia/Animations/BetterSlideNavigationTransition.cs
new file mode 100644
index 00000000..3a729ab2
--- /dev/null
+++ b/StabilityMatrix.Avalonia/Animations/BetterSlideNavigationTransition.cs
@@ -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);
+
+ ///
+ /// Gets or sets the type of animation effect to play during the slide transition.
+ ///
+ public SlideNavigationTransitionEffect Effect { get; set; } = SlideNavigationTransitionEffect.FromRight;
+
+ ///
+ /// Gets or sets the HorizontalOffset used when animating from the Left or Right
+ ///
+ public double FromHorizontalOffset { get; set; } = 56;
+
+ ///
+ /// Gets or sets the VerticalOffset used when animating from the Top or Bottom
+ ///
+ 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;
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/Services/NavigationService.cs b/StabilityMatrix.Avalonia/Services/NavigationService.cs
index 17a48160..8260383b 100644
--- a/StabilityMatrix.Avalonia/Services/NavigationService.cs
+++ b/StabilityMatrix.Avalonia/Services/NavigationService.cs
@@ -1,10 +1,14 @@
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;
@@ -25,7 +29,24 @@ public class NavigationService : INavigationService
{
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
@@ -33,7 +54,7 @@ public class NavigationService : INavigationService
IsNavigationStackEnabled = true,
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo()
});
-
+
if (!typeof(TViewModel).IsAssignableTo(typeof(PageViewModelBase)))
return;
@@ -52,6 +73,22 @@ public class NavigationService : INavigationService
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
{
diff --git a/StabilityMatrix.Avalonia/ViewLocator.cs b/StabilityMatrix.Avalonia/ViewLocator.cs
index 8e0fd8c9..4724a42f 100644
--- a/StabilityMatrix.Avalonia/ViewLocator.cs
+++ b/StabilityMatrix.Avalonia/ViewLocator.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using FluentAvalonia.UI.Controls;
@@ -51,28 +52,30 @@ public class ViewLocator : IDataTemplate, INavigationPageFactory
///
public Control? GetPage(Type srcType)
{
- if (Attribute.GetCustomAttribute(srcType, typeof(ViewAttribute)) is ViewAttribute viewAttr)
+ if (Attribute.GetCustomAttribute(srcType, typeof(ViewAttribute)) is not ViewAttribute
+ viewAttr)
{
- var viewType = viewAttr.GetViewType();
- var view = GetView(viewType);
- view.DataContext ??= App.Services.GetService(srcType);
- return view;
+ throw new InvalidOperationException("View not found for " + srcType.FullName);
}
-
- 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;
}
///
public Control GetPageFromObject(object target)
{
- if (Attribute.GetCustomAttribute(target.GetType(), typeof(ViewAttribute)) is ViewAttribute viewAttr)
+ if (Attribute.GetCustomAttribute(target.GetType(), typeof(ViewAttribute)) is not
+ ViewAttribute viewAttr)
{
- var viewType = viewAttr.GetViewType();
- var view = GetView(viewType);
- view.DataContext ??= target;
- return view;
+ throw new InvalidOperationException("View not found for " + target.GetType().FullName);
}
-
- throw new InvalidOperationException("View not found for " + target.GetType().FullName);
+
+ var viewType = viewAttr.GetViewType();
+ var view = GetView(viewType);
+ view.DataContext ??= target;
+ return view;
}
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
index dd8319c0..074ce9ae 100644
--- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
@@ -10,6 +10,7 @@ using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using NLog;
using Polly;
+using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
@@ -66,8 +67,8 @@ public partial class PackageCardViewModel : Base.ProgressViewModel
return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
-
- navigationService.NavigateTo(new EntranceNavigationTransitionInfo());
+
+ navigationService.NavigateTo(new BetterDrillInNavigationTransition());
EventManager.Instance.OnPackageLaunchRequested(Package.Id);
}
diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
index c6659983..54026801 100644
--- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
@@ -64,6 +64,21 @@ public partial class SettingsViewModel : PageViewModelBase
"Dark",
"System",
};
+
+ public IReadOnlyList AnimationScaleOptions { get; } = new[]
+ {
+ 0f,
+ 0.25f,
+ 0.5f,
+ 0.75f,
+ 1f,
+ 1.25f,
+ 1.5f,
+ 1.75f,
+ 2f,
+ };
+
+ [ObservableProperty] private float selectedAnimationScale;
// Shared folder options
[ObservableProperty] private bool removeSymlinksOnShutdown;
@@ -100,6 +115,7 @@ public partial class SettingsViewModel : PageViewModelBase
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
RemoveSymlinksOnShutdown = settingsManager.Settings.RemoveFolderLinksOnShutdown;
+ SelectedAnimationScale = settingsManager.Settings.AnimationScale;
settingsManager.RelayPropertyFor(this,
vm => vm.SelectedTheme,
@@ -108,6 +124,10 @@ public partial class SettingsViewModel : PageViewModelBase
settingsManager.RelayPropertyFor(this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled);
+
+ settingsManager.RelayPropertyFor(this,
+ vm => vm.SelectedAnimationScale,
+ settings => settings.AnimationScale);
}
partial void OnSelectedThemeChanged(string? value)
diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
index d2d540d1..603c432c 100644
--- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
@@ -20,6 +20,7 @@ using FluentAvalonia.UI.Media;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Windowing;
using Microsoft.Extensions.DependencyInjection;
+using StabilityMatrix.Avalonia.Animations;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
@@ -141,7 +142,7 @@ public partial class MainWindow : AppWindowBase
{
throw new InvalidOperationException($"NavigationViewItem Tag must be of type ViewModelBase, not {nvi.Tag?.GetType()}");
}
- navigationService.NavigateTo(vm, e.RecommendedNavigationTransitionInfo);
+ navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
}
diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
index a3a33385..8e3d6d0d 100644
--- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
@@ -204,6 +204,24 @@
TextWrapping="WrapWithOverflow" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Core/Models/Settings/Settings.cs b/StabilityMatrix.Core/Models/Settings/Settings.cs
index cf82886b..8848957b 100644
--- a/StabilityMatrix.Core/Models/Settings/Settings.cs
+++ b/StabilityMatrix.Core/Models/Settings/Settings.cs
@@ -51,6 +51,8 @@ public class Settings
public HashSet? InstalledModelHashes { get; set; } = new();
+ public float AnimationScale { get; set; } = 1.0f;
+
public void RemoveInstalledPackageAndUpdateActive(InstalledPackage package)
{
RemoveInstalledPackageAndUpdateActive(package.Id);