change language detection model

This commit is contained in:
Zhe Fang
2025-07-10 23:08:29 -04:00
parent f8c6060d32
commit 07b82191d0
53 changed files with 57366 additions and 1022 deletions

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.9.0" />
Version="1.0.10.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -65,7 +65,7 @@
</Style>
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Margin" Value="0" />

View File

@@ -37,15 +37,12 @@ namespace BetterLyrics.WinUI3
{
this.InitializeComponent();
var test = LanguageDetectionHelper.DetectLanguageCode("一隻烏龜");
DispatcherQueue = DispatcherQueue.GetForCurrentThread();
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
AppInfo.EnsureDirectories();
PathHelper.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
@@ -62,16 +59,6 @@ namespace BetterLyrics.WinUI3
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow == null) return;
string[] commandLineArguments = Environment.GetCommandLineArgs();
if (commandLineArguments.Length > 1)
{
commandLineArguments = commandLineArguments.Skip(1).ToArray();
if (commandLineArguments.First() == AppInfo.UnlockWindowTag)
{
lyricsWindow.AutoSelectLyricsMode(AutoStartWindowType.DesktopMode, false);
return;
}
}
lyricsWindow.AutoSelectLyricsMode();
}
@@ -79,7 +66,7 @@ namespace BetterLyrics.WinUI3
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.WriteTo.File(AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
@@ -93,9 +80,10 @@ namespace BetterLyrics.WinUI3
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ILibreTranslateService, LibreTranslateService>()
.AddSingleton<ITranslateService, TranslateService>()
// ViewModels
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Core14.profile.xml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
</ItemGroup>
@@ -42,7 +43,6 @@
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="LanguageDetection.NETStandard" Version="1.3.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
@@ -50,6 +50,7 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="NTextCat" Version="0.3.65" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />

View File

@@ -23,11 +23,11 @@ namespace BetterLyrics.WinUI3.Enums
{
return provider switch
{
LyricsSearchProvider.LrcLib => AppInfo.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => AppInfo.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => AppInfo.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => AppInfo.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => AppInfo.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
_ => throw new System.ArgumentOutOfRangeException(nameof(provider)),
};
}

View File

@@ -1,10 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum MusicSearchMatchMode
{
TitleAndArtist,
TitleArtistAlbumAndDuration,
}
}

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum WindowColorSampleMode
public enum WindowPixelSampleMode
{
BelowWindow,
WindowArea,

View File

@@ -1,116 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Helper
{
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.FileProperties;
public static class AppInfo
{
public const string AppAuthor = "Zhe Fang";
public const string AppDisplayName = "Better Lyrics";
public const string AppName = "BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
public const string UnlockWindowTag = "UnlockWindow";
public static string AmllTtmlDbIndexPath => Path.Combine(CacheFolder, "amll-ttml-db-index.json");
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LrcLibLyricsCacheDirectory => Path.Combine(CacheFolder, "lrclib-lyrics");
public static string NeteaseLyricsCacheDirectory => Path.Combine(CacheFolder, "netease-lyrics");
public static string QQLyricsCacheDirectory => Path.Combine(CacheFolder, "qq-lyrics");
public static string KugouLyricsCacheDirectory => Path.Combine(CacheFolder, "kugou-lyrics");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(CacheFolder, "amll-ttml-db-lyrics");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(CacheFolder, "itunes-album-art");
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static void EnsureDirectories()
{
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}
public static async Task<DateTime> GetBuildDate()
{
var assembly = Assembly.GetExecutingAssembly();
var filePath = assembly.Location;
if (!File.Exists(filePath))
return DateTime.MinValue;
StorageFile file = await StorageFile.GetFileFromPathAsync(filePath);
// 获取文件基本属性
BasicProperties props = await file.GetBasicPropertiesAsync();
// 返回修改日期
return props.DateModified.DateTime;
}
public static List<LanguageInfo> TranslationLanguagesInfo =>
[
new LanguageInfo("ar", "العربية"),
new LanguageInfo("az", "Azərbaycan dili"),
new LanguageInfo("zh-Hans", "简体中文"),
new LanguageInfo("zh-Hant", "繁體中文"),
new LanguageInfo("cs", "Čeština"),
new LanguageInfo("da", "Dansk"),
new LanguageInfo("nl", "Nederlands"),
new LanguageInfo("en", "English"),
new LanguageInfo("eo", "Esperanto"),
new LanguageInfo("fi", "Suomi"),
new LanguageInfo("fr", "Français"),
new LanguageInfo("de", "Deutsch"),
new LanguageInfo("el", "Ελληνικά"),
new LanguageInfo("he", "עברית"),
new LanguageInfo("hi", "हिन्दी"),
new LanguageInfo("hu", "Magyar"),
new LanguageInfo("id", "Bahasa Indonesia"),
new LanguageInfo("ga", "Gaeilge"),
new LanguageInfo("it", "Italiano"),
new LanguageInfo("ja", "日本語"),
new LanguageInfo("ko", "한국어"),
new LanguageInfo("fa", "فارسی"),
new LanguageInfo("pl", "Polski"),
new LanguageInfo("pt", "Português"),
new LanguageInfo("ru", "Русский"),
new LanguageInfo("sk", "Slovenčina"),
new LanguageInfo("es", "Español"),
new LanguageInfo("sv", "Svenska"),
new LanguageInfo("tr", "Türkçe"),
new LanguageInfo("uk", "Українська"),
new LanguageInfo("vi", "Tiếng Việt"),
];
}
}

View File

@@ -1,10 +1,17 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using Vanara.PInvoke;
using Windows.UI;
using Color = Windows.UI.Color;
namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
@@ -98,5 +105,140 @@ namespace BetterLyrics.WinUI3.Helper
return CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, s, brightness);
}
public static System.Drawing.Color GetAccentColor(IntPtr myHwnd, WindowPixelSampleMode mode)
{
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return System.Drawing.Color.Transparent;
switch (mode)
{
case WindowPixelSampleMode.BelowWindow:
{
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
case WindowPixelSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
case WindowPixelSampleMode.WindowEdge:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
var edgeThickness = new Thickness(36, 0, 36, 0);
List<System.Drawing.Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// 合并四边平均色
if (edgeColors.Count == 0)
return System.Drawing.Color.Transparent;
long r = 0,
g = 0,
b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return System.Drawing.Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
);
}
default:
return System.Drawing.Color.Transparent;
}
}
private static System.Drawing.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 = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, Gdi32.RasterOperationMode.SRCCOPY);
gDest.ReleaseHdc(hdcDest);
User32.ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static System.Drawing.Color ComputeAverageColor(Bitmap bmp)
{
long r = 0, g = 0, b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
System.Drawing.Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0) return System.Drawing.Color.Transparent;
return System.Drawing.Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
}
}

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
@@ -15,7 +16,6 @@ namespace BetterLyrics.WinUI3.Helper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private static readonly Dictionary<IntPtr, bool> _clickThroughStates = [];
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<IntPtr, (double X, double Y, double Width, double Height)> _originalWindowBounds = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyles = [];
@@ -95,17 +95,8 @@ namespace BetterLyrics.WinUI3.Helper
window.SetIsAlwaysOnTop(true);
window.SetIsShownInSwitchers(false);
}
public static void Lock(Window window)
{
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
// <20><><EFBFBD><EFBFBD><EFBFBD>ޱ߿<DEB1><DFBF><EFBFBD>͸<EFBFBD><CDB8>
window.ToggleWindowStyle(true, WindowStyle.Popup | WindowStyle.Visible);
window.ExtendsContentIntoTitleBar = false;
SetClickThrough(window, true);
}
public static void SetClickThrough(Window window, bool enable)
@@ -115,30 +106,11 @@ namespace BetterLyrics.WinUI3.Helper
if (enable)
{
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT | (int)User32.WindowStylesEx.WS_EX_LAYERED);
_clickThroughStates[hwnd] = true;
}
else
{
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle & ~(int)User32.WindowStylesEx.WS_EX_TRANSPARENT);
_clickThroughStates[hwnd] = false;
}
}
public static void Unlock(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƴ<EFBFBD><C6B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD> Disable ʱ<><CAB1><EFBFBD>Ƴ<EFBFBD><C6B3><EFBFBD>
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
}
window.ExtendsContentIntoTitleBar = true;
SetClickThrough(window, false);
// To recover the system backdrop, we need to reopen the window
WindowHelper.RestartApp(AppInfo.UnlockWindowTag);
}
}
}

View File

@@ -1,5 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System;
using System.IO;
using System.Text;
using Ude;
@@ -21,5 +23,60 @@ namespace BetterLyrics.WinUI3.Helper
}
return Encoding.GetEncoding(encoding);
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
public static string? ReadLyricsCache(string title, string artist, LyricsFormat format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title}{format.ToFileExtension()}"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllBytes(cacheFilePath);
}
return null;
}
public static void WriteLyricsCache(string title, string artist, string lyrics, LyricsFormat format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title}{format.ToFileExtension()}"));
File.WriteAllText(cacheFilePath, lyrics);
}
public static void WriteAlbumArtCache(string album, string artist, byte[] img, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
File.WriteAllBytes(cacheFilePath, img);
}
public static bool IsSwitchableNormalizedMatch(string fileName, string q1, string q2)
{
var normFileName = fileName.Normalize();
var normQ1 = q1.Normalize();
var normQ2 = q2.Normalize();
// 常见两种顺序
return normFileName == normQ1 + normQ2
|| normFileName == normQ2 + normQ1;
}
}
}

