chore: split renderer viewmodel

This commit is contained in:
Zhe Fang
2025-06-26 08:30:19 -04:00
parent 3bdce0d975
commit 23bafc4d75
27 changed files with 1943 additions and 1802 deletions

View File

@@ -10,6 +10,14 @@
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="ViewModels\Lyrics\**" />
<Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
@@ -18,18 +26,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference
Include="CommunityToolkit.Labs.WinUI.OpacityMaskView"
Version="0.1.250513-build.2126"
/>
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference
Include="CommunityToolkit.WinUI.Controls.SettingsControls"
Version="8.2.250402"
/>
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
@@ -66,7 +68,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\" />
<Folder Include="ViewModels\Lyrics\" />
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>

View File

@@ -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"
),

View File

@@ -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,
}
}

View File

@@ -14,6 +14,8 @@ namespace BetterLyrics.WinUI3.Enums
/// </summary>
LrcLib,
//AmllTtmlDb,
/// <summary>
/// Defines the LocalMusicFile
/// </summary>

View File

@@ -89,7 +89,7 @@ namespace BetterLyrics.WinUI3.Helper
public ValueTransition(
T initialValue,
float durationSeconds,
Func<T, T, float, T> interpolator = null,
Func<T, T, float, T>? interpolator = null,
EasingType? easingType = null
)
{

View File

@@ -78,8 +78,14 @@ namespace BetterLyrics.WinUI3.Helper
/// <summary>
/// Gets the OnlineLyricsCacheDirectory
/// </summary>
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");
/// <summary>
/// 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

View File

@@ -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<IntPtr, WindowStyle> _originalWindowStyles = [];
private static readonly Dictionary<IntPtr, bool> _clickThroughStates = [];
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<IntPtr, nint> _oldWndProcs = [];
private static readonly Dictionary<IntPtr, WndProcDelegate> _wndProcDelegates = [];
// <20><><EFBFBD><EFBFBD><E0BBAF><EFBFBD><EFBFBD>
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);
// <20><><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD>ʽ<EFBFBD><CABD>͸<EFBFBD><CDB8><EFBFBD><EFBFBD>
if (!_originalWindowStyles.ContainsKey(hwnd))
_originalWindowStyles[hwnd] = window.GetWindowStyle();
// <20><><EFBFBD><EFBFBD>ԭTopMost״̬
if (!_originalTopmostStates.ContainsKey(hwnd))
_originalTopmostStates[hwnd] = IsWindowTopMost(hwnd);
// <20><><EFBFBD><EFBFBD><EFBFBD>ޱ߿<DEB1><DFBF><EFBFBD>͸<EFBFBD><CDB8>
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
window.ExtendsContentIntoTitleBar = false;
// <20><><EFBFBD>ô<EFBFBD><C3B4><EFBFBD><EFBFBD>ö<EFBFBD>
SetWindowTopMost(hwnd, true);
// <20><><EFBFBD><EFBFBD>ȫ<EFBFBD>ִ<EFBFBD>͸
SetClickThrough(window, true);
// <20><><EFBFBD>þֲ<C3BE><D6B2><EFBFBD>͸
EnablePartialClickThrough(window);
}
public static void Disable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ<EFBFBD><CABD>͸<EFBFBD><CDB8><EFBFBD><EFBFBD>
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
_originalWindowStyles.Remove(hwnd);
}
window.ExtendsContentIntoTitleBar = true;
// <20>ָ<EFBFBD>TopMost״̬
if (_originalTopmostStates.TryGetValue(hwnd, out var wasTopMost))
{
SetWindowTopMost(hwnd, wasTopMost);
_originalTopmostStates.Remove(hwnd);
}
// <20>رյ<D8B1><D5B5><EFBFBD><EFBFBD><EFBFBD>͸
SetClickThrough(window, false);
// <20>رվֲ<D5BE><D6B2><EFBFBD>͸
DisablePartialClickThrough(window);
}
/// <summary>
/// <20><><EFBFBD>ô<EFBFBD><C3B4><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ö<EFBFBD>
/// </summary>
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
);
}
/// <summary>
/// <20>жϴ<D0B6><CFB4><EFBFBD><EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD>ö<EFBFBD>
/// </summary>
private static bool IsWindowTopMost(IntPtr hwnd)
{
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
return (exStyle & WS_EX_TOPMOST) == WS_EX_TOPMOST;
}
/// <summary>
/// <20>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͸״̬
/// </summary>
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;
}
}
/// <summary>
/// <20><>ȡ<EFBFBD><C8A1>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͸״̬
/// </summary>
public static bool GetClickThrough(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
return _clickThroughStates.TryGetValue(hwnd, out var state) && state;
}
/// <summary>
/// <20><><EFBFBD>þֲ<C3BE><D6B2><EFBFBD>͸
/// </summary>
public static void EnablePartialClickThrough(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (_oldWndProcs.ContainsKey(hwnd))
return; // <20>Ѿ<EFBFBD><D1BE><EFBFBD><EFBFBD>
WndProcDelegate newWndProc = (hWnd, msg, wParam, lParam) =>
{
if (msg == WM_NCHITTEST)
{
int x = (short)(lParam.ToInt32() & 0xFFFF);
int y = (short)((lParam.ToInt32() >> 16) & 0xFFFF);
// <20><>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
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; // <20><>ֹGC
}
/// <summary>
/// <20>رվֲ<D5BE><D6B2><EFBFBD>͸
/// </summary>
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);
}
}
/// <summary>
/// <20>жϵ<D0B6><CFB5>Ƿ<EFBFBD><C7B7>ڿɽ<DABF><C9BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>򣨴˴<F2A3A8B4>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>ϰ벿<CFB0>֣<EFBFBD><D6A3><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6>
/// </summary>
private static bool IsInInteractiveRegion(Window window, int x, int y)
{
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϰ벿<CFB0>ֿɽ<D6BF><C9BD><EFBFBD>
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
}
}

