From 23bafc4d7556bd931d83da63984715d5db7ad643 Mon Sep 17 00:00:00 2001 From: Zhe Fang Date: Thu, 26 Jun 2025 08:30:19 -0400 Subject: [PATCH] chore: split renderer viewmodel --- .../BetterLyrics.WinUI3.csproj | 19 +- ...icsSearchProviderToDisplayNameConverter.cs | 11 +- .../BetterLyrics.WinUI3/Enums/LineMaskType.cs | 14 + .../Enums/LyricsSearchProvider.cs | 2 + .../Helper/AnimationHelper.cs | 2 +- .../BetterLyrics.WinUI3/Helper/AppInfo.cs | 13 +- .../Helper/DesktopModeHelper.cs | 245 +++ .../{DockHelper.cs => DockModeHelper.cs} | 2 +- .../Helper/ForegroundWindowWatcherHelper.cs | 197 +-- .../Helper/LyricsParser.cs | 36 +- .../Helper/WindowColorHelper.cs | 193 +-- .../BetterLyrics.WinUI3/Models/CharTiming.cs | 4 + .../BetterLyrics.WinUI3/Models/LyricsLine.cs | 44 +- .../Services/MusicSearchService.cs | 254 ++- .../Services/SettingsService.cs | 2 +- .../Strings/en-US/Resources.resw | 6 + .../Strings/ja-JP/Resources.resw | 6 + .../Strings/ko-KR/Resources.resw | 6 + .../Strings/zh-CN/Resources.resw | 6 + .../Strings/zh-TW/Resources.resw | 6 + .../ViewModels/HostWindowViewModel.cs | 45 +- .../ViewModels/LyricsPageViewModel.cs | 11 +- .../LyricsRendererViewModel.Layout.cs | 314 ++++ .../LyricsRendererViewModel.Messages.cs | 371 +++++ .../LyricsRendererViewModel.Renderer.cs | 531 +++++++ .../ViewModels/LyricsRendererViewModel.cs | 1400 +---------------- .../BetterLyrics.WinUI3/Views/HostWindow.xaml | 5 + 27 files changed, 1943 insertions(+), 1802 deletions(-) create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/Enums/LineMaskType.cs create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DesktopModeHelper.cs rename BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/{DockHelper.cs => DockModeHelper.cs} (99%) create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Layout.cs create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Messages.cs create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel.Renderer.cs 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}" /> +