View File

@@ -10,7 +10,7 @@ using Windows.System;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcherHelper
public class ForegroundWindowWatcher
{
private readonly User32.WinEventProc _winEventDelegate;
private readonly List<User32.HWINEVENTHOOK> _hooks = new();
@@ -20,11 +20,17 @@ namespace BetterLyrics.WinUI3.Helper
public delegate void WindowChangedHandler(HWND hwnd);
private readonly WindowChangedHandler _onWindowChanged;
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
private readonly DispatcherTimer _timer;
public ForegroundWindowWatcher(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new User32.WinEventProc(WinEventProc);
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += Timer_Tick;
}
public void Start()
@@ -54,6 +60,8 @@ namespace BetterLyrics.WinUI3.Helper
User32.WINEVENT.WINEVENT_OUTOFCONTEXT
)
);
_timer.Start();
}
public void Stop()
@@ -62,6 +70,16 @@ namespace BetterLyrics.WinUI3.Helper
User32.UnhookWinEvent(hook);
_hooks.Clear();
_timer.Stop();
}
private void Timer_Tick(object? sender, object e)
{
if (_currentForeground != HWND.NULL)
{
_onWindowChanged?.Invoke(_currentForeground);
}
}
private void WinEventProc(

View File

@@ -19,7 +19,7 @@ namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
public const int AccentColorCount = 3;
private const int _accentColorCount = 1;
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
@@ -127,7 +127,7 @@ namespace BetterLyrics.WinUI3.Helper
// 按出现次数排序,取前 AccentColorCount 个
var topColors = colorCount
.OrderByDescending(kv => kv.Value)
.Take(AccentColorCount)
.Take(_accentColorCount)
.Select(kv => kv.Key)
.ToList();
@@ -138,31 +138,31 @@ namespace BetterLyrics.WinUI3.Helper
}
public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
stream.Seek(0);
//public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
//{
// var stream = new InMemoryRandomAccessStream();
// await stream.WriteAsync(imageBytes.AsBuffer());
// stream.Seek(0);
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(stream);
// var bitmapImage = new BitmapImage();
// await bitmapImage.SetSourceAsync(stream);
return bitmapImage;
}
// return bitmapImage;
//}
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
//public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
// await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
{
if (imageBytes == null || imageBytes.Length == 0)
return null;
//public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
//{
// if (imageBytes == null || imageBytes.Length == 0)
// return null;
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
// InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
// await stream.WriteAsync(imageBytes.AsBuffer());
return stream;
}
// return stream;
//}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
@@ -172,21 +172,21 @@ namespace BetterLyrics.WinUI3.Helper
return memoryStream.ToArray();
}
public static float GetAverageLuminance(CanvasBitmap bitmap)
{
var pixels = bitmap.GetPixelBytes();
double sum = 0;
for (int i = 0; i < pixels.Length; i += 4)
{
// BGRA
byte b = pixels[i];
byte g = pixels[i + 1];
byte r = pixels[i + 2];
// 忽略A
double y = 0.299 * r + 0.587 * g + 0.114 * b;
sum += y / 255.0;
}
return (float)(sum / (pixels.Length / 4));
}
//public static float GetAverageLuminance(CanvasBitmap bitmap)
//{
// var pixels = bitmap.GetPixelBytes();
// double sum = 0;
// for (int i = 0; i < pixels.Length; i += 4)
// {
// // BGRA
// byte b = pixels[i];
// byte g = pixels[i + 1];
// byte r = pixels[i + 2];
// // 忽略A
// double y = 0.299 * r + 0.587 * g + 0.114 * b;
// sum += y / 255.0;
// }
// return (float)(sum / (pixels.Length / 4));
//}
}
}

View File

@@ -1,61 +0,0 @@
using LanguageDetection;
using Lyricify.Lyrics.Helpers.General;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class LanguageDetectionHelper
{
private static readonly LanguageDetector _detector = new();
static LanguageDetectionHelper()
{
_detector.AddAllLanguages();
}
private static string? ThreeLetterToTwoLetter(string threeLetterCode)
{
foreach (var ci in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
if (string.Equals(ci.ThreeLetterISOLanguageName, threeLetterCode, StringComparison.OrdinalIgnoreCase))
{
return ci.TwoLetterISOLanguageName;
}
}
return null;
}
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
string? code = ThreeLetterToTwoLetter(_detector.Detect(text));
if (code != null && code == "zh")
{
if (ChineseConverter.ConvertToTraditionalChinese(text) == text)
{
return "zh-Hant";
}
else
{
return "zh-Hans";
}
}
return code;
}
public static bool IsCJK(string text)
{
return DetectLanguageCode(text) switch
{
"zh" or "ja" or "ko" => true,
_ => false
};
}
}
}

View File

@@ -0,0 +1,110 @@
using BetterLyrics.WinUI3.Helper;
using Lyricify.Lyrics.Helpers.General;
using NTextCat;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace BetterLyrics.WinUI3.Services
{
public class LanguageHelper
{
private static readonly RankedLanguageIdentifierFactory _factory = new();
private static readonly RankedLanguageIdentifier _identifier;
public static List<Models.LanguageInfo> SupportedTargetLanguages =>
[
new Models.LanguageInfo("ar", "العربية"),
new Models.LanguageInfo("az", "Azərbaycan dili"),
new Models.LanguageInfo("zh-Hans", "简体中文"),
new Models.LanguageInfo("zh-Hant", "繁體中文"),
new Models.LanguageInfo("cs", "Čeština"),
new Models.LanguageInfo("da", "Dansk"),
new Models.LanguageInfo("nl", "Nederlands"),
new Models.LanguageInfo("en", "English"),
new Models.LanguageInfo("eo", "Esperanto"),
new Models.LanguageInfo("fi", "Suomi"),
new Models.LanguageInfo("fr", "Français"),
new Models.LanguageInfo("de", "Deutsch"),
new Models.LanguageInfo("el", "Ελληνικά"),
new Models.LanguageInfo("he", "עברית"),
new Models.LanguageInfo("hi", "हिन्दी"),
new Models.LanguageInfo("hu", "Magyar"),
new Models.LanguageInfo("id", "Bahasa Indonesia"),
new Models.LanguageInfo("ga", "Gaeilge"),
new Models.LanguageInfo("it", "Italiano"),
new Models.LanguageInfo("ja", "日本語"),
new Models.LanguageInfo("ko", "한국어"),
new Models.LanguageInfo("fa", "فارسی"),
new Models.LanguageInfo("pl", "Polski"),
new Models.LanguageInfo("pt", "Português"),
new Models.LanguageInfo("ru", "Русский"),
new Models.LanguageInfo("sk", "Slovenčina"),
new Models.LanguageInfo("es", "Español"),
new Models.LanguageInfo("sv", "Svenska"),
new Models.LanguageInfo("tr", "Türkçe"),
new Models.LanguageInfo("uk", "Українська"),
new Models.LanguageInfo("vi", "Tiếng Việt"),
];
static LanguageHelper()
{
_identifier = _factory.Load(PathHelper.LanguageProfilePath);
}
private static string? ThreeLetterToTwoLetter(string? threeLetterCode)
{
if (threeLetterCode == null) return null;
foreach (var ci in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
if (string.Equals(ci.ThreeLetterISOLanguageName, threeLetterCode, StringComparison.OrdinalIgnoreCase))
{
return ci.TwoLetterISOLanguageName;
}
}
return null;
}
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
string? code = ThreeLetterToTwoLetter(_identifier.Identify(text).FirstOrDefault()?.Item1.Iso639_2T);
if (code != null && code == "zh")
{
if (ChineseConverter.ConvertToTraditionalChinese(text) == text)
{
return "zh-Hant";
}
else
{
return "zh-Hans";
}
}
return code;
}
public static bool IsCJK(string text)
{
return DetectLanguageCode(text) switch
{
"zh" or "ja" or "ko" => true,
_ => false
};
}
public static string DetectCountryCode(string? text)
{
if (text == null) return "en";
var code = DetectLanguageCode(text);
if (code == null) return "en";
// 处理中文简体和繁体
if (code == "zh-Hans") return "cn";
if (code == "zh-Hant") return "cn";
// 其他语言直接返回两字母代码
return code;
}
}
}

View File

@@ -196,7 +196,7 @@ namespace BetterLyrics.WinUI3.Helper
// 原文(非 CJK 语言添加空格)
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
if (!LanguageDetectionHelper.IsCJK(originalText))
if (!LanguageHelper.IsCJK(originalText))
{
foreach (var span in originalTextSpans)
{
@@ -269,7 +269,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
private int ParseTtmlTime(string? t)
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
@@ -420,18 +420,21 @@ namespace BetterLyrics.WinUI3.Helper
}
}
}
if (linesInSingleLang.Count > 0 && linesInSingleLang[0].StartMs > 0)
if (linesInSingleLang.Count > 0)
{
linesInSingleLang.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = linesInSingleLang[0].StartMs,
OriginalText = "● ● ●",
CharTimings = [],
}
);
if (linesInSingleLang[0].StartMs > 0)
{
linesInSingleLang.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = linesInSingleLang[0].StartMs,
OriginalText = "● ● ●",
CharTimings = [],
}
);
}
}
}
}