View File

@@ -11,7 +11,7 @@ using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockHelper
public static class DockModeHelper
{
private static readonly HashSet<IntPtr> _registered = [];

View File

@@ -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
{
/// <summary>
/// Defines the <see cref="ForegroundWindowWatcherHelper" />
/// </summary>
public class ForegroundWindowWatcherHelper
{
#region Constants
/// <summary>
/// Defines the EVENT_OBJECT_LOCATIONCHANGE
/// </summary>
private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
/// <summary>
/// Defines the EVENT_SYSTEM_FOREGROUND
/// </summary>
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
/// <summary>
/// Defines the EVENT_SYSTEM_MINIMIZEEND
/// </summary>
private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
/// <summary>
/// Defines the ThrottleIntervalMs
/// </summary>
private readonly WinEventDelegate _winEventDelegate;
private readonly List<IntPtr> _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;
/// <summary>
/// Defines the WINEVENT_OUTOFCONTEXT
/// </summary>
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
#endregion
#region Fields
/// <summary>
/// Defines the _hooks
/// </summary>
private readonly List<IntPtr> _hooks = new();
/// <summary>
/// Defines the _onWindowChanged
/// </summary>
public delegate void WindowChangedHandler(IntPtr hwnd);
private readonly WindowChangedHandler _onWindowChanged;
/// <summary>
/// Defines the _pollingTimer
/// </summary>
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;
/// <summary>
/// Defines the _selfHwnd
/// </summary>
private readonly IntPtr _selfHwnd;
/// <summary>
/// Defines the _winEventDelegate
/// </summary>
private readonly WinEventDelegate _winEventDelegate;
/// <summary>
/// Defines the _currentForeground
/// </summary>
private IntPtr _currentForeground = IntPtr.Zero;
/// <summary>
/// Defines the _lastEventTime
/// </summary>
private DateTime _lastEventTime = DateTime.MinValue;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ForegroundWindowWatcherHelper"/> class.
/// </summary>
/// <param name="selfHwnd">The selfHwnd<see cref="IntPtr"/></param>
/// <param name="onWindowChanged">The onWindowChanged<see cref="WindowChangedHandler"/></param>
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
@@ -101,43 +40,6 @@ namespace BetterLyrics.WinUI3.Helper
};
}
#endregion
#region Delegates
/// <summary>
/// The WindowChangedHandler
/// </summary>
/// <param name="hwnd">The hwnd<see cref="IntPtr"/></param>
public delegate void WindowChangedHandler(IntPtr hwnd);
/// <summary>
/// The WinEventDelegate
/// </summary>
/// <param name="hWinEventHook">The hWinEventHook<see cref="IntPtr"/></param>
/// <param name="eventType">The eventType<see cref="uint"/></param>
/// <param name="hwnd">The hwnd<see cref="IntPtr"/></param>
/// <param name="idObject">The idObject<see cref="int"/></param>
/// <param name="idChild">The idChild<see cref="int"/></param>
/// <param name="dwEventThread">The dwEventThread<see cref="uint"/></param>
/// <param name="dwmsEventTime">The dwmsEventTime<see cref="uint"/></param>
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
);
#endregion
#region Methods
/// <summary>
/// The Start
/// </summary>
public void Start()
{
// Hook: foreground changes and minimize end
@@ -169,9 +71,6 @@ namespace BetterLyrics.WinUI3.Helper
_pollingTimer.Start();
}
/// <summary>
/// The Stop
/// </summary>
public void Stop()
{
foreach (var hook in _hooks)
@@ -181,46 +80,6 @@ namespace BetterLyrics.WinUI3.Helper
_pollingTimer.Stop();
}
/// <summary>
/// The SetWinEventHook
/// </summary>
/// <param name="eventMin">The eventMin<see cref="uint"/></param>
/// <param name="eventMax">The eventMax<see cref="uint"/></param>
/// <param name="hmodWinEventProc">The hmodWinEventProc<see cref="IntPtr"/></param>
/// <param name="lpfnWinEventProc">The lpfnWinEventProc<see cref="WinEventDelegate"/></param>
/// <param name="idProcess">The idProcess<see cref="uint"/></param>
/// <param name="idThread">The idThread<see cref="uint"/></param>
/// <param name="dwFlags">The dwFlags<see cref="uint"/></param>
/// <returns>The <see cref="IntPtr"/></returns>
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags
);
/// <summary>
/// The UnhookWinEvent
/// </summary>
/// <param name="hWinEventHook">The hWinEventHook<see cref="IntPtr"/></param>
/// <returns>The <see cref="bool"/></returns>
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
/// <summary>
/// The WinEventProc
/// </summary>
/// <param name="hWinEventHook">The hWinEventHook<see cref="IntPtr"/></param>
/// <param name="eventType">The eventType<see cref="uint"/></param>
/// <param name="hwnd">The hwnd<see cref="IntPtr"/></param>
/// <param name="idObject">The idObject<see cref="int"/></param>
/// <param name="idChild">The idChild<see cref="int"/></param>
/// <param name="dwEventThread">The dwEventThread<see cref="uint"/></param>
/// <param name="dwmsEventTime">The dwmsEventTime<see cref="uint"/></param>
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
}
}

