diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest
index cd30aa5..f91ad82 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest
@@ -11,7 +11,7 @@
+ Version="1.0.2.0" />
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/ShowNotificatonMessage.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/ShowNotificatonMessage.cs
new file mode 100644
index 0000000..e720895
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/ShowNotificatonMessage.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using BetterLyrics.WinUI3.Models;
+using CommunityToolkit.Mvvm.Messaging.Messages;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace BetterLyrics.WinUI3.Messages
+{
+ public class ShowNotificatonMessage(Notification value)
+ : ValueChangedMessage(value) { }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Notification.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Notification.cs
new file mode 100644
index 0000000..67cee90
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Notification.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace BetterLyrics.WinUI3.Models
+{
+ public partial class Notification : ObservableObject
+ {
+ [ObservableProperty]
+ private InfoBarSeverity _severity;
+
+ [ObservableProperty]
+ private string? _message;
+
+ [ObservableProperty]
+ private bool _isForeverDismissable;
+
+ [ObservableProperty]
+ private Visibility _visibility;
+
+ [ObservableProperty]
+ private string? _relatedSettingsKeyName;
+
+ public Notification(
+ string? message = null,
+ InfoBarSeverity severity = InfoBarSeverity.Informational,
+ bool isForeverDismissable = false,
+ string? relatedSettingsKeyName = null
+ )
+ {
+ Message = message;
+ Severity = severity;
+ IsForeverDismissable = isForeverDismissable;
+ Visibility = IsForeverDismissable ? Visibility.Visible : Visibility.Collapsed;
+ RelatedSettingsKeyName = relatedSettingsKeyName;
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Rendering/CoverBackgroundRenderer.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Rendering/CoverBackgroundRenderer.cs
new file mode 100644
index 0000000..2f8412b
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Rendering/CoverBackgroundRenderer.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using BetterLyrics.WinUI3.Services.Settings;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Effects;
+using Microsoft.Graphics.Canvas.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Graphics.Imaging;
+
+namespace BetterLyrics.WinUI3.Rendering
+{
+ public class CoverBackgroundRenderer
+ {
+ private readonly SettingsService _settingsService;
+
+ public float RotateAngle { get; set; } = 0f;
+
+ private SoftwareBitmap? _lastSoftwareBitmap = null;
+ private SoftwareBitmap? _softwareBitmap = null;
+ public SoftwareBitmap? SoftwareBitmap
+ {
+ get => _softwareBitmap;
+ set
+ {
+ if (_softwareBitmap != null)
+ {
+ _lastSoftwareBitmap = _softwareBitmap;
+ _transitionStartTime = DateTimeOffset.Now;
+ _isTransitioning = true;
+ _transitionAlpha = 0f;
+ }
+
+ _softwareBitmap = value;
+ }
+ }
+
+ private float _transitionAlpha = 1f;
+ private TimeSpan _transitionDuration = TimeSpan.FromMilliseconds(1000);
+ private DateTimeOffset _transitionStartTime;
+ private bool _isTransitioning = false;
+
+ public CoverBackgroundRenderer()
+ {
+ _settingsService = Ioc.Default.GetService()!;
+ }
+
+ public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
+ {
+ if (!_settingsService.IsCoverOverlayEnabled || SoftwareBitmap == null)
+ return;
+
+ ds.Transform = Matrix3x2.CreateRotation(RotateAngle, control.Size.ToVector2() * 0.5f);
+
+ var overlappedCovers = new CanvasCommandList(control);
+ using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
+
+ if (_isTransitioning && _lastSoftwareBitmap != null)
+ {
+ DrawImgae(control, overlappedCoversDs, _lastSoftwareBitmap, 1 - _transitionAlpha);
+ DrawImgae(control, overlappedCoversDs, SoftwareBitmap, _transitionAlpha);
+ }
+ else
+ {
+ DrawImgae(control, overlappedCoversDs, SoftwareBitmap, 1);
+ }
+
+ using var coverOverlayEffect = new OpacityEffect
+ {
+ Opacity = _settingsService.CoverOverlayOpacity / 100f,
+ Source = new GaussianBlurEffect
+ {
+ BlurAmount = _settingsService.CoverOverlayBlurAmount,
+ Source = overlappedCovers,
+ },
+ };
+ ds.DrawImage(coverOverlayEffect);
+
+ ds.Transform = Matrix3x2.Identity;
+ }
+
+ private void DrawImgae(
+ ICanvasAnimatedControl control,
+ CanvasDrawingSession ds,
+ SoftwareBitmap softwareBitmap,
+ float opacity
+ )
+ {
+ float imageWidth = (float)(softwareBitmap.PixelWidth * 96f / softwareBitmap.DpiX);
+ float imageHeight = (float)(softwareBitmap.PixelHeight * 96f / softwareBitmap.DpiY);
+ var scaleFactor =
+ (float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2))
+ / Math.Min(imageWidth, imageHeight);
+
+ ds.DrawImage(
+ new OpacityEffect
+ {
+ Source = new ScaleEffect
+ {
+ InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
+ BorderMode = EffectBorderMode.Hard,
+ Scale = new Vector2(scaleFactor),
+ Source = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap),
+ },
+ Opacity = opacity,
+ },
+ (float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
+ (float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
+ );
+ }
+
+ public void Calculate(ICanvasAnimatedControl control)
+ {
+ if (_isTransitioning)
+ {
+ var elapsed = DateTimeOffset.Now - _transitionStartTime;
+ float progress = (float)(
+ elapsed.TotalMilliseconds / _transitionDuration.TotalMilliseconds
+ );
+ _transitionAlpha = Math.Clamp(progress, 0f, 1f);
+
+ if (_transitionAlpha >= 1f)
+ {
+ _isTransitioning = false;
+ _lastSoftwareBitmap?.Dispose();
+ _lastSoftwareBitmap = null;
+ }
+ }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Rendering/PureLyricsRenderer.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Rendering/PureLyricsRenderer.cs
new file mode 100644
index 0000000..40a5cb1
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Rendering/PureLyricsRenderer.cs
@@ -0,0 +1,422 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Threading.Tasks;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Models;
+using BetterLyrics.WinUI3.Services.Settings;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Microsoft.Graphics.Canvas;
+using Microsoft.Graphics.Canvas.Brushes;
+using Microsoft.Graphics.Canvas.Text;
+using Microsoft.Graphics.Canvas.UI.Xaml;
+using Microsoft.UI;
+using Microsoft.UI.Text;
+using Windows.UI;
+
+namespace BetterLyrics.WinUI3.Rendering
+{
+ public class PureLyricsRenderer
+ {
+ private readonly SettingsService _settingsService;
+
+ private readonly float _defaultOpacity = 0.3f;
+ private readonly float _highlightedOpacity = 1.0f;
+
+ private readonly float _defaultScale = 0.95f;
+ private readonly float _highlightedScale = 1.0f;
+
+ private readonly int _lineEnteringDurationMs = 800;
+ private readonly int _lineExitingDurationMs = 800;
+ private readonly int _lineScrollDurationMs = 800;
+
+ private float _lastTotalYScroll = 0.0f;
+ private float _totalYScroll = 0.0f;
+
+ private int _startVisibleLineIndex = -1;
+ private int _endVisibleLineIndex = -1;
+
+ private bool _forceToScroll = false;
+
+ private readonly double _rightMargin = 36;
+
+ public double LimitedLineWidth { get; set; } = 0;
+ public double CanvasWidth { get; set; } = 0;
+ public double CanvasHeight { get; set; } = 0;
+
+ public TimeSpan CurrentTime { get; set; }
+
+ public List LyricsLines { get; set; } = [];
+
+ public PureLyricsRenderer()
+ {
+ _settingsService = Ioc.Default.GetService()!;
+ }
+
+ private Tuple GetVisibleLyricsLineIndexBoundaries()
+ {
+ // _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
+ return new Tuple(_startVisibleLineIndex, _endVisibleLineIndex);
+ }
+
+ private Tuple GetMaxLyricsLineIndexBoundaries()
+ {
+ if (LyricsLines.Count == 0)
+ {
+ return new Tuple(-1, -1);
+ }
+
+ return new Tuple(0, LyricsLines.Count - 1);
+ }
+
+ public void Draw(
+ ICanvasAnimatedControl control,
+ CanvasDrawingSession ds,
+ byte r,
+ byte g,
+ byte b
+ )
+ {
+ var (displayStartLineIndex, displayEndLineIndex) =
+ GetVisibleLyricsLineIndexBoundaries();
+
+ for (
+ int i = displayStartLineIndex;
+ LyricsLines.Count > 0
+ && i >= 0
+ && i < LyricsLines.Count
+ && i <= displayEndLineIndex;
+ i++
+ )
+ {
+ var line = LyricsLines[i];
+
+ if (line.TextLayout == null)
+ {
+ return;
+ }
+
+ float progressPerChar = 1f / line.Text.Length;
+
+ var position = line.Position;
+
+ float centerX = position.X;
+ float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
+
+ switch ((LyricsAlignmentType)_settingsService.LyricsAlignmentType)
+ {
+ case LyricsAlignmentType.Left:
+ line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
+ break;
+ case LyricsAlignmentType.Center:
+ line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
+ centerX += (float)LimitedLineWidth / 2;
+ break;
+ case LyricsAlignmentType.Right:
+ line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
+ centerX += (float)LimitedLineWidth;
+ break;
+ default:
+ break;
+ }
+
+ int startIndex = 0;
+
+ // Set brush
+ for (int j = 0; j < line.TextLayout.LineCount; j++)
+ {
+ int count = line.TextLayout.LineMetrics[j].CharacterCount;
+ var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
+ float subLinePlayingProgress = Math.Clamp(
+ (line.PlayingProgress * line.Text.Length - startIndex) / count,
+ 0,
+ 1
+ );
+
+ using var horizontalFillBrush = new CanvasLinearGradientBrush(
+ control,
+ [
+ new()
+ {
+ Position = 0,
+ Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
+ },
+ new()
+ {
+ Position =
+ subLinePlayingProgress * (1 + progressPerChar)
+ - progressPerChar,
+ Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
+ },
+ new()
+ {
+ Position = subLinePlayingProgress * (1 + progressPerChar),
+ Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
+ },
+ new()
+ {
+ Position = 1.5f,
+ Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
+ },
+ ]
+ )
+ {
+ StartPoint = new Vector2(
+ (float)(regions[0].LayoutBounds.Left + position.X),
+ 0
+ ),
+ EndPoint = new Vector2(
+ (float)(regions[^1].LayoutBounds.Right + position.X),
+ 0
+ ),
+ };
+
+ line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
+ startIndex += count;
+ }
+
+ // Scale
+ ds.Transform =
+ Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY))
+ * Matrix3x2.CreateTranslation(0, _totalYScroll);
+ // _logger.LogDebug(_totalYScroll);
+
+ ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
+
+ // Reset scale
+ ds.Transform = Matrix3x2.Identity;
+ }
+ }
+
+ public async Task ForceToScrollToCurrentPlayingLineAsync()
+ {
+ _forceToScroll = true;
+ await Task.Delay(1);
+ _forceToScroll = false;
+ }
+
+ public async Task ReLayoutAsync(ICanvasAnimatedControl control)
+ {
+ if (control == null)
+ return;
+
+ float leftMargin = (float)(CanvasWidth - LimitedLineWidth - _rightMargin);
+
+ using CanvasTextFormat textFormat = new()
+ {
+ FontSize = _settingsService.LyricsFontSize,
+ HorizontalAlignment = CanvasHorizontalAlignment.Left,
+ VerticalAlignment = CanvasVerticalAlignment.Top,
+ FontWeight = FontWeights.Bold,
+ //FontFamily = "Segoe UI Mono",
+ };
+ float y = (float)CanvasHeight / 2;
+
+ // Init Positions
+ for (int i = 0; i < LyricsLines.Count; i++)
+ {
+ var line = LyricsLines[i];
+
+ // Calculate layout bounds
+ line.TextLayout = new CanvasTextLayout(
+ control.Device,
+ line.Text,
+ textFormat,
+ (float)LimitedLineWidth,
+ (float)CanvasHeight
+ );
+ line.Position = new Vector2(leftMargin, y);
+
+ y +=
+ (float)line.TextLayout.LayoutBounds.Height
+ / line.TextLayout.LineCount
+ * (line.TextLayout.LineCount + _settingsService.LyricsLineSpacingFactor);
+ }
+
+ await ForceToScrollToCurrentPlayingLineAsync();
+ }
+
+ public void CalculateScaleAndOpacity(int currentPlayingLineIndex)
+ {
+ var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
+
+ for (int i = startLineIndex; LyricsLines.Count > 0 && i <= endLineIndex; i++)
+ {
+ var line = LyricsLines[i];
+
+ bool linePlaying = i == currentPlayingLineIndex;
+
+ var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
+ var lineExitingDurationMs = _lineExitingDurationMs;
+ if (i + 1 <= endLineIndex)
+ {
+ lineExitingDurationMs = Math.Min(
+ LyricsLines[i + 1].DurationMs,
+ lineExitingDurationMs
+ );
+ }
+
+ float lineEnteringProgress = 0.0f;
+ float lineExitingProgress = 0.0f;
+
+ bool lineEntering = false;
+ bool lineExiting = false;
+
+ float scale = _defaultScale;
+ float opacity = _defaultOpacity;
+
+ float playProgress = 0;
+
+ if (linePlaying)
+ {
+ line.PlayingState = LyricsPlayingState.Playing;
+
+ scale = _highlightedScale;
+ opacity = _highlightedOpacity;
+
+ playProgress =
+ ((float)CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
+ / line.DurationMs;
+
+ var durationFromStartMs =
+ CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
+ lineEntering = durationFromStartMs <= lineEnteringDurationMs;
+ if (lineEntering)
+ {
+ lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
+ scale =
+ _defaultScale
+ + (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
+ opacity =
+ _defaultOpacity
+ + (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
+ }
+ }
+ else
+ {
+ if (i < currentPlayingLineIndex)
+ {
+ line.PlayingState = LyricsPlayingState.Played;
+ playProgress = 1;
+
+ var durationToEndMs =
+ CurrentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
+ lineExiting = durationToEndMs <= lineExitingDurationMs;
+ if (lineExiting)
+ {
+ lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
+ scale =
+ _highlightedScale
+ - (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
+ opacity =
+ _highlightedOpacity
+ - (_highlightedOpacity - _defaultOpacity)
+ * (float)lineExitingProgress;
+ }
+ }
+ else
+ {
+ line.PlayingState = LyricsPlayingState.NotPlayed;
+ }
+ }
+
+ line.EnteringProgress = lineEnteringProgress;
+ line.ExitingProgress = lineExitingProgress;
+
+ line.Scale = scale;
+ line.Opacity = opacity;
+
+ line.PlayingProgress = playProgress;
+ }
+ }
+
+ public void CalculatePosition(ICanvasAnimatedControl control, int currentPlayingLineIndex)
+ {
+ if (currentPlayingLineIndex < 0)
+ {
+ return;
+ }
+
+ var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
+
+ if (startLineIndex < 0 || endLineIndex < 0)
+ {
+ return;
+ }
+
+ // Set _scrollOffsetY
+ LyricsLine? currentPlayingLine = LyricsLines?[currentPlayingLineIndex];
+
+ if (currentPlayingLine == null)
+ {
+ return;
+ }
+
+ if (currentPlayingLine.TextLayout == null)
+ {
+ return;
+ }
+
+ var lineScrollingProgress =
+ (CurrentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
+ / Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
+
+ var targetYScrollOffset = (float)(
+ -currentPlayingLine.Position.Y
+ + LyricsLines![0].Position.Y
+ - currentPlayingLine.TextLayout.LayoutBounds.Height / 2
+ - _lastTotalYScroll
+ );
+
+ var yScrollOffset =
+ targetYScrollOffset
+ * EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
+
+ bool isScrollingNow = lineScrollingProgress <= 1;
+
+ if (isScrollingNow)
+ {
+ _totalYScroll = _lastTotalYScroll + yScrollOffset;
+ }
+ else
+ {
+ if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1)
+ {
+ _totalYScroll = _lastTotalYScroll + targetYScrollOffset;
+ }
+ _lastTotalYScroll = _totalYScroll;
+ }
+
+ _startVisibleLineIndex = _endVisibleLineIndex = -1;
+
+ // Update Positions
+ for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++)
+ {
+ var line = LyricsLines[i];
+
+ if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0)
+ {
+ if (_startVisibleLineIndex == -1)
+ {
+ _startVisibleLineIndex = i;
+ }
+ }
+ if (
+ _totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height
+ >= control.Size.Height
+ )
+ {
+ if (_endVisibleLineIndex == -1)
+ {
+ _endVisibleLineIndex = i;
+ }
+ }
+ }
+
+ if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
+ {
+ _endVisibleLineIndex = endLineIndex;
+ }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs
index 9cf6bca..f8cbfa6 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs
@@ -43,5 +43,9 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const bool IsLyricsDynamicGlowEffectEnabled = false;
public const int LyricsFontColorType = 0; // Default
public const int LyricsFontSelectedAccentColorIndex = 0;
+
+ // Notification
+ public const bool NeverShowEnterFullScreenMessage = false;
+ public const bool NeverShowEnterImmersiveModeMessage = false;
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs
index f14ded0..3db6219 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs
@@ -43,5 +43,10 @@ namespace BetterLyrics.WinUI3.Services.Settings
public const string LyricsFontColorType = "LyricsFontColorType";
public const string LyricsFontSelectedAccentColorIndex =
"LyricsFontSelectedAccentColorIndex";
+
+ // Notification
+ public const string NeverShowEnterFullScreenMessage = "NeverShowEnterFullScreenMessage";
+ public const string NeverShowEnterImmersiveModeMessage =
+ "NeverShowEnterImmersiveModeMessage";
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs
index f954eae..d193367 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs
@@ -238,6 +238,28 @@ namespace BetterLyrics.WinUI3.Services.Settings
}
}
+ //Notification
+ public bool NeverShowEnterFullScreenMessage
+ {
+ get =>
+ Get(
+ SettingsKeys.NeverShowEnterFullScreenMessage,
+ SettingsDefaultValues.NeverShowEnterFullScreenMessage
+ );
+ set => Set(SettingsKeys.NeverShowEnterFullScreenMessage, value);
+ }
+ public bool NeverShowEnterImmersiveModeMessage
+ {
+ get =>
+ Get(
+ SettingsKeys.NeverShowEnterImmersiveModeMessage,
+ SettingsDefaultValues.NeverShowEnterImmersiveModeMessage
+ );
+ set => Set(SettingsKeys.NeverShowEnterImmersiveModeMessage, value);
+ }
+
+ // Utils
+
private T? Get(string key, T? defaultValue = default)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
index 7e3c40a..1a7b6bd 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
@@ -369,4 +369,7 @@
Hover back again to show the toggle button
+
+ Do not show this message again
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
index 194691e..1df0cff 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
@@ -369,4 +369,7 @@
再次悬停以显示切换按钮
+
+ 不再显示此消息
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
index 3e5d2cf..6429ac3 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
@@ -369,4 +369,7 @@
再次懸停以顯示切換按鈕
+
+ 不再顯示此訊息
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/BaseWindowModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/BaseWindowModel.cs
index 7a08c21..5a81144 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/BaseWindowModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/BaseWindowModel.cs
@@ -3,13 +3,80 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Messages;
+using BetterLyrics.WinUI3.Models;
+using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseWindowModel : ObservableObject
{
+ public SettingsService SettingsService { get; private set; }
+
[ObservableProperty]
private int _titleBarFontSize = 11;
+
+ [ObservableProperty]
+ private Notification _notification = new();
+
+ [ObservableProperty]
+ private bool _showInfoBar = false;
+
+ public BaseWindowModel(SettingsService settingsService)
+ {
+ SettingsService = settingsService;
+
+ WeakReferenceMessenger.Default.Register(
+ this,
+ async (r, m) =>
+ {
+ Notification = m.Value;
+ if (
+ !Notification.IsForeverDismissable
+ || AlreadyForeverDismissedThisMessage() == false
+ )
+ {
+ Notification.Visibility = Notification.IsForeverDismissable
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+ ShowInfoBar = true;
+ await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
+ ShowInfoBar = false;
+ }
+ }
+ );
+ }
+
+ [RelayCommand]
+ private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
+ {
+ switch (Notification.RelatedSettingsKeyName)
+ {
+ case SettingsKeys.NeverShowEnterFullScreenMessage:
+ SettingsService.NeverShowEnterFullScreenMessage = value;
+ break;
+ case SettingsKeys.NeverShowEnterImmersiveModeMessage:
+ SettingsService.NeverShowEnterImmersiveModeMessage = value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ private bool? AlreadyForeverDismissedThisMessage() =>
+ Notification.RelatedSettingsKeyName switch
+ {
+ SettingsKeys.NeverShowEnterFullScreenMessage =>
+ SettingsService.NeverShowEnterFullScreenMessage,
+ SettingsKeys.NeverShowEnterImmersiveModeMessage =>
+ SettingsService.NeverShowEnterImmersiveModeMessage,
+ _ => null,
+ };
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs
index 1d0b9ab..746c44f 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs
@@ -132,7 +132,7 @@ namespace BetterLyrics.WinUI3.ViewModels
return result;
}
- public async Task<(List, SoftwareBitmap?, uint, uint)> SetSongInfoAsync(
+ public async Task<(List, SoftwareBitmap?)> SetSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
@@ -201,12 +201,7 @@ namespace BetterLyrics.WinUI3.ViewModels
stream.Dispose();
}
- return (
- GetLyrics(track),
- coverSoftwareBitmap,
- coverImagePixelWidth,
- coverImagePixelHeight
- );
+ return (GetLyrics(track), coverSoftwareBitmap);
}
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs
index 9632f2f..e716377 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs
@@ -3,12 +3,14 @@ using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
using Windows.ApplicationModel.Core;
using Windows.Media;
using Windows.Media.Playback;
@@ -75,9 +77,12 @@ namespace BetterLyrics.WinUI3.ViewModels
bool existed = SettingsService.MusicLibraries.Any((x) => x == path);
if (existed)
{
- BaseWindow.StackedNotificationsBehavior?.Show(
- App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"),
- Helper.AnimationHelper.StackedNotificationsShowingDuration
+ WeakReferenceMessenger.Default.Send(
+ new ShowNotificatonMessage(
+ new Notification(
+ App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
+ )
+ )
);
}
else
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/BaseWindow.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/BaseWindow.xaml
index 2f79a6b..d0a95fa 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/BaseWindow.xaml
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/BaseWindow.xaml
@@ -23,41 +23,23 @@
Height="{StaticResource TitleBarCompactHeight}"
VerticalAlignment="Top"
Background="Transparent">
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
@@ -157,11 +139,21 @@
VerticalAlignment="Bottom"
Background="{ThemeResource SystemFillColorSolidAttentionBackgroundBrush}"
IsClosable="False"
+ IsOpen="{x:Bind WindowModel.ShowInfoBar, Mode=OneWay}"
+ Message="{x:Bind WindowModel.Notification.Message, Mode=OneWay}"
Opacity="0"
- Severity="Informational">
+ Severity="{x:Bind WindowModel.Notification.Severity, Mode=OneWay}">
+
+
+
@@ -182,7 +174,6 @@
-
()!;
- SettingsService = Ioc.Default.GetService()!;
- SettingsService.PropertyChanged += SettingsService_PropertyChanged;
+ WindowModel.SettingsService.PropertyChanged += SettingsService_PropertyChanged;
SettingsService_PropertyChanged(
null,
@@ -75,6 +58,11 @@ namespace BetterLyrics.WinUI3.Views
SetTitleBar(TopCommandGrid);
}
+ private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
+ {
+ UpdateTitleBarWindowButtonsVisibility();
+ }
+
private void SettingsService_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
@@ -82,17 +70,17 @@ namespace BetterLyrics.WinUI3.Views
{
switch (e.PropertyName)
{
- case nameof(Services.Settings.SettingsService.Theme):
- RootGrid.RequestedTheme = (ElementTheme)SettingsService.Theme;
+ case nameof(SettingsService.Theme):
+ RootGrid.RequestedTheme = (ElementTheme)WindowModel.SettingsService.Theme;
break;
- case nameof(Services.Settings.SettingsService.BackdropType):
+ case nameof(SettingsService.BackdropType):
SystemBackdrop = null;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
- (BackdropType)SettingsService.BackdropType
+ (BackdropType)WindowModel.SettingsService.BackdropType
);
break;
- case nameof(Services.Settings.SettingsService.TitleBarType):
- switch ((TitleBarType)SettingsService.TitleBarType)
+ case nameof(SettingsService.TitleBarType):
+ switch ((TitleBarType)WindowModel.SettingsService.TitleBarType)
{
case TitleBarType.Compact:
TopCommandGrid.Height = (double)
@@ -169,6 +157,8 @@ namespace BetterLyrics.WinUI3.Views
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
{
MinimiseButton.Visibility = AOTFlyoutItem.Visibility = Visibility.Visible;
+ FullScreenFlyoutItem.IsChecked = false;
+ AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{
@@ -188,6 +178,7 @@ namespace BetterLyrics.WinUI3.Views
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
Visibility.Collapsed;
+ FullScreenFlyoutItem.IsChecked = true;
}
}
@@ -201,7 +192,10 @@ namespace BetterLyrics.WinUI3.Views
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
- AOTFlyoutItem.IsChecked = presenter.IsAlwaysOnTop = !presenter.IsAlwaysOnTop;
+ {
+ presenter.IsAlwaysOnTop = !presenter.IsAlwaysOnTop;
+ UpdateTitleBarWindowButtonsVisibility();
+ }
}
private void FullScreenFlyoutItem_Click(object sender, RoutedEventArgs e)
@@ -217,9 +211,14 @@ namespace BetterLyrics.WinUI3.Views
break;
case AppWindowPresenterKind.Overlapped:
AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
- StackedNotificationsBehavior?.Show(
- App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"),
- AnimationHelper.StackedNotificationsShowingDuration
+ WeakReferenceMessenger.Default.Send(
+ new ShowNotificatonMessage(
+ new Models.Notification(
+ App.ResourceLoader!.GetString("BaseWindowEnterFullScreenHint"),
+ isForeverDismissable: true,
+ relatedSettingsKeyName: SettingsKeys.NeverShowEnterFullScreenMessage
+ )
+ )
);
break;
default:
@@ -227,9 +226,6 @@ namespace BetterLyrics.WinUI3.Views
}
UpdateTitleBarWindowButtonsVisibility();
-
- FullScreenFlyoutItem.IsChecked =
- AppWindow.Presenter.Kind == AppWindowPresenterKind.FullScreen;
}
private void RootGrid_KeyDown(object sender, KeyRoutedEventArgs e)
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml
index 8a6acc9..7930e6a 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml
@@ -15,7 +15,7 @@
NavigationCacheMode="Required"
mc:Ignorable="d">
-
+
0,16,0,0
@@ -35,38 +35,11 @@
Draw="LyricsCanvas_Draw"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Loaded="LyricsCanvas_Loaded"
+ SizeChanged="LyricsCanvas_SizeChanged"
Update="LyricsCanvas_Update">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -113,6 +86,11 @@
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -383,13 +345,13 @@
Binding="{x:Bind SettingsService.IsImmersiveMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
-
+
-
+
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs
index 716bbe4..0df9fc3 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs
@@ -1,33 +1,28 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
+using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
-using DevWinUI;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
-using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
-using Microsoft.UI;
using Microsoft.UI.Dispatching;
-using Microsoft.UI.Text;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Media.Animation;
using Windows.Foundation;
-using Windows.Graphics.Imaging;
-using Windows.Media;
using Windows.Media.Control;
using Color = Windows.UI.Color;
@@ -44,14 +39,8 @@ namespace BetterLyrics.WinUI3.Views
public MainViewModel ViewModel => (MainViewModel)DataContext;
private SettingsService SettingsService { get; set; }
- private List _lyricsLines = [];
-
- private SoftwareBitmap? _coverSoftwareBitmap = null;
- private uint _coverImagePixelWidth = 0;
- private uint _coverImagePixelHeight = 0;
-
- private float _coverBitmapRotateAngle = 0f;
- private float _coverScaleFactor = 1;
+ private readonly CoverBackgroundRenderer _coverImageAsBackgroundRenderer = new();
+ private readonly PureLyricsRenderer _pureLyricsRenderer = new();
private readonly float _coverRotateSpeed = 0.003f;
@@ -61,33 +50,6 @@ namespace BetterLyrics.WinUI3.Views
private readonly float _lyricsGlowEffectMinBlurAmount = 0f;
private readonly float _lyricsGlowEffectMaxBlurAmount = 6f;
- private TimeSpan _currentTime = TimeSpan.Zero;
-
- private readonly float _defaultOpacity = 0.3f;
- private readonly float _highlightedOpacity = 1.0f;
-
- private readonly float _defaultScale = 0.95f;
- private readonly float _highlightedScale = 1.0f;
-
- private readonly int _lineEnteringDurationMs = 800;
- private readonly int _lineExitingDurationMs = 800;
- private readonly int _lineScrollDurationMs = 800;
-
- private float _lastTotalYScroll = 0.0f;
- private float _totalYScroll = 0.0f;
-
- private double _lyricsAreaWidth = 0.0f;
- private double _lyricsAreaHeight = 0.0f;
-
- private readonly double _lyricsCanvasRightMargin = 36;
- private double _lyricsCanvasLeftMargin = 0;
- private double _lyricsCanvasMaxTextWidth = 0;
-
- private int _startVisibleLineIndex = -1;
- private int _endVisibleLineIndex = -1;
-
- private bool _forceToScroll = false;
-
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
@@ -114,13 +76,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
- private async Task ForceToScrollToCurrentPlayingLineAsync()
- {
- _forceToScroll = true;
- await Task.Delay(1);
- _forceToScroll = false;
- }
-
private async void SettingsService_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
@@ -130,8 +85,7 @@ namespace BetterLyrics.WinUI3.Views
{
case nameof(SettingsService.LyricsFontSize):
case nameof(SettingsService.LyricsLineSpacingFactor):
- LayoutLyrics();
- await ForceToScrollToCurrentPlayingLineAsync();
+ await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
break;
case nameof(SettingsService.IsRebuildingLyricsIndexDatabase):
if (!SettingsService.IsRebuildingLyricsIndexDatabase)
@@ -152,9 +106,14 @@ namespace BetterLyrics.WinUI3.Views
break;
case nameof(SettingsService.IsImmersiveMode):
if (SettingsService.IsImmersiveMode)
- BaseWindow.StackedNotificationsBehavior?.Show(
- App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
- AnimationHelper.StackedNotificationsShowingDuration
+ WeakReferenceMessenger.Default.Send(
+ new ShowNotificatonMessage(
+ new Notification(
+ App.ResourceLoader!.GetString("MainPageEnterImmersiveModeHint"),
+ isForeverDismissable: true,
+ relatedSettingsKeyName: SettingsKeys.NeverShowEnterImmersiveModeMessage
+ )
+ )
);
break;
default:
@@ -162,7 +121,7 @@ namespace BetterLyrics.WinUI3.Views
}
}
- private void ViewModel_PropertyChanged(
+ private async void ViewModel_PropertyChanged(
object? sender,
System.ComponentModel.PropertyChangedEventArgs e
)
@@ -170,7 +129,17 @@ namespace BetterLyrics.WinUI3.Views
switch (e.PropertyName)
{
case nameof(ViewModel.ShowLyricsOnly):
- RootGrid_SizeChanged(null, null);
+ if (ViewModel.ShowLyricsOnly)
+ {
+ Grid.SetColumn(LyricsPlaceholderGrid, 0);
+ Grid.SetColumnSpan(LyricsPlaceholderGrid, 3);
+ }
+ else
+ {
+ Grid.SetColumn(LyricsPlaceholderGrid, 2);
+ Grid.SetColumnSpan(LyricsPlaceholderGrid, 1);
+ }
+ await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
break;
default:
break;
@@ -216,11 +185,11 @@ namespace BetterLyrics.WinUI3.Views
{
if (sender == null)
{
- _currentTime = TimeSpan.Zero;
+ _pureLyricsRenderer.CurrentTime = TimeSpan.Zero;
return;
}
- _currentTime = sender.GetTimelineProperties().Position;
+ _pureLyricsRenderer.CurrentTime = sender.GetTimelineProperties().Position;
// _logger.LogDebug(_currentTime);
}
@@ -333,13 +302,7 @@ namespace BetterLyrics.WinUI3.Views
null;
if (_currentSession != null)
- {
- try
- {
- mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
- }
- catch (Exception) { }
- }
+ mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
ViewModel.IsAnyMusicSessionExisted = _currentSession != null;
@@ -347,15 +310,13 @@ namespace BetterLyrics.WinUI3.Views
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
(
- _lyricsLines,
- _coverSoftwareBitmap,
- _coverImagePixelWidth,
- _coverImagePixelHeight
+ _pureLyricsRenderer.LyricsLines,
+ _coverImageAsBackgroundRenderer.SoftwareBitmap
) = await ViewModel.SetSongInfoAsync(mediaProps);
// Force to show lyrics and scroll to current line even if the music is not playing
LyricsCanvas.Paused = false;
- await ForceToScrollToCurrentPlayingLineAsync();
+ await _pureLyricsRenderer.ForceToScrollToCurrentPlayingLineAsync();
await Task.Delay(1);
// Detect and recover the music state
CurrentSession_PlaybackInfoChanged(_currentSession, null);
@@ -363,7 +324,7 @@ namespace BetterLyrics.WinUI3.Views
ViewModel.AboutToUpdateUI = false;
- if (_lyricsLines.Count == 0)
+ if (_pureLyricsRenderer.LyricsLines.Count == 0)
{
Grid.SetColumnSpan(SongInfoInnerGrid, 3);
}
@@ -378,31 +339,6 @@ namespace BetterLyrics.WinUI3.Views
);
}
- private async void RootGrid_SizeChanged(object? sender, SizeChangedEventArgs? e)
- {
- //_queueTimer.Debounce(async () => {
-
- _lyricsAreaHeight = LyricsGrid.ActualHeight;
- _lyricsAreaWidth = LyricsGrid.ActualWidth;
-
- if (SongInfoColumnDefinition.ActualWidth == 0 || ViewModel.ShowLyricsOnly)
- {
- _lyricsCanvasLeftMargin = 36;
- }
- else
- {
- _lyricsCanvasLeftMargin = 36 + SongInfoColumnDefinition.ActualWidth + 36;
- }
-
- _lyricsCanvasMaxTextWidth =
- _lyricsAreaWidth - _lyricsCanvasLeftMargin - _lyricsCanvasRightMargin;
-
- LayoutLyrics();
- await ForceToScrollToCurrentPlayingLineAsync();
-
- //}, TimeSpan.FromMilliseconds(50));
- }
-
// Comsumes GPU related resources
private void LyricsCanvas_Draw(
ICanvasAnimatedControl sender,
@@ -416,16 +352,13 @@ namespace BetterLyrics.WinUI3.Views
var b = _lyricsColor.B;
// Draw (dynamic) cover image as the very first layer
- if (SettingsService.IsCoverOverlayEnabled && _coverSoftwareBitmap != null)
- {
- DrawCoverImage(sender, ds);
- }
+ _coverImageAsBackgroundRenderer.Draw(sender, ds);
// Lyrics only layer
using var lyrics = new CanvasCommandList(sender);
using (var lyricsDs = lyrics.CreateDrawingSession())
{
- DrawLyrics(sender, lyricsDs, r, g, b);
+ _pureLyricsRenderer.Draw(sender, lyricsDs, r, g, b);
}
using var glowedLyrics = new CanvasCommandList(sender);
@@ -529,158 +462,6 @@ namespace BetterLyrics.WinUI3.Views
ds.DrawImage(maskedCombinedBlurredLyrics);
}
- private void DrawLyrics(
- ICanvasAnimatedControl control,
- CanvasDrawingSession ds,
- byte r,
- byte g,
- byte b
- )
- {
- var (displayStartLineIndex, displayEndLineIndex) =
- GetVisibleLyricsLineIndexBoundaries();
-
- for (
- int i = displayStartLineIndex;
- _lyricsLines.Count > 0
- && i >= 0
- && i < _lyricsLines.Count
- && i <= displayEndLineIndex;
- i++
- )
- {
- var line = _lyricsLines[i];
-
- if (line.TextLayout == null)
- {
- return;
- }
-
- float progressPerChar = 1f / line.Text.Length;
-
- var position = line.Position;
-
- float centerX = position.X;
- float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
-
- switch ((LyricsAlignmentType)SettingsService.LyricsAlignmentType)
- {
- case LyricsAlignmentType.Left:
- line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
- break;
- case LyricsAlignmentType.Center:
- line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
- centerX += (float)_lyricsCanvasMaxTextWidth / 2;
- break;
- case LyricsAlignmentType.Right:
- line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
- centerX += (float)_lyricsCanvasMaxTextWidth;
- break;
- default:
- break;
- }
-
- int startIndex = 0;
-
- // Set brush
- for (int j = 0; j < line.TextLayout.LineCount; j++)
- {
- int count = line.TextLayout.LineMetrics[j].CharacterCount;
- var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
- float subLinePlayingProgress = Math.Clamp(
- (line.PlayingProgress * line.Text.Length - startIndex) / count,
- 0,
- 1
- );
-
- using var horizontalFillBrush = new CanvasLinearGradientBrush(
- control,
- [
- new()
- {
- Position = 0,
- Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
- },
- new()
- {
- Position =
- subLinePlayingProgress * (1 + progressPerChar)
- - progressPerChar,
- Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
- },
- new()
- {
- Position = subLinePlayingProgress * (1 + progressPerChar),
- Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
- },
- new()
- {
- Position = 1.5f,
- Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
- },
- ]
- )
- {
- StartPoint = new Vector2(
- (float)(regions[0].LayoutBounds.Left + position.X),
- 0
- ),
- EndPoint = new Vector2(
- (float)(regions[^1].LayoutBounds.Right + position.X),
- 0
- ),
- };
-
- line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
- startIndex += count;
- }
-
- // Scale
- ds.Transform =
- Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY))
- * Matrix3x2.CreateTranslation(0, _totalYScroll);
- // _logger.LogDebug(_totalYScroll);
-
- ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
-
- // Reset scale
- ds.Transform = Matrix3x2.Identity;
- }
- }
-
- private void DrawCoverImage(ICanvasAnimatedControl control, CanvasDrawingSession ds)
- {
- ds.Transform = Matrix3x2.CreateRotation(
- _coverBitmapRotateAngle,
- control.Size.ToVector2() * 0.5f
- );
-
- using var coverOverlayEffect = new OpacityEffect
- {
- Opacity = SettingsService.CoverOverlayOpacity / 100f,
- Source = new GaussianBlurEffect
- {
- BlurAmount = SettingsService.CoverOverlayBlurAmount,
- Source = new ScaleEffect
- {
- InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
- BorderMode = EffectBorderMode.Hard,
- Scale = new Vector2(_coverScaleFactor),
- Source = CanvasBitmap.CreateFromSoftwareBitmap(
- control,
- _coverSoftwareBitmap
- ),
- },
- },
- };
- ds.DrawImage(
- coverOverlayEffect,
- (float)control.Size.Width / 2 - _coverImagePixelWidth * _coverScaleFactor / 2,
- (float)control.Size.Height / 2 - _coverImagePixelHeight * _coverScaleFactor / 2
- );
- ds.Transform = Matrix3x2.Identity;
- }
-
private void DrawGradientOpacityMask(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
@@ -711,12 +492,12 @@ namespace BetterLyrics.WinUI3.Views
CanvasAnimatedUpdateEventArgs args
)
{
- _currentTime += args.Timing.ElapsedTime;
+ _pureLyricsRenderer.CurrentTime += args.Timing.ElapsedTime;
if (SettingsService.IsDynamicCoverOverlay)
{
- _coverBitmapRotateAngle += _coverRotateSpeed;
- _coverBitmapRotateAngle %= MathF.PI * 2;
+ _coverImageAsBackgroundRenderer.RotateAngle += _coverRotateSpeed;
+ _coverImageAsBackgroundRenderer.RotateAngle %= MathF.PI * 2;
}
if (SettingsService.IsLyricsDynamicGlowEffectEnabled)
{
@@ -724,32 +505,24 @@ namespace BetterLyrics.WinUI3.Views
_lyricsGlowEffectAngle %= MathF.PI * 2;
}
- if (SettingsService.IsCoverOverlayEnabled && _coverSoftwareBitmap != null)
- {
- var diagonal = Math.Sqrt(
- Math.Pow(_lyricsAreaWidth, 2) + Math.Pow(_lyricsAreaHeight, 2)
- );
+ _coverImageAsBackgroundRenderer.Calculate(sender);
- _coverScaleFactor =
- (float)diagonal / Math.Min(_coverImagePixelWidth, _coverImagePixelHeight);
- }
-
- if (_lyricsLines.LastOrDefault()?.TextLayout == null)
+ if (_pureLyricsRenderer.LyricsLines.LastOrDefault()?.TextLayout == null)
{
- LayoutLyrics();
+ _pureLyricsRenderer.ReLayoutAsync(sender);
}
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
- UpdateScaleAndOpacity(currentPlayingLineIndex);
- UpdatePosition(currentPlayingLineIndex);
+ _pureLyricsRenderer.CalculateScaleAndOpacity(currentPlayingLineIndex);
+ _pureLyricsRenderer.CalculatePosition(sender, currentPlayingLineIndex);
}
private int GetCurrentPlayingLineIndex()
{
- for (int i = 0; i < _lyricsLines.Count; i++)
+ for (int i = 0; i < _pureLyricsRenderer.LyricsLines.Count; i++)
{
- var line = _lyricsLines[i];
- if (line.EndPlayingTimestampMs < _currentTime.TotalMilliseconds)
+ var line = _pureLyricsRenderer.LyricsLines[i];
+ if (line.EndPlayingTimestampMs < _pureLyricsRenderer.CurrentTime.TotalMilliseconds)
{
continue;
}
@@ -759,239 +532,6 @@ namespace BetterLyrics.WinUI3.Views
return -1;
}
- private Tuple GetVisibleLyricsLineIndexBoundaries()
- {
- // _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
- return new Tuple(_startVisibleLineIndex, _endVisibleLineIndex);
- }
-
- private Tuple GetMaxLyricsLineIndexBoundaries()
- {
- if (_lyricsLines.Count == 0)
- {
- return new Tuple(-1, -1);
- }
-
- return new Tuple(0, _lyricsLines.Count - 1);
- }
-
- private void UpdateScaleAndOpacity(int currentPlayingLineIndex)
- {
- var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
-
- for (int i = startLineIndex; _lyricsLines.Count > 0 && i <= endLineIndex; i++)
- {
- var line = _lyricsLines[i];
-
- bool linePlaying = i == currentPlayingLineIndex;
-
- var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
- var lineExitingDurationMs = _lineExitingDurationMs;
- if (i + 1 <= endLineIndex)
- {
- lineExitingDurationMs = Math.Min(
- _lyricsLines[i + 1].DurationMs,
- lineExitingDurationMs
- );
- }
-
- float lineEnteringProgress = 0.0f;
- float lineExitingProgress = 0.0f;
-
- bool lineEntering = false;
- bool lineExiting = false;
-
- float scale = _defaultScale;
- float opacity = _defaultOpacity;
-
- float playProgress = 0;
-
- if (linePlaying)
- {
- line.PlayingState = LyricsPlayingState.Playing;
-
- scale = _highlightedScale;
- opacity = _highlightedOpacity;
-
- playProgress =
- ((float)_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
- / line.DurationMs;
-
- var durationFromStartMs =
- _currentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
- lineEntering = durationFromStartMs <= lineEnteringDurationMs;
- if (lineEntering)
- {
- lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
- scale =
- _defaultScale
- + (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
- opacity =
- _defaultOpacity
- + (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
- }
- }
- else
- {
- if (i < currentPlayingLineIndex)
- {
- line.PlayingState = LyricsPlayingState.Played;
- playProgress = 1;
-
- var durationToEndMs =
- _currentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
- lineExiting = durationToEndMs <= lineExitingDurationMs;
- if (lineExiting)
- {
- lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
- scale =
- _highlightedScale
- - (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
- opacity =
- _highlightedOpacity
- - (_highlightedOpacity - _defaultOpacity)
- * (float)lineExitingProgress;
- }
- }
- else
- {
- line.PlayingState = LyricsPlayingState.NotPlayed;
- }
- }
-
- line.EnteringProgress = lineEnteringProgress;
- line.ExitingProgress = lineExitingProgress;
-
- line.Scale = scale;
- line.Opacity = opacity;
-
- line.PlayingProgress = playProgress;
- }
- }
-
- private void LayoutLyrics()
- {
- using CanvasTextFormat textFormat = new()
- {
- FontSize = SettingsService.LyricsFontSize,
- HorizontalAlignment = CanvasHorizontalAlignment.Left,
- VerticalAlignment = CanvasVerticalAlignment.Top,
- FontWeight = FontWeights.Bold,
- //FontFamily = "Segoe UI Mono",
- };
- float y = (float)_lyricsAreaHeight / 2;
-
- // Init Positions
- for (int i = 0; i < _lyricsLines.Count; i++)
- {
- var line = _lyricsLines[i];
-
- // Calculate layout bounds
- line.TextLayout = new CanvasTextLayout(
- LyricsCanvas.Device,
- line.Text,
- textFormat,
- (float)_lyricsCanvasMaxTextWidth,
- (float)_lyricsAreaHeight
- );
- line.Position = new Vector2((float)_lyricsCanvasLeftMargin, y);
-
- y +=
- (float)line.TextLayout.LayoutBounds.Height
- / line.TextLayout.LineCount
- * (line.TextLayout.LineCount + SettingsService.LyricsLineSpacingFactor);
- }
- }
-
- private void UpdatePosition(int currentPlayingLineIndex)
- {
- if (currentPlayingLineIndex < 0)
- {
- return;
- }
-
- var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
-
- if (startLineIndex < 0 || endLineIndex < 0)
- {
- return;
- }
-
- // Set _scrollOffsetY
- LyricsLine? currentPlayingLine = _lyricsLines?[currentPlayingLineIndex];
-
- if (currentPlayingLine == null)
- {
- return;
- }
-
- if (currentPlayingLine.TextLayout == null)
- {
- return;
- }
-
- var lineScrollingProgress =
- (_currentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
- / Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
-
- var targetYScrollOffset = (float)(
- -currentPlayingLine.Position.Y
- + _lyricsLines![0].Position.Y
- - currentPlayingLine.TextLayout.LayoutBounds.Height / 2
- - _lastTotalYScroll
- );
-
- var yScrollOffset =
- targetYScrollOffset
- * EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
-
- bool isScrollingNow = lineScrollingProgress <= 1;
-
- if (isScrollingNow)
- {
- _totalYScroll = _lastTotalYScroll + yScrollOffset;
- }
- else
- {
- if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1)
- {
- _totalYScroll = _lastTotalYScroll + targetYScrollOffset;
- }
- _lastTotalYScroll = _totalYScroll;
- }
-
- _startVisibleLineIndex = _endVisibleLineIndex = -1;
-
- // Update Positions
- for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++)
- {
- var line = _lyricsLines[i];
-
- if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0)
- {
- if (_startVisibleLineIndex == -1)
- {
- _startVisibleLineIndex = i;
- }
- }
- if (
- _totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height
- >= _lyricsAreaHeight
- )
- {
- if (_endVisibleLineIndex == -1)
- {
- _endVisibleLineIndex = i;
- }
- }
- }
-
- if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
- {
- _endVisibleLineIndex = endLineIndex;
- }
- }
-
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
InitMediaManager();
@@ -1035,10 +575,8 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
- if (SettingsService.IsImmersiveMode)
- (
- (Storyboard)BottomCommandGrid.Resources["BottomCommandGridFadeInStoryboard"]
- ).Begin();
+ if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == 0)
+ BottomCommandGrid.Opacity = .5;
}
private void BottomCommandGrid_PointerExited(
@@ -1046,10 +584,21 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
- if (SettingsService.IsImmersiveMode)
- (
- (Storyboard)BottomCommandGrid.Resources["BottomCommandGridFadeOutStoryboard"]
- ).Begin();
+ if (SettingsService.IsImmersiveMode && BottomCommandGrid.Opacity == .5)
+ BottomCommandGrid.Opacity = 0;
+ }
+
+ private async void LyricsPlaceholderGrid_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ _pureLyricsRenderer.LimitedLineWidth = e.NewSize.Width;
+ await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
+ }
+
+ private async void LyricsCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ _pureLyricsRenderer.CanvasWidth = e.NewSize.Width;
+ _pureLyricsRenderer.CanvasHeight = e.NewSize.Height;
+ await _pureLyricsRenderer.ReLayoutAsync(LyricsCanvas);
}
}
}
diff --git a/README.md b/README.md
index 3420efe..41dfb01 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Your smooth dynamic local lyrics display built with WinUI 3
- Dynamic blur album art as background
- Smooth lyrics fade in/out, zoom in/out effects
- Smooth user interface change from song to song
-- Gradient Karaoke effect on every single character
+- **Gradient** Karaoke effect on every single character
Coding in progress...
@@ -31,7 +31,7 @@ We provide more than one setting item to better align with your preference
- Album art as background (dynamic, blur amount, opacity)
-- Lyrics (alignment, font size, line spacing, opacity, blur amount, dynamic glow effect)
+- Lyrics (alignment, font size, font color **(picked from album art accent color)** line spacing, opacity, blur amount, dynamic **glow** effect)
- Language (English, Simplified Chinese, Traditional Chinese)
@@ -49,22 +49,26 @@ Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(up
### Split view
-
+Non-immersive mode
+
+
+
+Immersive mode

+
+### Lyrics only
+

### Fullscreen
-
-

### Settings
-
-
-
-
+
+
+
## Download it now
@@ -74,13 +78,33 @@ Or watch our introduction video「BetterLyrics 阶段性开发成果展示」(up
> **Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version, if you like you can purchase to support me)
-Or alternatively get it from [![]()](https://shorturl.at/jXbd7)
+Or alternatively get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases/latest) page for the link)
-
-
-
+> Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
-> .zip file, please follow [this doc](How2Install/How2Install.md) to properly install it
+## Setup your app
+
+This project relies on listening messages from [SMTC](https://learn.microsoft.com/en-ca/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols).
+So technically, as long as you are using the music apps (like
+
+- Spotify
+- Groove Music
+- Apple Music
+- Windows Media Player
+- VLC Media Player
+- QQ 音乐
+- 网易云音乐
+- 酷狗音乐
+- 酷我音乐
+
+) which support SMTC, then possibly (I didn't test all of themif you find one fail to listen to, you can open an issue) all you need to do is just load your local music/lyrics lib and you are good to go.
+
+## Future work
+
+- Watching file changes
+ When you downloading lyrics (using some other tools or your own scripts) while listening to new musics (non-existed on your local disks), this app can automatically load those new files.
+
+> Please note: we are not planning support directly load lyrics files via some music software APIs due to copyright issues.
## Many thanks to
@@ -102,21 +126,21 @@ Or alternatively get it from [![]()](https://shorturl.at/jXbd7)
## Third-party libraries that this project uses
```
-
-
-
-
-
+
-
+
+
+
+
+
diff --git a/Screenshots/Snipaste_2025-06-03_16-46-55.png b/Screenshots/Snipaste_2025-06-03_16-46-55.png
deleted file mode 100644
index 29e63f6..0000000
Binary files a/Screenshots/Snipaste_2025-06-03_16-46-55.png and /dev/null differ
diff --git a/Screenshots/Snipaste_2025-06-03_17-51-52.png b/Screenshots/Snipaste_2025-06-03_17-51-52.png
deleted file mode 100644
index 8e31f6d..0000000
Binary files a/Screenshots/Snipaste_2025-06-03_17-51-52.png and /dev/null differ
diff --git a/Screenshots/Snipaste_2025-06-03_17-52-00.png b/Screenshots/Snipaste_2025-06-03_17-52-00.png
deleted file mode 100644
index 8cd0507..0000000
Binary files a/Screenshots/Snipaste_2025-06-03_17-52-00.png and /dev/null differ
diff --git a/Screenshots/Snipaste_2025-06-03_17-52-05.png b/Screenshots/Snipaste_2025-06-03_17-52-05.png
deleted file mode 100644
index e3d521d..0000000
Binary files a/Screenshots/Snipaste_2025-06-03_17-52-05.png and /dev/null differ
diff --git a/Screenshots/Snipaste_2025-06-03_17-52-11.png b/Screenshots/Snipaste_2025-06-03_17-52-11.png
deleted file mode 100644
index 21fde9a..0000000
Binary files a/Screenshots/Snipaste_2025-06-03_17-52-11.png and /dev/null differ
diff --git a/Screenshots/Snipaste_2025-06-07_17-32-02.png b/Screenshots/Snipaste_2025-06-07_17-32-02.png
new file mode 100644
index 0000000..c44f5ec
Binary files /dev/null and b/Screenshots/Snipaste_2025-06-07_17-32-02.png differ
diff --git a/Screenshots/Snipaste_2025-06-07_17-32-17.png b/Screenshots/Snipaste_2025-06-07_17-32-17.png
new file mode 100644
index 0000000..2559b84
Binary files /dev/null and b/Screenshots/Snipaste_2025-06-07_17-32-17.png differ
diff --git a/Screenshots/Snipaste_2025-06-07_17-32-23.png b/Screenshots/Snipaste_2025-06-07_17-32-23.png
new file mode 100644
index 0000000..409ddae
Binary files /dev/null and b/Screenshots/Snipaste_2025-06-07_17-32-23.png differ
diff --git a/Screenshots/Snipaste_2025-06-07_17-36-26.png b/Screenshots/Snipaste_2025-06-07_17-36-26.png
new file mode 100644
index 0000000..7d2700f
Binary files /dev/null and b/Screenshots/Snipaste_2025-06-07_17-36-26.png differ
diff --git a/image.png b/image.png
new file mode 100644
index 0000000..d68101d
Binary files /dev/null and b/image.png differ