View File

@@ -0,0 +1,45 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Helper
{
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.FileProperties;
public static class MetadataHelper
{
public const string AppAuthor = "Zhe Fang";
public const string AppDisplayName = "Better Lyrics";
public const string AppName = "BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public static async Task<DateTime> GetBuildDate()
{
var assembly = Assembly.GetExecutingAssembly();
var filePath = assembly.Location;
if (!File.Exists(filePath))
return DateTime.MinValue;
StorageFile file = await StorageFile.GetFileFromPathAsync(filePath);
// 获取文件基本属性
BasicProperties props = await file.GetBasicPropertiesAsync();
// 返回修改日期
return props.DateModified.DateTime;
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Helper
{
public class PathHelper
{
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
public static string LanguageProfilePath => Path.Combine(AssetsFolder, "Core14.profile.xml");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LrcLibLyricsCacheDirectory => Path.Combine(CacheFolder, "lrclib-lyrics");
public static string NeteaseLyricsCacheDirectory => Path.Combine(CacheFolder, "netease-lyrics");
public static string QQLyricsCacheDirectory => Path.Combine(CacheFolder, "qq-lyrics");
public static string KugouLyricsCacheDirectory => Path.Combine(CacheFolder, "kugou-lyrics");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(CacheFolder, "amll-ttml-db-lyrics");
public static string AmllTtmlDbIndexPath => Path.Combine(CacheFolder, "amll-ttml-db-index.json");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(CacheFolder, "itunes-album-art");
public static void EnsureDirectories()
{
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class StringHelper
{
// 去除空格、括号、下划线、横杠、点、大小写等
public static string Normalize(this string s) =>
new string(s
.Where(c => char.IsLetterOrDigit(c))
.ToArray())
.ToLowerInvariant();
}
}

View File

@@ -6,13 +6,6 @@ using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public class AnimationHelper
{
public const int DebounceDefaultDuration = 200;
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
}
public class ValueTransition<T>
where T : struct
{

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowColorHelper
{
public static Color GetDominantColor(IntPtr myHwnd, WindowColorSampleMode mode)
{
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return Color.Transparent;
switch (mode)
{
case WindowColorSampleMode.BelowWindow:
{
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
case WindowColorSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return Color.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
case WindowColorSampleMode.WindowEdge:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return Color.Transparent;
var edgeThickness = new Thickness(36, 0, 36, 0);
List<Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// 合并四边平均色
if (edgeColors.Count == 0)
return Color.Transparent;
long r = 0,
g = 0,
b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
);
}
default:
return Color.Transparent;
}
}
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 = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, Gdi32.RasterOperationMode.SRCCOPY);
gDest.ReleaseHdc(hdcDest);
User32.ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static Color ComputeAverageColor(Bitmap bmp)
{
long r = 0, g = 0, b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0) return Color.Transparent;
return Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
}
}

View File

@@ -1,10 +1,11 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using Windows.ApplicationModel.Core;
using WinRT.Interop;
using WinUIEx;
@@ -61,6 +62,7 @@ namespace BetterLyrics.WinUI3.Helper
if (typeof(T) == typeof(LyricsWindow))
{
newWindow = new LyricsWindow();
((LyricsWindow)newWindow).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
}
else if (typeof(T) == typeof(SettingsWindow))
{

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class DetectLanguageResult
{
[JsonPropertyName("confidence")]
public double Confidence { get; set; }
[JsonPropertyName("language")]
public string Language { get; set; }
}
}

View File

@@ -1,35 +0,0 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Models
{
public partial class Notification : ObservableObject
{
[ObservableProperty]
public partial bool IsForeverDismissable { get; set; }
[ObservableProperty]
public partial string? Message { get; set; }
[ObservableProperty]
public partial string? RelatedSettingsKeyName { get; set; }
[ObservableProperty]
public partial InfoBarSeverity Severity { get; set; }
[ObservableProperty]
public partial Visibility Visibility { get; set; }
public Notification(string? message = null, InfoBarSeverity severity = InfoBarSeverity.Informational, bool isForeverDismissable = false, string? relatedSettingsKeyName = null)
{
Message = message;
Severity = severity;
IsForeverDismissable = isForeverDismissable;
Visibility = IsForeverDismissable ? Visibility.Visible : Visibility.Collapsed;
RelatedSettingsKeyName = relatedSettingsKeyName;
}
}
}

View File

@@ -13,7 +13,6 @@ namespace BetterLyrics.WinUI3.Serialization
[JsonSerializable(typeof(List<MediaSourceProviderInfo>))]
[JsonSerializable(typeof(List<LocalLyricsFolder>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<DetectLanguageResult>))]
[JsonSerializable(typeof(TranslateResponse))]
[JsonSerializable(typeof(JsonElement))]
[JsonSourceGenerationOptions(WriteIndented = true)]

View File

@@ -0,0 +1,136 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class AlbumArtSearchService : IAlbumArtSearchService
{
private readonly HttpClient _iTunesHttpClinet;
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public AlbumArtSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_logger = Ioc.Default.GetRequiredService<ILogger<AlbumArtSearchService>>();
_iTunesHttpClinet = new();
}
public async Task<byte[]?> SearchAsync(string title, string artist, string album, byte[]? bytesFromSMTC = null)
{
byte[]? result = null;
foreach (var provider in _settingsService.AlbumArtSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchFile(artist, album);
break;
case AlbumArtSearchProvider.SMTC:
result = bytesFromSMTC;
break;
case AlbumArtSearchProvider.iTunes:
result = await SearchiTunesAsync(artist, album);
break;
default:
break;
}
if (result != null) return result;
}
return null;
}
private byte[]? SearchFile(string artist, string album)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), album, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
}
}
}
}
return null;
}
private async Task<byte[]?> SearchiTunesAsync(string artist, string album)
{
// Source: https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce
try
{
string format = ".jpg";
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(artist, album, format, PathHelper.iTunesAlbumArtCacheDirectory);
if (cachedAlbumArt != null)
{
return cachedAlbumArt;
}
// Build the iTunes API URL
string url = $"https://itunes.apple.com/search?term=" + artist + "+" + album + "&country=" + LanguageHelper.DetectCountryCode(album + artist) + "&entity=album";
url.Replace(" ", "-");
// Make a request to the API
HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
// Parse the JSON response
var data = JsonSerializer.Deserialize(responseBody, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
{
// Get the first result
var result = results[0];
if (result.TryGetProperty("artworkUrl100", out var artworkUrlProp))
{
string artworkUrl = artworkUrlProp.GetString()?.Replace("100x100bb.jpg", "1200x1200bb.jpg") ?? string.Empty;
var fetched = await _iTunesHttpClinet.GetByteArrayAsync(artworkUrl);
if (fetched != null && fetched.Length > 0)
{
// Write to cache
FileHelper.WriteAlbumArtCache(artist, album, fetched, format, PathHelper.iTunesAlbumArtCacheDirectory);
return fetched;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching iTunes album art for {Artist} - {Album}", artist, album);
}
return null;
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public interface IAlbumArtSearchService
{
Task<byte[]?> SearchAsync(string title, string artist, string album, byte[]? bytesFromSMTC = null);
}
}

View File

@@ -0,0 +1,14 @@
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services
{
public interface ILyricsSearchService
{
Task<string?> SearchAsync(string title, string artist, string album, double durationMs, CancellationToken token);
}
}

View File

@@ -1,22 +0,0 @@
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services
{
public interface IMusicSearchService
{
Task<byte[]?> SearchAlbumArtAsync(string title, string artist, string album, byte[]? bytesFromSMTC = null);
Task<string?> SearchLyricsAsync(
string title,
string artist,
string album,
double durationMs,
CancellationToken token
);
}
}

View File

@@ -74,6 +74,9 @@ namespace BetterLyrics.WinUI3.Services
List<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; }
List<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; }
EasingType LyricsScrollEasingType { get; set; }
int LyricsScrollDuration { get; set; }
int LyricsVerticalEdgeOpacity { get; set; }
bool IgnoreFullscreenWindow { get; set; }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public interface ILibreTranslateService
public interface ITranslateService
{
Task<string> TranslateAsync(string text, string targetLangCode, CancellationToken? token);
}

View File

@@ -18,27 +18,25 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class MusicSearchService : IMusicSearchService
public class LyricsSearchService : ILyricsSearchService
{
private readonly HttpClient _amllTtmlDbHttpClient;
private readonly HttpClient _lrcLibHttpClient;
private readonly HttpClient _iTunesHttpClinet;
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public MusicSearchService(ISettingsService settingsService)
public LyricsSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_logger = Ioc.Default.GetRequiredService<ILogger<MusicSearchService>>();
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsSearchService>>();
_lrcLibHttpClient = new();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
$"{MetadataHelper.AppName} {MetadataHelper.AppVersion} ({MetadataHelper.GithubUrl})"
);
_amllTtmlDbHttpClient = new();
_iTunesHttpClinet = new();
}
public async Task<bool> DownloadAmllTtmlDbIndexAsync()
@@ -51,7 +49,7 @@ namespace BetterLyrics.WinUI3.Services
await using var stream = await response.Content.ReadAsStreamAsync();
await using var fs = new FileStream(
AppInfo.AmllTtmlDbIndexPath,
PathHelper.AmllTtmlDbIndexPath,
FileMode.Create,
FileAccess.Write,
FileShare.None
@@ -66,126 +64,7 @@ namespace BetterLyrics.WinUI3.Services
}
}
private static string GuessCountryCode(string album, string artist)
{
string s = album + artist;
if (s.Any(c => c >= 0x4e00 && c <= 0x9fff)) // 中文
return "cn";
if (s.Any(c => (c >= 0x3040 && c <= 0x30ff) || (c >= 0x31f0 && c <= 0x31ff))) // 日文
return "jp";
if (s.Any(c => c >= 0xac00 && c <= 0xd7af)) // 韩文
return "kr";
if (s.Any(c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) // 英文
return "us";
// 其他情况
return "us";
}
public async Task<byte[]?> SearchAlbumArtAsync(string title, string artist, string album, byte[]? bytesFromSMTC = null)
{
byte[]? result = null;
foreach (var provider in _settingsService.AlbumArtSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchLocalAlbumArt(artist, album);
break;
case AlbumArtSearchProvider.SMTC:
result = bytesFromSMTC;
break;
case AlbumArtSearchProvider.iTunes:
result = await SearchiTunesAlbumArtAsync(artist, album);
break;
default:
break;
}
if (result != null) return result;
}
return null;
}
private byte[]? SearchLocalAlbumArt(string artist, string album)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), album, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
}
}
}
}
return null;
}
private async Task<byte[]?> SearchiTunesAlbumArtAsync(string artist, string album)
{
// Source: https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce
try
{
string format = ".jpg";
var cachedAlbumArt = ReadAlbumArtCache(artist, album, format, AppInfo.iTunesAlbumArtCacheDirectory);
if (cachedAlbumArt != null)
{
return cachedAlbumArt;
}
// Build the iTunes API URL
string url = $"https://itunes.apple.com/search?term=" + artist + "+" + album + "&country=" + GuessCountryCode(album, artist) + "&entity=album";
url.Replace(" ", "-");
// Make a request to the API
HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
// Parse the JSON response
var data = JsonSerializer.Deserialize(responseBody, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
{
// Get the first result
var result = results[0];
if (result.TryGetProperty("artworkUrl100", out var artworkUrlProp))
{
string artworkUrl = artworkUrlProp.GetString()?.Replace("100x100bb.jpg", "1200x1200bb.jpg") ?? string.Empty;
var fetched = await _iTunesHttpClinet.GetByteArrayAsync(artworkUrl);
if (fetched != null && fetched.Length > 0)
{
// Write to cache
WriteAlbumArtCache(artist, album, fetched, format, AppInfo.iTunesAlbumArtCacheDirectory);
return fetched;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching iTunes album art for {Artist} - {Album}", artist, album);
}
return null;
}
public async Task<string?> SearchLyricsAsync(string title, string artist, string album, double durationMs, CancellationToken token)
public async Task<string?> SearchAsync(string title, string artist, string album, double durationMs, CancellationToken token)
{
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
@@ -202,7 +81,7 @@ namespace BetterLyrics.WinUI3.Services
// Check cache first
if (provider.Provider.IsRemote())
{
cachedLyrics = ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
cachedLyrics = FileHelper.ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return cachedLyrics;
@@ -215,11 +94,11 @@ namespace BetterLyrics.WinUI3.Services
{
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = LocalLyricsSearchInMusicFiles(title, artist);
searchedLyrics = SearchEmbedded(title, artist);
}
else
{
searchedLyrics = await LocalLyricsSearchInLyricsFiles(title, artist, lyricsFormat);
searchedLyrics = await SearchFile(title, artist, lyricsFormat);
}
}
else
@@ -230,13 +109,13 @@ namespace BetterLyrics.WinUI3.Services
searchedLyrics = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
break;
case LyricsSearchProvider.QQ:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, Searchers.QQMusic);
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, Searchers.Netease);
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
@@ -252,7 +131,7 @@ namespace BetterLyrics.WinUI3.Services
{
if (provider.Provider.IsRemote())
{
WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
FileHelper.WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
}
return searchedLyrics;
@@ -262,36 +141,7 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private static bool MusicMatch(string fileName, string title, string artist)
{
var normFileName = Normalize(fileName);
var normTitle = Normalize(title);
var normArtist = Normalize(artist);
// 常见两种顺序
return normFileName == normTitle + normArtist
|| normFileName == normArtist + normTitle;
}
// 预处理:去除空格、括号、下划线、横杠、点、大小写等
static string Normalize(string s) =>
new string(s
.Where(c => char.IsLetterOrDigit(c))
.ToArray())
.ToLowerInvariant();
private static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
private async Task<string?> LocalLyricsSearchInLyricsFiles(string title, string artist, LyricsFormat format)
private async Task<string?> SearchFile(string title, string artist, LyricsFormat format)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
@@ -299,7 +149,7 @@ namespace BetterLyrics.WinUI3.Services
{
foreach (var file in Directory.GetFiles(folder.Path, $"*{format.ToFileExtension()}", SearchOption.AllDirectories))
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
@@ -313,7 +163,7 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private string? LocalLyricsSearchInMusicFiles(string title, string artist)
private string? SearchEmbedded(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
@@ -321,7 +171,7 @@ namespace BetterLyrics.WinUI3.Services
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
try
{
@@ -340,42 +190,18 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private string? ReadLyricsCache(string title, string artist, LyricsFormat format, string cacheFolderPath)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(cacheFolderPath, $"{safeArtist} - {safeTitle}{format.ToFileExtension()}");
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
private byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var safeArtist = SanitizeFileName(artist);
var safeAlbum = SanitizeFileName(album);
var cacheFilePath = Path.Combine(cacheFolderPath, $"{safeArtist} - {safeAlbum}{format}");
if (File.Exists(cacheFilePath))
{
return File.ReadAllBytes(cacheFilePath);
}
return null;
}
private async Task<string?> SearchAmllTtmlDbAsync(string title, string artist)
{
// 检索本地 JSONL 索引文件,查找 rawLyricFile
if (!File.Exists(AppInfo.AmllTtmlDbIndexPath))
if (!File.Exists(PathHelper.AmllTtmlDbIndexPath))
{
var downloadOk = await DownloadAmllTtmlDbIndexAsync();
if (!downloadOk || !File.Exists(AppInfo.AmllTtmlDbIndexPath))
if (!downloadOk || !File.Exists(PathHelper.AmllTtmlDbIndexPath))
return null;
}
string? rawLyricFile = null;
await foreach (var line in File.ReadLinesAsync(AppInfo.AmllTtmlDbIndexPath))
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
{
if (string.IsNullOrWhiteSpace(line))
continue;
@@ -401,7 +227,7 @@ namespace BetterLyrics.WinUI3.Services
if (musicName == null || artists == null)
continue;
if (MusicMatch($"{artists} - {musicName}", title, artist))
if (FileHelper.IsSwitchableNormalizedMatch($"{artists} - {musicName}", title, artist))
{
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
{
@@ -465,13 +291,7 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private async Task<string?> SearchUsingLyricifyAsync(
string title,
string artist,
string album,
int durationMs,
Searchers searchers
)
private static async Task<string?> SearchQQNeteaseKugouAsync(string title, string artist, string album, int durationMs, Searchers searchers)
{
var result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
@@ -508,39 +328,5 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private void WriteLyricsCache(
string title,
string artist,
string lyrics,
LyricsFormat format,
string cacheFolderPath
)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
cacheFolderPath,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
}
private void WriteAlbumArtCache(
string album,
string artist,
byte[] img,
string format,
string cacheFolderPath
)
{
var safeArtist = SanitizeFileName(artist);
var safeAlbum = SanitizeFileName(album);
var cacheFilePath = Path.Combine(
cacheFolderPath,
$"{safeArtist} - {safeAlbum}{format}"
);
File.WriteAllBytes(cacheFilePath, img);
}
}
}

View File

@@ -27,7 +27,7 @@ namespace BetterLyrics.WinUI3.Services
IRecipient<PropertyChangedMessage<ObservableCollection<MediaSourceProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<AlbumArtSearchProviderInfo>>>
{
private readonly IMusicSearchService _musicSearchService;
private readonly IAlbumArtSearchService _albumArtSearchService;
private readonly ILogger<PlaybackService> _logger;
private readonly MediaManager _mediaManager = new();
private readonly LatestOnlyTaskRunner _AlbumArtRefreshRunner = new();
@@ -44,9 +44,9 @@ namespace BetterLyrics.WinUI3.Services
public event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
public event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
public PlaybackService(ISettingsService settingsService, IMusicSearchService musicSearchService) : base(settingsService)
public PlaybackService(ISettingsService settingsService, IAlbumArtSearchService albumArtSearchService) : base(settingsService)
{
_musicSearchService = musicSearchService;
_albumArtSearchService = albumArtSearchService;
_logger = Ioc.Default.GetRequiredService<ILogger<PlaybackService>>();
_mediaSourceProvidersInfo = _settingsService.MediaSourceProvidersInfo;
@@ -222,7 +222,7 @@ namespace BetterLyrics.WinUI3.Services
return;
}
byte[]? bytes = await _musicSearchService.SearchAlbumArtAsync(
byte[]? bytes = await _albumArtSearchService.SearchAsync(
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo?.Album ?? string.Empty,

View File

@@ -74,11 +74,12 @@ namespace BetterLyrics.WinUI3.Services
private const string SelectedTargetLanguageIndexKey = "SelectedTargetLanguageIndex";
private const string LyricsBackgroundThemeKey = "LyricsBackgroundTheme";
private const string IgnoreFullscreenWindowKey = "IgnoreFullscreenWindow";
private const string PreferredDisplayTypeKey = "PreferredDisplayTypeKey";
private const string LyricsScrollEasingTypeKey = "LyricsScrollEasingType";
private const string LyricsScrollDurationKey = "LyricsScrollDuration";
private readonly ApplicationDataContainer _localSettings;
public SettingsService()
@@ -184,10 +185,23 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(SelectedTargetLanguageIndexKey, 6);
SetDefault(LyricsFontStrokeWidthKey, 3);
SetDefault(IgnoreFullscreenWindowKey, false);
SetDefault(PreferredDisplayTypeKey, (int)LyricsDisplayType.SplitView);
SetDefault(LyricsScrollEasingTypeKey, (int)EasingType.EaseInOutQuad);
SetDefault(LyricsScrollDurationKey, 500); // 500ms
}
public EasingType LyricsScrollEasingType
{
get => (EasingType)GetValue<int>(LyricsScrollEasingTypeKey);
set => SetValue(LyricsScrollEasingTypeKey, (int)value);
}
public int LyricsScrollDuration
{
get => GetValue<int>(LyricsScrollDurationKey);
set => SetValue(LyricsScrollDurationKey, value);
}
public LyricsDisplayType PreferredDisplayType

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using Lyricify.Lyrics.Helpers.General;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -12,13 +13,13 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class LibreTranslateService : ILibreTranslateService
public class TranslateService : ITranslateService
{
private readonly ISettingsService _settingsService;
private readonly HttpClient _httpClient;
public LibreTranslateService(ISettingsService settingsService)
public TranslateService(ISettingsService settingsService)
{
_settingsService = settingsService;
_httpClient = new HttpClient();
@@ -31,7 +32,19 @@ namespace BetterLyrics.WinUI3.Services
throw new ArgumentException("Text and target language must be provided.");
}
string? originalLangCode = LanguageDetectionHelper.DetectLanguageCode(text);
string? originalLangCode = LanguageHelper.DetectLanguageCode(text);
if (string.IsNullOrWhiteSpace(originalLangCode) || originalLangCode == targetLangCode)
{
return text; // No translation needed
}
else if (originalLangCode == "zh-Hant" && targetLangCode == "zh-Hans")
{
return ChineseConverter.ConvertToSimplifiedChinese(text);
}
else if (originalLangCode == "zh-Hans" && targetLangCode == "zh-Hant")
{
return ChineseConverter.ConvertToTraditionalChinese(text);
}
var url = $"{_settingsService.LibreTranslateServer}/translate";
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(

View File

@@ -526,7 +526,7 @@
<value>Exit</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>Unlock the window (Restart needed)</value>
<value>Unlock the window</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Lock</value>
@@ -675,4 +675,46 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>Media library</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>Lyrics scrolling animation type</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>Lyrics scrolling animation duration</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Linear</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>Smooth step</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Ease-in-out sine</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Ease-in-out quad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Ease-in-out elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Ease-in-out back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Ease-in-out bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Ease-in-out circ</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Ease-in-out expo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Ease-in-out quint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Ease-in-out quart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Ease-in-out cubic</value>
</data>
</root>

View File

@@ -526,7 +526,7 @@
<value>プログラムを終了します</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>ウィンドウのロックを解除する(再起動が必要)</value>
<value>ウィンドウのロックを解除します</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>ロック</value>
@@ -675,4 +675,46 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞スクロールアニメーションタイプ</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌詞スクロールアニメーションの期間</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>リニア</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>スムーズなステップ</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>サインがゆっくりと出入りします</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>セカンダリスローインとアウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>弾力性は内外に遅くなります</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>リバウンドはスローアウトで遅くなります</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>ゆっくりと出入りします</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>丸い、ゆっくりと出入り</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>インデックスは内外に遅くなります</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>5つの遅いインとアウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>4つの遅いインとアウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>3つの遅いインとアウト</value>
</data>
</root>

View File

@@ -526,7 +526,7 @@
<value>프로그램을 종료하십시오</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>창 잠금 해제 (다시 시작)</value>
<value>창 잠금 해제하십시오</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>잠금</value>
@@ -675,4 +675,46 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>미디어 라이브러리</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>가사 스크롤링 애니메이션 유형</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>가사 스크롤링 애니메이션 지속 시간</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>선의</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>부드러운 단계</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>천천히 입력하십시오</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>이차 느린 속도가 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>탄력성이 속도가 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>리바운드는 느리게 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>천천히 안팎으로 튀어 나옵니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>둥글고 느리게 안팎으로</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>인덱스 속도가 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>5 번 느리게 안팎으로</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>4 개의 느린 안팎</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>세 번 느리게 안팎으로</value>
</data>
</root>

View File

@@ -526,7 +526,7 @@
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>解锁窗口(需要重新启动)</value>
<value>解锁窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>锁定</value>
@@ -675,4 +675,46 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒体库</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌词滚动动画类型</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌词滚动动画持续时间</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>线性</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>平滑步进</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>正弦缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>二次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>弹性缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>回弹缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>弹跳缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>圆形缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>指数缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>五次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>四次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>三次缓入缓出</value>
</data>
</root>

View File

@@ -526,7 +526,7 @@
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>解鎖窗口(需要重新啟動)</value>
<value>解鎖窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>鎖定</value>
@@ -675,4 +675,46 @@
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒體庫</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞滾動動畫類型</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌詞滾動動畫持續時間</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>線性</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>平滑步進</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>正弦緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>二次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>彈性緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>回彈緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>彈跳緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>圓形緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>指數緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>五次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>四次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>三次緩入緩出</value>
</data>
</root>

View File

@@ -0,0 +1,62 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
public LyricsRendererViewModel(ISettingsService settingsService, IPlaybackService playbackService, ILyricsSearchService musicSearchService, ILibWatcherService libWatcherService, ITranslateService libreTranslateService) : base(settingsService)
{
_lyrcsSearchService = musicSearchService;
_playbackService = playbackService;
_libWatcherService = libWatcherService;
_translateService = libreTranslateService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
_albumArtCornerRadius = _settingsService.CoverImageRadius;
_isDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
_albumArtBgOpacity = _settingsService.CoverOverlayOpacity;
_albumArtBgBlurAmount = _settingsService.CoverOverlayBlurAmount;
_lyricsBgFontColorType = _settingsService.LyricsBgFontColorType;
_lyricsFgFontColorType = _settingsService.LyricsFgFontColorType;
_lyricsTextFormat.FontWeight = _settingsService.LyricsFontWeight.ToFontWeight();
_lyricsAlignmentType = _settingsService.LyricsAlignmentType;
_lyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
_lyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
_lyricsFontSize = _settingsService.LyricsFontSize;
_lyricsBlurAmount = _settingsService.LyricsBlurAmount;
_isLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
_lyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
_customBgFontColor = _settingsService.LyricsCustomBgFontColor;
_customFgFontColor = _settingsService.LyricsCustomFgFontColor;
_lyricsBgTheme = _settingsService.LyricsBackgroundTheme;
_isFanLyricsEnabled = _settingsService.IsFanLyricsEnabled;
_lyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
_isTranslationEnabled = _settingsService.IsTranslationEnabled;
_targetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
_canvasYScrollTransition.SetDuration(_settingsService.LyricsScrollDuration / 1000f);
_canvasYScrollTransition.SetEasingType(_settingsService.LyricsScrollEasingType);
_libWatcherService.MusicLibraryFilesChanged +=
LibWatcherService_MusicLibraryFilesChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.AlbumArtChangedChanged += PlaybackService_AlbumArtChangedChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
UpdateFontColor();
}
}
}

View File

@@ -44,22 +44,24 @@ namespace BetterLyrics.WinUI3.ViewModels
using var combined = new CanvasCommandList(control);
using var combinedDs = combined.CreateDrawingSession();
DrawAlbumArtBackground(control, combinedDs);
if (_isDockMode)
{
DrawImmersiveBackground(control, combinedDs);
DrawImmersiveBackground(control, combinedDs, 0f);
}
combinedDs.DrawImage(blurredLyrics);
if (_isDesktopMode)
else if (_isDesktopMode)
{
ds.DrawImage(blurredLyrics);
DrawImmersiveBackground(control, combinedDs, 12f);
}
else
{
ds.DrawImage(combined);
DrawAlbumArtBackground(control, combinedDs);
}
combinedDs.DrawImage(blurredLyrics);
ds.DrawImage(combined);
DrawAlbumArt(control, ds);
DrawTitleAndArtist(control, ds);
@@ -78,29 +80,29 @@ namespace BetterLyrics.WinUI3.ViewModels
out float charProgress
);
//ds.DrawText(
// $"[DEBUG]\n" +
// $"Cur playing {_playingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n" +
// $"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
// $"Cur time {_totalTime + _positionOffset}\n" +
// $"Lang size {_multiLangLyrics.Count}\n" +
// $"Song duration {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}",
// new Vector2(10, 10),
// ThemeTypeSent == Microsoft.UI.Xaml.ElementTheme.Light ? Colors.Black : Colors.White
//);
ds.DrawText(
$"[DEBUG]\n" +
$"Cur playing {_playingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n" +
$"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
$"Cur time {_totalTime + _positionOffset}\n" +
$"Lang size {_multiLangLyrics.Count}\n" +
$"Song duration {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}",
new Vector2(10, 10),
ThemeTypeSent == Microsoft.UI.Xaml.ElementTheme.Light ? Colors.Black : Colors.White
);
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
LyricsLine? line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line != null)
{
ds.DrawText(
$"[{i}] {line.OriginalText} {line.HighlightOpacityTransition.Value}",
new Vector2(10, 30 + (i - _startVisibleLineIndex) * 20),
ThemeTypeSent == ElementTheme.Light ? Colors.Black : Colors.White
);
}
}
//for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
//{
// LyricsLine? line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
// if (line != null)
// {
// ds.DrawText(
// $"[{i}] {line.OriginalText} {line.HighlightOpacityTransition.Value}",
// new Vector2(10, 30 + (i - _startVisibleLineIndex) * 20),
// ThemeTypeSent == ElementTheme.Light ? Colors.Black : Colors.White
// );
// }
//}
}
}
}
@@ -110,7 +112,8 @@ namespace BetterLyrics.WinUI3.ViewModels
float imageWidth = (float)canvasBitmap.Size.Width;
float imageHeight = (float)canvasBitmap.Size.Height;
float scaleFactor = MathF.Sqrt(MathF.Pow(_canvasWidth, 2) + MathF.Pow(_canvasHeight, 2)) / MathF.Min(imageWidth, imageHeight);
float targetSize = MathF.Sqrt(MathF.Pow(_canvasWidth, 2) + MathF.Pow(_canvasHeight, 2)) * 1.4f;
float scaleFactor = targetSize / MathF.Min(imageWidth, imageHeight);
float x = _canvasWidth / 2 - imageWidth * scaleFactor / 2;
float y = _canvasHeight / 2 - imageHeight * scaleFactor / 2;
@@ -193,11 +196,13 @@ namespace BetterLyrics.WinUI3.ViewModels
using var coverOverlayEffect = new OpacityEffect
{
Opacity = CoverOverlayOpacity / 100f,
Opacity = _albumArtBgOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = CoverOverlayBlurAmount,
BlurAmount = _albumArtBgBlurAmount,
Source = overlappedCovers,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Quality,
},
};
ds.DrawImage(coverOverlayEffect);
@@ -307,7 +312,7 @@ namespace BetterLyrics.WinUI3.ViewModels
float centerX = position.X;
float centerY = position.Y + layoutHeight / 2;
switch (LyricsAlignmentType)
switch (_lyricsAlignmentType)
{
case TextAlignmentType.Left:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
@@ -473,13 +478,13 @@ namespace BetterLyrics.WinUI3.ViewModels
{
Source = new BlendEffect
{
Background = IsLyricsGlowEffectEnabled
Background = _isLyricsGlowEffectEnabled
? new GaussianBlurEffect
{
Source = new AlphaMaskEffect
{
Source = fgLyrics,
AlphaMask = LyricsGlowEffectScope switch
AlphaMask = _lyricsGlowEffectScope switch
{
LineRenderingType.UntilCurrentChar => mask,
LineRenderingType.CurrentCharOnly => highlightMask,
@@ -506,41 +511,21 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
private void DrawImmersiveBackground(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
bool withGradient = true
)
private void DrawImmersiveBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds, float radius)
{
ds.FillRectangle(
CanvasCommandList list = new(control.Device);
using var listDs = list.CreateDrawingSession();
listDs.FillRoundedRectangle(
new Rect(0, 0, _canvasWidth, _canvasHeight),
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, _canvasHeight),
}
radius,
radius,
_immersiveBgTransition.Value
);
ds.DrawImage(new OpacityEffect
{
Source = list,
Opacity = _immersiveBgOpacityTransition.Value
});
}
private CanvasLinearGradientBrush GetHorizontalFillBrush(

View File

@@ -21,6 +21,7 @@ namespace BetterLyrics.WinUI3.ViewModels
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<LineRenderingType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<EasingType>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
{
@@ -64,7 +65,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(SettingsPageViewModel.IsDynamicCoverOverlayEnabled))
{
IsDynamicCoverOverlayEnabled = message.NewValue;
_isDynamicCoverOverlayEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsDebugOverlayEnabled))
{
@@ -72,7 +73,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsLyricsGlowEffectEnabled))
{
IsLyricsGlowEffectEnabled = message.NewValue;
_isLyricsGlowEffectEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsFanLyricsEnabled))
{
@@ -92,6 +93,15 @@ namespace BetterLyrics.WinUI3.ViewModels
_isDesktopMode = message.NewValue;
UpdateFontColor();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsLyricsWindowLocked))
{
_isLyricsWindowLocked = message.NewValue;
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsMouseWithinWindow))
{
_isMouseWithinWindow = message.NewValue;
_immersiveBgOpacityTransition.StartTransition(_isDesktopMode ? (_isMouseWithinWindow ? 1f : 0f) : 1f);
}
}
else if (message.Sender is LyricsPageViewModel)
{
@@ -141,7 +151,8 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsLineSpacingFactor))
{
LyricsLineSpacingFactor = message.NewValue;
_lyricsLineSpacingFactor = message.NewValue;
_isLayoutChanged = true;
}
}
}
@@ -156,25 +167,26 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayOpacity))
{
CoverOverlayOpacity = message.NewValue;
_albumArtBgOpacity = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayBlurAmount))
{
CoverOverlayBlurAmount = message.NewValue;
_albumArtBgBlurAmount = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsVerticalEdgeOpacity))
{
LyricsVerticalEdgeOpacity = message.NewValue;
_lyricsVerticalEdgeOpacity = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBlurAmount))
{
LyricsBlurAmount = message.NewValue;
_lyricsBlurAmount = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
_lyricsFontSize = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SelectedTargetLanguageIndex))
{
@@ -186,6 +198,10 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_lyricsFontStrokeWidth = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsScrollDuration))
{
_canvasYScrollTransition.SetDuration(message.NewValue / 1000f);
}
}
else if (message.Sender is LyricsPageViewModel)
{
@@ -202,7 +218,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsGlowEffectScope))
{
LyricsGlowEffectScope = message.NewValue;
_lyricsGlowEffectScope = message.NewValue;
}
}
}
@@ -213,7 +229,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsAlignmentType))
{
LyricsAlignmentType = message.NewValue;
_lyricsAlignmentType = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SongInfoAlignmentType))
{
@@ -256,7 +272,8 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontWeight))
{
LyricsFontWeight = message.NewValue;
_lyricsTextFormat.FontWeight = message.NewValue.ToFontWeight();
_isLayoutChanged = true;
}
}
}
@@ -273,20 +290,15 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
partial void OnLyricsFontSizeChanged(int value)
public void Receive(PropertyChangedMessage<EasingType> message)
{
_isLayoutChanged = true;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_lyricsTextFormat.FontWeight = value.ToFontWeight();
_isLayoutChanged = true;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_isLayoutChanged = true;
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsScrollEasingType))
{
_canvasYScrollTransition.SetEasingType(message.NewValue);
}
}
}
}
}