View File

@@ -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
/// <param name="durationMs">The durationMs<see cref="int"/></param>
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<CharTiming>();
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 = [],
}
);

View File

@@ -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
{
/// <summary>
/// Defines the <see cref="WindowColorHelper" />
/// </summary>
public static class WindowColorHelper
{
#region Constants
/// <summary>
/// Defines the SRCCOPY
/// </summary>
private const int SRCCOPY = 0x00CC0020;
#endregion
#region Enums
/// <summary>
/// Defines the SystemMetric
/// </summary>
private enum SystemMetric
{
/// <summary>
/// Defines the SM_CXSCREEN
/// </summary>
SM_CXSCREEN = 0,
/// <summary>
/// Defines the SM_CYSCREEN
/// </summary>
SM_CYSCREEN = 1,
}
#endregion
#region Methods
/// <summary>
/// The GetDominantColorBelow
/// </summary>
/// <param name="myHwnd">The myHwnd<see cref="IntPtr"/></param>
/// <returns>The <see cref="Color"/></returns>
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);
}
/// <summary>
/// The BitBlt
/// </summary>
/// <param name="hdcDest">The hdcDest<see cref="IntPtr"/></param>
/// <param name="nXDest">The nXDest<see cref="int"/></param>
/// <param name="nYDest">The nYDest<see cref="int"/></param>
/// <param name="nWidth">The nWidth<see cref="int"/></param>
/// <param name="nHeight">The nHeight<see cref="int"/></param>
/// <param name="hdcSrc">The hdcSrc<see cref="IntPtr"/></param>
/// <param name="nXSrc">The nXSrc<see cref="int"/></param>
/// <param name="nYSrc">The nYSrc<see cref="int"/></param>
/// <param name="dwRop">The dwRop<see cref="int"/></param>
/// <returns>The <see cref="bool"/></returns>
[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);
}
/// <summary>
/// The ComputeAverageColor
/// </summary>
/// <param name="bmp">The bmp<see cref="Bitmap"/></param>
/// <returns>The <see cref="Color"/></returns>
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));
}
/// <summary>
/// The GetAverageColorFromScreenRegion
/// </summary>
/// <param name="x">The x<see cref="int"/></param>
/// <param name="y">The y<see cref="int"/></param>
/// <param name="width">The width<see cref="int"/></param>
/// <param name="height">The height<see cref="int"/></param>
/// <returns>The <see cref="Color"/></returns>
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);
}
/// <summary>
/// The GetDC
/// </summary>
/// <param name="hWnd">The hWnd<see cref="IntPtr"/></param>
/// <returns>The <see cref="IntPtr"/></returns>
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
/// <summary>
/// The GetSystemMetrics
/// </summary>
/// <param name="smIndex">The smIndex<see cref="SystemMetric"/></param>
/// <returns>The <see cref="int"/></returns>
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(SystemMetric smIndex);
/// <summary>
/// The GetWindowRect
/// </summary>
/// <param name="hWnd">The hWnd<see cref="IntPtr"/></param>
/// <param name="lpRect">The lpRect<see cref="RECT"/></param>
/// <returns>The <see cref="bool"/></returns>
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
/// <summary>
/// The ReleaseDC
/// </summary>
/// <param name="hWnd">The hWnd<see cref="IntPtr"/></param>
/// <param name="hDC">The hDC<see cref="IntPtr"/></param>
/// <returns>The <see cref="int"/></returns>
[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,
}
/// <summary>
/// Defines the <see cref="RECT" />
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
#region Fields
/// <summary>
/// Defines the Bottom
/// </summary>
public int Bottom;
/// <summary>
/// Defines the Left
/// </summary>
public int Left;
/// <summary>
/// Defines the Right
/// </summary>
public int Right;
/// <summary>
/// Defines the Top
/// </summary>
public int Top;
#endregion
public int Right;
public int Bottom;
}
#endregion
}
}

