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);