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