View File

@@ -25,6 +25,10 @@ namespace BetterLyrics.WinUI3.Models
/// </summary>
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty;
public int StartIndex { get; set; }
#endregion
}
}

View File

@@ -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
/// <summary>
/// Gets or sets the BlurAmountTransition
/// </summary>
public ValueTransition<float> BlurAmountTransition { get; set; } =
new(initialValue: 0f, durationSeconds: 0.3f);
/// <summary>
/// Gets or sets the CanvasTextLayout
/// </summary>
public CanvasTextLayout? CanvasTextLayout { get; set; }
/// <summary>
@@ -37,30 +45,8 @@ namespace BetterLyrics.WinUI3.Models
/// </summary>
public int EndMs { get; set; }
/// <summary>
/// Gets or sets the EnteringProgress
/// </summary>
public float EnteringProgress { get; set; }
/// <summary>
/// Gets or sets the ExitingProgress
/// </summary>
public float ExitingProgress { get; set; }
/// <summary>
/// Gets or sets the Opacity
/// </summary>
public float Opacity { get; set; }
/// <summary>
/// Gets or sets the PlayingProgress
/// </summary>
public float PlayingProgress { get; set; }
/// <summary>
/// Gets or sets the PlayingState
/// </summary>
public LyricsPlayingState PlayingState { get; set; }
public ValueTransition<float> HighlightOpacityTransition { get; set; } =
new(initialValue: 0f, durationSeconds: 0.3f);
/// <summary>
/// Gets or sets the Position
@@ -68,9 +54,10 @@ namespace BetterLyrics.WinUI3.Models
public Vector2 Position { get; set; }
/// <summary>
/// Gets or sets the Scale
/// Gets or sets the ScaleTransition
/// </summary>
public float Scale { get; set; }
public ValueTransition<float> ScaleTransition { get; set; } =
new(initialValue: 0.95f, durationSeconds: 0.3f);
/// <summary>
/// Gets or sets the StartMs
@@ -82,9 +69,6 @@ namespace BetterLyrics.WinUI3.Models
/// </summary>
public string Text { get; set; } = "";
public ValueTransition<float> BlurAmountTransition { get; set; } =
new(initialValue: 0f, durationSeconds: 0.3f);
#endregion
}
}

View File