View File

@@ -14,8 +14,8 @@ namespace BetterLyrics.WinUI3.ViewModels
{
private readonly ValueTransition<float> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.8f,
easingType: EasingType.EaseInOutSine
durationSeconds: 0.5f,
easingType: EasingType.EaseInOutCubic
);
private readonly ValueTransition<Color> _immersiveBgTransition = new(
@@ -24,6 +24,11 @@ namespace BetterLyrics.WinUI3.ViewModels
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<float> _immersiveBgOpacityTransition = new(
initialValue: 1f,
durationSeconds: 0.2f
);
private readonly ValueTransition<float> _lyricsXTransition = new(
initialValue: 0f,
durationSeconds: 0.3f

View File

@@ -43,12 +43,13 @@ namespace BetterLyrics.WinUI3.ViewModels
_displayType = _displayTypeReceived;
_playingLineIndex = playingLineIndex;
_immersiveBgOpacityTransition.Update(_elapsedTime);
_immersiveBgTransition.Update(_elapsedTime);
_albumArtBgTransition.Update(_elapsedTime);
_lyricsBgBrightnessTransition.Update(_elapsedTime);
_songInfoOpacityTransition.Update(_elapsedTime);
if (IsDynamicCoverOverlayEnabled)
if (_isDynamicCoverOverlayEnabled)
{
_rotateAngle += _coverRotateSpeed;
_rotateAngle %= MathF.PI * 2;
@@ -129,7 +130,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (control == null)
return;
_lyricsTextFormat.FontSize = LyricsFontSize;
_lyricsTextFormat.FontSize = _lyricsFontSize;
float y = 0;
@@ -163,7 +164,7 @@ namespace BetterLyrics.WinUI3.ViewModels
y +=
(float)line.CanvasTextLayout.LayoutBounds.Height
/ line.CanvasTextLayout.LineCount
* (line.CanvasTextLayout.LineCount + LyricsLineSpacingFactor);
* (line.CanvasTextLayout.LineCount + _lyricsLineSpacingFactor);
}
}
@@ -384,9 +385,9 @@ namespace BetterLyrics.WinUI3.ViewModels
: 0
);
line.BlurAmountTransition.StartTransition(LyricsBlurAmount * distanceFactor);
line.BlurAmountTransition.StartTransition(_lyricsBlurAmount * distanceFactor);
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale));
line.OpacityTransition.StartTransition(_defaultOpacity - distanceFactor * _defaultOpacity * (1 - LyricsVerticalEdgeOpacity / 100f));
line.OpacityTransition.StartTransition(_defaultOpacity - distanceFactor * _defaultOpacity * (1 - _lyricsVerticalEdgeOpacity / 100f));
line.HighlightOpacityTransition.StartTransition(i == _playingLineIndex ? 1f : 0f);
}

