diff --git a/GenshinLyreMidiPlayer/GenshinLyreMidiPlayer.csproj b/GenshinLyreMidiPlayer/GenshinLyreMidiPlayer.csproj
index a098b1b..c7afed1 100644
--- a/GenshinLyreMidiPlayer/GenshinLyreMidiPlayer.csproj
+++ b/GenshinLyreMidiPlayer/GenshinLyreMidiPlayer.csproj
@@ -6,8 +6,15 @@
true
GenshinLyreMidiPlayer.App
app.manifest
- 1.3.3
+ 1.4.0
item_windsong_lyre.ico
+ enable
+ https://github.com/sabihoshi/GenshinLyreMidiPlayer
+ sabihoshi
+ Genshin Lyre MIDI Player
+
+ sabihoshi
+ A music player that plays MIDI files into Genshin Impact's Windsong Lyre.
diff --git a/GenshinLyreMidiPlayer/ModernWPF/AnimatedContentControl.cs b/GenshinLyreMidiPlayer/ModernWPF/AnimatedContentControl.cs
new file mode 100644
index 0000000..457e03a
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/AnimatedContentControl.cs
@@ -0,0 +1,28 @@
+using System.Windows.Controls;
+using GenshinLyreMidiPlayer.ModernWPF.Animation;
+using GenshinLyreMidiPlayer.ViewModels;
+
+namespace GenshinLyreMidiPlayer.ModernWPF
+{
+ public class AnimatedContentControl : ContentControl
+ {
+ private static Transition Transition => SettingsPageViewModel.Transition.Object;
+
+ protected override void OnContentChanged(object? oldContent, object? newContent)
+ {
+ if (oldContent != null)
+ {
+ var exit = Transition.GetExitAnimation(oldContent, false);
+ exit?.Begin();
+ }
+
+ if (newContent != null)
+ {
+ var enter = Transition.GetEnterAnimation(newContent, false);
+ enter?.Begin();
+ }
+
+ base.OnContentChanged(oldContent, newContent);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/Animation.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/Animation.cs
new file mode 100644
index 0000000..be1963b
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/Animation.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation
+{
+ public class Animation
+ {
+ private static readonly BitmapCache _defaultBitmapCache;
+ private readonly FrameworkElement _element;
+ private readonly Storyboard _storyboard;
+ private ClockState _currentState = ClockState.Stopped;
+
+ static Animation()
+ {
+ _defaultBitmapCache = new BitmapCache();
+ _defaultBitmapCache.Freeze();
+ }
+
+ public Animation(FrameworkElement element, Storyboard storyboard)
+ {
+ _element = element;
+ _storyboard = storyboard;
+ _storyboard.CurrentStateInvalidated += OnCurrentStateInvalidated;
+ _storyboard.Completed += OnCompleted;
+ }
+
+ public event EventHandler Completed;
+
+ public void Begin()
+ {
+ if (!(_element.CacheMode is BitmapCache))
+ _element.SetCurrentValue(UIElement.CacheModeProperty, GetBitmapCache());
+ _storyboard.Begin(_element, true);
+ }
+
+ public void Stop()
+ {
+ if (_currentState != ClockState.Stopped) _storyboard.Stop(_element);
+ _element.InvalidateProperty(UIElement.CacheModeProperty);
+ _element.InvalidateProperty(UIElement.RenderTransformProperty);
+ _element.InvalidateProperty(UIElement.RenderTransformOriginProperty);
+ }
+
+ private void OnCurrentStateInvalidated(object sender, EventArgs e)
+ {
+ if (sender is Clock clock) _currentState = clock.CurrentState;
+ }
+
+ private void OnCompleted(object sender, EventArgs e)
+ {
+ Completed?.Invoke(this, EventArgs.Empty);
+ }
+
+ private BitmapCache GetBitmapCache()
+ {
+#if NETCOREAPP || NET462
+ return new BitmapCache(VisualTreeHelper.GetDpi(_element).PixelsPerDip);
+#else
+ return _defaultBitmapCache;
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/Transition.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transition.cs
new file mode 100644
index 0000000..395472b
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transition.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Windows;
+using System.Windows.Media.Animation;
+
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation
+{
+ ///
+ /// Provides parameter info for the Frame.Navigate method. Controls how the transition
+ /// animation runs during the navigation action.
+ ///
+ public class Transition : DependencyObject
+ {
+ internal static readonly KeySpline AccelerateKeySpline;
+ internal static readonly KeySpline DecelerateKeySpline;
+ internal static readonly PropertyPath OpacityPath = new PropertyPath(UIElement.OpacityProperty);
+
+ internal static readonly PropertyPath TranslateXPath =
+ new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)");
+
+ internal static readonly PropertyPath TranslateYPath =
+ new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)");
+
+ internal static readonly PropertyPath ScaleXPath =
+ new PropertyPath("(UIElement.RenderTransform).(ScaleTransform.ScaleX)");
+
+ internal static readonly PropertyPath ScaleYPath =
+ new PropertyPath("(UIElement.RenderTransform).(ScaleTransform.ScaleY)");
+
+ internal static readonly TimeSpan ExitDuration = TimeSpan.FromMilliseconds(150);
+ internal static readonly TimeSpan EnterDuration = TimeSpan.FromMilliseconds(300);
+ internal static readonly TimeSpan MaxMoveDuration = TimeSpan.FromMilliseconds(500);
+
+ static Transition()
+ {
+ AccelerateKeySpline = new KeySpline(0.7, 0, 1, 0.5);
+ AccelerateKeySpline.Freeze();
+
+ DecelerateKeySpline = new KeySpline(0.1, 0.9, 0.2, 1);
+ DecelerateKeySpline.Freeze();
+ }
+
+ ///
+ /// Initializes a new instance of the Transition class.
+ ///
+ protected Transition()
+ {
+ }
+
+ //protected virtual string GetNavigationStateCore();
+ //protected virtual void SetNavigationStateCore(string navigationState);
+
+ protected virtual Animation GetEnterAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ return null;
+ }
+
+ protected virtual Animation GetExitAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ return null;
+ }
+
+ public Animation GetEnterAnimation(object element, bool movingBackwards)
+ {
+ return GetEnterAnimation(
+ (FrameworkElement) element, movingBackwards);
+ }
+
+ public Animation GetExitAnimation(object element, bool movingBackwards)
+ {
+ return GetExitAnimation(
+ (FrameworkElement) element, movingBackwards);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/TransitionCollection.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/TransitionCollection.cs
new file mode 100644
index 0000000..4db62bc
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/TransitionCollection.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using GenshinLyreMidiPlayer.ModernWPF.Animation.Transitions;
+
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation
+{
+ public class TransitionCollection : List>
+ {
+ public TransitionCollection()
+ {
+ Add(new CaptionedObject(new EntranceTransition(), "Entrance"));
+ Add(new CaptionedObject(new DrillInTransition(), "Drill in"));
+ Add(new CaptionedObject(new SlideTransition(Direction.FromLeft), "Slide from Left"));
+ Add(new CaptionedObject(new SlideTransition(Direction.FromRight), "Slide from Right"));
+ Add(new CaptionedObject(new SlideTransition(Direction.FromBottom), "Slide from Bottom"));
+ Add(new CaptionedObject(new SuppressTransition(), "Suppress"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/DrillInTransition.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/DrillInTransition.cs
new file mode 100644
index 0000000..9062098
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/DrillInTransition.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation.Transitions
+{
+ ///
+ /// Specifies the animation to run when a user navigates forward in a logical hierarchy,
+ /// like from a master list to a detail page.
+ ///
+ public sealed class DrillInTransition : Transition
+ {
+ protected override Animation GetEnterAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ var storyboard = new Storyboard();
+
+ if (movingBackwards)
+ {
+ var scaleXAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1.15, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(scaleXAnim, ScaleXPath);
+ storyboard.Children.Add(scaleXAnim);
+
+ var scaleYAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1.15, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(scaleYAnim, ScaleYPath);
+ storyboard.Children.Add(scaleYAnim);
+
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+ }
+ else
+ {
+ var scaleXAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0.9, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, MaxMoveDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(scaleXAnim, ScaleXPath);
+ storyboard.Children.Add(scaleXAnim);
+
+ var scaleYAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0.9, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, MaxMoveDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(scaleYAnim, ScaleYPath);
+ storyboard.Children.Add(scaleYAnim);
+
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, MaxMoveDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+ }
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new ScaleTransform());
+ element.SetCurrentValue(UIElement.RenderTransformOriginProperty, new Point(0.5, 0.5));
+
+ return new Animation(element, storyboard);
+ }
+
+ protected override Animation GetExitAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ var storyboard = new Storyboard();
+
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(0, ExitDuration, AccelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ return new Animation(element, storyboard);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/EntranceTransition.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/EntranceTransition.cs
new file mode 100644
index 0000000..1aef648
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/EntranceTransition.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation.Transitions
+{
+ ///
+ /// Specifies the animation to run when content appears on a Page.
+ ///
+ public sealed class EntranceTransition : Transition
+ {
+ protected override Animation GetEnterAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ var storyboard = new Storyboard();
+
+ if (movingBackwards)
+ {
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+ }
+ else
+ {
+ var yAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(200, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(0, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(yAnim, TranslateYPath);
+ storyboard.Children.Add(yAnim);
+
+ var opacityAnim = new DoubleAnimation(1, TimeSpan.Zero);
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new TranslateTransform());
+ }
+
+ return new Animation(element, storyboard);
+ }
+
+ protected override Animation GetExitAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ var storyboard = new Storyboard();
+
+ if (movingBackwards)
+ {
+ var yAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(200, ExitDuration, AccelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(yAnim, TranslateYPath);
+ storyboard.Children.Add(yAnim);
+
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1, TimeSpan.Zero),
+ new DiscreteDoubleKeyFrame(0, ExitDuration)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new TranslateTransform());
+ }
+ else
+ {
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(0, ExitDuration, AccelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+ }
+
+ return new Animation(element, storyboard);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/SlideTransition.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/SlideTransition.cs
new file mode 100644
index 0000000..a002709
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/SlideTransition.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation.Transitions
+{
+ ///
+ /// Defines constants that describe the type of animation to play during a slide
+ /// transition.
+ ///
+ public enum Direction
+ {
+ ///
+ /// The exiting page fades out and the entering page enters from the bottom.
+ ///
+ FromBottom = 0,
+
+ ///
+ /// The exiting page leaves to the right of the panel and the entering page enters
+ /// from the left.
+ ///
+ FromLeft = 1,
+
+ ///
+ /// The exiting page leaves to the left of the panel and the entering page enters
+ /// from the right.
+ ///
+ FromRight = 2
+ }
+
+ ///
+ /// Provides the parameters for a slide navigation transition.
+ ///
+ public sealed class SlideTransition : Transition, ISlideNavigationTransitionInfo2
+ {
+ public SlideTransition(Direction effect)
+ {
+ Effect = effect;
+ }
+
+ protected override Animation GetEnterAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ var storyboard = new Storyboard();
+
+ var effect = Effect;
+ if (effect == Direction.FromBottom)
+ {
+ if (movingBackwards)
+ {
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(1, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+ }
+ else
+ {
+ var yAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(200, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(0, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(yAnim, TranslateYPath);
+ storyboard.Children.Add(yAnim);
+
+ var opacityAnim = new DoubleAnimation(1, TimeSpan.Zero);
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new TranslateTransform());
+ }
+ }
+ else
+ {
+ bool fromLeft;
+ if (effect == Direction.FromLeft)
+ fromLeft = !movingBackwards;
+ else
+ fromLeft = movingBackwards;
+
+ var xAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(fromLeft ? -200 : 200, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(0, EnterDuration, DecelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(xAnim, TranslateXPath);
+ storyboard.Children.Add(xAnim);
+
+ var opacityAnim = new DoubleAnimation(1, TimeSpan.Zero);
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new TranslateTransform());
+ }
+
+ return new Animation(element, storyboard);
+ }
+
+ protected override Animation GetExitAnimation(FrameworkElement element, bool movingBackwards)
+ {
+ var storyboard = new Storyboard();
+
+ var effect = Effect;
+ if (effect == Direction.FromBottom)
+ {
+ if (movingBackwards)
+ {
+ var yAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(200, ExitDuration, AccelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(yAnim, TranslateYPath);
+ storyboard.Children.Add(yAnim);
+
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1, TimeSpan.Zero),
+ new DiscreteDoubleKeyFrame(0, ExitDuration)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new TranslateTransform());
+ }
+ else
+ {
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(0, ExitDuration, AccelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+ }
+ }
+ else
+ {
+ bool toLeft;
+ if (effect == Direction.FromLeft)
+ toLeft = movingBackwards;
+ else
+ toLeft = !movingBackwards;
+
+ var xAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(0, TimeSpan.Zero),
+ new SplineDoubleKeyFrame(toLeft ? -200 : 200, ExitDuration, AccelerateKeySpline)
+ }
+ };
+ Storyboard.SetTargetProperty(xAnim, TranslateXPath);
+ storyboard.Children.Add(xAnim);
+
+ var opacityAnim = new DoubleAnimationUsingKeyFrames
+ {
+ KeyFrames =
+ {
+ new DiscreteDoubleKeyFrame(1, TimeSpan.Zero),
+ new DiscreteDoubleKeyFrame(0, ExitDuration)
+ }
+ };
+ Storyboard.SetTargetProperty(opacityAnim, OpacityPath);
+ storyboard.Children.Add(opacityAnim);
+
+ element.SetCurrentValue(UIElement.RenderTransformProperty, new TranslateTransform());
+ }
+
+ return new Animation(element, storyboard);
+ }
+
+ #region Effect
+
+ ///
+ /// Identifies the Effect dependency property.
+ ///
+ public static readonly DependencyProperty EffectProperty =
+ DependencyProperty.Register(
+ nameof(Effect),
+ typeof(Direction),
+ typeof(SlideTransition),
+ null);
+
+ ///
+ /// Gets or sets the type of animation effect to play during the slide transition.
+ ///
+ ///
+ /// The type of animation effect to play during the slide transition.
+ ///
+ public Direction Effect
+ {
+ get => (Direction) GetValue(EffectProperty);
+ set => SetValue(EffectProperty, value);
+ }
+
+ #endregion
+ }
+
+ internal interface ISlideNavigationTransitionInfo2
+ {
+ Direction Effect { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/SuppressTransition.cs b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/SuppressTransition.cs
new file mode 100644
index 0000000..b43cc14
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/Animation/Transitions/SuppressTransition.cs
@@ -0,0 +1,9 @@
+namespace GenshinLyreMidiPlayer.ModernWPF.Animation.Transitions
+{
+ ///
+ /// Specifies that animations are suppressed during navigation.
+ ///
+ public sealed class SuppressTransition : Transition
+ {
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/AppTheme.cs b/GenshinLyreMidiPlayer/ModernWPF/AppTheme.cs
new file mode 100644
index 0000000..4a921e9
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/AppTheme.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Windows.Data;
+using ModernWpf;
+
+namespace GenshinLyreMidiPlayer.ModernWPF
+{
+ public class AppThemes : List
+ {
+ public AppThemes()
+ {
+ Add(AppTheme.Light);
+ Add(AppTheme.Dark);
+ Add(AppTheme.Default);
+ }
+ }
+
+ public class AppTheme
+ {
+ private AppTheme(string name, ApplicationTheme? value)
+ {
+ Name = name;
+ Value = value;
+ }
+
+ public ApplicationTheme? Value { get; }
+
+ public static AppTheme Light { get; } = new AppTheme("Light", ApplicationTheme.Light);
+
+ public static AppTheme Dark { get; } = new AppTheme("Dark", ApplicationTheme.Dark);
+
+ public static AppTheme Default { get; } = new AppTheme("Use system setting", null);
+
+ public string Name { get; }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ public class AppThemeConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ switch (value)
+ {
+ case ApplicationTheme.Light:
+ return AppTheme.Light;
+ case ApplicationTheme.Dark:
+ return AppTheme.Dark;
+ default:
+ return AppTheme.Default;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is AppTheme appTheme) return appTheme.Value;
+
+ return AppTheme.Default;
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/CaptionedObject.cs b/GenshinLyreMidiPlayer/ModernWPF/CaptionedObject.cs
new file mode 100644
index 0000000..08361eb
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/CaptionedObject.cs
@@ -0,0 +1,41 @@
+#nullable enable
+using System;
+
+namespace GenshinLyreMidiPlayer.ModernWPF
+{
+ public class CaptionedObject
+ {
+ public CaptionedObject(T o, string? caption = null)
+ {
+ Object = o;
+ Caption = caption;
+ }
+
+ public string? Caption { get; }
+
+ public T Object { get; }
+
+ public override string ToString()
+ {
+ return Caption ?? base.ToString() ?? string.Empty;
+ }
+ }
+
+ public class CaptionedObject : CaptionedObject where T : Enum
+ {
+ private readonly CaptionedObject _caption;
+
+ public CaptionedObject(T o, TEnum type, string? caption = null) : base(o, caption)
+ {
+ _caption = new CaptionedObject(o, caption);
+ Type = type;
+ }
+
+ public TEnum Type { get; }
+
+ public override string ToString()
+ {
+ return Caption ?? Type?.ToString() ?? base.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ModernWPF/OptionControl.cs b/GenshinLyreMidiPlayer/ModernWPF/OptionControl.cs
new file mode 100644
index 0000000..befea46
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ModernWPF/OptionControl.cs
@@ -0,0 +1,85 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Markup;
+
+namespace GenshinLyreMidiPlayer.ModernWPF
+{
+ [ContentProperty(nameof(Content))]
+ public class OptionControl : Control
+ {
+ static OptionControl()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(
+ typeof(OptionControl),
+ new FrameworkPropertyMetadata(typeof(OptionControl)));
+ }
+
+ #region HeaderText
+
+ public static readonly DependencyProperty HeaderTextProperty =
+ DependencyProperty.Register(
+ nameof(HeaderText),
+ typeof(string),
+ typeof(OptionControl),
+ new PropertyMetadata(string.Empty));
+
+ public string HeaderText
+ {
+ get => (string) GetValue(HeaderTextProperty);
+ set => SetValue(HeaderTextProperty, value);
+ }
+
+ #endregion
+
+ #region Content
+
+ public static readonly DependencyProperty ContentProperty =
+ DependencyProperty.Register(
+ nameof(Content),
+ typeof(object),
+ typeof(OptionControl),
+ null);
+
+ public object Content
+ {
+ get => GetValue(ContentProperty);
+ set => SetValue(ContentProperty, value);
+ }
+
+ #endregion
+
+ #region Options
+
+ public static readonly DependencyProperty OptionsProperty =
+ DependencyProperty.Register(
+ nameof(Options),
+ typeof(object),
+ typeof(OptionControl),
+ null);
+
+ public object Options
+ {
+ get => GetValue(OptionsProperty);
+ set => SetValue(OptionsProperty, value);
+ }
+
+ #endregion
+
+ #region MaxContentWidth
+
+ public static readonly DependencyProperty MaxContentWidthProperty =
+ DependencyProperty.Register(
+ nameof(MaxContentWidth),
+ typeof(double),
+ typeof(OptionControl),
+ new PropertyMetadata(1028d));
+
+ public double MaxContentWidth
+ {
+ get => (double) GetValue(MaxContentWidthProperty);
+ set => SetValue(MaxContentWidthProperty, value);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ViewModels/LyrePlayerViewModel.cs b/GenshinLyreMidiPlayer/ViewModels/LyrePlayerViewModel.cs
new file mode 100644
index 0000000..1a52066
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ViewModels/LyrePlayerViewModel.cs
@@ -0,0 +1,283 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Timers;
+using GenshinLyreMidiPlayer.Core;
+using GenshinLyreMidiPlayer.Models;
+using Melanchall.DryWetMidi.Core;
+using Melanchall.DryWetMidi.Devices;
+using Melanchall.DryWetMidi.Interaction;
+using Microsoft.Win32;
+using Stylet;
+
+namespace GenshinLyreMidiPlayer.ViewModels
+{
+ public class LyrePlayerViewModel : Screen, IHandle
+ {
+ private readonly SettingsPageViewModel _settings;
+ private bool _ignoreSliderChange;
+ private InputDevice? _inputDevice;
+ private MidiFile? _midiFile;
+ private Playback? _playback;
+ private ITimeSpan _playTime = new MidiTimeSpan();
+ private Timer _playTimer = new Timer();
+ private bool _reloadPlayback;
+ private MidiInputModel? _selectedMidiInput;
+ private double _songSlider;
+
+ public LyrePlayerViewModel(SettingsPageViewModel settings)
+ {
+ _settings = settings;
+ SelectedMidiInput = MidiInputs[0];
+ }
+
+ public BindableCollection MidiInputs { get; set; } = new BindableCollection
+ {
+ new MidiInputModel("None")
+ };
+
+ public double MaximumTime { get; set; } = 1;
+
+ public double SongSlider
+ {
+ get => _songSlider;
+ set
+ {
+ SetAndNotify(ref _songSlider, value);
+
+ if (!_ignoreSliderChange && _playback != null)
+ {
+ if (_playback.IsRunning)
+ {
+ _playback.Stop();
+ PlayPauseIcon = PlayIcon;
+ }
+
+ var time = TimeSpan.FromSeconds(_songSlider);
+
+ CurrentTime = time.ToString("m\\:ss");
+ _playback.MoveToTime((MetricTimeSpan) time);
+ }
+
+ _ignoreSliderChange = false;
+ }
+ }
+
+ public List MidiTracks { get; set; } = new List();
+
+ public MidiInputModel? SelectedMidiInput
+ {
+ get => _selectedMidiInput;
+ set
+ {
+ SetAndNotify(ref _selectedMidiInput, value);
+
+ _inputDevice?.Dispose();
+
+ if (_selectedMidiInput?.DeviceName != null && _selectedMidiInput.DeviceName != "None")
+ {
+ _inputDevice = InputDevice.GetByName(_selectedMidiInput.DeviceName);
+
+ _inputDevice.EventReceived += OnNoteEvent;
+ _inputDevice.StartEventsListening();
+ }
+ }
+ }
+
+ private static string PlayIcon => "\xEDB5";
+
+ private static string PauseIcon => "\xEDB4";
+
+ public string PlayPauseIcon { get; set; } = PlayIcon;
+
+ public string TotalTime { get; set; } = "0:00";
+
+ public string CurrentTime { get; set; } = "0:00";
+
+ public string SongName { get; set; } = "Open MIDI file...";
+
+ public void Handle(MidiTrackModel message)
+ {
+ _reloadPlayback = true;
+ }
+
+ public void OpenFile()
+ {
+ var openFileDialog = new OpenFileDialog
+ {
+ Filter = "MIDI file|*.mid;*.midi"
+ };
+
+ if (openFileDialog.ShowDialog() != true)
+ return;
+
+ CloseFile();
+
+ SongName = Path.GetFileNameWithoutExtension(openFileDialog.FileName);
+ _midiFile = MidiFile.Read(openFileDialog.FileName);
+ TimeSpan duration = _midiFile.GetDuration();
+
+ UpdateSlider(0);
+ CurrentTime = "0:00";
+ TotalTime = duration.ToString("m\\:ss");
+ MaximumTime = duration.TotalSeconds;
+
+ MidiTracks = _midiFile
+ .GetTrackChunks()
+ .Select(t => new MidiTrackModel(t))
+ .ToList();
+ MidiTracks.First().IsChecked = true;
+ }
+
+ public void CloseFile()
+ {
+ if (_playback != null)
+ {
+ _playback.Stop();
+ PlaybackCurrentTimeWatcher.Instance.RemovePlayback(_playback);
+ _playback.Dispose();
+ _playback = null;
+ }
+
+ _midiFile = null;
+ MidiTracks.Clear();
+
+ PlayPauseIcon = PlayIcon;
+ SongName = string.Empty;
+ TotalTime = "0:00";
+ CurrentTime = "0:00";
+ MaximumTime = 1;
+ }
+
+ public void Previous()
+ {
+ if (_playback != null)
+ {
+ _playback.MoveToStart();
+ UpdateSlider(0);
+ CurrentTime = "0:00";
+ }
+ }
+
+ public void Next()
+ {
+ CloseFile();
+ }
+
+ public void PlayPause()
+ {
+ if (_midiFile == null || MaximumTime == 0d) return;
+ if (_playback == null || _reloadPlayback)
+ {
+ if (_playback != null)
+ {
+ _playback.Stop();
+ _playTime = _playback.GetCurrentTime(TimeSpanType.Midi);
+ _playback.Dispose();
+ _playback = null;
+ PlayPauseIcon = PlayIcon;
+ }
+
+ _midiFile.Chunks.Clear();
+ _midiFile.Chunks.AddRange(MidiTracks
+ .Where(t => t.IsChecked)
+ .Select(t => t.Track));
+
+ _playback = _midiFile.GetPlayback();
+ _playback.Speed = _settings.SelectedSpeed.Speed;
+
+ _playback.MoveToTime(_playTime);
+ _playback.Finished += (s, e) => { CloseFile(); };
+
+ PlaybackCurrentTimeWatcher.Instance.AddPlayback(_playback, TimeSpanType.Metric);
+ PlaybackCurrentTimeWatcher.Instance.CurrentTimeChanged += OnSongTick;
+ PlaybackCurrentTimeWatcher.Instance.Start();
+
+ _playback.EventPlayed += OnNoteEvent;
+ _reloadPlayback = false;
+ }
+
+ if (_playback.IsRunning)
+ {
+ PlayPauseIcon = PlayIcon;
+ _playback.Stop();
+ }
+ else if (PlayPauseIcon == PauseIcon)
+ {
+ PlayPauseIcon = PlayIcon;
+ _playTimer.Dispose();
+ }
+ else
+ {
+ PlayPauseIcon = PauseIcon;
+
+ WindowHelper.EnsureGameOnTop();
+ _playTimer = new Timer {Interval = 100};
+ _playTimer.Elapsed += PlayTimerElapsed;
+ _playTimer.Start();
+ }
+ }
+
+ public void OnSongTick(object? sender, PlaybackCurrentTimeChangedEventArgs e)
+ {
+ foreach (var playbackTime in e.Times)
+ {
+ TimeSpan time = (MetricTimeSpan) playbackTime.Time;
+
+ UpdateSlider(time.TotalSeconds);
+ CurrentTime = time.ToString("m\\:ss");
+ }
+ }
+
+ private void OnNoteEvent(object? sender, MidiEventPlayedEventArgs e)
+ {
+ if (e.Event.EventType == MidiEventType.NoteOn)
+ PlayNote(e.Event as NoteOnEvent);
+ }
+
+ private void OnNoteEvent(object? sender, MidiEventReceivedEventArgs e)
+ {
+ if (e.Event.EventType == MidiEventType.NoteOn)
+ PlayNote(e.Event as NoteOnEvent);
+ }
+
+ private void PlayNote(NoteOnEvent? note)
+ {
+ if (note is null || note.Velocity <= 0)
+ return;
+
+ if (!LyrePlayer.PlayNote(note, _settings.TransposeNotes, _settings.KeyOffset,
+ _settings.SelectedLayout.Key))
+ PlayPause();
+ }
+
+ private void PlayTimerElapsed(object? sender, ElapsedEventArgs e)
+ {
+ if (WindowHelper.IsGameFocused())
+ {
+ _playback.Start();
+ _playTimer.Dispose();
+ }
+ }
+
+ private void UpdateSlider(double value)
+ {
+ _ignoreSliderChange = true;
+ SongSlider = value;
+ }
+
+ public void RefreshDevices()
+ {
+ MidiInputs.Clear();
+ MidiInputs.Add(new MidiInputModel("None"));
+
+ foreach (var device in InputDevice.GetAll())
+ {
+ MidiInputs.Add(new MidiInputModel(device.Name));
+ }
+
+ SelectedMidiInput = MidiInputs[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ViewModels/MainWindowViewModel.cs b/GenshinLyreMidiPlayer/ViewModels/MainWindowViewModel.cs
index 2292f4e..fb73f37 100644
--- a/GenshinLyreMidiPlayer/ViewModels/MainWindowViewModel.cs
+++ b/GenshinLyreMidiPlayer/ViewModels/MainWindowViewModel.cs
@@ -1,368 +1,63 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
+using System.Collections.Generic;
using System.Linq;
-using System.Timers;
-using GenshinLyreMidiPlayer.Core;
-using GenshinLyreMidiPlayer.Models;
-using Melanchall.DryWetMidi.Core;
-using Melanchall.DryWetMidi.Devices;
-using Melanchall.DryWetMidi.Interaction;
-using Microsoft.Win32;
+using GenshinLyreMidiPlayer.Views;
+using ModernWpf.Controls;
using Stylet;
+using StyletIoC;
namespace GenshinLyreMidiPlayer.ViewModels
{
- public class MainWindowViewModel : Screen, IHandle
+ public class MainWindowViewModel : Conductor.StackNavigation
{
- private bool _ignoreSliderChange;
- private InputDevice _inputDevice;
- private int _keyOffset;
- private MidiFile _midiFile;
- private Playback _playback;
- private ITimeSpan _playTime = new MidiTimeSpan();
- private Timer _playTimer;
- private bool _reloadPlayback;
- private MidiInputModel _selectedMidiInput;
- private double _songSlider;
+ private readonly Stack _history = new Stack();
- public MainWindowViewModel()
+ public MainWindowViewModel(IContainer ioc)
{
- SelectedSpeed = MidiSpeeds[3];
- SelectedMidiInput = MidiInputs[0];
- SelectedLayout = Keyboard.LayoutNames.First();
+ SettingsView = ioc.Get();
+ PlayerView = new LyrePlayerViewModel(SettingsView);
}
- public BindableCollection MidiInputs { get; set; } = new BindableCollection
- {
- new MidiInputModel("None")
- };
-
- public bool TransposeNotes { get; set; } = true;
+ public LyrePlayerViewModel PlayerView { get; set; }
- public Dictionary KeyOffsets { get; set; } = new Dictionary
- {
- [-27] = "A0",
- [-26] = "A♯0",
- [-25] = "B0",
- [-24] = "C1",
- [-23] = "C♯1",
- [-22] = "D1",
- [-21] = "D♯1",
- [-20] = "E1",
- [-19] = "F1",
- [-18] = "F♯1",
- [-17] = "G1",
- [-16] = "G♯1",
- [-15] = "A1",
- [-14] = "A♯1",
- [-13] = "B1",
- [-12] = "C2",
- [-11] = "C♯2",
- [-10] = "D2",
- [-9] = "D♯2",
- [-8] = "E2",
- [-7] = "F2",
- [-6] = "F♯2",
- [-5] = "G2",
- [-4] = "G♯2",
- [-3] = "A2",
- [-2] = "A♯2",
- [-1] = "B2",
- [0] = "C3",
- [1] = "C♯3",
- [2] = "D3",
- [3] = "D♯3",
- [4] = "E3",
- [5] = "F3",
- [6] = "F♯3",
- [7] = "G3",
- [8] = "G♯3",
- [9] = "A3",
- [10] = "A♯3",
- [11] = "B3",
- [12] = "C4 Middle C",
- [13] = "C♯4",
- [14] = "D4",
- [15] = "D♯4",
- [16] = "E4",
- [17] = "F4",
- [18] = "F♯4",
- [19] = "G4",
- [20] = "G♯4",
- [21] = "A4 Concert Pitch",
- [22] = "A♯4",
- [23] = "B4",
- [24] = "C5"
- };
+ public SettingsPageViewModel SettingsView { get; set; }
- public double MaximumTime { get; set; } = 1;
+ public string Title { get; set; } = "Genshin Lyre MIDI Player";
- public double SongSlider
+ protected override void OnViewLoaded()
{
- get => _songSlider;
- set
- {
- SetAndNotify(ref _songSlider, value);
-
- if (!_ignoreSliderChange && _playback != null)
- {
- if (_playback.IsRunning)
- {
- _playback.Stop();
- PlayPauseIcon = PlayIcon;
- }
-
- var time = TimeSpan.FromSeconds(_songSlider);
-
- CurrentTime = time.ToString("m\\:ss");
- _playback.MoveToTime((MetricTimeSpan) time);
- }
+ // Work around because events do not conform to the signatures Stylet supports
+ var navView = ((MainWindowView) View).NavView;
+ navView.SelectionChanged += Navigate;
+ navView.BackRequested += NavigateBack;
- _ignoreSliderChange = false;
- }
+ var menuItems = navView.MenuItems.Cast();
+ navView.SelectedItem = menuItems.FirstOrDefault(item => item is NavigationViewItem);
}
- public int MinOffset => KeyOffsets.Keys.Min();
-
- public int MaxOffset => KeyOffsets.Keys.Max();
-
- public int KeyOffset
+ private void NavigateBack(NavigationView sender, NavigationViewBackRequestedEventArgs args)
{
- get => _keyOffset;
- set => SetAndNotify(ref _keyOffset, Math.Clamp(value, MinOffset, MaxOffset));
- }
-
- public KeyValuePair SelectedLayout { get; set; }
+ GoBack();
- public List MidiSpeeds { get; set; } = new List
- {
- new MidiSpeedModel("0.25x", 0.25),
- new MidiSpeedModel("0.5x", 0.5),
- new MidiSpeedModel("0.75x", 0.75),
- new MidiSpeedModel("Normal", 1),
- new MidiSpeedModel("1.25x", 1.25),
- new MidiSpeedModel("1.5x", 1.5),
- new MidiSpeedModel("1.75x", 1.75),
- new MidiSpeedModel("2x", 2)
- };
-
- public List MidiTracks { get; set; } = new List();
-
- public MidiInputModel SelectedMidiInput
- {
- get => _selectedMidiInput;
- set
- {
- SetAndNotify(ref _selectedMidiInput, value);
-
- _inputDevice?.Dispose();
-
- if (_selectedMidiInput?.DeviceName != null && _selectedMidiInput.DeviceName != "None")
- {
- _inputDevice = InputDevice.GetByName(_selectedMidiInput.DeviceName);
-
- _inputDevice.EventReceived += OnNoteEvent;
- _inputDevice.StartEventsListening();
- }
- }
+ // Work around to select the navigation item that this IScreen is a part of
+ _history.Pop();
+ sender.IsBackEnabled = _history.Count > 2;
+ sender.SelectedItem = _history.Pop();
}
- public MidiSpeedModel SelectedSpeed { get; set; }
-
- private static string PlayIcon => "\xEDB5";
-
- private static string PauseIcon => "\xEDB4";
-
- public string PlayPauseIcon { get; set; } = PlayIcon;
-
- public string TotalTime { get; set; } = "0:00";
-
- public string CurrentTime { get; set; } = "0:00";
-
- public string SongName { get; set; } = "Open MIDI file...";
-
- public string Key => $"Key: {KeyOffsets[KeyOffset]}";
-
- public void Handle(MidiTrackModel message)
- {
- _reloadPlayback = true;
- }
-
- public void OpenFile()
- {
- var openFileDialog = new OpenFileDialog
- {
- Filter = "MIDI file|*.mid;*.midi"
- };
-
- if (openFileDialog.ShowDialog() != true)
- return;
-
- CloseFile();
-
- SongName = Path.GetFileNameWithoutExtension(openFileDialog.FileName);
- _midiFile = MidiFile.Read(openFileDialog.FileName);
- TimeSpan duration = _midiFile.GetDuration();
-
- UpdateSlider(0);
- CurrentTime = "0:00";
- TotalTime = duration.ToString("m\\:ss");
- MaximumTime = duration.TotalSeconds;
-
- MidiTracks = _midiFile
- .GetTrackChunks()
- .Select(t => new MidiTrackModel(t))
- .ToList();
- MidiTracks.First().IsChecked = true;
- }
-
- public void CloseFile()
- {
- if (_playback != null)
- {
- _playback.Stop();
- PlaybackCurrentTimeWatcher.Instance.RemovePlayback(_playback);
- _playback.Dispose();
- _playback = null;
- }
-
- _midiFile = null;
- MidiTracks.Clear();
-
- PlayPauseIcon = PlayIcon;
- SongName = string.Empty;
- TotalTime = "0:00";
- CurrentTime = "0:00";
- MaximumTime = 1;
- }
-
- public void Previous()
+ private void Navigate(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
- if (_playback != null)
+ if (args.IsSettingsSelected)
{
- _playback.MoveToStart();
- UpdateSlider(0);
- CurrentTime = "0:00";
+ ActivateItem(SettingsView);
+ _history.Push((NavigationViewItem) sender.SettingsItem);
}
- }
-
- public void Next()
- {
- CloseFile();
- }
-
- public void PlayPause()
- {
- if (_midiFile == null || MaximumTime == 0d) return;
- if (_playback == null || _reloadPlayback)
- {
- if (_playback != null)
- {
- _playback.Stop();
- _playTime = _playback.GetCurrentTime(TimeSpanType.Midi);
- _playback.Dispose();
- _playback = null;
- PlayPauseIcon = PlayIcon;
- }
-
- _midiFile.Chunks.Clear();
- _midiFile.Chunks.AddRange(MidiTracks
- .Where(t => t.IsChecked)
- .Select(t => t.Track));
-
- _playback = _midiFile.GetPlayback();
- _playback.Speed = SelectedSpeed.Speed;
-
- _playback.MoveToTime(_playTime);
- _playback.Finished += (s, e) => { CloseFile(); };
-
- PlaybackCurrentTimeWatcher.Instance.AddPlayback(_playback, TimeSpanType.Metric);
- PlaybackCurrentTimeWatcher.Instance.CurrentTimeChanged += OnSongTick;
- PlaybackCurrentTimeWatcher.Instance.Start();
-
- _playback.EventPlayed += OnNoteEvent;
- _reloadPlayback = false;
- }
-
- if (_playback.IsRunning)
- {
- PlayPauseIcon = PlayIcon;
- _playback.Stop();
- }
- else if (PlayPauseIcon == PauseIcon)
- {
- PlayPauseIcon = PlayIcon;
- _playTimer.Dispose();
- }
- else
- {
- PlayPauseIcon = PauseIcon;
-
- WindowHelper.EnsureGameOnTop();
- _playTimer = new Timer {Interval = 100};
- _playTimer.Elapsed += PlayTimerElapsed;
- _playTimer.Start();
- }
- }
-
- public void OnSongTick(object sender, PlaybackCurrentTimeChangedEventArgs e)
- {
- foreach (var playbackTime in e.Times)
- {
- TimeSpan time = (MetricTimeSpan) playbackTime.Time;
-
- UpdateSlider(time.TotalSeconds);
- CurrentTime = time.ToString("m\\:ss");
- }
- }
-
- private void OnNoteEvent(object sender, MidiEventPlayedEventArgs e)
- {
- if (e.Event.EventType == MidiEventType.NoteOn)
- PlayNote(e.Event as NoteOnEvent);
- }
-
- private void OnNoteEvent(object sender, MidiEventReceivedEventArgs e)
- {
- if (e.Event.EventType == MidiEventType.NoteOn)
- PlayNote(e.Event as NoteOnEvent);
- }
-
- private void PlayNote(NoteOnEvent note)
- {
- if (note != null && note.Velocity <= 0) return;
-
- if (!LyrePlayer.PlayNote(note, TransposeNotes, KeyOffset, SelectedLayout.Key))
- PlayPause();
- }
-
- private void PlayTimerElapsed(object sender, ElapsedEventArgs e)
- {
- if (WindowHelper.IsGameFocused())
- {
- _playback.Start();
- _playTimer.Dispose();
- }
- }
-
- private void UpdateSlider(double value)
- {
- _ignoreSliderChange = true;
- SongSlider = value;
- }
-
- public void RefreshDevices()
- {
- MidiInputs.Clear();
- MidiInputs.Add(new MidiInputModel("None"));
-
- foreach (var device in InputDevice.GetAll())
+ else if ((args.SelectedItem as NavigationViewItem)?.Tag is IScreen viewModel)
{
- MidiInputs.Add(new MidiInputModel(device.Name));
+ ActivateItem(viewModel);
+ _history.Push((NavigationViewItem) sender.SelectedItem);
}
- SelectedMidiInput = MidiInputs[0];
+ sender.IsBackEnabled = _history.Count > 1;
}
}
}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/ViewModels/SettingsPageViewModel.cs b/GenshinLyreMidiPlayer/ViewModels/SettingsPageViewModel.cs
new file mode 100644
index 0000000..ce4d979
--- /dev/null
+++ b/GenshinLyreMidiPlayer/ViewModels/SettingsPageViewModel.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using GenshinLyreMidiPlayer.Core;
+using GenshinLyreMidiPlayer.Models;
+using GenshinLyreMidiPlayer.ModernWPF;
+using GenshinLyreMidiPlayer.ModernWPF.Animation;
+using Stylet;
+
+namespace GenshinLyreMidiPlayer.ViewModels
+{
+ public class SettingsPageViewModel : Screen
+ {
+ private int _keyOffset;
+
+ public SettingsPageViewModel()
+ {
+ Transitions = new TransitionCollection();
+ Transition = Transitions.First();
+
+ SelectedLayout = Keyboard.LayoutNames.First();
+ SelectedSpeed = MidiSpeeds[3];
+ }
+
+ public bool TransposeNotes { get; set; } = true;
+
+ public static CaptionedObject Transition { get; set; }
+
+ public Dictionary KeyOffsets { get; set; } = new Dictionary
+ {
+ [-27] = "A0",
+ [-26] = "A♯0",
+ [-25] = "B0",
+ [-24] = "C1",
+ [-23] = "C♯1",
+ [-22] = "D1",
+ [-21] = "D♯1",
+ [-20] = "E1",
+ [-19] = "F1",
+ [-18] = "F♯1",
+ [-17] = "G1",
+ [-16] = "G♯1",
+ [-15] = "A1",
+ [-14] = "A♯1",
+ [-13] = "B1",
+ [-12] = "C2",
+ [-11] = "C♯2",
+ [-10] = "D2",
+ [-9] = "D♯2",
+ [-8] = "E2",
+ [-7] = "F2",
+ [-6] = "F♯2",
+ [-5] = "G2",
+ [-4] = "G♯2",
+ [-3] = "A2",
+ [-2] = "A♯2",
+ [-1] = "B2",
+ [0] = "C3",
+ [1] = "C♯3",
+ [2] = "D3",
+ [3] = "D♯3",
+ [4] = "E3",
+ [5] = "F3",
+ [6] = "F♯3",
+ [7] = "G3",
+ [8] = "G♯3",
+ [9] = "A3",
+ [10] = "A♯3",
+ [11] = "B3",
+ [12] = "C4 Middle C",
+ [13] = "C♯4",
+ [14] = "D4",
+ [15] = "D♯4",
+ [16] = "E4",
+ [17] = "F4",
+ [18] = "F♯4",
+ [19] = "G4",
+ [20] = "G♯4",
+ [21] = "A4 Concert Pitch",
+ [22] = "A♯4",
+ [23] = "B4",
+ [24] = "C5"
+ };
+
+ public static IEnumerable> Transitions { get; private set; }
+
+ public int MinOffset => KeyOffsets.Keys.Min();
+
+ public int MaxOffset => KeyOffsets.Keys.Max();
+
+ public int KeyOffset
+ {
+ get => _keyOffset;
+ set => SetAndNotify(ref _keyOffset, Math.Clamp(value, MinOffset, MaxOffset));
+ }
+
+ public KeyValuePair SelectedLayout { get; set; }
+
+ public List MidiSpeeds { get; set; } = new List
+ {
+ new MidiSpeedModel("0.25x", 0.25),
+ new MidiSpeedModel("0.5x", 0.5),
+ new MidiSpeedModel("0.75x", 0.75),
+ new MidiSpeedModel("Normal", 1),
+ new MidiSpeedModel("1.25x", 1.25),
+ new MidiSpeedModel("1.5x", 1.5),
+ new MidiSpeedModel("1.75x", 1.75),
+ new MidiSpeedModel("2x", 2)
+ };
+
+ public MidiSpeedModel SelectedSpeed { get; set; }
+
+ public string Key => $"Key: {KeyOffsets[KeyOffset]}";
+ }
+}
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/Views/LyrePlayerView.xaml b/GenshinLyreMidiPlayer/Views/LyrePlayerView.xaml
new file mode 100644
index 0000000..99ce34d
--- /dev/null
+++ b/GenshinLyreMidiPlayer/Views/LyrePlayerView.xaml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/Views/MainWindowView.xaml b/GenshinLyreMidiPlayer/Views/MainWindowView.xaml
index d5bb3bb..97f15a7 100644
--- a/GenshinLyreMidiPlayer/Views/MainWindowView.xaml
+++ b/GenshinLyreMidiPlayer/Views/MainWindowView.xaml
@@ -3,152 +3,43 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- mc:Ignorable="d"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:viewModels="clr-namespace:GenshinLyreMidiPlayer.ViewModels"
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:genshinLyreMidiPlayer="clr-namespace:GenshinLyreMidiPlayer"
- xmlns:core="clr-namespace:GenshinLyreMidiPlayer.Core"
+ xmlns:modernWpf="clr-namespace:GenshinLyreMidiPlayer.ModernWPF"
d:DataContext="{d:DesignInstance Type=viewModels:MainWindowViewModel}"
- Title="Genshin Lyre MIDI Player"
- Height="850" Width="435" MinWidth="435"
- ui:WindowHelper.UseModernWindowStyle="True">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Title="{Binding Title}"
+ Height="850" Width="650" MinWidth="435"
+ ui:WindowHelper.UseModernWindowStyle="True"
+ ui:TitleBar.ExtendViewIntoTitleBar="True">
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/GenshinLyreMidiPlayer/Views/SettingsPageView.xaml b/GenshinLyreMidiPlayer/Views/SettingsPageView.xaml
new file mode 100644
index 0000000..f93ca99
--- /dev/null
+++ b/GenshinLyreMidiPlayer/Views/SettingsPageView.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file