@@ -25,7 +25,9 @@ namespace BetterLyrics.WinUI3.Services
/// <summary>
/// Defines the _httpClient
/// </summary>
private readonly HttpClient _httpClient;
private readonly HttpClient _lrcLibHttpClient;
private readonly HttpClient _amllTtmlDbHttpClient;
/// <summary>
/// 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);
}
// 判断相似度
/// <summary>
/// The FuzzyMatch
/// </summary>
/// <param name="fileName">The fileName<see cref="string"/></param>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <returns>The <see cref="bool"/></returns>
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; // 阈值可调整
}
/// <summary>
/// The LevenshteinDistance
/// </summary>
/// <param name="a">The a<see cref="string"/></param>
/// <param name="b">The b<see cref="string"/></param>
/// <returns>The <see cref="int"/></returns>
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];
}
/// <summary>
/// The Normalize
/// </summary>
/// <param name="s">The s<see cref="string"/></param>
/// <returns>The <see cref="string"/></returns>
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);
}
/// <summary>
@@ -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
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="format">The format<see cref="LyricsFormat"/></param>
/// <returns>The <see cref="string?"/></returns>
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
/// <param name="duration">The duration<see cref="int"/></param>
/// <param name="matchMode">The matchMode<see cref="MusicSearchMatchMode"/></param>
/// <returns>The <see cref="Task{string?}"/></returns>
private async Task<string?> SearchLrcLib(
private async Task<string?> 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;
}
/// <summary>
/// 本地检索 amll-ttml-db 索引并下载歌词内容
/// </summary>
/// <param name="title">歌曲名</param>
/// <param name="artist">歌手名</param>
/// <returns>歌词内容字符串,找不到返回 null</returns>
private async Task<string?> 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;
}
}
/// <summary>
/// 下载 amll-ttml-db 的 JSONL 索引文件到本地缓存目录
/// </summary>
/// <returns>下载成功返回 true否则 false</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// The WriteCache
/// </summary>
@@ -443,12 +537,18 @@ namespace BetterLyrics.WinUI3.Services
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="lyrics">The lyrics<see cref="string"/></param>
/// <param name="format">The format<see cref="LyricsFormat"/></param>
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);

View File

@@ -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

View File

@@ -411,6 +411,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>Dock mode</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>Desktop mode</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>Font weight</value>
</data>
@@ -486,4 +489,7 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains added folders, please delete these folders to add the folder</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
</root>

View File

@@ -411,6 +411,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>ドックモード</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>フォント重量</value>
</data>
@@ -486,4 +489,7 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>このフォルダーには追加されたフォルダーが含まれています。これらのフォルダを削除してフォルダーを追加してください</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
</root>

View File

@@ -411,6 +411,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>도크 모드</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>글꼴 무게</value>
</data>
@@ -486,4 +489,7 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>이 폴더에는 추가 된 폴더가 포함되어 있습니다. 폴더를 추가하려면이 폴더를 삭제하십시오.</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
</root>

View File

@@ -411,6 +411,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字体粗细</value>
</data>
@@ -486,4 +489,7 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
</root>

View File

@@ -411,6 +411,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value />
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字體粗細</value>
</data>
@@ -486,4 +489,7 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>該文件夾包含已添加文件夾,請刪除這些文件夾以添加該文件夾</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
</root>

View File

@@ -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;
/// <summary>
/// Gets or sets the Notification
/// </summary>
@@ -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;
}
/// <summary>
/// The SwitchInfoBarNeverShowItAgainCheckBox
/// </summary>
/// <param name="value">The value<see cref="bool"/></param>
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
}
/// <summary>
/// The ToggleDockMode
/// </summary>
@@ -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);
}
}
/// <summary>
/// The OnFramePageTypeChanged
/// </summary>

View File

@@ -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 { }
}
}
}

View File

@@ -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
{
/// <summary>
/// The Update
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="args">The args<see cref="CanvasAnimatedUpdateEventArgs"/></param>
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();
}
/// <summary>
/// The UpdateCanvasYScrollOffset
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
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;
}
}
/// <summary>
/// The UpdateFontColor
/// </summary>
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;
}
}
/// <summary>
/// The UpdateLinesProps
/// </summary>
/// <param name="source">The source<see cref="List{LyricsLine}?"/></param>
/// <param name="defaultOpacity">The defaultOpacity<see cref="float"/></param>
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);
}
}
}
}
/// <summary>
/// Reassigns positions (x,y) to lyrics lines based on the current control size and font size
/// </summary>
/// <param name="control"></param>
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);
}
}
}
}

View File

