diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj
index c61e8eb..20b4937 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj
@@ -10,6 +10,14 @@
enable
preview
+
+
+
+
+
+
+
+
@@ -18,18 +26,12 @@
-
+
-
+
@@ -66,7 +68,6 @@
-
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/LyricsSearchProviderToDisplayNameConverter.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/LyricsSearchProviderToDisplayNameConverter.cs
index 014c8e2..855cd06 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/LyricsSearchProviderToDisplayNameConverter.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/LyricsSearchProviderToDisplayNameConverter.cs
@@ -1,8 +1,8 @@
// 2025/6/23 by Zhe Fang
+using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
-using System;
namespace BetterLyrics.WinUI3.Converter
{
@@ -27,15 +27,18 @@ namespace BetterLyrics.WinUI3.Converter
{
return provider switch
{
+ LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString(
+ "LyricsSearchProviderLrcLib"
+ ),
+ //LyricsSearchProvider.AmllTtmlDb => App.ResourceLoader!.GetString(
+ // "LyricsSearchProviderAmllTtmlDb"
+ //),
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalLrcFile"
),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalMusicFile"
),
- LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString(
- "LyricsSearchProviderLrcLib"
- ),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderEslrcFile"
),
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LineMaskType.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LineMaskType.cs
new file mode 100644
index 0000000..9487bda
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LineMaskType.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BetterLyrics.WinUI3.Enums
+{
+ public enum LineMaskType
+ {
+ Glow,
+ Highlight,
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LyricsSearchProvider.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LyricsSearchProvider.cs
index d732a7d..f16db2e 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LyricsSearchProvider.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LyricsSearchProvider.cs
@@ -14,6 +14,8 @@ namespace BetterLyrics.WinUI3.Enums
///
LrcLib,
+ //AmllTtmlDb,
+
///
/// Defines the LocalMusicFile
///
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs
index e061b92..2301d24 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs
@@ -89,7 +89,7 @@ namespace BetterLyrics.WinUI3.Helper
public ValueTransition(
T initialValue,
float durationSeconds,
- Func interpolator = null,
+ Func? interpolator = null,
EasingType? easingType = null
)
{
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AppInfo.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AppInfo.cs
index 92e2999..ae4dc6b 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AppInfo.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AppInfo.cs
@@ -78,8 +78,14 @@ namespace BetterLyrics.WinUI3.Helper
///
/// Gets the OnlineLyricsCacheDirectory
///
- public static string OnlineLyricsCacheDirectory =>
- Path.Combine(CacheFolder, "online-lyrics");
+ public static string LrcLibLyricsCacheDirectory =>
+ Path.Combine(CacheFolder, "lrclib-lyrics");
+
+ public static string AmllTtmlDbLyricsCacheDirectory =>
+ Path.Combine(CacheFolder, "amll-ttml-db-lyrics");
+
+ public static string AmllTtmlDbIndexPath =>
+ Path.Combine(CacheFolder, "amll-ttml-db-index.json");
///
/// Gets the TestMusicPath
@@ -109,7 +115,8 @@ namespace BetterLyrics.WinUI3.Helper
{
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
- Directory.CreateDirectory(OnlineLyricsCacheDirectory);
+ Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
+ Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
}
#endregion
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DesktopModeHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DesktopModeHelper.cs
new file mode 100644
index 0000000..6fc1169
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DesktopModeHelper.cs
@@ -0,0 +1,245 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Microsoft.UI;
+using Microsoft.UI.Input;
+using Microsoft.UI.Xaml;
+using WinRT.Interop;
+using WinUIEx;
+using static System.Net.WebRequestMethods;
+
+namespace BetterLyrics.WinUI3.Helper
+{
+ public static class DesktopModeHelper
+ {
+ private static readonly Dictionary _originalWindowStyles = [];
+ private static readonly Dictionary _clickThroughStates = [];
+ private static readonly Dictionary _originalTopmostStates = [];
+ private static readonly Dictionary _oldWndProcs = [];
+ private static readonly Dictionary _wndProcDelegates = [];
+
+ //
+ private delegate nint WndProcDelegate(nint hWnd, uint msg, nint wParam, nint lParam);
+
+ private const int WM_NCHITTEST = 0x0084;
+ private const int HTCLIENT = 1;
+ private const int HTTRANSPARENT = -1;
+ private const int GWL_WNDPROC = -4;
+
+ public static void Enable(Window window)
+ {
+ IntPtr hwnd = WindowNative.GetWindowHandle(window);
+
+ // ԭʽ
+ if (!_originalWindowStyles.ContainsKey(hwnd))
+ _originalWindowStyles[hwnd] = window.GetWindowStyle();
+
+ // ԭTopMost״̬
+ if (!_originalTopmostStates.ContainsKey(hwnd))
+ _originalTopmostStates[hwnd] = IsWindowTopMost(hwnd);
+
+ // ޱ߿
+ window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
+ window.ExtendsContentIntoTitleBar = false;
+
+ // ôö
+ SetWindowTopMost(hwnd, true);
+
+ // ȫִ
+ SetClickThrough(window, true);
+
+ // þֲ
+ EnablePartialClickThrough(window);
+ }
+
+ public static void Disable(Window window)
+ {
+ IntPtr hwnd = WindowNative.GetWindowHandle(window);
+
+ // ָʽ
+ if (_originalWindowStyles.TryGetValue(hwnd, out var style))
+ {
+ window.SetWindowStyle(style);
+ _originalWindowStyles.Remove(hwnd);
+ }
+
+ window.ExtendsContentIntoTitleBar = true;
+
+ // ָTopMost״̬
+ if (_originalTopmostStates.TryGetValue(hwnd, out var wasTopMost))
+ {
+ SetWindowTopMost(hwnd, wasTopMost);
+ _originalTopmostStates.Remove(hwnd);
+ }
+
+ // رյ
+ SetClickThrough(window, false);
+
+ // رվֲ
+ DisablePartialClickThrough(window);
+ }
+
+ ///
+ /// ôǷö
+ ///
+ private static void SetWindowTopMost(IntPtr hwnd, bool topmost)
+ {
+ const int SWP_NOMOVE = 0x0002;
+ const int SWP_NOSIZE = 0x0001;
+ const int SWP_NOACTIVATE = 0x0010;
+ IntPtr hWndInsertAfter = topmost ? (IntPtr)(-1) : (IntPtr)(1); // HWND_TOPMOST / HWND_NOTOPMOST
+
+ SetWindowPos(
+ hwnd,
+ hWndInsertAfter,
+ 0,
+ 0,
+ 0,
+ 0,
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE
+ );
+ }
+
+ ///
+ /// жϴǷΪö
+ ///
+ private static bool IsWindowTopMost(IntPtr hwnd)
+ {
+ int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
+ return (exStyle & WS_EX_TOPMOST) == WS_EX_TOPMOST;
+ }
+
+ ///
+ /// л״̬
+ ///
+ public static void SetClickThrough(Window window, bool enable)
+ {
+ IntPtr hwnd = WindowNative.GetWindowHandle(window);
+ int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
+ if (enable)
+ {
+ SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT | WS_EX_LAYERED);
+ _clickThroughStates[hwnd] = true;
+ }
+ else
+ {
+ SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT);
+ _clickThroughStates[hwnd] = false;
+ }
+ }
+
+ ///
+ /// ȡǰǷΪ״̬
+ ///
+ public static bool GetClickThrough(Window window)
+ {
+ IntPtr hwnd = WindowNative.GetWindowHandle(window);
+ return _clickThroughStates.TryGetValue(hwnd, out var state) && state;
+ }
+
+ ///
+ /// þֲ
+ ///
+ public static void EnablePartialClickThrough(Window window)
+ {
+ IntPtr hwnd = WindowNative.GetWindowHandle(window);
+
+ if (_oldWndProcs.ContainsKey(hwnd))
+ return; // Ѿ
+
+ WndProcDelegate newWndProc = (hWnd, msg, wParam, lParam) =>
+ {
+ if (msg == WM_NCHITTEST)
+ {
+ int x = (short)(lParam.ToInt32() & 0xFFFF);
+ int y = (short)((lParam.ToInt32() >> 16) & 0xFFFF);
+
+ // Ļת
+ var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(
+ Win32Interop.GetWindowIdFromWindow(hwnd)
+ );
+ var windowPos = appWindow.Position;
+ int localX = x - windowPos.X;
+ int localY = y - windowPos.Y;
+
+ if (IsInInteractiveRegion(window, localX, localY))
+ return HTCLIENT;
+ else
+ return HTTRANSPARENT;
+ }
+ return CallWindowProc(_oldWndProcs[hwnd], hWnd, msg, wParam, lParam);
+ };
+
+ nint oldWndProc = SetWindowLongPtr(
+ hwnd,
+ GWL_WNDPROC,
+ Marshal.GetFunctionPointerForDelegate(newWndProc)
+ );
+ _oldWndProcs[hwnd] = oldWndProc;
+ _wndProcDelegates[hwnd] = newWndProc; // ֹGC
+ }
+
+ ///
+ /// رվֲ
+ ///
+ public static void DisablePartialClickThrough(Window window)
+ {
+ IntPtr hwnd = WindowNative.GetWindowHandle(window);
+ if (_oldWndProcs.TryGetValue(hwnd, out var oldWndProc))
+ {
+ SetWindowLongPtr(hwnd, GWL_WNDPROC, oldWndProc);
+ _oldWndProcs.Remove(hwnd);
+ _wndProcDelegates.Remove(hwnd);
+ }
+ }
+
+ ///
+ /// жϵǷڿɽ˴Ϊϰ벿֣Զ壩
+ ///
+ private static bool IsInInteractiveRegion(Window window, int x, int y)
+ {
+ // ϰ벿ֿɽ
+ var bounds = window.Bounds;
+ return y < bounds.Height / 2;
+ }
+
+ #region Win32
+
+ private const int GWL_EXSTYLE = -20;
+ private const int WS_EX_TRANSPARENT = 0x00000020;
+ private const int WS_EX_LAYERED = 0x00080000;
+ private const int WS_EX_TOPMOST = 0x00000008;
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool SetWindowPos(
+ IntPtr hWnd,
+ IntPtr hWndInsertAfter,
+ int X,
+ int Y,
+ int cx,
+ int cy,
+ uint uFlags
+ );
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern nint CallWindowProc(
+ nint lpPrevWndFunc,
+ nint hWnd,
+ uint msg,
+ nint wParam,
+ nint lParam
+ );
+
+ #endregion
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DockHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DockModeHelper.cs
similarity index 99%
rename from BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DockHelper.cs
rename to BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DockModeHelper.cs
index 6d15847..f6af007 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DockHelper.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DockModeHelper.cs
@@ -11,7 +11,7 @@ using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
- public static class DockHelper
+ public static class DockModeHelper
{
private static readonly HashSet _registered = [];
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ForegroundWindowWatcherHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ForegroundWindowWatcherHelper.cs
index 0555f5b..08618a8 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ForegroundWindowWatcherHelper.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ForegroundWindowWatcherHelper.cs
@@ -1,92 +1,31 @@
-// 2025/6/23 by Zhe Fang
-
-using Microsoft.UI.Xaml;
-using System;
+using System;
using System.Collections.Generic;
+using System.Linq;
using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Helper
{
- ///
- /// Defines the
- ///
public class ForegroundWindowWatcherHelper
{
- #region Constants
-
- ///
- /// Defines the EVENT_OBJECT_LOCATIONCHANGE
- ///
- private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
-
- ///
- /// Defines the EVENT_SYSTEM_FOREGROUND
- ///
- private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
-
- ///
- /// Defines the EVENT_SYSTEM_MINIMIZEEND
- ///
- private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
-
- ///
- /// Defines the ThrottleIntervalMs
- ///
+ private readonly WinEventDelegate _winEventDelegate;
+ private readonly List _hooks = new();
+ private IntPtr _currentForeground = IntPtr.Zero;
+ private readonly IntPtr _selfHwnd;
+ private readonly DispatcherTimer _pollingTimer;
+ private DateTime _lastEventTime = DateTime.MinValue;
private const int ThrottleIntervalMs = 100;
- ///
- /// Defines the WINEVENT_OUTOFCONTEXT
- ///
- private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
-
- #endregion
-
- #region Fields
-
- ///
- /// Defines the _hooks
- ///
- private readonly List _hooks = new();
-
- ///
- /// Defines the _onWindowChanged
- ///
+ public delegate void WindowChangedHandler(IntPtr hwnd);
private readonly WindowChangedHandler _onWindowChanged;
- ///
- /// Defines the _pollingTimer
- ///
- private readonly DispatcherTimer _pollingTimer;
+ private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
+ private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
+ private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
+ private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
- ///
- /// Defines the _selfHwnd
- ///
- private readonly IntPtr _selfHwnd;
-
- ///
- /// Defines the _winEventDelegate
- ///
- private readonly WinEventDelegate _winEventDelegate;
-
- ///
- /// Defines the _currentForeground
- ///
- private IntPtr _currentForeground = IntPtr.Zero;
-
- ///
- /// Defines the _lastEventTime
- ///
- private DateTime _lastEventTime = DateTime.MinValue;
-
- #endregion
-
- #region Constructors
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The selfHwnd
- /// The onWindowChanged
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
@@ -101,43 +40,6 @@ namespace BetterLyrics.WinUI3.Helper
};
}
- #endregion
-
- #region Delegates
-
- ///
- /// The WindowChangedHandler
- ///
- /// The hwnd
- public delegate void WindowChangedHandler(IntPtr hwnd);
-
- ///
- /// The WinEventDelegate
- ///
- /// The hWinEventHook
- /// The eventType
- /// The hwnd
- /// The idObject
- /// The idChild
- /// The dwEventThread
- /// The dwmsEventTime
- private delegate void WinEventDelegate(
- IntPtr hWinEventHook,
- uint eventType,
- IntPtr hwnd,
- int idObject,
- int idChild,
- uint dwEventThread,
- uint dwmsEventTime
- );
-
- #endregion
-
- #region Methods
-
- ///
- /// The Start
- ///
public void Start()
{
// Hook: foreground changes and minimize end
@@ -169,9 +71,6 @@ namespace BetterLyrics.WinUI3.Helper
_pollingTimer.Start();
}
- ///
- /// The Stop
- ///
public void Stop()
{
foreach (var hook in _hooks)
@@ -181,46 +80,6 @@ namespace BetterLyrics.WinUI3.Helper
_pollingTimer.Stop();
}
- ///
- /// The SetWinEventHook
- ///
- /// The eventMin
- /// The eventMax
- /// The hmodWinEventProc
- /// The lpfnWinEventProc
- /// The idProcess
- /// The idThread
- /// The dwFlags
- /// The
- [DllImport("user32.dll")]
- private static extern IntPtr SetWinEventHook(
- uint eventMin,
- uint eventMax,
- IntPtr hmodWinEventProc,
- WinEventDelegate lpfnWinEventProc,
- uint idProcess,
- uint idThread,
- uint dwFlags
- );
-
- ///
- /// The UnhookWinEvent
- ///
- /// The hWinEventHook
- /// The
- [DllImport("user32.dll")]
- private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
-
- ///
- /// The WinEventProc
- ///
- /// The hWinEventHook
- /// The eventType
- /// The hwnd
- /// The idObject
- /// The idChild
- /// The dwEventThread
- /// The dwmsEventTime
private void WinEventProc(
IntPtr hWinEventHook,
uint eventType,
@@ -254,6 +113,30 @@ namespace BetterLyrics.WinUI3.Helper
}
}
+ #region WinAPI
+ private delegate void WinEventDelegate(
+ IntPtr hWinEventHook,
+ uint eventType,
+ IntPtr hwnd,
+ int idObject,
+ int idChild,
+ uint dwEventThread,
+ uint dwmsEventTime
+ );
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr SetWinEventHook(
+ uint eventMin,
+ uint eventMax,
+ IntPtr hmodWinEventProc,
+ WinEventDelegate lpfnWinEventProc,
+ uint idProcess,
+ uint idThread,
+ uint dwFlags
+ );
+
+ [DllImport("user32.dll")]
+ private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endregion
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/LyricsParser.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/LyricsParser.cs
index 655ab6b..6140c62 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/LyricsParser.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/LyricsParser.cs
@@ -1,12 +1,12 @@
// 2025/6/23 by Zhe Fang
-using BetterLyrics.WinUI3.Enums;
-using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
+using BetterLyrics.WinUI3.Enums;
+using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Helper
{
@@ -66,7 +66,7 @@ namespace BetterLyrics.WinUI3.Helper
/// The durationMs
private void ParseLrc(string raw, int durationMs)
{
- var lines = raw.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
+ var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
new List<(int time, string text, List<(int time, string text)> syllables)>();
@@ -114,7 +114,7 @@ namespace BetterLyrics.WinUI3.Helper
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[3].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
- content = bracketRegex.Replace(line, "").Trim();
+ content = bracketRegex.Replace(line, "");
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
@@ -147,13 +147,21 @@ namespace BetterLyrics.WinUI3.Helper
};
if (syllables != null && syllables.Count > 0)
{
+ int currentIndex = 0;
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
- int charEnd = (j + 1 < syllables.Count) ? syllables[j + 1].Item1 : 0;
+ int startIndex = currentIndex;
line.CharTimings.Add(
- new CharTiming { StartMs = charStart, EndMs = charEnd }
+ new CharTiming
+ {
+ StartMs = charStart,
+ EndMs = 0, // Fixed later
+ Text = charText ?? "",
+ StartIndex = startIndex,
+ }
);
+ currentIndex += charText?.Length ?? 0;
}
}
_multiLangLyricsLines[langIdx].Add(line);
@@ -167,20 +175,28 @@ namespace BetterLyrics.WinUI3.Helper
for (int i = 0; i < linesInSingleLang.Count; i++)
{
if (i + 1 < linesInSingleLang.Count)
+ {
linesInSingleLang[i].EndMs = linesInSingleLang[i + 1].StartMs;
+ }
else
+ {
linesInSingleLang[i].EndMs = durationMs;
+ }
- // 修正 CharTimings 的最后一个 EndMs
+ // 修正 CharTimings 的 EndMs
var timings = linesInSingleLang[i].CharTimings;
if (timings.Count > 0)
{
for (int j = 0; j < timings.Count; j++)
{
if (j + 1 < timings.Count)
+ {
timings[j].EndMs = timings[j + 1].StartMs;
+ }
else
+ {
timings[j].EndMs = linesInSingleLang[i].EndMs;
+ }
}
}
}
@@ -220,7 +236,7 @@ namespace BetterLyrics.WinUI3.Helper
)
.ToList();
- string text = string.Concat(spans.Select(s => s.Value));
+ string text = string.Concat(spans.Select(s => s));
var charTimings = new List();
for (int i = 0; i < spans.Count; i++)
@@ -244,7 +260,7 @@ namespace BetterLyrics.WinUI3.Helper
}
if (spans.Count == 0)
- text = p.Value.Trim();
+ text = p.Value;
singleLangLyricsLine.Add(
new LyricsLine
@@ -345,7 +361,7 @@ namespace BetterLyrics.WinUI3.Helper
{
StartMs = 0,
EndMs = lines[0].StartMs,
- Text = "",
+ Text = "● ● ●",
CharTimings = [],
}
);
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/WindowColorHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/WindowColorHelper.cs
index 00d17c1..9bec2ea 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/WindowColorHelper.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/WindowColorHelper.cs
@@ -1,53 +1,12 @@
-// 2025/6/23 by Zhe Fang
-
-using System;
+using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace BetterLyrics.WinUI3.Helper
{
- ///
- /// Defines the
- ///
public static class WindowColorHelper
{
- #region Constants
-
- ///
- /// Defines the SRCCOPY
- ///
- private const int SRCCOPY = 0x00CC0020;
-
- #endregion
-
- #region Enums
-
- ///
- /// Defines the SystemMetric
- ///
- private enum SystemMetric
- {
- ///
- /// Defines the SM_CXSCREEN
- ///
- SM_CXSCREEN = 0,
-
- ///
- /// Defines the SM_CYSCREEN
- ///
- SM_CYSCREEN = 1,
- }
-
- #endregion
-
- #region Methods
-
- ///
- /// The GetDominantColorBelow
- ///
- /// The myHwnd
- /// The
public static Color GetDominantColorBelow(IntPtr myHwnd)
{
if (!GetWindowRect(myHwnd, out RECT myRect))
@@ -60,37 +19,22 @@ namespace BetterLyrics.WinUI3.Helper
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
- ///
- /// The BitBlt
- ///
- /// The hdcDest
- /// The nXDest
- /// The nYDest
- /// The nWidth
- /// The nHeight
- /// The hdcSrc
- /// The nXSrc
- /// The nYSrc
- /// The dwRop
- /// The
- [DllImport("gdi32.dll")]
- private static extern bool BitBlt(
- IntPtr hdcDest,
- int nXDest,
- int nYDest,
- int nWidth,
- int nHeight,
- IntPtr hdcSrc,
- int nXSrc,
- int nYSrc,
- int dwRop
- );
+ private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
+ {
+ using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
+ using Graphics gDest = Graphics.FromImage(bmp);
+
+ IntPtr hdcDest = gDest.GetHdc();
+ IntPtr hdcSrc = GetDC(IntPtr.Zero); // Entire screen
+
+ BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, SRCCOPY);
+
+ gDest.ReleaseHdc(hdcDest);
+ ReleaseDC(IntPtr.Zero, hdcSrc);
+
+ return ComputeAverageColor(bmp);
+ }
- ///
- /// The ComputeAverageColor
- ///
- /// The bmp
- /// The
private static Color ComputeAverageColor(Bitmap bmp)
{
long r = 0,
@@ -115,95 +59,48 @@ namespace BetterLyrics.WinUI3.Helper
return Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
- ///
- /// The GetAverageColorFromScreenRegion
- ///
- /// The x
- /// The y
- /// The width
- /// The height
- /// The
- private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
- {
- using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
- using Graphics gDest = Graphics.FromImage(bmp);
+ #region Win32 Imports & Structs
+ private const int SRCCOPY = 0x00CC0020;
- IntPtr hdcDest = gDest.GetHdc();
- IntPtr hdcSrc = GetDC(IntPtr.Zero); // Entire screen
-
- BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, SRCCOPY);
-
- gDest.ReleaseHdc(hdcDest);
- ReleaseDC(IntPtr.Zero, hdcSrc);
-
- return ComputeAverageColor(bmp);
- }
-
- ///
- /// The GetDC
- ///
- /// The hWnd
- /// The
- [DllImport("user32.dll")]
- private static extern IntPtr GetDC(IntPtr hWnd);
-
- ///
- /// The GetSystemMetrics
- ///
- /// The smIndex
- /// The
- [DllImport("user32.dll")]
- private static extern int GetSystemMetrics(SystemMetric smIndex);
-
- ///
- /// The GetWindowRect
- ///
- /// The hWnd
- /// The lpRect
- /// The
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
- ///
- /// The ReleaseDC
- ///
- /// The hWnd
- /// The hDC
- /// The
+ [DllImport("user32.dll")]
+ private static extern IntPtr GetDC(IntPtr hWnd);
+
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
- #endregion
+ [DllImport("gdi32.dll")]
+ private static extern bool BitBlt(
+ IntPtr hdcDest,
+ int nXDest,
+ int nYDest,
+ int nWidth,
+ int nHeight,
+ IntPtr hdcSrc,
+ int nXSrc,
+ int nYSrc,
+ int dwRop
+ );
+
+ [DllImport("user32.dll")]
+ private static extern int GetSystemMetrics(SystemMetric smIndex);
+
+ private enum SystemMetric
+ {
+ SM_CXSCREEN = 0,
+ SM_CYSCREEN = 1,
+ }
- ///
- /// Defines the
- ///
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
- #region Fields
-
- ///
- /// Defines the Bottom
- ///
- public int Bottom;
-
- ///
- /// Defines the Left
- ///
public int Left;
-
- ///
- /// Defines the Right
- ///
- public int Right;
-
- ///
- /// Defines the Top
- ///
public int Top;
-
- #endregion
+ public int Right;
+ public int Bottom;
}
+ #endregion
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/CharTiming.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/CharTiming.cs
index efd83e5..b0efd89 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/CharTiming.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/CharTiming.cs
@@ -25,6 +25,10 @@ namespace BetterLyrics.WinUI3.Models
///
public int StartMs { get; set; }
+ public string Text { get; set; } = string.Empty;
+
+ public int StartIndex { get; set; }
+
#endregion
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs
index 4a0e181..2ac32bc 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Numerics;
-using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Text;
@@ -15,6 +14,15 @@ namespace BetterLyrics.WinUI3.Models
{
#region Properties
+ ///
+ /// Gets or sets the BlurAmountTransition
+ ///
+ public ValueTransition BlurAmountTransition { get; set; } =
+ new(initialValue: 0f, durationSeconds: 0.3f);
+
+ ///
+ /// Gets or sets the CanvasTextLayout
+ ///
public CanvasTextLayout? CanvasTextLayout { get; set; }
///
@@ -37,30 +45,8 @@ namespace BetterLyrics.WinUI3.Models
///
public int EndMs { get; set; }
- ///
- /// Gets or sets the EnteringProgress
- ///
- public float EnteringProgress { get; set; }
-
- ///
- /// Gets or sets the ExitingProgress
- ///
- public float ExitingProgress { get; set; }
-
- ///
- /// Gets or sets the Opacity
- ///
- public float Opacity { get; set; }
-
- ///
- /// Gets or sets the PlayingProgress
- ///
- public float PlayingProgress { get; set; }
-
- ///
- /// Gets or sets the PlayingState
- ///
- public LyricsPlayingState PlayingState { get; set; }
+ public ValueTransition HighlightOpacityTransition { get; set; } =
+ new(initialValue: 0f, durationSeconds: 0.3f);
///
/// Gets or sets the Position
@@ -68,9 +54,10 @@ namespace BetterLyrics.WinUI3.Models
public Vector2 Position { get; set; }
///
- /// Gets or sets the Scale
+ /// Gets or sets the ScaleTransition
///
- public float Scale { get; set; }
+ public ValueTransition ScaleTransition { get; set; } =
+ new(initialValue: 0.95f, durationSeconds: 0.3f);
///
/// Gets or sets the StartMs
@@ -82,9 +69,6 @@ namespace BetterLyrics.WinUI3.Models
///
public string Text { get; set; } = "";
- public ValueTransition BlurAmountTransition { get; set; } =
- new(initialValue: 0f, durationSeconds: 0.3f);
-
#endregion
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MusicSearchService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MusicSearchService.cs
index 0f6f73f..b1e7d6f 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MusicSearchService.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MusicSearchService.cs
@@ -25,7 +25,9 @@ namespace BetterLyrics.WinUI3.Services
///
/// Defines the _httpClient
///
- private readonly HttpClient _httpClient;
+ private readonly HttpClient _lrcLibHttpClient;
+
+ private readonly HttpClient _amllTtmlDbHttpClient;
///
/// Defines the _settingsService
@@ -43,11 +45,12 @@ namespace BetterLyrics.WinUI3.Services
public MusicSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
- _httpClient = new HttpClient();
- _httpClient.DefaultRequestHeaders.Add(
+ _lrcLibHttpClient = new HttpClient();
+ _lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
);
+ _amllTtmlDbHttpClient = new HttpClient();
}
#endregion
@@ -74,7 +77,7 @@ namespace BetterLyrics.WinUI3.Services
)
)
{
- if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
+ if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
@@ -114,16 +117,36 @@ namespace BetterLyrics.WinUI3.Services
continue;
}
+ string? cachedLyrics;
+
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
// Check cache first
- var cachedLyrics = ReadCache(title, artist, LyricsFormat.Lrc);
+ cachedLyrics = ReadCache(
+ title,
+ artist,
+ LyricsFormat.Lrc,
+ AppInfo.LrcLibLyricsCacheDirectory
+ );
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, LyricsFormat.Lrc);
}
break;
+ //case LyricsSearchProvider.AmllTtmlDb:
+ // // Check cache first
+ // cachedLyrics = ReadCache(
+ // title,
+ // artist,
+ // LyricsFormat.Ttml,
+ // AppInfo.AmllTtmlDbLyricsCacheDirectory
+ // );
+ // if (!string.IsNullOrWhiteSpace(cachedLyrics))
+ // {
+ // return (cachedLyrics, LyricsFormat.Ttml);
+ // }
+ // break;
default:
break;
}
@@ -157,7 +180,7 @@ namespace BetterLyrics.WinUI3.Services
);
break;
case LyricsSearchProvider.LrcLib:
- searchedLyrics = await SearchLrcLib(
+ searchedLyrics = await SearchLrcLibAsync(
title,
artist,
album,
@@ -165,6 +188,9 @@ namespace BetterLyrics.WinUI3.Services
matchMode
);
break;
+ //case LyricsSearchProvider.AmllTtmlDb:
+ // searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
+ // break;
default:
break;
}
@@ -174,8 +200,23 @@ namespace BetterLyrics.WinUI3.Services
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
- WriteCache(title, artist, searchedLyrics, LyricsFormat.Lrc);
+ WriteCache(
+ title,
+ artist,
+ searchedLyrics,
+ LyricsFormat.Lrc,
+ AppInfo.LrcLibLyricsCacheDirectory
+ );
return (searchedLyrics, LyricsFormat.Lrc);
+ //case LyricsSearchProvider.AmllTtmlDb:
+ // WriteCache(
+ // title,
+ // artist,
+ // searchedLyrics,
+ // LyricsFormat.Ttml,
+ // AppInfo.AmllTtmlDbLyricsCacheDirectory
+ // );
+ // return (searchedLyrics, LyricsFormat.Ttml);
case LyricsSearchProvider.LocalMusicFile:
return (searchedLyrics, LyricsFormatExtensions.Detect(searchedLyrics));
case LyricsSearchProvider.LocalLrcFile:
@@ -193,69 +234,9 @@ namespace BetterLyrics.WinUI3.Services
return (null, null);
}
- // 判断相似度
-
- ///
- /// The FuzzyMatch
- ///
- /// The fileName
- /// The title
- /// The artist
- /// The
- private static bool FuzzyMatch(string fileName, string title, string artist)
+ private static bool MusicMatch(string fileName, string title, string artist)
{
- var normFile = Normalize(fileName);
- var normTarget1 = Normalize(title + artist);
- var normTarget2 = Normalize(artist + title);
-
- int dist1 = LevenshteinDistance(normFile, normTarget1);
- int dist2 = LevenshteinDistance(normFile, normTarget2);
-
- return dist1 <= 2 || dist2 <= 2; // 阈值可调整
- }
-
- ///
- /// The LevenshteinDistance
- ///
- /// The a
- /// The b
- /// The
- private static int LevenshteinDistance(string a, string b)
- {
- if (string.IsNullOrEmpty(a))
- return b.Length;
- if (string.IsNullOrEmpty(b))
- return a.Length;
- int[,] d = new int[a.Length + 1, b.Length + 1];
- for (int i = 0; i <= a.Length; i++)
- d[i, 0] = i;
- for (int j = 0; j <= b.Length; j++)
- d[0, j] = j;
- for (int i = 1; i <= a.Length; i++)
- for (int j = 1; j <= b.Length; j++)
- d[i, j] = Math.Min(
- Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
- d[i - 1, j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1)
- );
- return d[a.Length, b.Length];
- }
-
- ///
- /// The Normalize
- ///
- /// The s
- /// The
- private static string Normalize(string s)
- {
- if (string.IsNullOrWhiteSpace(s))
- return "";
- var sb = new StringBuilder();
- foreach (var c in s.ToLowerInvariant())
- {
- if (char.IsLetterOrDigit(c))
- sb.Append(c);
- }
- return sb.ToString();
+ return fileName.Contains(title) && fileName.Contains(artist);
}
///
@@ -300,7 +281,7 @@ namespace BetterLyrics.WinUI3.Services
)
)
{
- if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
+ if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(
file,
@@ -337,7 +318,7 @@ namespace BetterLyrics.WinUI3.Services
)
)
{
- if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
+ if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
//Track track = new(file);
//var test1 = track.Lyrics.SynchronizedLyrics;
@@ -367,12 +348,17 @@ namespace BetterLyrics.WinUI3.Services
/// The artist
/// The format
/// The
- private string? ReadCache(string title, string artist, LyricsFormat format)
+ private string? ReadCache(
+ string title,
+ string artist,
+ LyricsFormat format,
+ string cacheFolderPath
+ )
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
- AppInfo.OnlineLyricsCacheDirectory,
+ cacheFolderPath,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
if (File.Exists(cacheFilePath))
@@ -391,7 +377,7 @@ namespace BetterLyrics.WinUI3.Services
/// The duration
/// The matchMode
/// The
- private async Task SearchLrcLib(
+ private async Task SearchLrcLibAsync(
string title,
string artist,
string album,
@@ -412,7 +398,7 @@ namespace BetterLyrics.WinUI3.Services
+ $"&durationMs={Uri.EscapeDataString(duration.ToString())}";
}
- var response = await _httpClient.GetAsync(url);
+ var response = await _lrcLibHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
@@ -436,6 +422,114 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
+ ///
+ /// 本地检索 amll-ttml-db 索引并下载歌词内容
+ ///
+ /// 歌曲名
+ /// 歌手名
+ /// 歌词内容字符串,找不到返回 null
+ private async Task SearchAmllTtmlDbAsync(string title, string artist)
+ {
+ // 检索本地 JSONL 索引文件,查找 rawLyricFile
+ if (!File.Exists(AppInfo.AmllTtmlDbIndexPath))
+ {
+ var downloadOk = await DownloadAmllTtmlDbIndexAsync();
+ if (!downloadOk || !File.Exists(AppInfo.AmllTtmlDbIndexPath))
+ return null;
+ }
+
+ string? rawLyricFile = null;
+ foreach (var line in File.ReadLines(AppInfo.AmllTtmlDbIndexPath))
+ {
+ if (string.IsNullOrWhiteSpace(line))
+ continue;
+ try
+ {
+ using var doc = JsonDocument.Parse(line);
+ var root = doc.RootElement;
+ if (!root.TryGetProperty("metadata", out var metadataArr))
+ continue;
+ string? musicName = null;
+ string? artists = null;
+ foreach (var meta in metadataArr.EnumerateArray())
+ {
+ if (meta.GetArrayLength() != 2)
+ continue;
+ var key = meta[0].GetString();
+ var valueArr = meta[1];
+ if (key == "musicName" && valueArr.GetArrayLength() > 0)
+ musicName = valueArr[0].GetString();
+ if (key == "artists" && valueArr.GetArrayLength() > 0)
+ artists = valueArr[0].GetString();
+ }
+ if (musicName == null || artists == null)
+ continue;
+
+ if (MusicMatch($"{artists} - {musicName}", title, artist))
+ {
+ if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
+ {
+ rawLyricFile = rawLyricFileProp.GetString();
+ break;
+ }
+ }
+ }
+ catch { }
+ }
+
+ if (string.IsNullOrWhiteSpace(rawLyricFile))
+ return null;
+
+ // 下载歌词内容
+ var url =
+ $"https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/raw-lyrics/{rawLyricFile}";
+ try
+ {
+ var response = await _amllTtmlDbHttpClient.GetAsync(url);
+ if (!response.IsSuccessStatusCode)
+ return null;
+ return await response.Content.ReadAsStringAsync();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 下载 amll-ttml-db 的 JSONL 索引文件到本地缓存目录
+ ///
+ /// 下载成功返回 true,否则 false
+ public async Task DownloadAmllTtmlDbIndexAsync()
+ {
+ const string url =
+ "https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/metadata/raw-lyrics-index.jsonl";
+ try
+ {
+ using var response = await _amllTtmlDbHttpClient.GetAsync(
+ url,
+ HttpCompletionOption.ResponseHeadersRead
+ );
+ if (!response.IsSuccessStatusCode)
+ return false;
+
+ await using var stream = await response.Content.ReadAsStreamAsync();
+ await using var fs = new FileStream(
+ AppInfo.AmllTtmlDbIndexPath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None
+ );
+ await stream.CopyToAsync(fs);
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
///
/// The WriteCache
///
@@ -443,12 +537,18 @@ namespace BetterLyrics.WinUI3.Services
/// The artist
/// The lyrics
/// The format
- private void WriteCache(string title, string artist, string lyrics, LyricsFormat format)
+ private void WriteCache(
+ string title,
+ string artist,
+ string lyrics,
+ LyricsFormat format,
+ string cacheFolderPath
+ )
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
- AppInfo.OnlineLyricsCacheDirectory,
+ cacheFolderPath,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/SettingsService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/SettingsService.cs
index 546cc8b..21c8980 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/SettingsService.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/SettingsService.cs
@@ -202,7 +202,7 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
- SetDefault(LyricsGlowEffectScopeKey, (int)LineRenderingType.CurrentCharOnly);
+ SetDefault(LyricsGlowEffectScopeKey, (int)LineRenderingType.UntilCurrentChar);
}
#endregion
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
index f3f2fa6..2dbda65 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
@@ -411,6 +411,9 @@
Dock mode
+
+ Desktop mode
+
Font weight
@@ -486,4 +489,7 @@
This folder contains added folders, please delete these folders to add the folder
+
+ amll-ttml-db
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw
index 97251c5..2efc883 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ja-JP/Resources.resw
@@ -411,6 +411,9 @@
ドックモード
+
+
+
フォント重量
@@ -486,4 +489,7 @@
このフォルダーには追加されたフォルダーが含まれています。これらのフォルダを削除してフォルダーを追加してください
+
+ amll-ttml-db
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw
index bb6a7b6..b83992d 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ko-KR/Resources.resw
@@ -411,6 +411,9 @@
도크 모드
+
+
+
글꼴 무게
@@ -486,4 +489,7 @@
이 폴더에는 추가 된 폴더가 포함되어 있습니다. 폴더를 추가하려면이 폴더를 삭제하십시오.
+
+ amll-ttml-db
+
\ 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 fc94a83..2d7fe51 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw
@@ -411,6 +411,9 @@
停靠模式
+
+ 桌面模式
+
字体粗细
@@ -486,4 +489,7 @@
该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹
+
+ amll-ttml-db
+
\ 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 fd74f82..f20595c 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw
@@ -411,6 +411,9 @@
停靠模式
+
+
+
字體粗細
@@ -486,4 +489,7 @@
該文件夾包含已添加文件夾,請刪除這些文件夾以添加該文件夾
+
+ amll-ttml-db
+
\ No newline at end of file
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/HostWindowViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/HostWindowViewModel.cs
index 57b86ac..63d41e0 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/HostWindowViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/HostWindowViewModel.cs
@@ -1,5 +1,7 @@
// 2025/6/23 by Zhe Fang
+using System;
+using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
@@ -7,17 +9,17 @@ using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
+using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
-using System;
-using System.Threading.Tasks;
using Windows.UI;
using WinRT.Interop;
using WinUIEx;
+using WinUIEx.Messaging;
namespace BetterLyrics.WinUI3
{
@@ -104,6 +106,10 @@ namespace BetterLyrics.WinUI3
[NotifyPropertyChangedRecipients]
public partial bool IsDockMode { get; set; } = false;
+ [ObservableProperty]
+ [NotifyPropertyChangedRecipients]
+ public partial bool IsDesktopMode { get; set; } = false;
+
///
/// Gets or sets the Notification
///
@@ -175,7 +181,7 @@ namespace BetterLyrics.WinUI3
{
if (IsDockMode)
{
- DockHelper.UpdateAppBarHeight(
+ DockModeHelper.UpdateAppBarHeight(
WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
),
@@ -252,15 +258,6 @@ namespace BetterLyrics.WinUI3
_watcherHelper = null;
}
- ///
- /// The SwitchInfoBarNeverShowItAgainCheckBox
- ///
- /// The value
- [RelayCommand]
- private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
- {
- }
-
///
/// The ToggleDockMode
///
@@ -272,16 +269,36 @@ namespace BetterLyrics.WinUI3
IsDockMode = !IsDockMode;
if (IsDockMode)
{
- DockHelper.Enable(window, _settingsService.LyricsFontSize * 3);
+ DockModeHelper.Enable(window, _settingsService.LyricsFontSize * 3);
StartWatchWindowColorChange();
}
else
{
- DockHelper.Disable(window);
+ DockModeHelper.Disable(window);
StopWatchWindowColorChange();
}
}
+ [RelayCommand]
+ private void ToggleDesktopMode()
+ {
+ var window = WindowHelper.GetWindowByFramePageType(FramePageType);
+
+ IsDesktopMode = !IsDesktopMode;
+ if (IsDesktopMode)
+ {
+ DesktopModeHelper.Enable(window);
+ WindowHelper.GetWindowByFramePageType(typeof(LyricsPage)).SystemBackdrop =
+ SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
+ }
+ else
+ {
+ DesktopModeHelper.Disable(window);
+ WindowHelper.GetWindowByFramePageType(typeof(LyricsPage)).SystemBackdrop =
+ SystemBackdropHelper.CreateSystemBackdrop(_settingsService.BackdropType);
+ }
+ }
+
///
/// The OnFramePageTypeChanged
///
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsPageViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsPageViewModel.cs
index eec54c7..e950341 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsPageViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsPageViewModel.cs
@@ -1,5 +1,8 @@
// 2025/6/23 by Zhe Fang
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
@@ -11,9 +14,6 @@ using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
-using System;
-using System.Diagnostics;
-using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -199,6 +199,11 @@ namespace BetterLyrics.WinUI3.ViewModels
}
TrySwitchToPreferredDisplayType(SongInfo);
}
+ else if (message.PropertyName == nameof(HostWindowViewModel.IsDesktopMode))
+ {
+ if (message.NewValue) { }
+ else { }
+ }
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Layout.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Layout.cs
new file mode 100644
index 0000000..753fa12
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Layout.cs
@@ -0,0 +1,314 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Models;
+using Microsoft.Graphics.Canvas.Text;
+using Microsoft.Graphics.Canvas.UI.Xaml;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Windows.UI;
+
+namespace BetterLyrics.WinUI3.ViewModels
+{
+ public partial class LyricsRendererViewModel
+ {
+ ///
+ /// The Update
+ ///
+ /// The control
+ /// The args
+ public void Update(ICanvasAnimatedControl control, CanvasAnimatedUpdateEventArgs args)
+ {
+ if (_isPlaying)
+ {
+ TotalTime += args.Timing.ElapsedTime;
+ }
+
+ ElapsedTime = args.Timing.ElapsedTime;
+
+ if (_immersiveBgTransition.IsTransitioning)
+ {
+ _immersiveBgTransition.Update(ElapsedTime);
+ }
+
+ if (_albumArtBgTransition.IsTransitioning)
+ {
+ _albumArtBgTransition.Update(ElapsedTime);
+ }
+
+ if (IsDynamicCoverOverlayEnabled)
+ {
+ _rotateAngle += _coverRotateSpeed;
+ _rotateAngle %= MathF.PI * 2;
+ }
+
+ if (_limitedLineWidthTransition.IsTransitioning)
+ {
+ _limitedLineWidthTransition.Update(ElapsedTime);
+ _isRelayoutNeeded = true;
+ }
+
+ if (_isRelayoutNeeded)
+ {
+ ReLayout(control);
+ _isRelayoutNeeded = false;
+ }
+
+ UpdateCanvasYScrollOffset(control);
+ UpdateLinesProps();
+ }
+
+ ///
+ /// The UpdateCanvasYScrollOffset
+ ///
+ /// The control
+ private void UpdateCanvasYScrollOffset(ICanvasAnimatedControl control)
+ {
+ var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
+
+ var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
+
+ if (startLineIndex < 0 || endLineIndex < 0)
+ {
+ return;
+ }
+
+ // Set _scrollOffsetY
+ LyricsLine? currentPlayingLine = _multiLangLyrics
+ .SafeGet(_langIndex)
+ ?.SafeGet(currentPlayingLineIndex);
+
+ var playingTextLayout = currentPlayingLine?.CanvasTextLayout;
+
+ if (currentPlayingLine == null || playingTextLayout == null)
+ {
+ return;
+ }
+
+ float targetYScrollOffset =
+ (float?)(
+ -currentPlayingLine.Position.Y
+ + _multiLangLyrics.SafeGet(_langIndex)?[0].Position.Y
+ - playingTextLayout.LayoutBounds.Height / 2
+ ) ?? 0f;
+
+ if (!_canvasYScrollTransition.IsTransitioning)
+ {
+ _canvasYScrollTransition.StartTransition(targetYScrollOffset);
+ }
+
+ if (_canvasYScrollTransition.IsTransitioning)
+ {
+ _canvasYScrollTransition.Update(ElapsedTime);
+ }
+
+ _startVisibleLineIndex = _endVisibleLineIndex = -1;
+
+ // Update visible line indices
+ for (int i = startLineIndex; i <= endLineIndex; i++)
+ {
+ var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
+
+ if (line == null || line.CanvasTextLayout == null)
+ {
+ continue;
+ }
+
+ var textLayout = line.CanvasTextLayout;
+
+ if (
+ _canvasYScrollTransition.Value
+ + (float)(control.Size.Height / 2)
+ + line.Position.Y
+ + textLayout.LayoutBounds.Height
+ >= 0
+ )
+ {
+ if (_startVisibleLineIndex == -1)
+ {
+ _startVisibleLineIndex = i;
+ }
+ }
+ if (
+ _canvasYScrollTransition.Value
+ + (float)(control.Size.Height / 2)
+ + line.Position.Y
+ + textLayout.LayoutBounds.Height
+ >= control.Size.Height
+ )
+ {
+ if (_endVisibleLineIndex == -1)
+ {
+ _endVisibleLineIndex = i;
+ }
+ }
+ }
+
+ if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
+ {
+ _endVisibleLineIndex = endLineIndex;
+ }
+ }
+
+ ///
+ /// The UpdateFontColor
+ ///
+ private protected void UpdateFontColor()
+ {
+ Color fallback = Colors.Transparent;
+ switch (Theme)
+ {
+ case ElementTheme.Default:
+ switch (Application.Current.RequestedTheme)
+ {
+ case ApplicationTheme.Light:
+ fallback = _darkFontColor;
+ break;
+ case ApplicationTheme.Dark:
+ fallback = _lightFontColor;
+ break;
+ default:
+ break;
+ }
+ break;
+ case ElementTheme.Light:
+ fallback = _darkFontColor;
+ break;
+ case ElementTheme.Dark:
+ fallback = _lightFontColor;
+ break;
+ default:
+ break;
+ }
+
+ switch (LyricsFontColorType)
+ {
+ case Enums.LyricsFontColorType.Default:
+ _fontColor = fallback;
+ break;
+ case Enums.LyricsFontColorType.Dominant:
+ _fontColor = _albumArtAccentColor ?? fallback;
+ break;
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// The UpdateLinesProps
+ ///
+ /// The source
+ /// The defaultOpacity
+ private void UpdateLinesProps()
+ {
+ var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
+
+ int halfVisibleLineCount =
+ Math.Max(
+ currentPlayingLineIndex - _startVisibleLineIndex,
+ _endVisibleLineIndex - currentPlayingLineIndex
+ ) + 1;
+
+ if (halfVisibleLineCount < 1)
+ {
+ return;
+ }
+
+ for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
+ {
+ var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
+
+ if (line == null)
+ {
+ return;
+ }
+
+ int distanceFromPlayingLine = Math.Abs(i - currentPlayingLineIndex);
+ if (distanceFromPlayingLine > halfVisibleLineCount)
+ {
+ return;
+ }
+
+ float distanceZoomFactor = distanceFromPlayingLine / (float)halfVisibleLineCount;
+
+ line.BlurAmountTransition.StartTransition(LyricsBlurAmount * distanceZoomFactor);
+ line.ScaleTransition.StartTransition(
+ _highlightedScale - distanceZoomFactor * (_highlightedScale - _defaultScale)
+ );
+ // Only calculate highlight opacity for the current line and the two lines around it
+ // to avoid unnecessary calculations
+ if (distanceFromPlayingLine <= 1)
+ {
+ line.HighlightOpacityTransition.StartTransition(
+ distanceFromPlayingLine == 0 ? 1 : 0
+ );
+ }
+
+ if (line.ScaleTransition.IsTransitioning)
+ {
+ line.ScaleTransition.Update(ElapsedTime);
+ }
+ if (line.BlurAmountTransition.IsTransitioning)
+ {
+ line.BlurAmountTransition.Update(ElapsedTime);
+ }
+ // Only update highlight opacity for the current line and the two lines around it
+ if (distanceFromPlayingLine <= 1)
+ {
+ if (line.HighlightOpacityTransition.IsTransitioning)
+ {
+ line.HighlightOpacityTransition.Update(ElapsedTime);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Reassigns positions (x,y) to lyrics lines based on the current control size and font size
+ ///
+ ///
+ private void ReLayout(ICanvasAnimatedControl control)
+ {
+ if (control == null)
+ return;
+
+ _textFormat.FontSize = LyricsFontSize;
+
+ float y = _topMargin;
+
+ // Init Positions
+ for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
+ {
+ var line = _multiLangLyrics[_langIndex].SafeGet(i);
+
+ if (line == null)
+ {
+ continue;
+ }
+
+ if (line.CanvasTextLayout != null)
+ {
+ line.CanvasTextLayout.Dispose();
+ line.CanvasTextLayout = null;
+ }
+
+ // Calculate layout bounds
+ line.CanvasTextLayout = new CanvasTextLayout(
+ control,
+ line.Text,
+ _textFormat,
+ (float)_limitedLineWidthTransition.Value,
+ (float)control.Size.Height
+ );
+
+ line.Position = new Vector2(0, y);
+
+ y +=
+ (float)line.CanvasTextLayout.LayoutBounds.Height
+ / line.CanvasTextLayout.LineCount
+ * (line.CanvasTextLayout.LineCount + LyricsLineSpacingFactor);
+ }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Messages.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Messages.cs
new file mode 100644
index 0000000..a610095
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Messages.cs
@@ -0,0 +1,371 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using BetterInAppLyrics.WinUI3.ViewModels;
+using BetterLyrics.WinUI3.Enums;
+using BetterLyrics.WinUI3.Helper;
+using BetterLyrics.WinUI3.Models;
+using CommunityToolkit.Mvvm.Messaging;
+using CommunityToolkit.Mvvm.Messaging.Messages;
+using Microsoft.UI.Xaml;
+using Windows.Graphics.Imaging;
+using Windows.UI;
+
+namespace BetterLyrics.WinUI3.ViewModels
+{
+ public partial class LyricsRendererViewModel
+ : IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>,
+ IRecipient>>,
+ IRecipient>>
+ {
+ ///
+ /// The OnLyricsFontColorTypeChanged
+ ///
+ /// The value
+ partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
+ {
+ UpdateFontColor();
+ }
+
+ ///
+ /// The OnLyricsFontSizeChanged
+ ///
+ /// The value
+ partial void OnLyricsFontSizeChanged(int value)
+ {
+ _isRelayoutNeeded = true;
+ }
+
+ ///
+ /// The OnLyricsFontWeightChanged
+ ///
+ /// The value
+ partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
+ {
+ _textFormat.FontWeight = value.ToFontWeight();
+ }
+
+ ///
+ /// The OnLyricsLineSpacingFactorChanged
+ ///
+ /// The value
+ partial void OnLyricsLineSpacingFactorChanged(float value)
+ {
+ _isRelayoutNeeded = true;
+ }
+
+ ///
+ /// The OnSongInfoChanged
+ ///
+ /// The oldValue
+ /// The newValue
+ async partial void OnSongInfoChanged(SongInfo? oldValue, SongInfo? newValue)
+ {
+ TotalTime = TimeSpan.Zero;
+
+ _lastAlbumArtBitmap = _albumArtBitmap;
+
+ if (newValue?.AlbumArt is byte[] bytes)
+ {
+ _albumArtBitmap = await (
+ await ImageHelper.GetDecoderFromByte(bytes)
+ ).GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
+ _albumArtAccentColor = (
+ await ImageHelper.GetAccentColorsFromByte(bytes)
+ ).FirstOrDefault();
+ }
+ else
+ {
+ _albumArtBitmap = null;
+ _albumArtAccentColor = null;
+ }
+
+ UpdateFontColor();
+
+ _albumArtBgTransition.Reset(0f);
+ _albumArtBgTransition.StartTransition(1f);
+
+ await RefreshLyricsAsync();
+ }
+
+ ///
+ /// The OnThemeChanged
+ ///
+ /// The value
+ partial void OnThemeChanged(ElementTheme value)
+ {
+ UpdateFontColor();
+ }
+
+ // Receive methods for handling messages from other view models
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is SettingsViewModel)
+ {
+ if (message.PropertyName == nameof(SettingsViewModel.IsDynamicCoverOverlayEnabled))
+ {
+ IsDynamicCoverOverlayEnabled = message.NewValue;
+ }
+ else if (message.PropertyName == nameof(SettingsViewModel.IsCoverOverlayEnabled))
+ {
+ IsCoverOverlayEnabled = message.NewValue;
+ }
+ }
+ else if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (
+ message.PropertyName
+ == nameof(LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled)
+ )
+ {
+ IsLyricsGlowEffectEnabled = message.NewValue;
+ }
+ }
+ else if (message.Sender is HostWindowViewModel)
+ {
+ if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode))
+ {
+ IsDockMode = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is HostWindowViewModel)
+ {
+ if (message.PropertyName == nameof(HostWindowViewModel.ActivatedWindowAccentColor))
+ {
+ _immersiveBgTransition.StartTransition(message.NewValue);
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is LyricsPageViewModel)
+ {
+ if (message.PropertyName == nameof(LyricsPageViewModel.LimitedLineWidth))
+ {
+ _limitedLineWidthTransition.StartTransition((float)message.NewValue);
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is SettingsViewModel)
+ {
+ if (message.PropertyName == nameof(SettingsViewModel.ThemeType))
+ {
+ Theme = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (
+ message.PropertyName
+ == nameof(LyricsSettingsControlViewModel.LyricsLineSpacingFactor)
+ )
+ {
+ LyricsLineSpacingFactor = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is SettingsViewModel)
+ {
+ if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
+ {
+ CoverImageRadius = message.NewValue;
+ }
+ else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayOpacity))
+ {
+ CoverOverlayOpacity = message.NewValue;
+ }
+ else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayBlurAmount))
+ {
+ CoverOverlayBlurAmount = message.NewValue;
+ }
+ }
+ else if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (
+ message.PropertyName
+ == nameof(LyricsSettingsControlViewModel.LyricsVerticalEdgeOpacity)
+ )
+ {
+ LyricsVerticalEdgeOpacity = message.NewValue;
+ }
+ else if (
+ message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsBlurAmount)
+ )
+ {
+ LyricsBlurAmount = message.NewValue;
+ }
+ else if (
+ message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize)
+ )
+ {
+ LyricsFontSize = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (
+ message.PropertyName
+ == nameof(LyricsSettingsControlViewModel.LyricsAlignmentType)
+ )
+ {
+ LyricsAlignmentType = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ DisplayType = message.NewValue;
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (
+ message.PropertyName
+ == nameof(LyricsSettingsControlViewModel.LyricsFontColorType)
+ )
+ {
+ LyricsFontColorType = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontWeight))
+ {
+ LyricsFontWeight = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage message)
+ {
+ if (message.Sender is LyricsSettingsControlViewModel)
+ {
+ if (
+ message.PropertyName
+ == nameof(LyricsSettingsControlViewModel.LyricsGlowEffectScope)
+ )
+ {
+ LyricsGlowEffectScope = message.NewValue;
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(PropertyChangedMessage> message)
+ {
+ if (message.Sender is SettingsViewModel)
+ {
+ if (message.PropertyName == nameof(SettingsViewModel.LocalLyricsFolders))
+ {
+ // Music lib changed, re-fetch lyrics
+ RefreshLyricsAsync().ConfigureAwait(true);
+ }
+ }
+ }
+
+ ///
+ /// The Receive
+ ///
+ /// The message
+ public void Receive(
+ PropertyChangedMessage> message
+ )
+ {
+ if (message.Sender is SettingsViewModel)
+ {
+ if (message.PropertyName == nameof(SettingsViewModel.LyricsSearchProvidersInfo))
+ {
+ // Lyrics search providers info changed, re-fetch lyrics
+ RefreshLyricsAsync().ConfigureAwait(true);
+ }
+ }
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Renderer.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Renderer.cs
new file mode 100644
index 0000000..80e7543
--- /dev/null
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Renderer.cs
@@ -0,0 +1,531 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using BetterLyrics.WinUI3.Enums;
+using BetterLyrics.WinUI3.Helper;
+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 Windows.Foundation;
+using Windows.Graphics.Imaging;
+using Windows.UI;
+
+namespace BetterLyrics.WinUI3.ViewModels
+{
+ public partial class LyricsRendererViewModel
+ {
+ ///
+ /// The Draw
+ ///
+ /// The control
+ /// The ds
+ public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
+ {
+ if (IsCoverOverlayEnabled)
+ {
+ DrawAlbumArtBackground(control, ds);
+ }
+
+ if (IsDockMode)
+ {
+ DrawImmersiveBackground(control, ds, IsCoverOverlayEnabled);
+ }
+
+ // Blurred lyrics layer
+ using var blurredLyrics = new CanvasCommandList(control);
+ using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
+ {
+ switch (DisplayType)
+ {
+ case LyricsDisplayType.AlbumArtOnly:
+ case LyricsDisplayType.PlaceholderOnly:
+ break;
+ case LyricsDisplayType.LyricsOnly:
+ case LyricsDisplayType.SplitView:
+ DrawBlurredLyrics(control, blurredLyricsDs);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Masked mock gradient blurred lyrics layer
+ using var maskedBlurredLyrics = new CanvasCommandList(control);
+ using (var maskedBlurredLyricsDs = maskedBlurredLyrics.CreateDrawingSession())
+ {
+ if (LyricsVerticalEdgeOpacity == 100)
+ {
+ maskedBlurredLyricsDs.DrawImage(blurredLyrics);
+ }
+ else
+ {
+ using var mask = new CanvasCommandList(control);
+ using (var maskDs = mask.CreateDrawingSession())
+ {
+ DrawGradientOpacityMask(control, maskDs);
+ }
+ maskedBlurredLyricsDs.DrawImage(
+ new AlphaMaskEffect { Source = blurredLyrics, AlphaMask = mask }
+ );
+ }
+ }
+
+ // For desktop mode
+ //ds.DrawImage(
+ // new ShadowEffect
+ // {
+ // Source = maskedBlurredLyrics,
+ // ShadowColor = Colors.Black,
+ // BlurAmount = 8f,
+ // Optimization = EffectOptimization.Quality,
+ // }
+ //);
+
+ ds.DrawImage(maskedBlurredLyrics);
+
+ var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
+ var currentPlayingLine = _multiLangLyrics
+ .SafeGet(_langIndex)
+ ?.SafeGet(currentPlayingLineIndex);
+ if (currentPlayingLine != null)
+ {
+ GetLinePlayingProgress(
+ currentPlayingLine,
+ out int charStartIndex,
+ out int charLength,
+ out float charProgress
+ );
+ //ds.DrawText(
+ // $"DEBUG: "
+ // + $"播放行 {currentPlayingLineIndex}, 字符 {charStartIndex}, 长度 {charLength}, 进度 {charProgress}\n"
+ // + $"可见行 [{_startVisibleLineIndex}, {_endVisibleLineIndex}]"
+ // + $"当前时刻 {TotalTime}",
+ // new Vector2(10, 10),
+ // Colors.Red
+ //);
+ }
+ }
+
+ ///
+ /// The DrawImgae
+ ///
+ /// The control
+ /// The ds
+ /// The softwareBitmap
+ /// The opacity
+ private static void DrawImgae(
+ ICanvasAnimatedControl control,
+ CanvasDrawingSession ds,
+ SoftwareBitmap softwareBitmap,
+ float opacity
+ )
+ {
+ using var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap);
+ float imageWidth = (float)canvasBitmap.Size.Width;
+ float imageHeight = (float)canvasBitmap.Size.Height;
+
+ 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,
+ },
+ Opacity = opacity,
+ },
+ (float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
+ (float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
+ );
+ }
+
+ ///
+ /// The DrawAlbumArtBackground
+ ///
+ /// The control
+ /// The ds
+ private void DrawAlbumArtBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds)
+ {
+ ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
+
+ var overlappedCovers = new CanvasCommandList(control.Device);
+ using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
+
+ if (_albumArtBgTransition.IsTransitioning)
+ {
+ if (_lastAlbumArtBitmap != null)
+ {
+ DrawImgae(
+ control,
+ overlappedCoversDs,
+ _lastAlbumArtBitmap,
+ 1 - _albumArtBgTransition.Value
+ );
+ }
+ if (_albumArtBitmap != null)
+ {
+ DrawImgae(
+ control,
+ overlappedCoversDs,
+ _albumArtBitmap,
+ _albumArtBgTransition.Value
+ );
+ }
+ }
+ else if (_albumArtBitmap != null)
+ {
+ DrawImgae(control, overlappedCoversDs, _albumArtBitmap, 1f);
+ }
+
+ using var coverOverlayEffect = new OpacityEffect
+ {
+ Opacity = CoverOverlayOpacity / 100f,
+ Source = new GaussianBlurEffect
+ {
+ BlurAmount = CoverOverlayBlurAmount,
+ Source = overlappedCovers,
+ },
+ };
+ ds.DrawImage(coverOverlayEffect);
+
+ ds.Transform = Matrix3x2.Identity;
+ }
+
+ ///
+ /// The DrawGradientOpacityMask
+ ///
+ /// The control
+ /// The ds
+ private void DrawGradientOpacityMask(
+ ICanvasAnimatedControl control,
+ CanvasDrawingSession ds
+ )
+ {
+ byte verticalEdgeAlpha = (byte)(255 * LyricsVerticalEdgeOpacity / 100f);
+ using var maskBrush = new CanvasLinearGradientBrush(
+ control,
+ [
+ new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
+ new() { Position = 0.5f, Color = Color.FromArgb(255, 0, 0, 0) },
+ new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
+ ]
+ )
+ {
+ StartPoint = new Vector2(0, 0),
+ EndPoint = new Vector2(0, (float)control.Size.Height),
+ };
+ ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
+ }
+
+ ///
+ /// The DrawImmersiveBackground
+ ///
+ /// The control
+ /// The ds
+ /// The withGradient
+ private void DrawImmersiveBackground(
+ ICanvasAnimatedControl control,
+ CanvasDrawingSession ds,
+ bool withGradient
+ )
+ {
+ ds.FillRectangle(
+ new Rect(0, 0, control.Size.Width, control.Size.Height),
+ new CanvasLinearGradientBrush(
+ control,
+ [
+ new CanvasGradientStop
+ {
+ Position = 0f,
+ Color = withGradient
+ ? Color.FromArgb(
+ 211,
+ _immersiveBgTransition.Value.R,
+ _immersiveBgTransition.Value.G,
+ _immersiveBgTransition.Value.B
+ )
+ : _immersiveBgTransition.Value,
+ },
+ new CanvasGradientStop
+ {
+ Position = 1,
+ Color = _immersiveBgTransition.Value,
+ },
+ ]
+ )
+ {
+ StartPoint = new Vector2(0, 0),
+ EndPoint = new Vector2(0, (float)control.Size.Height),
+ }
+ );
+ }
+
+ ///
+ /// The DrawLyrics
+ ///
+ /// The control
+ /// The ds
+ /// The currentLineHighlightType
+ private void DrawBlurredLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
+ {
+ var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
+
+ for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
+ {
+ var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
+
+ if (line == null)
+ {
+ continue;
+ }
+
+ var textLayout = line.CanvasTextLayout;
+
+ if (textLayout == null)
+ {
+ continue;
+ }
+
+ var position = new Vector2(line.Position.X, line.Position.Y);
+
+ float layoutWidth = (float)textLayout.LayoutBounds.Width;
+ float layoutHeight = (float)textLayout.LayoutBounds.Height;
+
+ if (layoutWidth <= 0 || layoutHeight <= 0)
+ {
+ continue;
+ }
+
+ float centerX = position.X;
+ float centerY = position.Y + layoutHeight / 2;
+
+ switch (LyricsAlignmentType)
+ {
+ case LyricsAlignmentType.Left:
+ textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
+ break;
+ case LyricsAlignmentType.Center:
+ textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
+ centerX += (float)_limitedLineWidthTransition.Value / 2;
+ break;
+ case LyricsAlignmentType.Right:
+ textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
+ centerX += (float)_limitedLineWidthTransition.Value;
+ break;
+ default:
+ break;
+ }
+
+ float offsetToLeft =
+ (float)control.Size.Width - _rightMargin - _limitedLineWidthTransition.Value;
+
+ // Scale
+ ds.Transform =
+ Matrix3x2.CreateScale(line.ScaleTransition.Value, new Vector2(centerX, centerY))
+ * Matrix3x2.CreateTranslation(
+ offsetToLeft,
+ _canvasYScrollTransition.Value + (float)(control.Size.Height / 2)
+ );
+
+ // Create the original lyrics line
+ using var lyrics = new CanvasCommandList(control.Device);
+ using var lyricsDs = lyrics.CreateDrawingSession();
+ lyricsDs.DrawTextLayout(textLayout, position, _fontColor);
+
+ // Mock gradient blurred lyrics layer
+ // 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层
+ // Current line will not be blurred
+ ds.DrawImage(
+ new GaussianBlurEffect
+ {
+ Source = new OpacityEffect { Source = lyrics, Opacity = _defaultOpacity },
+ BlurAmount = line.BlurAmountTransition.Value,
+ Optimization = EffectOptimization.Quality,
+ BorderMode = EffectBorderMode.Soft,
+ }
+ );
+
+ // 再叠加当前行歌词层
+ // Only draw the current line and the two lines around it
+ // This layer is to highlight the current line
+ // and for fade-in and fade-out effects, two lines around it is also drawn
+ if (Math.Abs(i - currentPlayingLineIndex) <= 1)
+ {
+ using var mask = new CanvasCommandList(control.Device);
+ using var maskDs = mask.CreateDrawingSession();
+
+ using var highlightMask = new CanvasCommandList(control.Device);
+ using var highlightMaskDs = highlightMask.CreateDrawingSession();
+
+ if (i == currentPlayingLineIndex)
+ {
+ GetLinePlayingProgress(
+ line,
+ out int charStartIndex,
+ out int charLength,
+ out float charProgress
+ );
+ var regions = textLayout.GetCharacterRegions(0, charStartIndex);
+ var highlightRegion = textLayout
+ .GetCharacterRegions(charStartIndex, charLength)
+ .FirstOrDefault();
+ if (regions.Length > 0)
+ {
+ // Draw the mask for the current line
+ for (int j = 0; j < regions.Length; j++)
+ {
+ var region = regions[j];
+ var rect = new Rect(
+ region.LayoutBounds.X,
+ region.LayoutBounds.Y + position.Y,
+ region.LayoutBounds.Width,
+ region.LayoutBounds.Height
+ );
+ maskDs.FillRectangle(rect, Colors.Black);
+ }
+ }
+
+ float highlightTotalWidth = (float)highlightRegion.LayoutBounds.Width;
+ // Draw the highlight for the current character
+ float highlightWidth = highlightTotalWidth * charProgress;
+
+ float fadingWidth = (float)highlightRegion.LayoutBounds.Height / 2;
+
+ // Rects
+ var highlightRect = new Rect(
+ highlightRegion.LayoutBounds.X,
+ highlightRegion.LayoutBounds.Y + position.Y,
+ highlightWidth,
+ highlightRegion.LayoutBounds.Height
+ );
+
+ var fadeInRect = new Rect(
+ highlightRect.Right - fadingWidth,
+ highlightRegion.LayoutBounds.Y + position.Y,
+ fadingWidth,
+ highlightRegion.LayoutBounds.Height
+ );
+ var fadeOutRect = new Rect(
+ highlightRect.Right,
+ highlightRegion.LayoutBounds.Y + position.Y,
+ fadingWidth,
+ highlightRegion.LayoutBounds.Height
+ );
+
+ // Brushes
+ using var fadeInBrush = GetHorizontalFillBrush(
+ control,
+ [(0f, 0f), (1f, 1f)],
+ (float)highlightRect.Right - fadingWidth,
+ fadingWidth
+ );
+ using var fadeOutBrush = GetHorizontalFillBrush(
+ control,
+ [(0f, 1f), (1f, 0f)],
+ (float)highlightRect.Right,
+ fadingWidth
+ );
+
+ maskDs.FillRectangle(highlightRect, Colors.White);
+ maskDs.FillRectangle(fadeOutRect, fadeOutBrush);
+
+ highlightMaskDs.FillRectangle(fadeInRect, fadeInBrush);
+ highlightMaskDs.FillRectangle(fadeOutRect, fadeOutBrush);
+ }
+ else
+ {
+ maskDs.FillRectangle(
+ new Rect(
+ textLayout.LayoutBounds.X,
+ position.Y,
+ textLayout.LayoutBounds.Width,
+ textLayout.LayoutBounds.Height
+ ),
+ Colors.White
+ );
+ }
+
+ ds.DrawImage(
+ new OpacityEffect
+ {
+ Source = new BlendEffect
+ {
+ Background = IsLyricsGlowEffectEnabled
+ ? new GaussianBlurEffect
+ {
+ Source = new AlphaMaskEffect
+ {
+ Source = lyrics,
+ AlphaMask = LyricsGlowEffectScope switch
+ {
+ LineRenderingType.UntilCurrentChar => mask,
+ LineRenderingType.CurrentCharOnly => highlightMask,
+ _ => mask,
+ },
+ },
+ BlurAmount = _lyricsGlowEffectAmount,
+ Optimization = EffectOptimization.Quality,
+ BorderMode = EffectBorderMode.Soft,
+ }
+ : new CanvasCommandList(control.Device),
+ Foreground = new AlphaMaskEffect
+ {
+ Source = lyrics,
+ AlphaMask = mask,
+ },
+ },
+ Opacity = line.HighlightOpacityTransition.Value,
+ }
+ );
+ }
+
+ // Reset scale
+ ds.Transform = Matrix3x2.Identity;
+ }
+ }
+
+ ///
+ /// The GetHorizontalFillBrush
+ ///
+ /// The control
+ /// The stopPosition
+ /// The stopOpacity
+ /// The startX
+ /// The endX
+ /// The
+ private CanvasLinearGradientBrush GetHorizontalFillBrush(
+ ICanvasAnimatedControl control,
+ List<(float position, float opacity)> stops,
+ float startX,
+ float width
+ )
+ {
+ return new CanvasLinearGradientBrush(
+ control,
+ stops
+ .Select(stops => new CanvasGradientStop
+ {
+ Position = stops.position,
+ Color = Color.FromArgb((byte)(stops.opacity * 255), 0, 0, 0),
+ })
+ .ToArray()
+ )
+ {
+ StartPoint = new Vector2(startX, 0),
+ EndPoint = new Vector2(startX + width, 0),
+ };
+ }
+ }
+}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.cs
index 8fb04fe..fb9ab3f 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.cs
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.cs
@@ -32,21 +32,7 @@ namespace BetterLyrics.WinUI3.ViewModels
///
/// Defines the
///
- public partial class LyricsRendererViewModel
- : BaseViewModel,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>,
- IRecipient>>,
- IRecipient>>
+ public partial class LyricsRendererViewModel : BaseViewModel
{
#region Fields
@@ -80,7 +66,7 @@ namespace BetterLyrics.WinUI3.ViewModels
///
/// Defines the _defaultScale
///
- private readonly float _defaultScale = 0.95f;
+ private readonly float _defaultScale = 0.75f;
///
/// Defines the _highlightedOpacity
@@ -95,7 +81,7 @@ namespace BetterLyrics.WinUI3.ViewModels
///
/// Defines the _immersiveBgrTransition
///
- private readonly ValueTransition _immersiveBgrTransition = new(
+ private readonly ValueTransition _immersiveBgTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) =>
@@ -116,16 +102,6 @@ namespace BetterLyrics.WinUI3.ViewModels
interpolator: (from, to, progress) => to
);
- ///
- /// Defines the _lineEnteringDurationMs
- ///
- private readonly int _lineEnteringDurationMs = 800;
-
- ///
- /// Defines the _lineExitingDurationMs
- ///
- private readonly int _lineExitingDurationMs = 800;
-
///
/// Defines the _lyricsGlowEffectAmount
///
@@ -399,872 +375,6 @@ namespace BetterLyrics.WinUI3.ViewModels
#region Methods
- ///
- /// The Draw
- ///
- /// The control
- /// The ds
- public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
- {
- if (IsCoverOverlayEnabled)
- {
- DrawAlbumArtBackground(control, ds);
- }
-
- if (IsDockMode)
- {
- DrawImmersiveBackground(control, ds, IsCoverOverlayEnabled);
- }
-
- // Original lyrics only layer
- using var blurredLyrics = new CanvasCommandList(control);
- using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
- {
- switch (DisplayType)
- {
- case LyricsDisplayType.AlbumArtOnly:
- case LyricsDisplayType.PlaceholderOnly:
- break;
- case LyricsDisplayType.LyricsOnly:
- case LyricsDisplayType.SplitView:
- DrawLyrics(control, blurredLyricsDs, LineRenderingType.UntilCurrentChar);
- break;
- default:
- break;
- }
- }
-
- // Masked mock gradient blurred lyrics layer
- using var maskedBlurredLyrics = new CanvasCommandList(control);
- using (var maskedBlurredLyricsDs = maskedBlurredLyrics.CreateDrawingSession())
- {
- if (LyricsVerticalEdgeOpacity == 100)
- {
- maskedBlurredLyricsDs.DrawImage(blurredLyrics);
- }
- else
- {
- using var mask = new CanvasCommandList(control);
- using (var maskDs = mask.CreateDrawingSession())
- {
- DrawGradientOpacityMask(control, maskDs);
- }
- maskedBlurredLyricsDs.DrawImage(
- new AlphaMaskEffect { Source = blurredLyrics, AlphaMask = mask }
- );
- }
- }
-
- // Draw the final composed layer
- ds.DrawImage(maskedBlurredLyrics);
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(
- PropertyChangedMessage> message
- )
- {
- if (message.Sender is SettingsViewModel)
- {
- if (message.PropertyName == nameof(SettingsViewModel.LyricsSearchProvidersInfo))
- {
- // Lyrics search providers info changed, re-fetch lyrics
- RefreshLyricsAsync().ConfigureAwait(true);
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is SettingsViewModel)
- {
- if (message.PropertyName == nameof(SettingsViewModel.IsDynamicCoverOverlayEnabled))
- {
- IsDynamicCoverOverlayEnabled = message.NewValue;
- }
- else if (message.PropertyName == nameof(SettingsViewModel.IsCoverOverlayEnabled))
- {
- IsCoverOverlayEnabled = message.NewValue;
- }
- }
- else if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (
- message.PropertyName
- == nameof(LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled)
- )
- {
- IsLyricsGlowEffectEnabled = message.NewValue;
- }
- }
- else if (message.Sender is HostWindowViewModel)
- {
- if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode))
- {
- IsDockMode = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is HostWindowViewModel)
- {
- if (message.PropertyName == nameof(HostWindowViewModel.ActivatedWindowAccentColor))
- {
- _immersiveBgrTransition.StartTransition(message.NewValue);
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is LyricsPageViewModel)
- {
- if (message.PropertyName == nameof(LyricsPageViewModel.LimitedLineWidth))
- {
- _limitedLineWidthTransition.StartTransition((float)message.NewValue);
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is SettingsViewModel)
- {
- if (message.PropertyName == nameof(SettingsViewModel.ThemeType))
- {
- Theme = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (
- message.PropertyName
- == nameof(LyricsSettingsControlViewModel.LyricsLineSpacingFactor)
- )
- {
- LyricsLineSpacingFactor = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is SettingsViewModel)
- {
- if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
- {
- CoverImageRadius = message.NewValue;
- }
- else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayOpacity))
- {
- CoverOverlayOpacity = message.NewValue;
- }
- else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayBlurAmount))
- {
- CoverOverlayBlurAmount = message.NewValue;
- }
- }
- else if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (
- message.PropertyName
- == nameof(LyricsSettingsControlViewModel.LyricsVerticalEdgeOpacity)
- )
- {
- LyricsVerticalEdgeOpacity = message.NewValue;
- }
- else if (
- message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsBlurAmount)
- )
- {
- LyricsBlurAmount = message.NewValue;
- }
- else if (
- message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize)
- )
- {
- LyricsFontSize = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (
- message.PropertyName
- == nameof(LyricsSettingsControlViewModel.LyricsAlignmentType)
- )
- {
- LyricsAlignmentType = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- DisplayType = message.NewValue;
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (
- message.PropertyName
- == nameof(LyricsSettingsControlViewModel.LyricsFontColorType)
- )
- {
- LyricsFontColorType = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontWeight))
- {
- LyricsFontWeight = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage message)
- {
- if (message.Sender is LyricsSettingsControlViewModel)
- {
- if (
- message.PropertyName
- == nameof(LyricsSettingsControlViewModel.LyricsGlowEffectScope)
- )
- {
- LyricsGlowEffectScope = message.NewValue;
- }
- }
- }
-
- ///
- /// The Receive
- ///
- /// The message
- public void Receive(PropertyChangedMessage> message)
- {
- if (message.Sender is SettingsViewModel)
- {
- if (message.PropertyName == nameof(SettingsViewModel.LocalLyricsFolders))
- {
- // Music lib changed, re-fetch lyrics
- RefreshLyricsAsync().ConfigureAwait(true);
- }
- }
- }
-
- ///
- /// The Update
- ///
- /// The control
- /// The args
- public void Update(ICanvasAnimatedControl control, CanvasAnimatedUpdateEventArgs args)
- {
- if (_isPlaying)
- {
- TotalTime += args.Timing.ElapsedTime;
- }
-
- ElapsedTime = args.Timing.ElapsedTime;
-
- if (_immersiveBgrTransition.IsTransitioning)
- {
- _immersiveBgrTransition.Update(ElapsedTime);
- }
-
- if (_albumArtBgTransition.IsTransitioning)
- {
- _albumArtBgTransition.Update(ElapsedTime);
- }
-
- if (IsDynamicCoverOverlayEnabled)
- {
- _rotateAngle += _coverRotateSpeed;
- _rotateAngle %= MathF.PI * 2;
- }
-
- if (_limitedLineWidthTransition.IsTransitioning)
- {
- _limitedLineWidthTransition.Update(ElapsedTime);
- _isRelayoutNeeded = true;
- }
-
- if (_isRelayoutNeeded)
- {
- ReLayout(control);
- _isRelayoutNeeded = false;
- }
-
- UpdateLinesProps();
- UpdateCanvasYScrollOffset(control);
- }
-
- ///
- /// The DrawImgae
- ///
- /// The control
- /// The ds
- /// The softwareBitmap
- /// The opacity
- private static void DrawImgae(
- ICanvasAnimatedControl control,
- CanvasDrawingSession ds,
- SoftwareBitmap softwareBitmap,
- float opacity
- )
- {
- using var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap);
- float imageWidth = (float)canvasBitmap.Size.Width;
- float imageHeight = (float)canvasBitmap.Size.Height;
-
- 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,
- },
- Opacity = opacity,
- },
- (float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
- (float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
- );
- }
-
- ///
- /// The DrawAlbumArtBackground
- ///
- /// The control
- /// The ds
- private void DrawAlbumArtBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds)
- {
- ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
-
- var overlappedCovers = new CanvasCommandList(control.Device);
- using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
-
- if (_albumArtBgTransition.IsTransitioning)
- {
- if (_lastAlbumArtBitmap != null)
- {
- DrawImgae(
- control,
- overlappedCoversDs,
- _lastAlbumArtBitmap,
- 1 - _albumArtBgTransition.Value
- );
- }
- if (_albumArtBitmap != null)
- {
- DrawImgae(
- control,
- overlappedCoversDs,
- _albumArtBitmap,
- _albumArtBgTransition.Value
- );
- }
- }
- else if (_albumArtBitmap != null)
- {
- DrawImgae(control, overlappedCoversDs, _albumArtBitmap, 1f);
- }
-
- using var coverOverlayEffect = new OpacityEffect
- {
- Opacity = CoverOverlayOpacity / 100f,
- Source = new GaussianBlurEffect
- {
- BlurAmount = CoverOverlayBlurAmount,
- Source = overlappedCovers,
- },
- };
- ds.DrawImage(coverOverlayEffect);
-
- ds.Transform = Matrix3x2.Identity;
- }
-
- ///
- /// The DrawGradientOpacityMask
- ///
- /// The control
- /// The ds
- private void DrawGradientOpacityMask(
- ICanvasAnimatedControl control,
- CanvasDrawingSession ds
- )
- {
- byte verticalEdgeAlpha = (byte)(255 * LyricsVerticalEdgeOpacity / 100f);
- using var maskBrush = new CanvasLinearGradientBrush(
- control,
- [
- new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
- new() { Position = 0.5f, Color = Color.FromArgb(255, 0, 0, 0) },
- new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
- ]
- )
- {
- StartPoint = new Vector2(0, 0),
- EndPoint = new Vector2(0, (float)control.Size.Height),
- };
- ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
- }
-
- ///
- /// The DrawImmersiveBackground
- ///
- /// The control
- /// The ds
- /// The withGradient
- private void DrawImmersiveBackground(
- ICanvasAnimatedControl control,
- CanvasDrawingSession ds,
- bool withGradient
- )
- {
- ds.FillRectangle(
- new Rect(0, 0, control.Size.Width, control.Size.Height),
- new CanvasLinearGradientBrush(
- control,
- [
- new CanvasGradientStop
- {
- Position = 0f,
- Color = withGradient
- ? Color.FromArgb(
- 211,
- _immersiveBgrTransition.Value.R,
- _immersiveBgrTransition.Value.G,
- _immersiveBgrTransition.Value.B
- )
- : _immersiveBgrTransition.Value,
- },
- new CanvasGradientStop
- {
- Position = 1,
- Color = _immersiveBgrTransition.Value,
- },
- ]
- )
- {
- StartPoint = new Vector2(0, 0),
- EndPoint = new Vector2(0, (float)control.Size.Height),
- }
- );
- }
-
- ///
- /// The DrawLyrics
- ///
- /// The control
- /// The ds
- /// The currentLineHighlightType
- private void DrawLyrics(
- ICanvasAnimatedControl control,
- CanvasDrawingSession ds,
- LineRenderingType currentLineHighlightType
- )
- {
- var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
-
- for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
- {
- var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
-
- if (line == null)
- {
- continue;
- }
-
- var textLayout = line.CanvasTextLayout;
-
- if (textLayout == null)
- {
- continue;
- }
-
- var position = new Vector2(line.Position.X, line.Position.Y);
-
- float layoutWidth = (float)textLayout.LayoutBounds.Width;
- float layoutHeight = (float)textLayout.LayoutBounds.Height;
-
- if (layoutWidth <= 0 || layoutHeight <= 0)
- {
- continue;
- }
-
- float centerX = position.X;
- float centerY = position.Y + layoutHeight / 2;
-
- switch (LyricsAlignmentType)
- {
- case LyricsAlignmentType.Left:
- textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
- break;
- case LyricsAlignmentType.Center:
- textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
- centerX += (float)_limitedLineWidthTransition.Value / 2;
- break;
- case LyricsAlignmentType.Right:
- textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
- centerX += (float)_limitedLineWidthTransition.Value;
- break;
- default:
- break;
- }
-
- float offsetToLeft =
- (float)control.Size.Width - _rightMargin - _limitedLineWidthTransition.Value;
-
- // Scale
- ds.Transform =
- Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY))
- * Matrix3x2.CreateTranslation(
- offsetToLeft,
- _canvasYScrollTransition.Value + (float)(control.Size.Height / 2)
- );
-
- // Create the original lyrics line
- using var pureLyricsLine = new CanvasCommandList(control);
- using (var pureLyricsLineDs = pureLyricsLine.CreateDrawingSession())
- {
- pureLyricsLineDs.DrawTextLayout(textLayout, position, _fontColor);
- }
-
- using var glowedLyrics = new CanvasCommandList(control);
- using (var lyricsDs = glowedLyrics.CreateDrawingSession())
- {
- // Create and draw glow (shadow) effect
- if (IsLyricsGlowEffectEnabled)
- {
- lyricsDs.DrawImage(
- new ShadowEffect
- {
- Source = new AlphaMaskEffect
- {
- Source = pureLyricsLine,
- AlphaMask = CreateLineMask(
- control,
- line,
- LyricsGlowEffectScope,
- false
- ),
- },
- BlurAmount = _lyricsGlowEffectAmount,
- ShadowColor = _fontColor,
- Optimization = EffectOptimization.Quality,
- }
- );
- }
-
- // Create and draw highlight (opacity changed) effect
- lyricsDs.DrawImage(
- new AlphaMaskEffect
- {
- Source = pureLyricsLine,
- AlphaMask = CreateLineMask(
- control,
- line,
- LineRenderingType.UntilCurrentChar,
- true
- ),
- }
- );
- }
-
- // Mock gradient blurred lyrics layer
- using var blurredLyrics = new CanvasCommandList(control);
- using var blurredLyricsDs = blurredLyrics.CreateDrawingSession();
- if (LyricsBlurAmount == 0)
- {
- blurredLyricsDs.DrawImage(glowedLyrics);
- }
- else
- {
- int visibleLineCount = _endVisibleLineIndex - _startVisibleLineIndex + 1;
- int distanceFromPlayingLine = Math.Abs(i - currentPlayingLineIndex);
-
- line.BlurAmountTransition.StartTransition(
- LyricsBlurAmount * (distanceFromPlayingLine / (visibleLineCount / 2f))
- );
- if (line.BlurAmountTransition.IsTransitioning)
- {
- line.BlurAmountTransition.Update(ElapsedTime);
- }
- blurredLyricsDs.DrawImage(
- new GaussianBlurEffect
- {
- Source = glowedLyrics,
- BlurAmount = line.BlurAmountTransition.Value,
- Optimization = EffectOptimization.Quality,
- BorderMode = EffectBorderMode.Hard,
- }
- );
- }
-
- ds.DrawImage(blurredLyrics);
-
- // Reset scale
- ds.Transform = Matrix3x2.Identity;
- }
- }
-
- private CanvasCommandList CreateLineMask(
- ICanvasAnimatedControl control,
- LyricsLine line,
- LineRenderingType lineMaskType,
- bool isUnhighlightedAreaVisible
- )
- {
- var alphaMask = new CanvasCommandList(control);
-
- var textLayout = line.CanvasTextLayout;
- if (textLayout == null)
- {
- return alphaMask;
- }
-
- using (var ds = alphaMask.CreateDrawingSession())
- {
- // Current playing char index
- int charIndex = (int)(line.PlayingProgress * line.Text.Length);
- int totalCharCountBefore = 0;
- foreach (var lineMatrix in textLayout.LineMetrics)
- {
- int lineCharCount = lineMatrix.CharacterCount;
-
- var region = textLayout
- .GetCharacterRegions(totalCharCountBefore, lineCharCount)
- .FirstOrDefault();
-
- var lineWidth = region.LayoutBounds.Width;
- var lineHeight = region.LayoutBounds.Height;
-
- var lineLeft = (float)region.LayoutBounds.X;
- var lineTop = (float)region.LayoutBounds.Y + line.Position.Y;
- var lineRight = lineLeft + lineWidth;
-
- if (
- totalCharCountBefore <= charIndex
- && charIndex < totalCharCountBefore + lineCharCount
- )
- {
- var currentRegion = textLayout
- .GetCharacterRegions(charIndex, 1)
- .FirstOrDefault();
- var charPlayingProgress =
- line.PlayingProgress * line.Text.Length - charIndex;
-
- // 确保小于右边距
- float fadeSpacing = 18f;
-
- float currentCharWidth = (float)currentRegion.LayoutBounds.Width;
-
- float currentPlayingX = 0f;
-
- // 行首行尾增加 fadeSpacing 以完成完整的淡入淡出效果
- if (region.LayoutBounds.Left == currentRegion.LayoutBounds.Left)
- {
- currentCharWidth += fadeSpacing;
- currentPlayingX =
- lineLeft - fadeSpacing + currentCharWidth * charPlayingProgress;
- }
- else if (region.LayoutBounds.Right == currentRegion.LayoutBounds.Right)
- {
- currentCharWidth += fadeSpacing;
- currentPlayingX =
- (float)currentRegion.LayoutBounds.Left
- + currentCharWidth * charPlayingProgress;
- }
- else
- {
- currentPlayingX =
- (float)currentRegion.LayoutBounds.Left
- + currentCharWidth * charPlayingProgress;
- }
-
- float beforeFadeInX = lineLeft - fadeSpacing * 2;
-
- float beforePlayingOpacity = lineMaskType switch
- {
- LineRenderingType.UntilCurrentChar => line.Opacity,
- LineRenderingType.CurrentCharOnly => isUnhighlightedAreaVisible
- ? _defaultOpacity
- : 0,
- _ => line.Opacity,
- };
-
- // 画当前字符淡入之前部分(已播放)
- ds?.FillRectangle(
- new Rect(
- beforeFadeInX,
- lineTop,
- currentPlayingX - fadeSpacing - beforeFadeInX,
- lineHeight
- ),
- Color.FromArgb((byte)(255 * beforePlayingOpacity), 200, 0, 0)
- );
-
- float fadeInStartX = currentPlayingX - fadeSpacing;
- float fadeInEndX = currentPlayingX;
-
- // 画正处在高亮字符之前 fadeSpaing 距离的渐变部分(淡入)
- ds?.FillRectangle(
- new Rect(fadeInStartX, lineTop, fadeSpacing, lineHeight),
- GetHorizontalFillBrush(
- control,
- [(0f, beforePlayingOpacity), (1f, line.Opacity)],
- fadeInStartX,
- fadeInEndX
- )
- );
-
- float afterPlayingOpacity = isUnhighlightedAreaVisible
- ? _defaultOpacity
- : 0;
-
- float fadeOutStartX = currentPlayingX;
- float fadeOutEndX = fadeOutStartX + fadeSpacing;
-
- // 画正处在高亮之后 fadeSpaing 距离的渐变部分(淡出)
- ds?.FillRectangle(
- new Rect(fadeOutStartX, lineTop, fadeSpacing, lineHeight),
- GetHorizontalFillBrush(
- control,
- [(0f, line.Opacity), (1f, afterPlayingOpacity)],
- fadeOutStartX,
- fadeOutEndX
- )
- );
-
- // 画渐变之后透明度为 _defaultOpacity 的部分(未播放)
- ds?.FillRectangle(
- new Rect(
- fadeOutEndX,
- lineTop,
- lineRight + fadeSpacing * 2 - fadeOutEndX,
- lineHeight
- ),
- Color.FromArgb((byte)(255 * afterPlayingOpacity), 0, 200, 0)
- );
- }
- else
- {
- if (charIndex < totalCharCountBefore)
- {
- // 当前子行未播放
- float opacity = isUnhighlightedAreaVisible ? _defaultOpacity : 0;
- ds?.FillRectangle(
- new Rect(lineLeft, lineTop, lineWidth, lineHeight),
- Color.FromArgb((byte)(255 * opacity), 0, 200, 0)
- );
- }
- else
- {
- // 当前子行已完全播放
- float opacity = lineMaskType switch
- {
- LineRenderingType.UntilCurrentChar => line.Opacity,
- LineRenderingType.CurrentCharOnly => _defaultOpacity,
- _ => line.Opacity,
- };
- if (!isUnhighlightedAreaVisible)
- {
- switch (lineMaskType)
- {
- case LineRenderingType.UntilCurrentChar:
- opacity *=
- (line.Opacity - _defaultOpacity)
- / (_highlightedOpacity - _defaultOpacity);
- break;
- case LineRenderingType.CurrentCharOnly:
- opacity = 0;
- break;
- default:
- break;
- }
- }
- ds?.FillRectangle(
- new Rect(lineLeft, lineTop, lineWidth, lineHeight),
- Color.FromArgb((byte)(255 * opacity), 200, 0, 0)
- );
- }
- }
- totalCharCountBefore += lineCharCount;
- }
- }
-
- return alphaMask;
- }
-
///
/// The GetCurrentPlayingLineIndex
///
@@ -1274,110 +384,85 @@ namespace BetterLyrics.WinUI3.ViewModels
for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?[i];
- if (line?.EndMs < TotalTime.TotalMilliseconds)
+ if (line == null)
{
continue;
}
- return i;
+ if (
+ line.StartMs <= TotalTime.TotalMilliseconds
+ && TotalTime.TotalMilliseconds <= line.EndMs
+ )
+ {
+ return i;
+ }
}
return -1;
}
- ///
- /// The GetHorizontalFillBrush
- ///
- /// The control
- /// The stopPosition
- /// The stopOpacity
- /// The startX
- /// The endX
- /// The
- private CanvasLinearGradientBrush GetHorizontalFillBrush(
- ICanvasAnimatedControl control,
- List<(float position, float opacity)> stops,
- float startX,
- float endX
- )
- {
- var r = _fontColor.R;
- var g = _fontColor.G;
- var b = _fontColor.B;
-
- return new CanvasLinearGradientBrush(
- control,
- stops
- .Select(stops => new CanvasGradientStop
- {
- Position = stops.position,
- Color = Color.FromArgb((byte)(stops.opacity * 255), r, g, b),
- })
- .ToArray()
- )
- {
- StartPoint = new Vector2(startX, 0),
- EndPoint = new Vector2(endX, 0),
- };
- }
-
///
/// The GetLinePlayingProgress
///
/// The line
/// The
- private float GetLinePlayingProgress(LyricsLine line)
+ private void GetLinePlayingProgress(
+ LyricsLine line,
+ out int charStartIndex,
+ out int charLength,
+ out float charProgress
+ )
{
- float playProgress = 0f;
- int now = (int)TotalTime.TotalMilliseconds;
+ charStartIndex = 0;
+ charLength = 0;
+ charProgress = 0f;
+ float now = (float)TotalTime.TotalMilliseconds;
+
+ // 1. 还没到本句
+ if (now < line.StartMs)
+ {
+ return;
+ }
+
+ // 2. 已经超过本句
+ if (now > line.EndMs)
+ {
+ return;
+ }
+
+ // 3. 有逐字时间轴
if (line.CharTimings != null && line.CharTimings.Count > 0)
{
- int charIndex = 0;
- for (; charIndex < line.CharTimings.Count; charIndex++)
+ int charTimingsCount = line.CharTimings.Count;
+ for (int i = 0; i < charTimingsCount; i++)
{
- var timing = line.CharTimings[charIndex];
- if (now < timing.StartMs)
- {
- // 当前时间还没到这个字,停在上一个字
- break;
- }
+ var timing = line.CharTimings[i];
+
+ // 当前时间在某个字的高亮区间
if (now >= timing.StartMs && now <= timing.EndMs)
{
- float charProgress = 1f;
+ charStartIndex = timing.StartIndex;
+ charLength = timing.Text.Length;
if (timing.EndMs != timing.StartMs)
{
- charProgress =
- (now - timing.StartMs) / (float)(timing.EndMs - timing.StartMs);
+ charProgress = (now - timing.StartMs) / (timing.EndMs - timing.StartMs);
}
- // 当前时间在这个字的高亮区间
- playProgress = charIndex + charProgress;
- playProgress /= line.CharTimings.Count;
- return playProgress;
+ else
+ {
+ charProgress = 0f;
+ }
+ return;
}
}
- // 如果超出最后一个字的结束时间
- if (now > line.CharTimings[^1].EndMs)
- {
- // 如果还没到行尾,保持最后一个字高亮
- if (now < line.EndMs)
- {
- playProgress = 1f; // 全部字高亮
- }
- else
- {
- playProgress = 1f; // 行已结束
- }
- }
- else if (charIndex == 0)
- {
- playProgress = 0f; // 还没到第一个字
- }
}
else
{
- playProgress = (now - line.StartMs) / (float)(line.DurationMs);
+ // 没有逐字时间轴,直接线性
+ charProgress = (now - line.StartMs) / line.DurationMs;
+ charProgress = Math.Clamp(charProgress, 0f, 1f);
+ charStartIndex = 0;
+ charLength = line.Text.Length;
}
- return playProgress;
}
///
@@ -1398,16 +483,6 @@ namespace BetterLyrics.WinUI3.ViewModels
return new Tuple(0, _multiLangLyrics[_langIndex].Count - 1);
}
- ///
- /// The GetVisibleLyricsLineIndexBoundaries
- ///
- /// The
- private Tuple GetVisibleLyricsLineIndexBoundaries()
- {
- // _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
- return new Tuple(_startVisibleLineIndex, _endVisibleLineIndex);
- }
-
///
/// The LibWatcherService_MusicLibraryFilesChanged
///
@@ -1415,7 +490,7 @@ namespace BetterLyrics.WinUI3.ViewModels
/// The e
private void LibWatcherService_MusicLibraryFilesChanged(
object? sender,
- Events.LibChangedEventArgs e
+ LibChangedEventArgs e
)
{
RefreshLyricsAsync().ConfigureAwait(true);
@@ -1438,7 +513,8 @@ namespace BetterLyrics.WinUI3.ViewModels
/// The e
private void PlaybackService_PositionChanged(object? sender, PositionChangedEventArgs e)
{
- TotalTime = e.Position;
+ if (Math.Abs(TotalTime.TotalMilliseconds - e.Position.TotalMilliseconds) > 100)
+ TotalTime = e.Position;
}
///
@@ -1494,370 +570,6 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
- ///
- /// Reassigns positions (x,y) to lyrics lines based on the current control size and font size
- ///
- ///
- private void ReLayout(ICanvasAnimatedControl control)
- {
- if (control == null)
- return;
-
- _textFormat.FontSize = LyricsFontSize;
-
- float y = _topMargin;
-
- // Init Positions
- for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
- {
- var line = _multiLangLyrics[_langIndex].SafeGet(i);
-
- if (line == null)
- {
- continue;
- }
-
- if (line.CanvasTextLayout != null)
- {
- line.CanvasTextLayout.Dispose();
- line.CanvasTextLayout = null;
- }
-
- // Calculate layout bounds
- line.CanvasTextLayout = new CanvasTextLayout(
- control,
- line.Text,
- _textFormat,
- (float)_limitedLineWidthTransition.Value,
- (float)control.Size.Height
- );
-
- line.Position = new Vector2(0, y);
-
- y +=
- (float)line.CanvasTextLayout.LayoutBounds.Height
- / line.CanvasTextLayout.LineCount
- * (line.CanvasTextLayout.LineCount + LyricsLineSpacingFactor);
- }
- }
-
- ///
- /// The UpdateCanvasYScrollOffset
- ///
- /// The control
- private void UpdateCanvasYScrollOffset(ICanvasAnimatedControl control)
- {
- var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
-
- var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
-
- if (startLineIndex < 0 || endLineIndex < 0)
- {
- return;
- }
-
- // Set _scrollOffsetY
- LyricsLine? currentPlayingLine = _multiLangLyrics
- .SafeGet(_langIndex)
- ?.SafeGet(currentPlayingLineIndex);
-
- var playingTextLayout = currentPlayingLine?.CanvasTextLayout;
-
- if (currentPlayingLine == null || playingTextLayout == null)
- {
- return;
- }
-
- float targetYScrollOffset =
- (float?)(
- -currentPlayingLine.Position.Y
- + _multiLangLyrics.SafeGet(_langIndex)?[0].Position.Y
- - playingTextLayout.LayoutBounds.Height / 2
- ) ?? 0f;
-
- if (!_canvasYScrollTransition.IsTransitioning)
- {
- _canvasYScrollTransition.StartTransition(targetYScrollOffset);
- }
-
- if (_canvasYScrollTransition.IsTransitioning)
- {
- _canvasYScrollTransition.Update(ElapsedTime);
- }
-
- _startVisibleLineIndex = _endVisibleLineIndex = -1;
-
- // Update visible line indices
- for (int i = startLineIndex; i <= endLineIndex; i++)
- {
- var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
-
- if (line == null || line.CanvasTextLayout == null)
- {
- continue;
- }
-
- var textLayout = line.CanvasTextLayout;
-
- if (
- _canvasYScrollTransition.Value
- + (float)(control.Size.Height / 2)
- + line.Position.Y
- + textLayout.LayoutBounds.Height
- >= 0
- )
- {
- if (_startVisibleLineIndex == -1)
- {
- _startVisibleLineIndex = i;
- }
- }
- if (
- _canvasYScrollTransition.Value
- + (float)(control.Size.Height / 2)
- + line.Position.Y
- + textLayout.LayoutBounds.Height
- >= control.Size.Height
- )
- {
- if (_endVisibleLineIndex == -1)
- {
- _endVisibleLineIndex = i;
- }
- }
- }
-
- if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
- {
- _endVisibleLineIndex = endLineIndex;
- }
- }
-
- ///
- /// The UpdateFontColor
- ///
- private protected void UpdateFontColor()
- {
- Color fallback = Colors.Transparent;
- switch (Theme)
- {
- case ElementTheme.Default:
- switch (Application.Current.RequestedTheme)
- {
- case ApplicationTheme.Light:
- fallback = _darkFontColor;
- break;
- case ApplicationTheme.Dark:
- fallback = _lightFontColor;
- break;
- default:
- break;
- }
- break;
- case ElementTheme.Light:
- fallback = _darkFontColor;
- break;
- case ElementTheme.Dark:
- fallback = _lightFontColor;
- break;
- default:
- break;
- }
-
- switch (LyricsFontColorType)
- {
- case LyricsFontColorType.Default:
- _fontColor = fallback;
- break;
- case LyricsFontColorType.Dominant:
- _fontColor = _albumArtAccentColor ?? fallback;
- break;
- default:
- break;
- }
- }
-
- ///
- /// The UpdateLinesProps
- ///
- /// The source
- /// The defaultOpacity
- private void UpdateLinesProps()
- {
- var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
-
- var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
-
- for (int i = startLineIndex; i <= endLineIndex; i++)
- {
- var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
-
- if (line == null)
- {
- continue;
- }
-
- bool linePlaying = i == currentPlayingLineIndex;
-
- var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
- var lineExitingDurationMs = _lineExitingDurationMs;
- if (i + 1 <= endLineIndex)
- {
- lineExitingDurationMs = Math.Min(
- _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i + 1)?.DurationMs ?? 0,
- 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 = GetLinePlayingProgress(line);
-
- var durationFromStartMs = TotalTime.TotalMilliseconds - line.StartMs;
- 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 = TotalTime.TotalMilliseconds - line.EndMs;
- 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;
- }
- }
-
- ///
- /// The OnLyricsFontColorTypeChanged
- ///
- /// The value
- partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
- {
- UpdateFontColor();
- }
-
- ///
- /// The OnLyricsFontSizeChanged
- ///
- /// The value
- partial void OnLyricsFontSizeChanged(int value)
- {
- _isRelayoutNeeded = true;
- }
-
- ///
- /// The OnLyricsFontWeightChanged
- ///
- /// The value
- partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
- {
- _textFormat.FontWeight = value.ToFontWeight();
- }
-
- ///
- /// The OnLyricsLineSpacingFactorChanged
- ///
- /// The value
- partial void OnLyricsLineSpacingFactorChanged(float value)
- {
- _isRelayoutNeeded = true;
- }
-
- ///
- /// The OnSongInfoChanged
- ///
- /// The oldValue
- /// The newValue
- async partial void OnSongInfoChanged(SongInfo? oldValue, SongInfo? newValue)
- {
- TotalTime = TimeSpan.Zero;
-
- _lastAlbumArtBitmap = _albumArtBitmap;
-
- if (newValue?.AlbumArt is byte[] bytes)
- {
- _albumArtBitmap = await (
- await ImageHelper.GetDecoderFromByte(bytes)
- ).GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
- _albumArtAccentColor = (
- await ImageHelper.GetAccentColorsFromByte(bytes)
- ).FirstOrDefault();
- }
- else
- {
- _albumArtBitmap = null;
- _albumArtAccentColor = null;
- }
-
- UpdateFontColor();
-
- _albumArtBgTransition.Reset(0f);
- _albumArtBgTransition.StartTransition(1f);
-
- await RefreshLyricsAsync();
- }
-
- ///
- /// The OnThemeChanged
- ///
- /// The value
- partial void OnThemeChanged(ElementTheme value)
- {
- UpdateFontColor();
- }
-
#endregion
}
}
diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/HostWindow.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/HostWindow.xaml
index bd768fe..148e9fb 100644
--- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/HostWindow.xaml
+++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/HostWindow.xaml
@@ -86,6 +86,11 @@
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
+