View File

@@ -64,14 +64,29 @@ namespace BetterLyrics.WinUI3.ViewModels
private readonly float _coverRotateSpeed = 0.003f;
private float _rotateAngle = 0f;
private TextAlignmentType _lyricsAlignmentType;
private readonly float _lyricsGlowEffectAmount = 8f;
private int _lyricsBlurAmount;
private int _lyricsVerticalEdgeOpacity;
private ElementTheme _lyricsBgTheme;
private LineRenderingType _lyricsGlowEffectScope;
private int _lyricsFontStrokeWidth;
private int _lyricsFontSize;
private float _lyricsLineSpacingFactor;
private LyricsFontColorType _lyricsBgFontColorType;
private LyricsFontColorType _lyricsFgFontColorType;
private LyricsFontColorType _lyricsStrokeFontColorType;
private float _maxLyricsWidth = 0f;
private readonly IMusicSearchService _musicSearchService;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
private readonly ILibreTranslateService _libreTranslateService;
private readonly ITranslateService _translateService;
private readonly ILogger _logger;
private readonly float _leftMargin = 36f;
@@ -97,14 +112,6 @@ namespace BetterLyrics.WinUI3.ViewModels
private Color? _customFgFontColor;
private Color? _customStrokeFontColor;
private LyricsFontColorType _lyricsBgFontColorType;
private LyricsFontColorType _lyricsFgFontColorType;
private LyricsFontColorType _lyricsStrokeFontColorType;
private ElementTheme _lyricsBgTheme;
private int _lyricsFontStrokeWidth;
private int _playingLineIndex = -1;
private int _startVisibleLineIndex = -1;
@@ -117,6 +124,12 @@ namespace BetterLyrics.WinUI3.ViewModels
private bool _isPlaying = true;
private bool _isLyricsWindowLocked = false;
private bool _isMouseWithinWindow = false;
private bool _isDynamicCoverOverlayEnabled;
private bool _isLyricsGlowEffectEnabled;
private bool _isLayoutChanged = true;
private int _langIndex = 0;
@@ -153,106 +166,14 @@ namespace BetterLyrics.WinUI3.ViewModels
private LatestOnlyTaskRunner _refreshLyricsRunner = new();
private LatestOnlyTaskRunner _showTranslationsRunner = new();
public LyricsRendererViewModel(ISettingsService settingsService, IPlaybackService playbackService, IMusicSearchService musicSearchService, ILibWatcherService libWatcherService, ILibreTranslateService libreTranslateService) : base(settingsService)
{
_musicSearchService = musicSearchService;
_playbackService = playbackService;
_libWatcherService = libWatcherService;
_libreTranslateService = libreTranslateService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
_albumArtCornerRadius = _settingsService.CoverImageRadius;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
_lyricsBgFontColorType = _settingsService.LyricsBgFontColorType;
_lyricsFgFontColorType = _settingsService.LyricsFgFontColorType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
_customBgFontColor = _settingsService.LyricsCustomBgFontColor;
_customFgFontColor = _settingsService.LyricsCustomFgFontColor;
_lyricsBgTheme = _settingsService.LyricsBackgroundTheme;
_isFanLyricsEnabled = _settingsService.IsFanLyricsEnabled;
_lyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
_isTranslationEnabled = _settingsService.IsTranslationEnabled;
_targetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
_libWatcherService.MusicLibraryFilesChanged +=
LibWatcherService_MusicLibraryFilesChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.AlbumArtChangedChanged += _playbackService_AlbumArtChangedChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
UpdateFontColor();
}
private void _playbackService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
{
if (e.AlbumArtSwBitmap != _albumArtSwBitmap)
{
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
_lastAlbumArtCanvasBitmap = null;
_albumArtSwBitmap = e.AlbumArtSwBitmap;
_albumArtCanvasBitmap = null;
_albumArtAccentColor = e.AlbumArtAccentColor;
_albumArtBgTransition.Reset(0f);
_albumArtBgTransition.StartTransition(1f);
UpdateFontColor();
}
}
[ObservableProperty]
public partial bool IsTranslating { get; set; } = false;
public int CoverOverlayBlurAmount { get; set; }
public int CoverOverlayOpacity { get; set; }
private LyricsDisplayType _displayTypeReceived = LyricsDisplayType.PlaceholderOnly;
private LyricsDisplayType _displayType = LyricsDisplayType.PlaceholderOnly;
public bool IsDynamicCoverOverlayEnabled { get; set; }
public bool IsLyricsGlowEffectEnabled { get; set; }
public TextAlignmentType LyricsAlignmentType { get; set; }
public int LyricsBlurAmount { get; set; }
private int _albumArtBgBlurAmount;
private int _albumArtBgOpacity;
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
public LineRenderingType LyricsGlowEffectScope { get; set; }
[ObservableProperty]
public partial float LyricsLineSpacingFactor { get; set; }
public int LyricsVerticalEdgeOpacity { get; set; }
public partial bool IsTranslating { get; set; } = false;
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; }
@@ -390,6 +311,26 @@ namespace BetterLyrics.WinUI3.ViewModels
{
await RefreshLyricsAsync(token);
});
_totalTime = TimeSpan.Zero;
}
}
private void PlaybackService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
{
if (e.AlbumArtSwBitmap != _albumArtSwBitmap)
{
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
_lastAlbumArtCanvasBitmap = null;
_albumArtSwBitmap = e.AlbumArtSwBitmap;
_albumArtCanvasBitmap = null;
_albumArtAccentColor = e.AlbumArtAccentColor;
_albumArtBgTransition.Reset(0f);
_albumArtBgTransition.StartTransition(1f);
UpdateFontColor();
}
}
@@ -414,9 +355,9 @@ namespace BetterLyrics.WinUI3.ViewModels
private async Task ShowTranslationsAsync(CancellationToken token)
{
_logger.LogInformation("Showing translation for lyrics...");
string targetLangCode = AppInfo.TranslationLanguagesInfo[_settingsService.SelectedTargetLanguageIndex].Code;
string targetLangCode = LanguageHelper.SupportedTargetLanguages[_settingsService.SelectedTargetLanguageIndex].Code;
var originalText = string.Join("\n", _multiLangLyrics.FirstOrDefault()?.Select(x => x.OriginalText) ?? []);
string? originalLangCode = LanguageDetectionHelper.DetectLanguageCode(originalText);
string? originalLangCode = LanguageHelper.DetectLanguageCode(originalText);
if (originalLangCode == targetLangCode)
{
@@ -432,7 +373,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
var translationList = langLyrics.Select(x => x.OriginalText).ToList();
var translation = string.Join("\n", translationList);
if (LanguageDetectionHelper.DetectLanguageCode(translation) == targetLangCode)
if (LanguageHelper.DetectLanguageCode(translation) == targetLangCode)
{
_translationList = translationList;
break;
@@ -454,7 +395,7 @@ namespace BetterLyrics.WinUI3.ViewModels
return;
}
var translated = await _libreTranslateService.TranslateAsync(originalText, targetLangCode, token);
var translated = await _translateService.TranslateAsync(originalText, targetLangCode, token);
token.ThrowIfCancellationRequested();
_translationList = translated.Split('\n').ToList();
@@ -492,7 +433,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (SongInfo != null)
{
lyricsRaw = await _musicSearchService.SearchLyricsAsync(
lyricsRaw = await _lyrcsSearchService.SearchAsync(
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",

View File

@@ -26,7 +26,7 @@ namespace BetterLyrics.WinUI3
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<bool>>
{
private ForegroundWindowWatcherHelper? _watcherHelper = null;
private ForegroundWindowWatcher? _watcherHelper = null;
public LyricsWindowViewModel(ISettingsService settingsService) : base(settingsService)
{
@@ -49,9 +49,6 @@ namespace BetterLyrics.WinUI3
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsWindowLocked { get; set; } = false;
[ObservableProperty]
public partial Notification Notification { get; set; } = new();
[ObservableProperty]
public partial bool ShowInfoBar { get; set; } = false;
@@ -64,6 +61,10 @@ namespace BetterLyrics.WinUI3
[ObservableProperty]
public partial double TitleBarHeight { get; set; } = 36;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsMouseWithinWindow { get; set; } = false;
private bool _ignoreFullscreenWindow = false;
public void Receive(PropertyChangedMessage<bool> message)
@@ -115,13 +116,13 @@ namespace BetterLyrics.WinUI3
}
}
public void StartWatchWindowColorChange(WindowColorSampleMode mode)
public void StartWatchWindowColorChange(WindowPixelSampleMode mode)
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
_watcherHelper = new ForegroundWindowWatcherHelper(
_watcherHelper = new ForegroundWindowWatcher(
hwnd,
onWindowChanged =>
{
@@ -136,9 +137,9 @@ namespace BetterLyrics.WinUI3
UpdateAccentColor(hwnd, mode);
}
public void UpdateAccentColor(nint hwnd, WindowColorSampleMode mode)
public void UpdateAccentColor(nint hwnd, WindowPixelSampleMode mode)
{
ActivatedWindowAccentColor = WindowColorHelper.GetDominantColor(hwnd, mode).ToColor();
ActivatedWindowAccentColor = Helper.ColorHelper.GetAccentColor(hwnd, mode).ToColor();
}
[RelayCommand]
@@ -147,9 +148,8 @@ namespace BetterLyrics.WinUI3
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
DesktopModeHelper.Lock(window);
DesktopModeHelper.SetClickThrough(window, true);
IsLyricsWindowLocked = true;
StartWatchWindowColorChange(WindowColorSampleMode.WindowEdge);
}
private void StopWatchWindowColorChange()
@@ -170,6 +170,7 @@ namespace BetterLyrics.WinUI3
if (IsDesktopMode)
{
DesktopModeHelper.Enable(window);
StartWatchWindowColorChange(WindowPixelSampleMode.WindowEdge);
}
else
{
@@ -189,7 +190,7 @@ namespace BetterLyrics.WinUI3
if (IsDockMode)
{
DockModeHelper.Enable(window, _settingsService.LyricsFontSize * 4);
StartWatchWindowColorChange(WindowColorSampleMode.BelowWindow);
StartWatchWindowColorChange(WindowPixelSampleMode.BelowWindow);
}
else
{

View File

@@ -25,7 +25,7 @@ using Windows.System;
using Windows.UI;
using Windows.UI.Popups;
using WinRT.Interop;
using AppInfo = BetterLyrics.WinUI3.Helper.AppInfo;
using MetadataHelper = BetterLyrics.WinUI3.Helper.MetadataHelper;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -33,11 +33,11 @@ namespace BetterLyrics.WinUI3.ViewModels
{
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
private readonly ILibreTranslateService _libreTranslateService;
private readonly ITranslateService _libreTranslateService;
private readonly string _autoStartupTaskId = "AutoStartup";
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, IPlaybackService playbackService, ILibreTranslateService libreTranslateService) : base(settingsService)
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, IPlaybackService playbackService, ITranslateService libreTranslateService) : base(settingsService)
{
_libWatcherService = libWatcherService;
_playbackService = playbackService;
@@ -80,18 +80,18 @@ namespace BetterLyrics.WinUI3.ViewModels
LyricsCustomStrokeFontColor = _settingsService.LyricsCustomStrokeFontColor;
LyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
LyricsBackgroundTheme = _settingsService.LyricsBackgroundTheme;
MediaSourceProvidersInfo = [.. _settingsService.MediaSourceProvidersInfo];
IgnoreFullscreenWindow = _settingsService.IgnoreFullscreenWindow;
LyricsScrollEasingType = _settingsService.LyricsScrollEasingType;
LyricsScrollDuration = _settingsService.LyricsScrollDuration;
_playbackService.MediaSourceProvidersInfoChanged += PlaybackService_SessionIdsChanged;
Task.Run(async () =>
{
BuildDate = (await Helper.AppInfo.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
BuildDate = (await Helper.MetadataHelper.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
});
}
@@ -218,7 +218,7 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; }
public string Version { get; set; } = Helper.AppInfo.AppVersion;
public string Version { get; set; } = Helper.MetadataHelper.AppVersion;
public string BuildDate { get; set; } = string.Empty;
@@ -240,6 +240,24 @@ namespace BetterLyrics.WinUI3.ViewModels
[NotifyPropertyChangedRecipients]
public partial bool IgnoreFullscreenWindow { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial EasingType LyricsScrollEasingType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsScrollDuration { get; set; }
partial void OnLyricsScrollEasingTypeChanged(EasingType value)
{
_settingsService.LyricsScrollEasingType = value;
}
partial void OnLyricsScrollDurationChanged(int value)
{
_settingsService.LyricsScrollDuration = value;
}
partial void OnLyricsBackgroundThemeChanged(ElementTheme value)
{
_settingsService.LyricsBackgroundTheme = value;
@@ -349,13 +367,13 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(Helper.AppInfo.GithubUrl));
await Launcher.LaunchUriAsync(new Uri(MetadataHelper.GithubUrl));
}
[RelayCommand]
private void OpenCacheFolder()
{
OpenFolderInFileExplorer(Helper.AppInfo.CacheFolder);
OpenFolderInFileExplorer(PathHelper.CacheFolder);
}
private void OpenFolderInFileExplorer(string path)
@@ -410,7 +428,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
try
{
string targetLangCode = AppInfo.TranslationLanguagesInfo[SelectedTargetLanguageIndex].Code;
string targetLangCode = LanguageHelper.SupportedTargetLanguages[SelectedTargetLanguageIndex].Code;
string result = await _libreTranslateService.TranslateAsync("Hello, world!", targetLangCode, null);
_dispatcherQueue.TryEnqueue(() =>
{

View File

@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial bool IsLyricsWindowLocked { get; set; } = false;
[ObservableProperty]
public partial string ToolTipText { get; set; } = AppInfo.AppName;
public partial string ToolTipText { get; set; } = MetadataHelper.AppName;
public void Receive(PropertyChangedMessage<bool> message)
{
@@ -52,7 +52,7 @@ namespace BetterLyrics.WinUI3.ViewModels
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
DesktopModeHelper.Unlock(window);
DesktopModeHelper.SetClickThrough(window, false);
IsLyricsWindowLocked = false;
}
}

View File

@@ -12,19 +12,20 @@
xmlns:scontrols="using:ShadowViewer.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid x:Name="RootGrid" RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<Grid
x:Name="RootGrid"
PointerEntered="RootGrid_PointerEntered"
PointerExited="RootGrid_PointerExited"
RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<local:LyricsPage />
<!-- Top command -->
<Grid
x:Name="TopCommandGrid"
Margin="6"
VerticalAlignment="Top"
Background="Transparent"
Opacity="0"
PointerEntered="TopCommandGrid_PointerEntered"
PointerExited="TopCommandGrid_PointerExited">
@@ -113,7 +114,7 @@
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE654;" />
Glyph="&#xE738;" />
</Button>
<!-- Window Maximise -->
<Button
@@ -123,7 +124,7 @@
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE655;" />
Glyph="&#xE71A;" />
</Button>
<!-- Window Restore -->
<Button
@@ -134,7 +135,7 @@
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE656;" />
Glyph="&#xE744;" />
</Button>
<!-- Window Close -->
<Button
@@ -144,7 +145,7 @@
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE653;" />
Glyph="&#xE711;" />
</Button>
</StackPanel>

View File

@@ -267,5 +267,15 @@ namespace BetterLyrics.WinUI3.Views
{
App.Current.LyricsWindowNotificationPanel = TipContainerCenter;
}
private void RootGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
{
ViewModel.IsMouseWithinWindow = true;
}
private void RootGrid_PointerExited(object sender, PointerRoutedEventArgs e)
{
ViewModel.IsMouseWithinWindow = false;
}
}
}

View File

@@ -673,6 +673,47 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.IsFanLyricsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageScrollEasing"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xECE7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.LyricsScrollEasingType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingTypeLinear" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeSmoothStep" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutSine" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuad" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCubic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuart" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuint" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutExpo" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCirc" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBack" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutElastic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBounce" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageScrollDuration">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.LyricsScrollDuration, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" ms" />
<Slider
Maximum="1000"
Minimum="100"
SnapsTo="Ticks"
StepFrequency="100"
TickFrequency="100"
TickPlacement="Outside"
Value="{x:Bind ViewModel.LyricsScrollDuration, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</StackPanel>
</controls:Case>