@@ -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<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<float>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<LyricsFontColorType>>,
IRecipient<PropertyChangedMessage<LyricsAlignmentType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<LineRenderingType>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
{
/// <summary>
/// The OnLyricsFontColorTypeChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsFontColorType"/></param>
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
{
UpdateFontColor();
}
/// <summary>
/// The OnLyricsFontSizeChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnLyricsFontSizeChanged(int value)
{
_isRelayoutNeeded = true;
}
/// <summary>
/// The OnLyricsFontWeightChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsFontWeight"/></param>
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_textFormat.FontWeight = value.ToFontWeight();
}
/// <summary>
/// The OnLyricsLineSpacingFactorChanged
/// </summary>
/// <param name="value">The value<see cref="float"/></param>
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_isRelayoutNeeded = true;
}
/// <summary>
/// The OnSongInfoChanged
/// </summary>
/// <param name="oldValue">The oldValue<see cref="SongInfo?"/></param>
/// <param name="newValue">The newValue<see cref="SongInfo?"/></param>
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();
}
/// <summary>
/// The OnThemeChanged
/// </summary>
/// <param name="value">The value<see cref="ElementTheme"/></param>
partial void OnThemeChanged(ElementTheme value)
{
UpdateFontColor();
}
// Receive methods for handling messages from other view models
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{bool}"/></param>
public void Receive(PropertyChangedMessage<bool> 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;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{Color}"/></param>
public void Receive(PropertyChangedMessage<Color> message)
{
if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.ActivatedWindowAccentColor))
{
_immersiveBgTransition.StartTransition(message.NewValue);
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{double}"/></param>
public void Receive(PropertyChangedMessage<double> message)
{
if (message.Sender is LyricsPageViewModel)
{
if (message.PropertyName == nameof(LyricsPageViewModel.LimitedLineWidth))
{
_limitedLineWidthTransition.StartTransition((float)message.NewValue);
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ElementTheme}"/></param>
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.ThemeType))
{
Theme = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{float}"/></param>
public void Receive(PropertyChangedMessage<float> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsLineSpacingFactor)
)
{
LyricsLineSpacingFactor = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{int}"/></param>
public void Receive(PropertyChangedMessage<int> 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;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsAlignmentType}"/></param>
public void Receive(PropertyChangedMessage<LyricsAlignmentType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsAlignmentType)
)
{
LyricsAlignmentType = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsDisplayType}"/></param>
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
DisplayType = message.NewValue;
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsFontColorType}"/></param>
public void Receive(PropertyChangedMessage<LyricsFontColorType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsFontColorType)
)
{
LyricsFontColorType = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsFontWeight}"/></param>
public void Receive(PropertyChangedMessage<LyricsFontWeight> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontWeight))
{
LyricsFontWeight = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsGlowEffectScope}"/></param>
public void Receive(PropertyChangedMessage<LineRenderingType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsGlowEffectScope)
)
{
LyricsGlowEffectScope = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ObservableCollection{LocalLyricsFolder}}"/></param>
public void Receive(PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.LocalLyricsFolders))
{
// Music lib changed, re-fetch lyrics
RefreshLyricsAsync().ConfigureAwait(true);
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ObservableCollection{LyricsSearchProviderInfo}}"/></param>
public void Receive(
PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>> message
)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.LyricsSearchProvidersInfo))
{
// Lyrics search providers info changed, re-fetch lyrics
RefreshLyricsAsync().ConfigureAwait(true);
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// The Draw
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
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
//);
}
}
/// <summary>
/// The DrawImgae
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
/// <param name="softwareBitmap">The softwareBitmap<see cref="SoftwareBitmap"/></param>
/// <param name="opacity">The opacity<see cref="float"/></param>
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
);
}
/// <summary>
/// The DrawAlbumArtBackground
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
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;
}
/// <summary>
/// The DrawGradientOpacityMask
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
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);
}
/// <summary>
/// The DrawImmersiveBackground
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
/// <param name="withGradient">The withGradient<see cref="bool"/></param>
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),
}
);
}
/// <summary>
/// The DrawLyrics
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
/// <param name="currentLineHighlightType">The currentLineHighlightType<see cref="LyricsHighlightType"/></param>
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;
}
}
/// <summary>
/// The GetHorizontalFillBrush
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="stopPosition">The stopPosition<see cref="float[]"/></param>
/// <param name="stopOpacity">The stopOpacity<see cref="float[]"/></param>
/// <param name="startX">The startX<see cref="float"/></param>
/// <param name="endX">The endX<see cref="float"/></param>
/// <returns>The <see cref="CanvasLinearGradientBrush"/></returns>
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),
};
}
}
}

View File

@@ -86,6 +86,11 @@
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="DesktopFlyoutItem"
x:Uid="HostWindowDesktopFlyoutItem"
Command="{x:Bind ViewModel.ToggleDesktopModeCommand}"
IsChecked="{x:Bind ViewModel.IsDesktopMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="MiniFlyoutItem"
x:Uid="BaseWindowMiniFlyoutItem"