Compare commits

..

93 Commits

Author SHA1 Message Date
Zhe Fang
5e74468194 Merge pull request #8 from jayfunc/dev
add multiple online lyrics providers; add desktop mode; improve blur/shadow/scrolling effect performance; fix bugs
2025-06-26 21:51:36 -04:00
Zhe Fang
ff65429b16 add desktop mode; fix 2025-06-26 21:48:07 -04:00
Zhe Fang
ab03870b6a add: support qq music, kugou music, netease music as lyrics providers 2025-06-26 14:43:06 -04:00
Zhe Fang
23bafc4d75 chore: split renderer viewmodel 2025-06-26 08:30:19 -04:00
Zhe Fang
3bdce0d975 fix: dock mode; improve lyrics blur effect 2025-06-24 19:31:17 -04:00
Zhe Fang
454edbeaba change opacity effect rendering method 2025-06-24 16:18:47 -04:00
Zhe Fang
1e7e63032a chore: format code 2025-06-23 16:26:19 -04:00
Zhe Fang
a93b535667 Merge pull request #7 from jayfunc/dev
update to v1.0.5.0
2025-06-23 13:41:33 -04:00
Zhe Fang
0eca011054 fix 2025-06-23 13:37:35 -04:00
Zhe Fang
68b7601b0f update readme 2025-06-22 23:14:17 -04:00
Zhe Fang
894fe935a5 fix: listview can not be placed in settingsexapnder 2025-06-22 23:13:10 -04:00
Zhe Fang
0befdf48dd add support for ttml format 2025-06-22 22:38:03 -04:00
Zhe Fang
827602766d add: advanced lrc support 2025-06-22 21:59:26 -04:00
Zhe Fang
11c3002b77 add: x animation for lyrics; update: y scroll animation 2025-06-22 08:46:39 -04:00
Zhe Fang
9d193b7b71 add: support lyrics search provider reorder 2025-06-21 14:21:04 -04:00
Zhe Fang
811cd760d4 add: support char by char animation in real condition (not mock) (only work woth QQ music lyrics source selected) 2025-06-21 12:45:29 -04:00
Zhe Fang
fe5039db78 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-21 08:29:33 -04:00
Zhe Fang
a42a3cdb88 fix: incorrect aot state after switching back to standard mode 2025-06-21 08:29:30 -04:00
Zhe Fang
ee003e1764 Create README.CN.md 2025-06-20 23:00:58 -04:00
Zhe Fang
749ab2ca1a Update README.md 2025-06-20 22:48:27 -04:00
Zhe Fang
4bbde71bfa Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-20 22:38:33 -04:00
Zhe Fang
7f3fbda237 remove unused deps 2025-06-20 22:38:26 -04:00
Zhe Fang
06d2e19ee2 update README.md 2025-06-20 22:29:39 -04:00
Zhe Fang
cbcb140bec update README.md 2025-06-20 22:28:57 -04:00
Zhe Fang
bc8fd4de31 fix: add title and artist as the first line lyrics 2025-06-20 21:03:43 -04:00
Zhe Fang
9e8bc3b7df Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-20 19:17:24 -04:00
Zhe Fang
f9070eed5d add: qq music lyrics provider 2025-06-20 19:17:17 -04:00
Zhe Fang
447533db12 Update README.md 2025-06-19 22:24:38 -04:00
Zhe Fang
1fe8675743 update: credit in ColorThief.cs file 2025-06-19 18:02:36 -04:00
Zhe Fang
6775f9af57 fix: title bar not hide when switching between standard mode and dock mode 2025-06-19 16:39:42 -04:00
Zhe Fang
bf9107754d update: readme 2025-06-19 16:32:25 -04:00
Zhe Fang
906d8d7d49 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-19 16:25:19 -04:00
Zhe Fang
517e026ca9 add: support get lyrics from web (thanks https://lrclib.net/); fix: unstable titlebar show and hide animation 2025-06-19 16:25:14 -04:00
Zhe Fang
9535306a92 Update README.md 2025-06-18 21:46:05 -04:00
Zhe Fang
222ac42357 update: readme 2025-06-18 19:58:34 -04:00
Zhe Fang
2c55b11e70 Merge pull request #6 from jayfunc/dev
fix
2025-06-18 17:16:06 -04:00
Zhe Fang
703fc26ceb Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-18 17:07:58 -04:00
Zhe Fang
52ae59154a fix: add lyrics folder 2025-06-18 17:07:56 -04:00
Zhe Fang
82f22ac5d1 chore: update README.md 2025-06-18 05:51:08 -04:00
Zhe Fang
f0f74ba195 Delete .github/workflows/dotnet-desktop.yml 2025-06-17 21:53:48 -04:00
Zhe Fang
7bca1d1205 Merge pull request #5 from jayfunc/dev
Add dock mode, improve glow effect, fix bugs ...
2025-06-17 21:50:22 -04:00
Zhe Fang
8e5e35ec23 Merge branch 'stable' into dev 2025-06-17 21:49:58 -04:00
Zhe Fang
0a63b6d8cd fix: language 2025-06-17 21:47:17 -04:00
Zhe Fang
8b01e47c8e fix: bugs; change: glow effect 2025-06-17 21:34:21 -04:00
Zhe Fang
66d8705066 change: remove system tray, combine desktop and in-app lyrics mode into one single window 2025-06-17 14:01:52 -04:00
Zhe Fang
aa1f9f071f fix: bugs about switching between desktop mode and in-app mode; add: system tray icon; todo: add lyrics entry in system tray; limit window opening counts (pre-release now) 2025-06-16 21:57:56 -04:00
Zhe Fang
81c2f45f95 add: system tray entry 2025-06-16 11:57:10 -04:00
Zhe Fang
d5ae75e5a4 chore: update readme 2025-06-15 22:59:05 -04:00
Zhe Fang
3ca2b90eff refactor: restructure project to follow MVVM architecture (changes are still in progress, more bugs may be existed) 2025-06-15 22:33:32 -04:00
Zhe Fang
d1e48e95b7 fix: treat empty lyrics content in metadata as a mark for successfully resolve lyrics 2025-06-14 18:01:29 -04:00
Zhe Fang
12b78b374c fix: dynamic background on desktop lyrics not working after re-open it 2025-06-14 15:56:20 -04:00
Zhe Fang
cd857a2807 add: auto adaptive (immersive) background for desktop lyrics; fix: change get method (cached in memory if not changed) for settings viewmodel; fix: incorrect display position for desktop lyrics 2025-06-14 13:14:38 -04:00
Zhe Fang
def2c9820a fix: sometimes incorrect album art background scale 2025-06-13 19:22:56 -04:00
Zhe Fang
63c0577e73 chore: temporarily stop github actions 2025-06-13 16:34:32 -04:00
Zhe Fang
e028ec2f0f feat: no lyrics placeholder 2025-06-13 16:31:47 -04:00
Zhe Fang
4258ab6957 feat: add desktop lyrics support 2025-06-13 14:33:10 -04:00
Zhe Fang
894081b097 fix: update artifact upload path to specify the correct package directory 2025-06-12 11:58:23 -04:00
Zhe Fang
772f41b236 fix: update artifact upload path to use github.workspace 2025-06-12 11:49:36 -04:00
Zhe Fang
e6db08c593 fix: refactor pfx path handling and improve msbuild command readability 2025-06-12 11:37:25 -04:00
Zhe Fang
ea4a4ad072 fix: update certificate path in workflow and remove temporary key file 2025-06-12 11:27:48 -04:00
Zhe Fang
b4bd479d3c fix: update .NET version and action versions in workflow configuration 2025-06-12 10:57:12 -04:00
Zhe Fang
9745b7e558 fix: update solution name in workflow configuration 2025-06-12 10:55:31 -04:00
Zhe Fang
eb666fd8f2 fix: update branch triggers for workflow to stable and dev 2025-06-12 10:54:42 -04:00
Zhe Fang
2404c54bb6 feat: add temporary key file for package signing 2025-06-12 10:53:51 -04:00
Zhe Fang
221cd67c39 fix: update .gitignore to exclude temporary key file for package signing 2025-06-12 10:53:41 -04:00
Zhe Fang
7c311972b5 chore: update dotnet-desktop.yml for improved workflow configuration 2025-06-12 07:36:55 -04:00
Zhe Fang
9b42aebb56 feat: add temporary key file for package signing 2025-06-12 07:36:43 -04:00
Zhe Fang
e96320c9ad fix: specify PackageCertificateKeyFile in msbuild command for app package creation 2025-06-12 07:30:15 -04:00
Zhe Fang
fdafb96852 fix: update publish profiles and build configuration for ARM64, x64, and x86 platforms 2025-06-12 07:23:29 -04:00
Zhe Fang
cf5e4a7e8c feat: add ARM64 publish profile for better deployment support 2025-06-12 07:23:03 -04:00
Zhe Fang
4b4651bf6e Update dotnet-desktop.yml 2025-06-12 06:39:55 -04:00
Zhe Fang
a3e7503537 Update dotnet-desktop.yml 2025-06-12 06:18:39 -04:00
Zhe Fang
30fab426df add .pubxml 2025-06-11 22:37:25 -04:00
Zhe Fang
7746a04bd9 fix: update MSIX package upload path in GitHub Actions workflow 2025-06-11 22:30:28 -04:00
Zhe Fang
a437f382cb fix: update MSIX package creation settings for store upload 2025-06-11 22:18:29 -04:00
Zhe Fang
852741bbfc add: implement GitHub Actions workflow for building and packaging WinUI 3 MSIX application 2025-06-11 22:08:09 -04:00
Zhe Fang
7d839c655f support desktop lyrics 2025-06-11 22:03:50 -04:00
Zhe Fang
ba8aad9831 Update dotnet-desktop.yml 2025-06-11 10:22:38 -04:00
Zhe Fang
2970b5e246 Update dotnet-desktop.yml 2025-06-11 10:19:05 -04:00
Zhe Fang
80a68b2612 Update dotnet-desktop.yml 2025-06-11 10:16:47 -04:00
Zhe Fang
088d2fa78b Update dotnet-desktop.yml 2025-06-11 10:16:31 -04:00
Zhe Fang
a4bc63d352 Update dotnet-desktop.yml 2025-06-11 10:08:18 -04:00
Zhe Fang
e8c428614a Update dotnet-desktop.yml 2025-06-11 10:01:30 -04:00
Zhe Fang
4f336282bb Update dotnet-desktop.yml 2025-06-11 09:54:03 -04:00
Zhe Fang
74daa48536 Update dotnet-desktop.yml 2025-06-11 09:51:54 -04:00
Zhe Fang
3dc14e52d8 Update dotnet-desktop.yml 2025-06-11 09:42:48 -04:00
Zhe Fang
1a736c13d5 Delete nuget.config 2025-06-11 09:36:58 -04:00
Zhe Fang
377d68d83c Update dotnet-desktop.yml 2025-06-11 09:33:40 -04:00
Zhe Fang
f512e686b0 Create nuget.config 2025-06-11 09:27:09 -04:00
Zhe Fang
004dcbb4f4 Create dotnet-desktop.yml 2025-06-11 09:19:00 -04:00
Zhe Fang
1bfad8740c update README.md 2025-06-11 05:47:11 -04:00
Zhe Fang
5b71f44bf3 fix 2025-06-10 22:37:32 -04:00
Zhe Fang
81651abfec Split and reorganize Service ViewModel Renderer 2025-06-10 20:39:29 -04:00
138 changed files with 13817 additions and 4322 deletions

3
.gitignore vendored
View File

@@ -404,4 +404,5 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
*.sln.iml
*.sln.iml
/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package)_TemporaryKey.pfx

View File

@@ -11,7 +11,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.2.0" />
Version="1.0.6.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
@@ -30,6 +30,8 @@
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Applications>

View File

@@ -5,7 +5,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:local="using:BetterLyrics.WinUI3">
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:media="using:CommunityToolkit.WinUI.Media">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -40,8 +41,12 @@
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:ThemeTypeToElementThemeConverter x:Key="ThemeTypeToElementThemeConverter" />
<converter:EnumToIntConverter x:Key="EnumToIntConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />

View File

@@ -1,23 +1,23 @@
using System;
using System.Collections.Generic;
using System.IO;
// 2025/6/23 by Zhe Fang
using System;
using System.Text;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Services.BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.AppLifecycle;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Windows.ApplicationModel.Core;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -25,24 +25,23 @@ using Windows.ApplicationModel.Core;
namespace BetterLyrics.WinUI3
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// Provides application-specific behavior to supplement the default Application class
/// </summary>
public partial class App : Application
{
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
public BaseWindow? MainWindow { get; private set; }
public BaseWindow? SettingsWindow { get; set; }
public static ResourceLoader? ResourceLoader { get; private set; }
public static DispatcherQueue? DispatcherQueue { get; private set; }
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
#region Fields
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// Defines the _logger
/// </summary>
private readonly ILogger<App> _logger;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// </summary>
public App()
{
@@ -52,19 +51,63 @@ namespace BetterLyrics.WinUI3
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
Helper.AppInfo.EnsureDirectories();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
AppInfo.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetService<ILogger<App>>()!;
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
#endregion
#region Properties
/// <summary>
/// Gets the Current
/// </summary>
public static new App Current => (App)Application.Current;
/// <summary>
/// Gets the DispatcherQueue
/// </summary>
public static DispatcherQueue? DispatcherQueue { get; private set; }
/// <summary>
/// Gets the DispatcherQueueTimer
/// </summary>
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
/// <summary>
/// Gets the ResourceLoader
/// </summary>
public static ResourceLoader? ResourceLoader { get; private set; }
#endregion
#region Methods
/// <summary>
/// Invoked when the application is launched
/// </summary>
/// <param name="args">Details about the launch request and process</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenLyricsWindow();
}
/// <summary>
/// The ConfigureServices
/// </summary>
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(Helper.AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.WriteTo.File(AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
@@ -76,16 +119,26 @@ namespace BetterLyrics.WinUI3
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
// ViewModels
.AddSingleton<BaseWindowModel>()
.AddSingleton<MainViewModel>()
.AddTransient<HostWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<LyricsSettingsControlViewModel>()
.BuildServiceProvider()
);
}
/// <summary>
/// The App_UnhandledException
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="Microsoft.UI.Xaml.UnhandledExceptionEventArgs"/></param>
private void App_UnhandledException(
object sender,
Microsoft.UI.Xaml.UnhandledExceptionEventArgs e
@@ -96,17 +149,44 @@ namespace BetterLyrics.WinUI3
}
/// <summary>
/// Invoked when the application is launched.
/// The CurrentDomain_FirstChanceException
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
/// <param name="sender">The sender<see cref="object?"/></param>
/// <param name="e">The e<see cref="System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs"/></param>
private void CurrentDomain_FirstChanceException(
object? sender,
System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e
)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Activate the window
MainWindow = new BaseWindow();
MainWindow!.Navigate(typeof(MainPage));
MainWindow.Activate();
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
/// <summary>
/// The CurrentDomain_UnhandledException
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="System.UnhandledExceptionEventArgs"/></param>
private void CurrentDomain_UnhandledException(
object sender,
System.UnhandledExceptionEventArgs e
)
{
_logger.LogError(e.ExceptionObject.ToString(), "CurrentDomain_UnhandledException");
}
/// <summary>
/// The TaskScheduler_UnobservedTaskException
/// </summary>
/// <param name="sender">The sender<see cref="object?"/></param>
/// <param name="e">The e<see cref="UnobservedTaskExceptionEventArgs"/></param>
private void TaskScheduler_UnobservedTaskException(
object? sender,
UnobservedTaskExceptionEventArgs e
)
{
//_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
#endregion
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -8,7 +8,20 @@
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="ViewModels\Lyrics\**" />
<Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Controls\DependenciesSettingsExpander.xaml" />
<None Remove="Controls\SystemTray.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
@@ -17,41 +30,62 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference
Include="CommunityToolkit.Labs.WinUI.OpacityMaskView"
Version="0.1.250513-build.2126"
/>
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference
Include="CommunityToolkit.WinUI.Controls.SettingsControls"
Version="8.2.250402"
/>
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="DevWinUI" Version="8.3.0" />
<PackageReference Include="DevWinUI.Controls" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<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" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="z440.atl.core" Version="6.24.0" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\DesktopLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\SystemTray.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\DependenciesSettingsExpander.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
@@ -61,8 +95,21 @@
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<DefineConstants>$(DefineConstants)</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShouldCreateLogs>True</ShouldCreateLogs>
<AdvancedSettingsExpanded>True</AdvancedSettingsExpanded>
<UpdateAssemblyVersion>False</UpdateAssemblyVersion>
<UpdateAssemblyFileVersion>False</UpdateAssemblyFileVersion>
<UpdateAssemblyInfoVersion>False</UpdateAssemblyInfoVersion>
<UpdatePackageVersion>True</UpdatePackageVersion>
<AssemblyInfoVersionType>SettingsVersion</AssemblyInfoVersionType>
<InheritWinAppVersionFrom>AssemblyVersion</InheritWinAppVersionFrom>
<PackageVersionSettings>AssemblyVersion.None.None</PackageVersionSettings>
<Version>2025.6.0</Version>
<AssemblyVersion>2025.6.18.0110</AssemblyVersion>
<FileVersion>2025.6.18.0110</FileVersion>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.DependenciesSettingsExpander"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<controls:SettingsExpander
x:Uid="DependenciesSettingsExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard Header="CommunityToolkit.Labs.WinUI.MarqueeText">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.Labs.WinUI.MarqueeText" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.Labs.WinUI.MarqueeText" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.Labs.WinUI.OpacityMaskView">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.Labs.WinUI.OpacityMaskView" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.Labs.WinUI.OpacityMaskView" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.Mvvm">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.Mvvm" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.Mvvm" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Behaviors">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Behaviors" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Behaviors" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Controls.Primitives">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.Primitives" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.Primitives" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Controls.Segmented">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.Segmented" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.Segmented" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Controls.SettingsControls">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.SettingsControls" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.SettingsControls" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Converters">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Converters" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Converters" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Extensions">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Extensions" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Extensions" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Helpers">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Helpers" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Helpers" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="CommunityToolkit.WinUI.Media">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/CommunityToolkit.WinUI.Media" NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Media" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="H.NotifyIcon.WinUI">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/H.NotifyIcon.WinUI" NavigateUri="https://www.nuget.org/packages/H.NotifyIcon.WinUI" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Lyricify.Lyrics.Helper-NativeAot">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Lyricify.Lyrics.Helper-NativeAot" NavigateUri="https://www.nuget.org/packages/Lyricify.Lyrics.Helper-NativeAot" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Microsoft.Extensions.DependencyInjection">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection" NavigateUri="https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Microsoft.Extensions.Logging">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Microsoft.Extensions.Logging" NavigateUri="https://www.nuget.org/packages/Microsoft.Extensions.Logging" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Microsoft.Graphics.Win2D">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Microsoft.Graphics.Win2D" NavigateUri="https://www.nuget.org/packages/Microsoft.Graphics.Win2D" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Microsoft.Windows.SDK.BuildTools">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools" NavigateUri="https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Microsoft.WindowsAppSDK">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Microsoft.WindowsAppSDK" NavigateUri="https://www.nuget.org/packages/Microsoft.WindowsAppSDK" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Microsoft.Xaml.Behaviors.WinUI.Managed">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.WinUI.Managed" NavigateUri="https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.WinUI.Managed" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Serilog.Extensions.Logging">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Serilog.Extensions.Logging" NavigateUri="https://www.nuget.org/packages/Serilog.Extensions.Logging" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Serilog.Sinks.File">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Serilog.Sinks.File" NavigateUri="https://www.nuget.org/packages/Serilog.Sinks.File" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="System.Drawing.Common">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/System.Drawing.Common" NavigateUri="https://www.nuget.org/packages/System.Drawing.Common" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="System.Text.Encoding.CodePages">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/System.Text.Encoding.CodePages" NavigateUri="https://www.nuget.org/packages/System.Text.Encoding.CodePages" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="TagLibSharp">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/TagLibSharp" NavigateUri="https://www.nuget.org/packages/TagLibSharp" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="Ude.NetStandard">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/Ude.NetStandard" NavigateUri="https://www.nuget.org/packages/Ude.NetStandard" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="WinUIEx">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/WinUIEx" NavigateUri="https://www.nuget.org/packages/WinUIEx" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard Header="z440.atl.core">
<controls:SettingsCard.Description>
<HyperlinkButton Content="https://www.nuget.org/packages/z440.atl.core" NavigateUri="https://www.nuget.org/packages/z440.atl.core" />
</controls:SettingsCard.Description>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</UserControl>

View File

@@ -0,0 +1,28 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class DependenciesSettingsExpander : UserControl
{
public DependenciesSettingsExpander()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.SystemTray"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tb="using:H.NotifyIcon"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<tb:TaskbarIcon
x:Name="TrayIcon"
x:FieldModifier="public"
ContextMenuMode="SecondWindow"
IconSource="ms-appx:///Assets/Logo.ico"
NoLeftClickDelay="True"
ToolTipText="{x:Bind ViewModel.ToolTipText, Mode=OneWay}">
<tb:TaskbarIcon.ContextFlyout>
<MenuFlyout
AreOpenCloseAnimationsEnabled="True"
LightDismissOverlayMode="On"
ShowMode="TransientWithDismissOnPointerMoveAway">
<MenuFlyoutItem x:Uid="SystemTraySettings" Command="{x:Bind ViewModel.OpenSettingsCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayExit" Command="{x:Bind ViewModel.ExitAppCommand}" />
<MenuFlyoutItem
x:Uid="SystemTrayUnlock"
Command="{x:Bind ViewModel.UnlockWindowCommand}"
Visibility="{x:Bind ViewModel.IsLyricsWindowLocked, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</MenuFlyout>
</tb:TaskbarIcon.ContextFlyout>
</tb:TaskbarIcon>
</UserControl>

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class SystemTray : UserControl
{
public SystemTrayViewModel ViewModel => (SystemTrayViewModel)DataContext;
public SystemTray()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<SystemTrayViewModel>();
}
}
}

View File

@@ -1,16 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace BetterLyrics.WinUI3.Converter
{
public class ColorToBrushConverter : IValueConverter
/// <summary>
/// Defines the <see cref="ColorToBrushConverter" />
/// </summary>
public partial class ColorToBrushConverter : IValueConverter
{
#region Methods
/// <summary>
/// The Convert
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Color color)
@@ -20,9 +31,19 @@ namespace BetterLyrics.WinUI3.Converter
return new SolidColorBrush();
}
/// <summary>
/// The ConvertBack
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -3,25 +3,25 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
internal class ThemeTypeToElementThemeConverter : IValueConverter
internal partial class CornerRadiusToDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int themeType)
if (value is Microsoft.UI.Xaml.CornerRadius cornerRadius)
{
return (ElementTheme)themeType;
// Convert CornerRadius to an integer value, e.g., using the top-left radius
return (double)cornerRadius.TopLeft;
}
return ElementTheme.Default;
return .0; // or handle the case where value is not a CornerRadius
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return 0;
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,51 @@
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
/// <summary>
/// Defines the <see cref="EnumToIntConverter" />
/// </summary>
internal partial class EnumToIntConverter : IValueConverter
{
#region Methods
/// <summary>
/// The Convert
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Enum)
{
return System.Convert.ToInt32(value);
}
return 0;
}
/// <summary>
/// The ConvertBack
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is int && targetType.IsEnum)
{
return Enum.ToObject(targetType, value);
}
return Enum.ToObject(targetType, 0);
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
/// <summary>
/// Defines the <see cref="IntToCornerRadius" />
/// </summary>
public partial class IntToCornerRadius : IValueConverter
{
#region Methods
/// <summary>
/// The Convert
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int intValue && parameter is double controlHeight)
{
return new Microsoft.UI.Xaml.CornerRadius(intValue / 100f * controlHeight / 2);
}
return new Microsoft.UI.Xaml.CornerRadius(0);
}
/// <summary>
/// The ConvertBack
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -0,0 +1,78 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
/// <summary>
/// Defines the <see cref="LyricsSearchProviderToDisplayNameConverter" />
/// </summary>
public partial class LyricsSearchProviderToDisplayNameConverter : IValueConverter
{
#region Methods
/// <summary>
/// The Convert
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString(
"LyricsSearchProviderLrcLib"
),
LyricsSearchProvider.QQ => App.ResourceLoader!.GetString(
"LyricsSearchProviderQQ"
),
LyricsSearchProvider.Netease => App.ResourceLoader!.GetString(
"LyricsSearchProviderNetease"
),
LyricsSearchProvider.Kugou => App.ResourceLoader!.GetString(
"LyricsSearchProviderKugou"
),
LyricsSearchProvider.AmllTtmlDb => App.ResourceLoader!.GetString(
"LyricsSearchProviderAmllTtmlDb"
),
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalLrcFile"
),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalMusicFile"
),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderEslrcFile"
),
LyricsSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderTtmlFile"
),
_ => "",
};
}
return "";
}
/// <summary>
/// The ConvertBack
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -0,0 +1,55 @@
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
/// <summary>
/// Defines the <see cref="MatchedLocalFilesPathToVisibilityConverter" />
/// </summary>
public partial class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
{
#region Methods
/// <summary>
/// The Convert
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string path)
{
if (path == App.ResourceLoader!.GetString("MainPageNoLocalFilesMatched"))
{
return Visibility.Collapsed;
}
else
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
/// <summary>
/// The ConvertBack
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
/// <param name="targetType">The targetType<see cref="Type"/></param>
/// <param name="parameter">The parameter<see cref="object"/></param>
/// <param name="language">The language<see cref="string"/></param>
/// <returns>The <see cref="object"/></returns>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -0,0 +1,24 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the AutoStartWindowType
/// </summary>
public enum AutoStartWindowType
{
/// <summary>
/// Defines the StandardMode
/// </summary>
StandardMode,
/// <summary>
/// Defines the DockMode
/// </summary>
DockMode,
}
#endregion
}

View File

@@ -0,0 +1,45 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the BackdropType
/// </summary>
public enum BackdropType
{
/// <summary>
/// Defines the None
/// </summary>
None = 0,
/// <summary>
/// Defines the Mica
/// </summary>
Mica = 1,
/// <summary>
/// Defines the MicaAlt
/// </summary>
MicaAlt = 2,
/// <summary>
/// Defines the DesktopAcrylic
/// </summary>
DesktopAcrylic = 3,
/// <summary>
/// Defines the Transparent
/// </summary>
Transparent = 4,
}
#endregion
}

View File

@@ -0,0 +1,39 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the EasingType
/// </summary>
public enum EasingType
{
/// <summary>
/// Defines the EaseInOutQuad
/// </summary>
EaseInOutQuad,
/// <summary>
/// Defines the EaseInQuad
/// </summary>
EaseInQuad,
/// <summary>
/// Defines the EaseOutQuad
/// </summary>
EaseOutQuad,
/// <summary>
/// Defines the Linear
/// </summary>
Linear,
/// <summary>
/// Defines the SmootherStep
/// </summary>
SmootherStep,
}
#endregion
}

View File

@@ -0,0 +1,50 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the Language
/// </summary>
public enum Language
{
/// <summary>
/// Defines the FollowSystem
/// </summary>
FollowSystem,
/// <summary>
/// Defines the English
/// </summary>
English,
/// <summary>
/// Defines the SimplifiedChinese
/// </summary>
SimplifiedChinese,
/// <summary>
/// Defines the TraditionalChinese
/// </summary>
TraditionalChinese,
/// <summary>
/// Defines the Japanese
/// </summary>
Japanese,
/// <summary>
/// Defines the Korean
/// </summary>
Korean,
}
#endregion
}

View File

@@ -4,11 +4,11 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Enums
{
public enum TitleBarType
public enum LineMaskType
{
Compact,
Extended,
Glow,
Highlight,
}
}

View File

@@ -0,0 +1,21 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LineMaskType
/// </summary>
public enum LineRenderingType
{
UntilCurrentChar,
/// <summary>
/// Current char only
/// </summary>
CurrentCharOnly,
}
#endregion
}

View File

@@ -0,0 +1,30 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LocalSearchTargetProps
/// </summary>
public enum LocalSearchTargetProps
{
/// <summary>
/// Defines the LyricsOnly
/// </summary>
LyricsOnly,
/// <summary>
/// Defines the LyricsAndAlbumArt
/// </summary>
LyricsAndAlbumArt,
}
#endregion
}

View File

@@ -0,0 +1,35 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsAlignmentType
/// </summary>
public enum LyricsAlignmentType
{
/// <summary>
/// Defines the Left
/// </summary>
Left,
/// <summary>
/// Defines the Center
/// </summary>
Center,
/// <summary>
/// Defines the Right
/// </summary>
Right,
}
#endregion
}

View File

@@ -0,0 +1,34 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsDisplayType
/// </summary>
public enum LyricsDisplayType
{
/// <summary>
/// Defines the AlbumArtOnly
/// </summary>
AlbumArtOnly,
/// <summary>
/// Defines the LyricsOnly
/// </summary>
LyricsOnly,
/// <summary>
/// Defines the SplitView
/// </summary>
SplitView,
/// <summary>
/// Defines the PlaceholderOnly
/// </summary>
PlaceholderOnly,
}
#endregion
}

View File

@@ -0,0 +1,30 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsFontColorType
/// </summary>
public enum LyricsFontColorType
{
/// <summary>
/// Defines the Default
/// </summary>
Default,
/// <summary>
/// Defines the Dominant
/// </summary>
Dominant,
}
#endregion
}

View File

@@ -0,0 +1,115 @@
// 2025/6/23 by Zhe Fang
using Microsoft.UI.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsFontWeight
/// </summary>
public enum LyricsFontWeight
{
/// <summary>
/// Defines the Thin
/// </summary>
Thin,
/// <summary>
/// Defines the ExtraLight
/// </summary>
ExtraLight,
/// <summary>
/// Defines the Light
/// </summary>
Light,
/// <summary>
/// Defines the SemiLight
/// </summary>
SemiLight,
/// <summary>
/// Defines the Normal
/// </summary>
Normal,
/// <summary>
/// Defines the Medium
/// </summary>
Medium,
/// <summary>
/// Defines the SemiBold
/// </summary>
SemiBold,
/// <summary>
/// Defines the Bold
/// </summary>
Bold,
/// <summary>
/// Defines the ExtraBold
/// </summary>
ExtraBold,
/// <summary>
/// Defines the Black
/// </summary>
Black,
/// <summary>
/// Defines the ExtraBlack
/// </summary>
ExtraBlack,
}
#endregion
/// <summary>
/// Defines the <see cref="LyricsFontWeightExtensions" />
/// </summary>
public static class LyricsFontWeightExtensions
{
#region Methods
/// <summary>
/// The ToFontWeight
/// </summary>
/// <param name="weight">The weight<see cref="LyricsFontWeight"/></param>
/// <returns>The <see cref="FontWeight"/></returns>
public static FontWeight ToFontWeight(this LyricsFontWeight weight)
{
return weight switch
{
LyricsFontWeight.Thin => FontWeights.Thin,
LyricsFontWeight.ExtraLight => FontWeights.ExtraLight,
LyricsFontWeight.Light => FontWeights.Light,
LyricsFontWeight.SemiLight => FontWeights.SemiLight,
LyricsFontWeight.Normal => FontWeights.Normal,
LyricsFontWeight.Medium => FontWeights.Medium,
LyricsFontWeight.SemiBold => FontWeights.SemiBold,
LyricsFontWeight.Bold => FontWeights.Bold,
LyricsFontWeight.ExtraBold => FontWeights.ExtraBold,
LyricsFontWeight.Black => FontWeights.Black,
LyricsFontWeight.ExtraBlack => FontWeights.ExtraBlack,
LyricsFontWeight _ => throw new ArgumentOutOfRangeException(
nameof(weight),
weight,
null
),
};
}
#endregion
}
}

View File

@@ -0,0 +1,97 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsFormat
/// </summary>
public enum LyricsFormat
{
/// <summary>
/// Defines the Lrc
/// </summary>
Lrc,
/// <summary>
/// Defines the Eslrc
/// </summary>
Eslrc,
/// <summary>
/// Defines the Ttml
/// </summary>
Ttml,
Qrc,
Krc,
NotSpecified,
}
#endregion
/// <summary>
/// Defines the <see cref="LyricsFormatExtensions" />
/// </summary>
public static class LyricsFormatExtensions
{
#region Methods
/// <summary>
/// The Detect
/// </summary>
/// <param name="content">The content<see cref="string"/></param>
/// <returns>The <see cref="LyricsFormat?"/></returns>
public static LyricsFormat? DetectFormat(this string content)
{
if (
content.StartsWith("<?xml")
&& System.Text.RegularExpressions.Regex.IsMatch(content, @"<tt(:\w+)?\b")
)
{
return LyricsFormat.Ttml;
}
// 检测标准LRC和增强型LRC
else if (
System.Text.RegularExpressions.Regex.IsMatch(content, @"\[\d{1,2}:\d{2}")
|| System.Text.RegularExpressions.Regex.IsMatch(
content,
@"<\d{1,2}:\d{2}\.\d{2,3}>"
)
)
{
return LyricsFormat.Lrc;
}
else
{
return null;
}
}
/// <summary>
/// The ToFileExtension
/// </summary>
/// <param name="format">The format<see cref="LyricsFormat"/></param>
/// <returns>The <see cref="string"/></returns>
public static string ToFileExtension(this LyricsFormat format)
{
return format switch
{
LyricsFormat.Lrc => ".lrc",
LyricsFormat.Qrc => ".qrc",
LyricsFormat.Krc => ".krc",
LyricsFormat.Eslrc => ".eslrc",
LyricsFormat.Ttml => ".ttml",
_ => ".*",
};
}
#endregion
}
}

View File

@@ -1,26 +1,35 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsPlayingState
/// </summary>
public enum LyricsPlayingState
{
/// <summary>
/// Not played yet, will be playing in the future
/// Defines the NotPlayed
/// </summary>
NotPlayed,
/// <summary>
/// Playing
/// Defines the Playing
/// </summary>
Playing,
/// <summary>
/// Has already played
/// Defines the Played
/// </summary>
Played,
}
#endregion
}

View File

@@ -0,0 +1,94 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsSearchProvider
/// </summary>
public enum LyricsSearchProvider
{
QQ,
Kugou,
Netease,
LrcLib,
AmllTtmlDb,
/// <summary>
/// Defines the LocalMusicFile
/// </summary>
LocalMusicFile,
/// <summary>
/// Defines the LocalLrcFile
/// </summary>
LocalLrcFile,
/// <summary>
/// Defines the LocalEslrcFile
/// </summary>
LocalEslrcFile,
/// <summary>
/// Defines the LocalTtmlFile
/// </summary>
LocalTtmlFile,
}
public static class LyricsSearchProviderExtensions
{
/// <summary>
/// The IsLocal
/// </summary>
/// <param name="provider">The provider<see cref="LyricsSearchProvider"/></param>
/// <returns>The <see cref="bool"/></returns>
public static bool IsLocal(this LyricsSearchProvider provider)
{
return provider
is LyricsSearchProvider.LocalMusicFile
or LyricsSearchProvider.LocalLrcFile
or LyricsSearchProvider.LocalEslrcFile
or LyricsSearchProvider.LocalTtmlFile;
}
public static bool IsRemote(this LyricsSearchProvider provider)
{
return !provider.IsLocal();
}
public static string GetCacheDirectory(this LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => AppInfo.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => AppInfo.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => AppInfo.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => AppInfo.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => AppInfo.AmllTtmlDbLyricsCacheDirectory,
_ => throw new System.ArgumentOutOfRangeException(nameof(provider)),
};
}
public static LyricsFormat GetLyricsFormat(this LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
LyricsSearchProvider.QQ => LyricsFormat.Qrc,
LyricsSearchProvider.Kugou => LyricsFormat.Krc,
LyricsSearchProvider.Netease => LyricsFormat.Lrc,
LyricsSearchProvider.AmllTtmlDb => LyricsFormat.Ttml,
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
LyricsSearchProvider.LocalEslrcFile => LyricsFormat.Eslrc,
LyricsSearchProvider.LocalTtmlFile => LyricsFormat.Ttml,
_ => LyricsFormat.NotSpecified,
};
}
}
#endregion
}

View File

@@ -0,0 +1,35 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsStatus
/// </summary>
public enum LyricsStatus
{
/// <summary>
/// Defines the NotFound
/// </summary>
NotFound,
/// <summary>
/// Defines the Found
/// </summary>
Found,
/// <summary>
/// Defines the Loading
/// </summary>
Loading,
}
#endregion
}

View File

@@ -0,0 +1,30 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the LyricsType
/// </summary>
public enum LyricsType
{
/// <summary>
/// Defines the InAppLyrics
/// </summary>
InAppLyrics,
/// <summary>
/// Defines the DesktopLyrics
/// </summary>
DesktopLyrics,
}
#endregion
}

View File

@@ -0,0 +1,24 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the MusicSearchMatchMode
/// </summary>
public enum MusicSearchMatchMode
{
/// <summary>
/// Defines the TitleAndArtist
/// </summary>
TitleAndArtist,
/// <summary>
/// Defines the TitleArtistAlbumAndDuration
/// </summary>
TitleArtistAlbumAndDuration,
}
#endregion
}

View File

@@ -0,0 +1,59 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
#region Enums
/// <summary>
/// Defines the TitleBarType
/// </summary>
public enum TitleBarType
{
/// <summary>
/// Defines the Compact
/// </summary>
Compact,
/// <summary>
/// Defines the Extended
/// </summary>
Extended,
}
#endregion
/// <summary>
/// Defines the <see cref="TitleBarTypeExtensions" />
/// </summary>
public static class TitleBarTypeExtensions
{
#region Methods
/// <summary>
/// The GetHeight
/// </summary>
/// <param name="titleBarType">The titleBarType<see cref="TitleBarType"/></param>
/// <returns>The <see cref="double"/></returns>
public static double GetHeight(this TitleBarType titleBarType)
{
return titleBarType switch
{
TitleBarType.Compact => 32.0,
TitleBarType.Extended => 48.0,
_ => throw new ArgumentOutOfRangeException(
nameof(titleBarType),
titleBarType,
null
),
};
}
#endregion
}
}

View File

@@ -0,0 +1,21 @@
// 2025/6/23 by Zhe Fang
using System;
namespace BetterLyrics.WinUI3.Events
{
/// <summary>
/// Defines the <see cref="IsPlayingChangedEventArgs" />
/// </summary>
public class IsPlayingChangedEventArgs(bool isPlaying) : EventArgs
{
#region Properties
/// <summary>
/// Gets or sets a value indicating whether IsPlaying
/// </summary>
public bool IsPlaying { get; set; } = isPlaying;
#endregion
}
}

View File

@@ -0,0 +1,53 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
/// <summary>
/// Defines the <see cref="LibChangedEventArgs" />
/// </summary>
public class LibChangedEventArgs : EventArgs
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LibChangedEventArgs"/> class.
/// </summary>
/// <param name="folder">The folder<see cref="string"/></param>
/// <param name="filePath">The filePath<see cref="string"/></param>
/// <param name="changeType">The changeType<see cref="WatcherChangeTypes"/></param>
public LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType)
{
Folder = folder;
FilePath = filePath;
ChangeType = changeType;
}
#endregion
#region Properties
/// <summary>
/// Gets the ChangeType
/// </summary>
public WatcherChangeTypes ChangeType { get; }
/// <summary>
/// Gets the FilePath
/// </summary>
public string FilePath { get; }
/// <summary>
/// Gets the Folder
/// </summary>
public string Folder { get; }
#endregion
}
}

View File

@@ -0,0 +1,21 @@
// 2025/6/23 by Zhe Fang
using System;
namespace BetterLyrics.WinUI3.Events
{
/// <summary>
/// Defines the <see cref="PositionChangedEventArgs" />
/// </summary>
public class PositionChangedEventArgs(TimeSpan position) : EventArgs()
{
#region Properties
/// <summary>
/// Gets or sets the Position
/// </summary>
public TimeSpan Position { get; set; } = position;
#endregion
}
}

View File

@@ -0,0 +1,26 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
/// <summary>
/// Defines the <see cref="SongInfoChangedEventArgs" />
/// </summary>
public class SongInfoChangedEventArgs(SongInfo? songInfo) : EventArgs
{
#region Properties
/// <summary>
/// Gets or sets the SongInfo
/// </summary>
public SongInfo? SongInfo { get; set; } = songInfo;
#endregion
}
}

View File

@@ -1,13 +1,239 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Animation;
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public static class AnimationHelper
/// <summary>
/// Defines the <see cref="AnimationHelper" />
/// </summary>
public class AnimationHelper
{
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
#region Constants
/// <summary>
/// Defines the DebounceDefaultDuration
/// </summary>
public const int DebounceDefaultDuration = 200;
/// <summary>
/// Defines the StackedNotificationsShowingDuration
/// </summary>
public const int StackedNotificationsShowingDuration = 3900;
/// <summary>
/// Defines the StoryboardDefaultDuration
/// </summary>
public const int StoryboardDefaultDuration = 200;
#endregion
}
/// <summary>
/// Defines the <see cref="ValueTransition{T}" />
/// </summary>
/// <typeparam name="T"></typeparam>
public class ValueTransition<T>
where T : struct
{
#region Fields
/// <summary>
/// Defines the _currentValue
/// </summary>
private T _currentValue;
/// <summary>
/// Defines the _durationSeconds
/// </summary>
private float _durationSeconds;
/// <summary>
/// Defines the _interpolator
/// </summary>
private Func<T, T, float, T> _interpolator;
/// <summary>
/// Defines the _isTransitioning
/// </summary>
private bool _isTransitioning;
/// <summary>
/// Defines the _progress
/// </summary>
private float _progress;
/// <summary>
/// Defines the _startValue
/// </summary>
private T _startValue;
/// <summary>
/// Defines the _targetValue
/// </summary>
private T _targetValue;
private EasingType? _easingType;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ValueTransition{T}"/> class.
/// </summary>
/// <param name="initialValue">The initialValue<see cref="T"/></param>
/// <param name="durationSeconds">The durationSeconds<see cref="float"/></param>
/// <param name="interpolator">The interpolator<see cref="Func{T, T, float, T}"/></param>
public ValueTransition(
T initialValue,
float durationSeconds,
Func<T, T, float, T>? interpolator = null,
EasingType? easingType = null
)
{
_currentValue = initialValue;
_startValue = initialValue;
_targetValue = initialValue;
_durationSeconds = durationSeconds;
_progress = 1f;
_isTransitioning = false;
if (interpolator != null)
{
_interpolator = interpolator;
_easingType = null;
}
else if (easingType.HasValue)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType.Value);
}
else
{
_interpolator = GetInterpolatorByEasingType(EasingType.Linear);
_easingType = EasingType.Linear;
}
}
#endregion
#region Properties
/// <summary>
/// Gets a value indicating whether IsTransitioning
/// </summary>
public bool IsTransitioning => _isTransitioning;
/// <summary>
/// Gets the Value
/// </summary>
public T Value => _currentValue;
#endregion
#region Methods
private Func<T, T, float, T> GetInterpolatorByEasingType(EasingType type)
{
// 这里只以float为例实际可根据T类型扩展
if (typeof(T) == typeof(float))
{
return (start, end, progress) =>
{
float s = (float)(object)start;
float e = (float)(object)end;
float t = progress;
switch (type)
{
case EasingType.EaseInOutQuad:
t = EasingHelper.EaseInOutQuad(t);
break;
case EasingType.EaseInQuad:
t = EasingHelper.EaseInQuad(t);
break;
case EasingType.EaseOutQuad:
t = EasingHelper.EaseOutQuad(t);
break;
case EasingType.Linear:
t = EasingHelper.Linear(t);
break;
case EasingType.SmootherStep:
t = EasingHelper.SmootherStep(t);
break;
default:
break;
}
return (T)(object)(s + (e - s) * t);
};
}
throw new NotSupportedException("当前类型未实现默认缓动插值");
}
/// <summary>
/// The Reset
/// </summary>
/// <param name="value">The value<see cref="T"/></param>
public void Reset(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 0f;
_isTransitioning = false;
}
/// <summary>
/// The StartTransition
/// </summary>
/// <param name="targetValue">The targetValue<see cref="T"/></param>
public void StartTransition(T targetValue)
{
if (!targetValue.Equals(_currentValue))
{
_startValue = _currentValue;
_targetValue = targetValue;
_progress = 0f;
_isTransitioning = true;
}
}
/// <summary>
/// 立即跳转到指定值,无动画
/// </summary>
/// <param name="value">目标值</param>
public void JumpTo(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 1f;
_isTransitioning = false;
}
/// <summary>
/// The Update
/// </summary>
/// <param name="elapsedTime">The elapsedTime<see cref="TimeSpan"/></param>
public void Update(TimeSpan elapsedTime)
{
if (!_isTransitioning)
return;
_progress += (float)elapsedTime.TotalSeconds / _durationSeconds;
if (_progress >= 1f)
{
_progress = 1f;
_currentValue = _targetValue;
_isTransitioning = false;
}
else
{
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
}
#endregion
}
}

View File

@@ -1,23 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Helper
{
using System;
using System.IO;
using Windows.ApplicationModel;
using Windows.Storage;
/// <summary>
/// Defines the <see cref="AppInfo" />
/// </summary>
public static class AppInfo
{
// App Metadata
public const string AppName = "BetterLyrics";
public const string AppDisplayName = "Better Lyrics";
#region Constants
/// <summary>
/// Defines the AppAuthor
/// </summary>
public const string AppAuthor = "Zhe Fang";
/// <summary>
/// Defines the AppDisplayName
/// </summary>
public const string AppDisplayName = "Better Lyrics";
// App Metadata
/// <summary>
/// Defines the AppName
/// </summary>
public const string AppName = "BetterLyrics";
/// <summary>
/// Defines the GithubUrl
/// </summary>
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
#endregion
#region Properties
/// <summary>
/// Gets the AppVersion
/// </summary>
public static string AppVersion
{
get
@@ -27,33 +51,81 @@ namespace BetterLyrics.WinUI3.Helper
}
}
// Environment Info
public static bool IsDebug =>
#if DEBUG
true;
#else
false;
#endif
// Base Folders
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
/// <summary>
/// Gets the AssetsFolder
/// </summary>
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
// Data Files
private static string DatabaseFileName => "database.db";
public static string DatabasePath => Path.Combine(LocalFolder, DatabaseFileName);
/// <summary>
/// Gets the CacheFolder
/// </summary>
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
// Environment Info
// Data Files
/// <summary>
/// Gets the LogDirectory
/// </summary>
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
/// <summary>
/// Gets the LogFilePattern
/// </summary>
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
private static string TestMusicFileName => "AI - 甜度爆表.mp3";
/// <summary>
/// Gets the OnlineLyricsCacheDirectory
/// </summary>
public static string LrcLibLyricsCacheDirectory =>
Path.Combine(CacheFolder, "lrclib-lyrics");
public static string AmllTtmlDbLyricsCacheDirectory =>
Path.Combine(CacheFolder, "amll-ttml-db-lyrics");
public static string QQLyricsCacheDirectory => Path.Combine(CacheFolder, "qq-lyrics");
public static string KugouLyricsCacheDirectory => Path.Combine(CacheFolder, "kugou-lyrics");
public static string NeteaseLyricsCacheDirectory =>
Path.Combine(CacheFolder, "netease-lyrics");
public static string AmllTtmlDbIndexPath =>
Path.Combine(CacheFolder, "amll-ttml-db-index.json");
/// <summary>
/// Gets the TestMusicPath
/// </summary>
public static string TestMusicPath => Path.Combine(AssetsFolder, TestMusicFileName);
// Base Folders
/// <summary>
/// Gets the LocalFolder
/// </summary>
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
/// <summary>
/// Gets the TestMusicFileName
/// </summary>
private static string TestMusicFileName => "AI - 甜度爆表.mp3";
#endregion
#region Methods
/// <summary>
/// The EnsureDirectories
/// </summary>
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
}
#endregion
}
}

View File

@@ -1,4 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -6,13 +8,27 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="CollectionHelper" />
/// </summary>
public static class CollectionHelper
{
#region Methods
/// <summary>
/// The SafeGet
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">The list<see cref="IList{T}"/></param>
/// <param name="index">The index<see cref="int"/></param>
/// <returns>The <see cref="T?"/></returns>
public static T? SafeGet<T>(this IList<T> list, int index)
{
if (list == null || index < 0 || index >= list.Count)
return default;
return list[index];
}
#endregion
}
}

View File

@@ -1,14 +1,48 @@
namespace BetterLyrics.WinUI3.Helper
// 2025/6/23 by Zhe Fang
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
public class ColorHelper
/// <summary>
/// Defines the <see cref="ColorHelper" />
/// </summary>
public static class ColorHelper
{
public static Windows.UI.Color LerpColor(Windows.UI.Color a, Windows.UI.Color b, double t)
#region Methods
/// <summary>
/// The GetInterpolatedColor
/// </summary>
/// <param name="progress">The progress<see cref="float"/></param>
/// <param name="startColor">The startColor<see cref="Color"/></param>
/// <param name="targetColor">The targetColor<see cref="Color"/></param>
/// <returns>The <see cref="Color"/></returns>
public static Color GetInterpolatedColor(
float progress,
Color startColor,
Color targetColor
)
{
byte A = (byte)(a.A + (b.A - a.A) * t);
byte R = (byte)(a.R + (b.R - a.R) * t);
byte G = (byte)(a.G + (b.G - a.G) * t);
byte B = (byte)(a.B + (b.B - a.B) * t);
return Windows.UI.Color.FromArgb(A, R, G, B);
byte Lerp(byte a, byte b) => (byte)(a + (progress * (b - a)));
return Color.FromArgb(
Lerp(startColor.A, targetColor.A),
Lerp(startColor.R, targetColor.R),
Lerp(startColor.G, targetColor.G),
Lerp(startColor.B, targetColor.B)
);
}
/// <summary>
/// The ToWindowsUIColor
/// </summary>
/// <param name="color">The color<see cref="System.Drawing.Color"/></param>
/// <returns>The <see cref="Windows.UI.Color"/></returns>
public static Windows.UI.Color ToWindowsUIColor(this System.Drawing.Color color)
{
return Windows.UI.Color.FromArgb(color.A, color.R, color.G, color.B);
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.UI.Xaml;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DesktopModeHelper
{
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyles = [];
private static readonly Dictionary<IntPtr, bool> _clickThroughStates = [];
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<
IntPtr,
(double X, double Y, double Width, double Height)
> _originalWindowBounds = [];
// <20><><EFBFBD><EFBFBD><E0BBAF><EFBFBD><EFBFBD>
private delegate nint WndProcDelegate(nint hWnd, uint msg, nint wParam, nint lParam);
public static void Enable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20><>¼ԭʼ<D4AD><CABC><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
var windowManager = WindowManager.Get(window);
if (!_originalWindowBounds.ContainsKey(hwnd))
{
_originalWindowBounds[hwnd] = (
windowManager.AppWindow.Position.X,
windowManager.AppWindow.Position.Y,
windowManager.Width,
windowManager.Height
);
}
// <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
var displayArea = Microsoft.UI.Windowing.DisplayArea.GetFromWindowId(
windowManager.AppWindow.Id,
Microsoft.UI.Windowing.DisplayAreaFallback.Primary
);
var workArea = displayArea.WorkArea;
// <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD>ߺ<EFBFBD>λ<EFBFBD><CEBB>
int targetWidth = workArea.Width / 3;
int targetHeight = workArea.Height / 4;
int targetX = workArea.X + (workArea.Width - targetWidth) / 2; // <20><><EFBFBD><EFBFBD>
int targetY = workArea.Y + workArea.Height - targetHeight - 64;
// <20><><EFBFBD>ô<EFBFBD><C3B4>ڴ<EFBFBD>С<EFBFBD><D0A1>λ<EFBFBD><CEBB>
windowManager.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(targetX, targetY, targetWidth, targetHeight)
);
// <20><><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD>ʽ
if (!_originalWindowStyles.ContainsKey(hwnd))
_originalWindowStyles[hwnd] = window.GetWindowStyle();
// <20><><EFBFBD><EFBFBD>ԭTopMost״̬
if (!_originalTopmostStates.ContainsKey(hwnd))
_originalTopmostStates[hwnd] = window.GetIsAlwaysOnTop();
// <20><><EFBFBD>ô<EFBFBD><C3B4><EFBFBD><EFBFBD>ö<EFBFBD>
window.SetIsAlwaysOnTop(true);
window.SetIsShownInSwitchers(false);
}
public static void Disable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20>ָ<EFBFBD>TopMost״̬
if (_originalTopmostStates.TryGetValue(hwnd, out var wasTopMost))
{
window.SetIsAlwaysOnTop(wasTopMost);
_originalTopmostStates.Remove(hwnd);
}
// <20>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
var windowManager = WindowManager.Get(window);
if (_originalWindowBounds.TryGetValue(hwnd, out var bounds))
{
windowManager.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(
(int)bounds.X,
(int)bounds.Y,
(int)bounds.Width,
(int)bounds.Height
)
);
_originalWindowBounds.Remove(hwnd);
}
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
_originalWindowStyles.Remove(hwnd);
}
window.SetIsShownInSwitchers(true);
}
public static void Lock(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20><><EFBFBD><EFBFBD><EFBFBD>ޱ߿<DEB1><DFBF><EFBFBD>͸<EFBFBD><CDB8>
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
window.ExtendsContentIntoTitleBar = false;
SetClickThrough(window, true);
}
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);
}
/// <summary>
/// <20>л<EFBFBD><D0BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͸״̬
/// </summary>
public static void SetClickThrough(Window window, bool enable)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
if (enable)
{
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT | WS_EX_LAYERED);
_clickThroughStates[hwnd] = true;
}
else
{
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT);
_clickThroughStates[hwnd] = false;
}
}
#region Win32
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TRANSPARENT = 0x00000020;
private const int WS_EX_LAYERED = 0x00080000;
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
#endregion
}
}

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockModeHelper
{
private static readonly HashSet<IntPtr> _registered = [];
private static readonly Dictionary<IntPtr, RECT> _originalPositions = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyle = [];
public static void Disable(Window window)
{
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
window.SetIsAlwaysOnTop(false);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
SetWindowPos(
hwnd,
IntPtr.Zero,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
_originalPositions.Remove(hwnd);
}
UnregisterAppBar(hwnd);
}
public static void Enable(Window window, int appBarHeight)
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_originalWindowStyle.ContainsKey(hwnd))
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
if (!_originalPositions.ContainsKey(hwnd))
{
if (GetWindowRect(hwnd, out var rect))
{
_originalPositions[hwnd] = rect;
}
}
RegisterAppBar(hwnd, appBarHeight);
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
screenWidth,
appBarHeight,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
#region AppBar registration
private const uint ABM_NEW = 0x00000000;
private const uint ABM_REMOVE = 0x00000001;
private const uint ABM_SETPOS = 0x00000003;
private const int ABE_TOP = 1;
[StructLayout(LayoutKind.Sequential)]
private struct APPBARDATA
{
public int cbSize;
public IntPtr hWnd;
public uint uCallbackMessage;
public uint uEdge;
public RECT rc;
public int lParam;
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int left,
top,
right,
bottom;
}
[DllImport("shell32.dll", SetLastError = true)]
private static extern uint SHAppBarMessage(uint dwMessage, ref APPBARDATA pData);
private static void RegisterAppBar(IntPtr hwnd, int height)
{
if (_registered.Contains(hwnd))
return;
APPBARDATA abd = new()
{
cbSize = Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uEdge = ABE_TOP,
rc = new RECT
{
left = 0,
top = 0,
right = GetSystemMetrics(SM_CXSCREEN),
bottom = height,
},
};
SHAppBarMessage(ABM_NEW, ref abd);
SHAppBarMessage(ABM_SETPOS, ref abd);
_registered.Add(hwnd);
}
private static void UnregisterAppBar(IntPtr hwnd)
{
if (!_registered.Contains(hwnd))
return;
APPBARDATA abd = new() { cbSize = Marshal.SizeOf<APPBARDATA>(), hWnd = hwnd };
SHAppBarMessage(ABM_REMOVE, ref abd);
_registered.Remove(hwnd);
}
#endregion
#region Win32 Helper and Constants
private const int SWP_NOACTIVATE = 0x0010;
private const int SWP_NOOWNERZORDER = 0x0200;
private const int SWP_SHOWWINDOW = 0x0040;
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 0;
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int X,
int Y,
int cx,
int cy,
uint uFlags
);
/// <summary>
/// 更改已注册 AppBar 的高度。
/// </summary>
/// <param name="window">目标窗口</param>
/// <param name="newHeight">新的高度</param>
public static void UpdateAppBarHeight(IntPtr hwnd, int newHeight)
{
if (!_registered.Contains(hwnd))
return;
APPBARDATA abd = new()
{
cbSize = Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uEdge = ABE_TOP,
rc = new RECT
{
left = 0,
top = 0,
right = GetSystemMetrics(SM_CXSCREEN),
bottom = newHeight,
},
};
SHAppBarMessage(ABM_SETPOS, ref abd);
// 同步窗口实际高度
SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
GetSystemMetrics(SM_CXSCREEN),
newHeight,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
}
#endregion
}
}

View File

@@ -1,4 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -6,45 +8,64 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="EasingHelper" />
/// </summary>
public class EasingHelper
{
/// <summary>
/// No easing
/// </summary>
public static float Linear(float t) => t;
/// <summary>
/// Accelerating from 0
/// </summary>
public static float EaseInQuad(float t) => t * t;
/// <summary>
/// Decelerating to 0
/// </summary>
public static float EaseOutQuad(float t) => t * (2 - t);
#region Methods
/// <summary>
/// Acceleration until halfway then deceleration
/// </summary>
/// <param name="t">The t<see cref="float"/></param>
/// <returns>The <see cref="float"/></returns>
public static float EaseInOutQuad(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
/// <summary>
/// Accelerating from 0
/// </summary>
/// <param name="t">The t<see cref="float"/></param>
/// <returns>The <see cref="float"/></returns>
public static float EaseInQuad(float t) => t * t;
/// <summary>
/// Decelerating to 0
/// </summary>
/// <param name="t">The t<see cref="float"/></param>
/// <returns>The <see cref="float"/></returns>
public static float EaseOutQuad(float t) => t * (2 - t);
/// <summary>
/// No easing
/// </summary>
/// <param name="t">The t<see cref="float"/></param>
/// <returns>The <see cref="float"/></returns>
public static float Linear(float t) => t;
/// <summary>
/// Even smoother transition with continuous first and second derivatives
/// </summary>
/// <param name="t">The t<see cref="float"/></param>
/// <returns>The <see cref="float"/></returns>
public static float SmootherStep(float t)
{
return t * t * t * (t * (6 * t - 15) + 10);
}
/// <summary>
/// Smoother transition than linear
/// </summary>
/// <param name="t">The t<see cref="float"/></param>
/// <returns>The <see cref="float"/></returns>
public static float SmoothStep(float t)
{
return t * t * (3 - 2 * t);
}
/// <summary>
/// Even smoother transition with continuous first and second derivatives
/// </summary>
public static float SmootherStep(float t)
{
return t * t * t * (t * (6 * t - 15) + 10);
}
#endregion
}
}

View File

@@ -0,0 +1,41 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="FileHelper" />
/// </summary>
public class FileHelper
{
#region Methods
/// <summary>
/// The GetEncoding
/// </summary>
/// <param name="filename">The filename<see cref="string"/></param>
/// <returns>The <see cref="Encoding"/></returns>
public static Encoding GetEncoding(string filename)
{
var bytes = File.ReadAllBytes(filename);
var cdet = new CharsetDetector();
cdet.Feed(bytes, 0, bytes.Length);
cdet.DataEnd();
var encoding = cdet.Charset;
if (encoding == null)
{
return Encoding.UTF8;
}
return Encoding.GetEncoding(encoding);
}
#endregion
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcherHelper
{
private readonly WinEventDelegate _winEventDelegate;
private readonly List<IntPtr> _hooks = new();
private IntPtr _currentForeground = IntPtr.Zero;
private readonly IntPtr _selfHwnd;
private readonly DispatcherTimer _pollingTimer;
private DateTime _lastEventTime = DateTime.MinValue;
private const int ThrottleIntervalMs = 100;
public delegate void WindowChangedHandler(IntPtr hwnd);
private readonly WindowChangedHandler _onWindowChanged;
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new WinEventDelegate(WinEventProc);
_pollingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_pollingTimer.Tick += (_, _) =>
{
if (_currentForeground != IntPtr.Zero && _currentForeground != _selfHwnd)
_onWindowChanged?.Invoke(_currentForeground);
};
}
public void Start()
{
// Hook: foreground changes and minimize end
_hooks.Add(
SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_MINIMIZEEND,
IntPtr.Zero,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
)
);
// Hook: window move/resize (location change)
_hooks.Add(
SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_OBJECT_LOCATIONCHANGE,
IntPtr.Zero,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
)
);
_pollingTimer.Start();
}
public void Stop()
{
foreach (var hook in _hooks)
UnhookWinEvent(hook);
_hooks.Clear();
_pollingTimer.Stop();
}
private void WinEventProc(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
)
{
if (hwnd == IntPtr.Zero || hwnd == _selfHwnd)
return;
var now = DateTime.Now;
if ((now - _lastEventTime).TotalMilliseconds < ThrottleIntervalMs)
return;
_lastEventTime = now;
if (eventType == EVENT_SYSTEM_FOREGROUND)
{
_currentForeground = hwnd;
_onWindowChanged?.Invoke(hwnd);
}
else if (
(eventType == EVENT_OBJECT_LOCATIONCHANGE || eventType == EVENT_SYSTEM_MINIMIZEEND)
&& hwnd == _currentForeground
)
{
_onWindowChanged?.Invoke(hwnd);
}
}
#region WinAPI
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags
);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endregion
}
}

View File

@@ -1,17 +1,191 @@
using System;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="ImageHelper" />
/// </summary>
public class ImageHelper
{
#region Constants
/// <summary>
/// Defines the AccentColorCount
/// </summary>
public const int AccentColorCount = 3;
#endregion
#region Fields
/// <summary>
/// Defines the _colorThief
/// </summary>
private static readonly ColorThief _colorThief = new();
#endregion
#region Methods
/// <summary>
/// The ByteArrayToStream
/// </summary>
/// <param name="bytes">The bytes<see cref="byte[]"/></param>
/// <returns>The <see cref="Task{InMemoryRandomAccessStream}"/></returns>
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
return stream;
}
/// <summary>
/// The CreateTextPlaceholderBytesAsync
/// </summary>
/// <param name="text">The text<see cref="string"/></param>
/// <param name="width">The width<see cref="int"/></param>
/// <param name="height">The height<see cref="int"/></param>
/// <returns>The <see cref="Task{byte[]}"/></returns>
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(
string text,
int width,
int height
)
{
var device = CanvasDevice.GetSharedDevice();
var renderTarget = new CanvasRenderTarget(device, width, height, 96);
// 居中绘制文字
using (var ds = renderTarget.CreateDrawingSession())
{
// 背景色
ds.Clear(Colors.LightGray);
// 文字格式
var format = new CanvasTextFormat
{
FontSize = Math.Min(width, height) / 6f,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
HorizontalAlignment = CanvasHorizontalAlignment.Center,
VerticalAlignment = CanvasVerticalAlignment.Center,
WordWrapping = CanvasWordWrapping.Wrap,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
Options = CanvasDrawTextOptions.Default,
};
// 设定边距
float margin = Math.Min(width, height) / 12f;
float availableWidth = width - 2 * margin;
float availableHeight = height - 2 * margin;
// 计算合适的字体大小以适应内容区域
float fontSize = format.FontSize;
float minFontSize = 8f;
float maxFontSize = format.FontSize;
CanvasTextLayout layout;
do
{
format.FontSize = fontSize;
layout = new CanvasTextLayout(
ds,
text,
format,
availableWidth,
availableHeight
);
if (
layout.LayoutBounds.Width <= availableWidth
&& layout.LayoutBounds.Height <= availableHeight
)
break;
fontSize -= 1f;
} while (fontSize >= minFontSize);
// 居中绘制文字(在内容区域内居中)
var bounds = layout.LayoutBounds;
var x = margin + (availableWidth - (float)bounds.Width) / 2f - (float)bounds.X;
var y = margin + (availableHeight - (float)bounds.Height) / 2f - (float)bounds.Y;
ds.DrawTextLayout(layout, new Vector2(x, y), Colors.DarkGray);
}
// 保存为 PNG 并转为 byte[]
using (var stream = new InMemoryRandomAccessStream())
{
await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png);
var buffer = new byte[stream.Size];
using (var reader = new DataReader(stream.GetInputStreamAt(0)))
{
await reader.LoadAsync((uint)stream.Size);
reader.ReadBytes(buffer);
}
return buffer;
}
}
/// <summary>
/// The GetAccentColorsFromByte
/// </summary>
/// <param name="bytes">The bytes<see cref="byte[]"/></param>
/// <returns>The <see cref="Task{List{Color}}"/></returns>
public static async Task<List<Color>> GetAccentColorsFromByte(byte[] bytes) =>
[
.. (
await _colorThief.GetPalette(await GetDecoderFromByte(bytes), AccentColorCount)
).Select(color =>
Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)
),
];
/// <summary>
/// The GetBitmapImageFromBytesAsync
/// </summary>
/// <param name="imageBytes">The imageBytes<see cref="byte[]"/></param>
/// <returns>The <see cref="Task{BitmapImage}"/></returns>
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);
return bitmapImage;
}
/// <summary>
/// The GetDecoderFromByte
/// </summary>
/// <param name="bytes">The bytes<see cref="byte[]"/></param>
/// <returns>The <see cref="Task{BitmapDecoder}"/></returns>
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
/// <summary>
/// The GetStreamFromBytesAsync
/// </summary>
/// <param name="imageBytes">The imageBytes<see cref="byte[]"/></param>
/// <returns>The <see cref="Task{InMemoryRandomAccessStream}"/></returns>
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(
byte[] imageBytes
)
@@ -21,11 +195,15 @@ namespace BetterLyrics.WinUI3.Helper
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
stream.Seek(0);
return stream;
}
/// <summary>
/// The ToByteArrayAsync
/// </summary>
/// <param name="streamRef">The streamRef<see cref="IRandomAccessStreamReference"/></param>
/// <returns>The <see cref="Task{byte[]}"/></returns>
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
@@ -33,5 +211,7 @@ namespace BetterLyrics.WinUI3.Helper
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
#endregion
}
}

View File

@@ -0,0 +1,450 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Models;
using Microsoft.UI.Xaml.Shapes;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="LyricsParser" />
/// </summary>
public class LyricsParser
{
#region Fields
/// <summary>
/// Defines the _multiLangLyricsLines
/// </summary>
private List<List<LyricsLine>> _multiLangLyricsLines = [];
#endregion
#region Methods
/// <summary>
/// The Parse
/// </summary>
/// <param name="raw">The raw<see cref="string"/></param>
/// <param name="lyricsFormat">The lyricsFormat<see cref="LyricsFormat?"/></param>
/// <param name="title">The title<see cref="string?"/></param>
/// <param name="artist">The artist<see cref="string?"/></param>
/// <param name="durationMs">The durationMs<see cref="int"/></param>
/// <returns>The <see cref="List{List{LyricsLine}}"/></returns>
public List<List<LyricsLine>> Parse(
string raw,
LyricsFormat? lyricsFormat = null,
string? title = null,
string? artist = null,
int durationMs = 0
)
{
_multiLangLyricsLines = [];
switch (lyricsFormat)
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw, durationMs);
break;
case LyricsFormat.Qrc:
ParseUsingLyricify(
Lyricify.Lyrics.Parsers.QrcParser.Parse(raw).Lines,
durationMs
);
break;
case LyricsFormat.Krc:
ParseUsingLyricify(
Lyricify.Lyrics.Parsers.KrcParser.Parse(raw).Lines,
durationMs
);
break;
case LyricsFormat.Ttml:
ParseTtml(raw, durationMs);
break;
default:
break;
}
return _multiLangLyricsLines;
}
/// <summary>
/// The ParseLrc
/// </summary>
/// <param name="raw">The raw<see cref="string"/></param>
/// <param name="durationMs">The durationMs<see cref="int"/></param>
private void ParseLrc(string raw, int durationMs)
{
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
new List<(int time, string text, List<(int time, string text)> syllables)>();
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
var syllableRegex = new Regex(
@"(\[|\<)(\d{2}):(\d{2})\.(\d{2,3})(\]|\>)([^\[\]\<\>]*)"
);
foreach (var line in lines)
{
var matches = syllableRegex.Matches(line);
var syllables = new List<(int, string)>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
int min = int.Parse(m.Groups[2].Value);
int sec = int.Parse(m.Groups[3].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
int totalMs = min * 60_000 + sec * 1000 + ms;
string text = m.Groups[6].Value;
syllables.Add((totalMs, text));
}
if (syllables.Count > 0)
{
lrcLines.Add(
(
syllables[0].Item1,
string.Concat(syllables.Select(s => s.Item2)),
syllables
)
);
}
else
{
// 普通LRC行
var bracketRegex = new Regex(@"\[(\d{2}):(\d{2})\.(\d{2,3})\]");
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 0)
{
var m = bracketMatches[0];
int min = int.Parse(m.Groups[1].Value);
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[3].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex.Replace(line, "");
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
}
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = grouped.Max(g => g.Count());
// 初始化每种语言的歌词列表
_multiLangLyricsLines.Clear();
for (int i = 0; i < languageCount; i++)
_multiLangLyricsLines.Add(new List<LyricsLine>());
// 遍历每个时间分组
foreach (var group in grouped)
{
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
// 如果该语言有翻译,取对应行,否则用原文(第一行)
var (start, text, syllables) =
langIdx < linesInGroup.Count ? linesInGroup[langIdx] : linesInGroup[0];
var line = new LyricsLine
{
StartMs = start,
EndMs = 0, // 稍后统一修正
Text = text,
CharTimings = [],
};
if (syllables != null && syllables.Count > 0)
{
int currentIndex = 0;
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int startIndex = currentIndex;
line.CharTimings.Add(
new CharTiming
{
StartMs = charStart,
EndMs = 0, // Fixed later
Text = charText ?? "",
StartIndex = startIndex,
}
);
currentIndex += charText?.Length ?? 0;
}
}
_multiLangLyricsLines[langIdx].Add(line);
}
}
// 修正 EndMs
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
var linesInSingleLang = _multiLangLyricsLines[langIdx];
for (int i = 0; i < linesInSingleLang.Count; i++)
{
if (i + 1 < linesInSingleLang.Count)
{
linesInSingleLang[i].EndMs = linesInSingleLang[i + 1].StartMs;
}
else
{
linesInSingleLang[i].EndMs = durationMs;
}
// 修正 CharTimings 的 EndMs
var timings = linesInSingleLang[i].CharTimings;
if (timings.Count > 0)
{
for (int j = 0; j < timings.Count; j++)
{
if (j + 1 < timings.Count)
{
timings[j].EndMs = timings[j + 1].StartMs;
}
else
{
timings[j].EndMs = linesInSingleLang[i].EndMs;
}
}
}
}
PostProcessLyricsLines(linesInSingleLang);
}
}
private void ParseUsingLyricify(List<ILineInfo>? lines, int durationMs)
{
List<LyricsLine> lyricsLines = [];
if (lines != null && lines.Count > 0)
{
lyricsLines = [];
for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
{
var lineRead = lines[lineIndex];
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
Text = lineRead.Text,
CharTimings = [],
};
if (lineIndex + 1 < lines.Count)
{
lineWrite.EndMs = lines[lineIndex + 1].StartTime ?? 0;
}
else
{
lineWrite.EndMs = durationMs;
}
var syllables = (lineRead as SyllableLineInfo)?.Syllables;
if (syllables != null)
{
int startIndex = 0;
for (
int syllableIndex = 0;
syllableIndex < syllables.Count;
syllableIndex++
)
{
var syllable = syllables[syllableIndex];
var charTiming = new CharTiming
{
StartMs = syllable.StartTime,
Text = syllable.Text,
StartIndex = startIndex,
};
if (syllableIndex + 1 < syllables.Count)
{
charTiming.EndMs = syllables[syllableIndex + 1].StartTime;
}
else
{
charTiming.EndMs = lineWrite.EndMs;
}
lineWrite.CharTimings.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
lyricsLines.Add(lineWrite);
}
}
_multiLangLyricsLines.Add(lyricsLines);
}
/// <summary>
/// The ParseTtml
/// </summary>
/// <param name="raw">The raw<see cref="string"/></param>
/// <param name="durationMs">The durationMs<see cref="int"/></param>
private void ParseTtml(string raw, int durationMs)
{
try
{
List<LyricsLine> singleLangLyricsLine = [];
var xdoc = XDocument.Parse(raw);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null)
return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 处理分词分时
var spans = p.Elements()
.Where(s =>
s.Name.LocalName == "span"
&& s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))
== null
)
.ToList();
string text = string.Concat(spans.Select(s => s));
var charTimings = new List<CharTiming>();
for (int i = 0; i < spans.Count; i++)
{
var span = spans[i];
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
if (sStartMs == 0 && sEndMs == 0)
continue;
if (sEndMs == 0)
sEndMs =
(i + 1 < spans.Count)
? ParseTtmlTime(spans[i + 1].Attribute("begin")?.Value)
: pEndMs;
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = sEndMs });
}
if (spans.Count == 0)
text = p.Value;
singleLangLyricsLine.Add(
new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
Text = text,
CharTimings = charTimings,
}
);
}
PostProcessLyricsLines(singleLangLyricsLine);
_multiLangLyricsLines.Add(singleLangLyricsLine);
}
catch
{
// 解析失败,忽略
}
}
/// <summary>
/// The ParseTtmlTime
/// </summary>
/// <param name="t">The t<see cref="string?"/></param>
/// <returns>The <see cref="int"/></returns>
private int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
/// <summary>
/// The PostProcessLyricsLines
/// </summary>
/// <param name="lines">The lines<see cref="List{LyricsLine}"/></param>
private void PostProcessLyricsLines(List<LyricsLine> lines)
{
if (lines.Count > 0 && lines[0].StartMs > 0)
{
lines.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = lines[0].StartMs,
Text = "● ● ●",
CharTimings = [],
}
);
}
}
#endregion
}
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class MathHelper
{
public static List<int> GetAllFactors(int n)
{
var result = new SortedSet<int>();
for (int i = 1; i <= Math.Sqrt(n); i++)
{
if (n % i == 0)
{
result.Add(i);
result.Add(n / i);
}
}
return [.. result];
}
}
}

View File

@@ -1,24 +1,36 @@
using DevWinUI;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml.Media;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="SystemBackdropHelper" />
/// </summary>
public class SystemBackdropHelper
{
#region Methods
/// <summary>
/// The CreateSystemBackdrop
/// </summary>
/// <param name="backdropType">The backdropType<see cref="BackdropType"/></param>
/// <returns>The <see cref="SystemBackdrop?"/></returns>
public static SystemBackdrop? CreateSystemBackdrop(BackdropType backdropType)
{
return backdropType switch
{
BackdropType.None => null,
BackdropType.Mica => new MicaSystemBackdrop(MicaKind.Base),
BackdropType.MicaAlt => new MicaSystemBackdrop(MicaKind.BaseAlt),
BackdropType.Mica => new MicaBackdrop { Kind = MicaKind.Base },
BackdropType.MicaAlt => new MicaBackdrop { Kind = MicaKind.BaseAlt },
BackdropType.DesktopAcrylic => new DesktopAcrylicBackdrop(),
BackdropType.AcrylicThin => new AcrylicSystemBackdrop(DesktopAcrylicKind.Thin),
BackdropType.AcrylicBase => new AcrylicSystemBackdrop(DesktopAcrylicKind.Base),
BackdropType.Transparent => new TransparentBackdrop(),
BackdropType.Transparent => new WinUIEx.TransparentTintBackdrop(),
_ => null,
};
}
#endregion
}
}

View File

@@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace BetterLyrics.WinUI3.Helper
{
public class VisualHelper
{
/// <summary>
/// Source: https://stackoverflow.com/a/61626933/11048731
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="depObj"></param>
/// <returns></returns>
public static List<T> FindVisualChildren<T>(DependencyObject depObj)
where T : DependencyObject
{
List<T> list = [];
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T t)
{
list.Add(t);
}
List<T> childItems = FindVisualChildren<T>(child);
if (childItems != null && childItems.Count > 0)
{
foreach (var item in childItems)
{
list.Add(item);
}
}
}
}
return list;
}
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowColorHelper
{
public static Color GetDominantColorBelow(IntPtr myHwnd)
{
if (!GetWindowRect(myHwnd, out RECT myRect))
return Color.Transparent;
int screenWidth = GetSystemMetrics(SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = GetDC(IntPtr.Zero); // Entire screen
BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, SRCCOPY);
gDest.ReleaseHdc(hdcDest);
ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
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));
}
#region Win32 Imports & Structs
private const int SRCCOPY = 0x00CC0020;
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
private static extern bool BitBlt(
IntPtr hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
IntPtr hdcSrc,
int nXSrc,
int nYSrc,
int dwRop
);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(SystemMetric smIndex);
private enum SystemMetric
{
SM_CXSCREEN = 0,
SM_CYSCREEN = 1,
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
}
}

View File

@@ -0,0 +1,228 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="WindowHelper" />
/// </summary>
public static class WindowHelper
{
#region Fields
/// <summary>
/// Defines the _windowCache
/// </summary>
private static readonly Dictionary<Type, Window> _windowCache = new();
/// <summary>
/// Defines the _activeWindows
/// </summary>
private static List<Window> _activeWindows = new List<Window>();
#endregion
#region Properties
/// <summary>
/// Gets the ActiveWindows
/// </summary>
public static List<Window> ActiveWindows
{
get { return _activeWindows; }
}
#endregion
#region Methods
/// <summary>
/// The GetWindowByFramePageType
/// </summary>
/// <param name="type">The type<see cref="Type"/></param>
/// <returns>The <see cref="Window"/></returns>
public static Window GetWindowByFramePageType(Type type)
{
foreach (var cachedWindow in _windowCache)
{
if (cachedWindow.Key == type)
{
return cachedWindow.Value;
}
}
return null;
}
/// <summary>
/// The GetWindowForElement
/// </summary>
/// <param name="element">The element<see cref="UIElement"/></param>
/// <returns>The <see cref="Window"/></returns>
public static Window GetWindowForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return window;
}
}
}
return null;
}
/// <summary>
/// The HideSystemTitleBar
/// </summary>
/// <param name="window">The window<see cref="Window"/></param>
public static void HideSystemTitleBar(this Window window)
{
window.ExtendsContentIntoTitleBar = true;
window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
}
/// <summary>
/// The HideSystemTitleBarAndSetCustomTitleBar
/// </summary>
/// <param name="window">The window<see cref="Window"/></param>
/// <param name="titleBar">The titleBar<see cref="UIElement"/></param>
public static void HideSystemTitleBarAndSetCustomTitleBar(
this Window window,
UIElement titleBar
)
{
window.HideSystemTitleBar();
window.SetTitleBar(titleBar);
}
/// <summary>
/// The OpenLyricsWindow
/// </summary>
public static void OpenLyricsWindow()
{
OpenOrShowWindow(typeof(LyricsPage));
}
/// <summary>
/// The OpenSettingsWindow
/// </summary>
public static void OpenSettingsWindow()
{
OpenOrShowWindow(typeof(SettingsPage));
}
/// <summary>
/// The TrackWindow
/// </summary>
/// <param name="window">The window<see cref="Window"/></param>
/// <param name="pageType">The pageType<see cref="Type"/></param>
public static void TrackWindow(Window window, Type pageType = null)
{
if (pageType != null)
{
_windowCache[pageType] = window;
}
if (!_activeWindows.Contains(window))
_activeWindows.Add(window);
window.Closed -= Window_Closed;
window.Closed += Window_Closed;
}
private static void Window_Closed(object sender, WindowEventArgs e)
{
if (sender is Window closedWindow)
{
_activeWindows.Remove(closedWindow);
// 从缓存移除
foreach (var kvp in _windowCache)
{
if (kvp.Value == closedWindow)
{
_windowCache.Remove(kvp.Key);
break;
}
}
}
}
/// <summary>
/// The TryHide
/// </summary>
/// <param name="window">The window<see cref="Window"/></param>
public static void TryHide(this Window window)
{
if (window is not null)
{
window.Hide();
}
}
/// <summary>
/// The TryShow
/// </summary>
/// <param name="window">The window<see cref="Window"/></param>
public static void TryShow(this Window window)
{
if (window is not null)
{
window.Activate();
}
}
/// <summary>
/// The OpenOrShowWindow
/// </summary>
/// <param name="pageType">The pageType<see cref="Type"/></param>
private static void OpenOrShowWindow(Type pageType)
{
if (_windowCache.TryGetValue(pageType, out var window))
{
window.TryShow();
}
else
{
var newWindow = new HostWindow();
TrackWindow(newWindow, pageType);
newWindow.ViewModel.FramePageType = pageType;
newWindow.Navigate(pageType);
newWindow.Activate();
}
}
// get dpi for an element
/// <summary>
/// The GetRasterizationScaleForElement
/// </summary>
/// <param name="element">The element<see cref="UIElement"/></param>
/// <returns>The <see cref="double"/></returns>
static public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return element.XamlRoot.RasterizationScale;
}
}
}
return 0.0;
}
#endregion
}
}

View File

@@ -1,15 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Messages
{
/// <summary>
/// Defines the <see cref="ShowNotificatonMessage" />
/// </summary>
public class ShowNotificatonMessage(Notification value)
: ValueChangedMessage<Notification>(value) { }
: ValueChangedMessage<Notification>(value)
{
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
using DevWinUI;
namespace BetterLyrics.WinUI3.Messages
{
public class SystemBackdropChangedMessage(BackdropType value)
: ValueChangedMessage<BackdropType>(value) { }
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Messages
{
public class ThemeChangedMessage(ElementTheme value)
: ValueChangedMessage<ElementTheme>(value) { }
}

View File

@@ -0,0 +1,34 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="CharTiming" />
/// </summary>
public class CharTiming
{
#region Properties
/// <summary>
/// Gets or sets the EndMs
/// </summary>
public int EndMs { get; set; }
/// <summary>
/// Gets or sets the StartMs
/// </summary>
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty;
public int StartIndex { get; set; }
#endregion
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public enum Language
{
FollowSystem,
English,
SimplifiedChinese,
TraditionalChinese,
}
}

View File

@@ -0,0 +1,50 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="LocalLyricsFolder" />
/// </summary>
public partial class LocalLyricsFolder : ObservableObject
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LocalLyricsFolder"/> class.
/// </summary>
public LocalLyricsFolder()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LocalLyricsFolder"/> class.
/// </summary>
/// <param name="path">The path<see cref="string"/></param>
/// <param name="isEnabled">The isEnabled<see cref="bool"/></param>
public LocalLyricsFolder(string path, bool isEnabled)
{
Path = path;
IsEnabled = isEnabled;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets a value indicating whether IsEnabled
/// </summary>
[ObservableProperty]
public partial bool IsEnabled { get; set; }
/// <summary>
/// Gets or sets the Path
/// </summary>
[ObservableProperty]
public partial string Path { get; set; }
#endregion
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public enum LyricsAlignmentType
{
Left,
Center,
Right,
}
}

View File

@@ -0,0 +1,31 @@
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="LyricsData" />
/// </summary>
public class LyricsData
{
#region Properties
/// <summary>
/// Gets or sets the LanguageIndex
/// </summary>
public int LanguageIndex { get; set; } = 0;
/// <summary>
/// Gets the LyricsLines
/// </summary>
public List<LyricsLine> LyricsLines => MultiLangLyricsLines[LanguageIndex];
/// <summary>
/// Gets or sets the MultiLangLyricsLines
/// </summary>
public List<List<LyricsLine>> MultiLangLyricsLines { get; set; } = [];
#endregion
}
}

View File

@@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public enum LyricsFontColorType
{
Default,
Dominant,
}
}

View File

@@ -1,42 +1,74 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Numerics;
using CommunityToolkit.Mvvm.ComponentModel;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Text;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="LyricsLine" />
/// </summary>
public class LyricsLine
{
public List<string> Texts { get; set; } = [];
#region Properties
public int LanguageIndex { get; set; } = 0;
/// <summary>
/// Gets or sets the BlurAmountTransition
/// </summary>
public ValueTransition<float> BlurAmountTransition { get; set; } =
new(initialValue: 0f, durationSeconds: 0.3f);
public string Text => Texts[LanguageIndex];
public int StartPlayingTimestampMs { get; set; }
public int EndPlayingTimestampMs { get; set; }
public LyricsPlayingState PlayingState { get; set; }
public int DurationMs => EndPlayingTimestampMs - StartPlayingTimestampMs;
public float EnteringProgress { get; set; }
public float ExitingProgress { get; set; }
public float PlayingProgress { get; set; }
public Vector2 Position { get; set; }
/// <summary>
/// Gets or sets the CanvasTextLayout
/// </summary>
public CanvasTextLayout? CanvasTextLayout { get; set; }
/// <summary>
/// Gets or sets the CenterPosition
/// </summary>
public Vector2 CenterPosition { get; set; }
public float Scale { get; set; }
/// <summary>
/// Gets or sets the CharTimings
/// </summary>
public List<CharTiming> CharTimings { get; set; } = [];
public float Opacity { get; set; }
/// <summary>
/// Gets the DurationMs
/// </summary>
public int DurationMs => EndMs - StartMs;
public CanvasTextLayout TextLayout { get; set; }
/// <summary>
/// Gets or sets the EndMs
/// </summary>
public int EndMs { get; set; }
public ValueTransition<float> HighlightOpacityTransition { get; set; } =
new(initialValue: 0f, durationSeconds: 0.3f);
/// <summary>
/// Gets or sets the Position
/// </summary>
public Vector2 Position { get; set; }
/// <summary>
/// Gets or sets the ScaleTransition
/// </summary>
public ValueTransition<float> ScaleTransition { get; set; } =
new(initialValue: 0.95f, durationSeconds: 0.3f);
/// <summary>
/// Gets or sets the StartMs
/// </summary>
public int StartMs { get; set; }
/// <summary>
/// Gets or sets the Text
/// </summary>
public string Text { get; set; } = "";
#endregion
}
}

View File

@@ -0,0 +1,51 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="LyricsSearchProviderInfo" />
/// </summary>
public partial class LyricsSearchProviderInfo : ObservableObject
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LyricsSearchProviderInfo"/> class.
/// </summary>
public LyricsSearchProviderInfo()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LyricsSearchProviderInfo"/> class.
/// </summary>
/// <param name="provider">The provider<see cref="LyricsSearchProvider"/></param>
/// <param name="isEnabled">The isEnabled<see cref="bool"/></param>
public LyricsSearchProviderInfo(LyricsSearchProvider provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets a value indicating whether IsEnabled
/// </summary>
[ObservableProperty]
public partial bool IsEnabled { get; set; }
/// <summary>
/// Gets or sets the Provider
/// </summary>
[ObservableProperty]
public partial LyricsSearchProvider Provider { get; set; }
#endregion
}
}

View File

@@ -1,12 +0,0 @@
using SQLite;
namespace BetterLyrics.WinUI3.Models
{
public class MetadataIndex
{
[PrimaryKey]
public string? Path { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
}
}

View File

@@ -1,31 +1,30 @@
using System;
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="Notification" />
/// </summary>
public partial class Notification : ObservableObject
{
[ObservableProperty]
private InfoBarSeverity _severity;
[ObservableProperty]
private string? _message;
[ObservableProperty]
private bool _isForeverDismissable;
[ObservableProperty]
private Visibility _visibility;
[ObservableProperty]
private string? _relatedSettingsKeyName;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Notification"/> class.
/// </summary>
/// <param name="message">The message<see cref="string?"/></param>
/// <param name="severity">The severity<see cref="InfoBarSeverity"/></param>
/// <param name="isForeverDismissable">The isForeverDismissable<see cref="bool"/></param>
/// <param name="relatedSettingsKeyName">The relatedSettingsKeyName<see cref="string?"/></param>
public Notification(
string? message = null,
InfoBarSeverity severity = InfoBarSeverity.Informational,
@@ -39,5 +38,41 @@ namespace BetterLyrics.WinUI3.Models
Visibility = IsForeverDismissable ? Visibility.Visible : Visibility.Collapsed;
RelatedSettingsKeyName = relatedSettingsKeyName;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets a value indicating whether IsForeverDismissable
/// </summary>
[ObservableProperty]
public partial bool IsForeverDismissable { get; set; }
/// <summary>
/// Gets or sets the Message
/// </summary>
[ObservableProperty]
public partial string? Message { get; set; }
/// <summary>
/// Gets or sets the RelatedSettingsKeyName
/// </summary>
[ObservableProperty]
public partial string? RelatedSettingsKeyName { get; set; }
/// <summary>
/// Gets or sets the Severity
/// </summary>
[ObservableProperty]
public partial InfoBarSeverity Severity { get; set; }
/// <summary>
/// Gets or sets the Visibility
/// </summary>
[ObservableProperty]
public partial Visibility Visibility { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,63 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="SongInfo" />
/// </summary>
public partial class SongInfo : ObservableObject
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SongInfo"/> class.
/// </summary>
public SongInfo()
{
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the Album
/// </summary>
[ObservableProperty]
public partial string? Album { get; set; }
/// <summary>
/// Gets or sets the AlbumArt
/// </summary>
public byte[]? AlbumArt { get; set; } = null;
/// <summary>
/// Gets or sets the Artist
/// </summary>
[ObservableProperty]
public partial string Artist { get; set; }
/// <summary>
/// Gets or sets the DurationMs
/// In milliseconds
/// </summary>
[ObservableProperty]
public partial double? DurationMs { get; set; }
/// <summary>
/// Gets or sets the SourceAppUserModelId
/// </summary>
[ObservableProperty]
public partial string? SourceAppUserModelId { get; set; } = null;
/// <summary>
/// Gets or sets the Title
/// </summary>
[ObservableProperty]
public partial string Title { get; set; }
#endregion
}
}

View File

@@ -1,116 +0,0 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
namespace BetterLyrics.WinUI3
{
public class Program
{
[STAThread]
static int Main(string[] args)
{
WinRT.ComWrappersSupport.InitializeComWrappers();
bool isRedirect = DecideRedirection();
if (!isRedirect)
{
Application.Start(
(p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread()
);
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
}
);
}
return 0;
}
private static bool DecideRedirection()
{
bool isRedirect = false;
AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
ExtendedActivationKind kind = args.Kind;
AppInstance keyInstance = AppInstance.FindOrRegisterForKey(Helper.AppInfo.AppName);
if (keyInstance.IsCurrent)
{
keyInstance.Activated += OnActivated;
}
else
{
isRedirect = true;
RedirectActivationTo(args, keyInstance);
}
return isRedirect;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr CreateEvent(
IntPtr lpEventAttributes,
bool bManualReset,
bool bInitialState,
string lpName
);
[DllImport("kernel32.dll")]
private static extern bool SetEvent(IntPtr hEvent);
[DllImport("ole32.dll")]
private static extern uint CoWaitForMultipleObjects(
uint dwFlags,
uint dwMilliseconds,
ulong nHandles,
IntPtr[] pHandles,
out uint dwIndex
);
[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);
private static IntPtr redirectEventHandle = IntPtr.Zero;
// Do the redirection on another thread, and use a non-blocking
// wait method to wait for the redirection to complete.
public static void RedirectActivationTo(
AppActivationArguments args,
AppInstance keyInstance
)
{
redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null);
Task.Run(() =>
{
keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
SetEvent(redirectEventHandle);
});
uint CWMO_DEFAULT = 0;
uint INFINITE = 0xFFFFFFFF;
_ = CoWaitForMultipleObjects(
CWMO_DEFAULT,
INFINITE,
1,
[redirectEventHandle],
out uint handleIndex
);
// Bring the window to the foreground
Process process = Process.GetProcessById((int)keyInstance.ProcessId);
SetForegroundWindow(process.MainWindowHandle);
}
private static void OnActivated(object sender, AppActivationArguments args)
{
ExtendedActivationKind kind = args.Kind;
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>ARM64</Platform>
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
<PublishDir>bin\\\win-arm64\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<Configuration>Release</Configuration>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x64</Platform>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishDir>bin\\\win-x64\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<Configuration>Release</Configuration>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x86</Platform>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<PublishDir>bin\\\win-x86\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<Configuration>Release</Configuration>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Renderer.LyricsRenderer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Renderer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Update="LyricsCanvas_Update" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,74 @@
// 2025/6/23 by Zhe Fang
using System.Diagnostics;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Renderer
{
/// <summary>
/// Defines the <see cref="LyricsRenderer" />
/// </summary>
public sealed partial class LyricsRenderer : UserControl
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LyricsRenderer"/> class.
/// </summary>
public LyricsRenderer()
{
InitializeComponent();
ViewModel = Ioc.Default.GetRequiredService<LyricsRendererViewModel>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the ViewModel
/// </summary>
public LyricsRendererViewModel ViewModel { get; set; }
#endregion
#region Methods
/// <summary>
/// The LyricsCanvas_Draw
/// </summary>
/// <param name="sender">The sender<see cref="Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl"/></param>
/// <param name="args">The args<see cref="Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs"/></param>
private void LyricsCanvas_Draw(
Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs args
)
{
ViewModel.Draw(sender, args.DrawingSession);
}
/// <summary>
/// The LyricsCanvas_Update
/// </summary>
/// <param name="sender">The sender<see cref="Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl"/></param>
/// <param name="args">The args<see cref="Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs"/></param>
private void LyricsCanvas_Update(
Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs args
)
{
ViewModel.Update(sender, args);
}
#endregion
}
}

View File

@@ -1,136 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Graphics.Imaging;
namespace BetterLyrics.WinUI3.Rendering
{
public class CoverBackgroundRenderer
{
private readonly SettingsService _settingsService;
public float RotateAngle { get; set; } = 0f;
private SoftwareBitmap? _lastSoftwareBitmap = null;
private SoftwareBitmap? _softwareBitmap = null;
public SoftwareBitmap? SoftwareBitmap
{
get => _softwareBitmap;
set
{
if (_softwareBitmap != null)
{
_lastSoftwareBitmap = _softwareBitmap;
_transitionStartTime = DateTimeOffset.Now;
_isTransitioning = true;
_transitionAlpha = 0f;
}
_softwareBitmap = value;
}
}
private float _transitionAlpha = 1f;
private TimeSpan _transitionDuration = TimeSpan.FromMilliseconds(1000);
private DateTimeOffset _transitionStartTime;
private bool _isTransitioning = false;
public CoverBackgroundRenderer()
{
_settingsService = Ioc.Default.GetService<SettingsService>()!;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (!_settingsService.IsCoverOverlayEnabled || SoftwareBitmap == null)
return;
ds.Transform = Matrix3x2.CreateRotation(RotateAngle, control.Size.ToVector2() * 0.5f);
var overlappedCovers = new CanvasCommandList(control);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
if (_isTransitioning && _lastSoftwareBitmap != null)
{
DrawImgae(control, overlappedCoversDs, _lastSoftwareBitmap, 1 - _transitionAlpha);
DrawImgae(control, overlappedCoversDs, SoftwareBitmap, _transitionAlpha);
}
else
{
DrawImgae(control, overlappedCoversDs, SoftwareBitmap, 1);
}
using var coverOverlayEffect = new OpacityEffect
{
Opacity = _settingsService.CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = _settingsService.CoverOverlayBlurAmount,
Source = overlappedCovers,
},
};
ds.DrawImage(coverOverlayEffect);
ds.Transform = Matrix3x2.Identity;
}
private void DrawImgae(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
SoftwareBitmap softwareBitmap,
float opacity
)
{
float imageWidth = (float)(softwareBitmap.PixelWidth * 96f / softwareBitmap.DpiX);
float imageHeight = (float)(softwareBitmap.PixelHeight * 96f / softwareBitmap.DpiY);
var scaleFactor =
(float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2))
/ Math.Min(imageWidth, imageHeight);
ds.DrawImage(
new OpacityEffect
{
Source = new ScaleEffect
{
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
BorderMode = EffectBorderMode.Hard,
Scale = new Vector2(scaleFactor),
Source = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap),
},
Opacity = opacity,
},
(float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
(float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
);
}
public void Calculate(ICanvasAnimatedControl control)
{
if (_isTransitioning)
{
var elapsed = DateTimeOffset.Now - _transitionStartTime;
float progress = (float)(
elapsed.TotalMilliseconds / _transitionDuration.TotalMilliseconds
);
_transitionAlpha = Math.Clamp(progress, 0f, 1f);
if (_transitionAlpha >= 1f)
{
_isTransitioning = false;
_lastSoftwareBitmap?.Dispose();
_lastSoftwareBitmap = null;
}
}
}
}
}

View File

@@ -1,422 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Text;
using Windows.UI;
namespace BetterLyrics.WinUI3.Rendering
{
public class PureLyricsRenderer
{
private readonly SettingsService _settingsService;
private readonly float _defaultOpacity = 0.3f;
private readonly float _highlightedOpacity = 1.0f;
private readonly float _defaultScale = 0.95f;
private readonly float _highlightedScale = 1.0f;
private readonly int _lineEnteringDurationMs = 800;
private readonly int _lineExitingDurationMs = 800;
private readonly int _lineScrollDurationMs = 800;
private float _lastTotalYScroll = 0.0f;
private float _totalYScroll = 0.0f;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private bool _forceToScroll = false;
private readonly double _rightMargin = 36;
public double LimitedLineWidth { get; set; } = 0;
public double CanvasWidth { get; set; } = 0;
public double CanvasHeight { get; set; } = 0;
public TimeSpan CurrentTime { get; set; }
public List<LyricsLine> LyricsLines { get; set; } = [];
public PureLyricsRenderer()
{
_settingsService = Ioc.Default.GetService<SettingsService>()!;
}
private Tuple<int, int> GetVisibleLyricsLineIndexBoundaries()
{
// _logger.LogDebug($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
return new Tuple<int, int>(_startVisibleLineIndex, _endVisibleLineIndex);
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (LyricsLines.Count == 0)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, LyricsLines.Count - 1);
}
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
byte r,
byte g,
byte b
)
{
var (displayStartLineIndex, displayEndLineIndex) =
GetVisibleLyricsLineIndexBoundaries();
for (
int i = displayStartLineIndex;
LyricsLines.Count > 0
&& i >= 0
&& i < LyricsLines.Count
&& i <= displayEndLineIndex;
i++
)
{
var line = LyricsLines[i];
if (line.TextLayout == null)
{
return;
}
float progressPerChar = 1f / line.Text.Length;
var position = line.Position;
float centerX = position.X;
float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
switch ((LyricsAlignmentType)_settingsService.LyricsAlignmentType)
{
case LyricsAlignmentType.Left:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case LyricsAlignmentType.Center:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += (float)LimitedLineWidth / 2;
break;
case LyricsAlignmentType.Right:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += (float)LimitedLineWidth;
break;
default:
break;
}
int startIndex = 0;
// Set brush
for (int j = 0; j < line.TextLayout.LineCount; j++)
{
int count = line.TextLayout.LineMetrics[j].CharacterCount;
var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
float subLinePlayingProgress = Math.Clamp(
(line.PlayingProgress * line.Text.Length - startIndex) / count,
0,
1
);
using var horizontalFillBrush = new CanvasLinearGradientBrush(
control,
[
new()
{
Position = 0,
Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
},
new()
{
Position =
subLinePlayingProgress * (1 + progressPerChar)
- progressPerChar,
Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b),
},
new()
{
Position = subLinePlayingProgress * (1 + progressPerChar),
Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
},
new()
{
Position = 1.5f,
Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b),
},
]
)
{
StartPoint = new Vector2(
(float)(regions[0].LayoutBounds.Left + position.X),
0
),
EndPoint = new Vector2(
(float)(regions[^1].LayoutBounds.Right + position.X),
0
),
};
line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
startIndex += count;
}
// Scale
ds.Transform =
Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY))
* Matrix3x2.CreateTranslation(0, _totalYScroll);
// _logger.LogDebug(_totalYScroll);
ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
public async Task ForceToScrollToCurrentPlayingLineAsync()
{
_forceToScroll = true;
await Task.Delay(1);
_forceToScroll = false;
}
public async Task ReLayoutAsync(ICanvasAnimatedControl control)
{
if (control == null)
return;
float leftMargin = (float)(CanvasWidth - LimitedLineWidth - _rightMargin);
using CanvasTextFormat textFormat = new()
{
FontSize = _settingsService.LyricsFontSize,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontWeight = FontWeights.Bold,
//FontFamily = "Segoe UI Mono",
};
float y = (float)CanvasHeight / 2;
// Init Positions
for (int i = 0; i < LyricsLines.Count; i++)
{
var line = LyricsLines[i];
// Calculate layout bounds
line.TextLayout = new CanvasTextLayout(
control.Device,
line.Text,
textFormat,
(float)LimitedLineWidth,
(float)CanvasHeight
);
line.Position = new Vector2(leftMargin, y);
y +=
(float)line.TextLayout.LayoutBounds.Height
/ line.TextLayout.LineCount
* (line.TextLayout.LineCount + _settingsService.LyricsLineSpacingFactor);
}
await ForceToScrollToCurrentPlayingLineAsync();
}
public void CalculateScaleAndOpacity(int currentPlayingLineIndex)
{
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
for (int i = startLineIndex; LyricsLines.Count > 0 && i <= endLineIndex; i++)
{
var line = LyricsLines[i];
bool linePlaying = i == currentPlayingLineIndex;
var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
var lineExitingDurationMs = _lineExitingDurationMs;
if (i + 1 <= endLineIndex)
{
lineExitingDurationMs = Math.Min(
LyricsLines[i + 1].DurationMs,
lineExitingDurationMs
);
}
float lineEnteringProgress = 0.0f;
float lineExitingProgress = 0.0f;
bool lineEntering = false;
bool lineExiting = false;
float scale = _defaultScale;
float opacity = _defaultOpacity;
float playProgress = 0;
if (linePlaying)
{
line.PlayingState = LyricsPlayingState.Playing;
scale = _highlightedScale;
opacity = _highlightedOpacity;
playProgress =
((float)CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs)
/ line.DurationMs;
var durationFromStartMs =
CurrentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
if (lineEntering)
{
lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
scale =
_defaultScale
+ (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
opacity =
_defaultOpacity
+ (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
}
}
else
{
if (i < currentPlayingLineIndex)
{
line.PlayingState = LyricsPlayingState.Played;
playProgress = 1;
var durationToEndMs =
CurrentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
lineExiting = durationToEndMs <= lineExitingDurationMs;
if (lineExiting)
{
lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
scale =
_highlightedScale
- (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
opacity =
_highlightedOpacity
- (_highlightedOpacity - _defaultOpacity)
* (float)lineExitingProgress;
}
}
else
{
line.PlayingState = LyricsPlayingState.NotPlayed;
}
}
line.EnteringProgress = lineEnteringProgress;
line.ExitingProgress = lineExitingProgress;
line.Scale = scale;
line.Opacity = opacity;
line.PlayingProgress = playProgress;
}
}
public void CalculatePosition(ICanvasAnimatedControl control, int currentPlayingLineIndex)
{
if (currentPlayingLineIndex < 0)
{
return;
}
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0)
{
return;
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = LyricsLines?[currentPlayingLineIndex];
if (currentPlayingLine == null)
{
return;
}
if (currentPlayingLine.TextLayout == null)
{
return;
}
var lineScrollingProgress =
(CurrentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs)
/ Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
var targetYScrollOffset = (float)(
-currentPlayingLine.Position.Y
+ LyricsLines![0].Position.Y
- currentPlayingLine.TextLayout.LayoutBounds.Height / 2
- _lastTotalYScroll
);
var yScrollOffset =
targetYScrollOffset
* EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
bool isScrollingNow = lineScrollingProgress <= 1;
if (isScrollingNow)
{
_totalYScroll = _lastTotalYScroll + yScrollOffset;
}
else
{
if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1)
{
_totalYScroll = _lastTotalYScroll + targetYScrollOffset;
}
_lastTotalYScroll = _totalYScroll;
}
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update Positions
for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++)
{
var line = LyricsLines[i];
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0)
{
if (_startVisibleLineIndex == -1)
{
_startVisibleLineIndex = i;
}
}
if (
_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (_endVisibleLineIndex == -1)
{
_endVisibleLineIndex = i;
}
}
}
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
{
_endVisibleLineIndex = endLineIndex;
}
}
}
}

View File

@@ -0,0 +1,21 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Models;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Serialization
{
/// <summary>
/// Defines the <see cref="SourceGenerationContext" />
/// </summary>
[JsonSerializable(typeof(List<LyricsSearchProviderInfo>))]
[JsonSerializable(typeof(List<LocalLyricsFolder>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}
}

View File

@@ -1,138 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using SQLite;
using Ude;
using Windows.Media.Control;
namespace BetterLyrics.WinUI3.Services.Database
{
public class DatabaseService
{
private readonly SQLiteConnection _connection;
private readonly CharsetDetector _charsetDetector = new();
public DatabaseService()
{
_connection = new SQLiteConnection(Helper.AppInfo.DatabasePath);
if (_connection.GetTableInfo("MetadataIndex").Count == 0)
{
_connection.CreateTable<MetadataIndex>();
}
}
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<string> paths)
{
await Task.Run(() =>
{
_connection.DeleteAll<MetadataIndex>();
foreach (var path in paths)
{
if (Directory.Exists(path))
{
foreach (var file in Directory.GetFiles(path))
{
var fileExtension = Path.GetExtension(file);
var track = new Track(file);
_connection.Insert(
new MetadataIndex
{
Path = file,
Title = track.Title,
Artist = track.Artist,
}
);
}
}
}
});
}
public Track? GetMusicMetadata(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
if (mediaProps == null || mediaProps.Title == null || mediaProps.Artist == null)
return null;
var founds = _connection
.Table<MetadataIndex>()
// Look up by Title and Artist (these two props were fetched by reading metadata in music file befoe) first
// then by Path (music file name usually contains song name and artist so this can be a second way to look up for)
// Please note for .lrc file, only the second way works for it
.Where(m =>
(
m.Title != null
&& m.Artist != null
&& m.Title.Contains(mediaProps.Title)
&& m.Artist.Contains(mediaProps.Artist)
)
|| (
m.Path != null
&& m.Path.Contains(mediaProps.Title)
&& m.Path.Contains(mediaProps.Artist)
)
)
.ToList();
if (founds == null || founds.Count == 0)
{
return null;
}
else
{
var first = new Track(founds[0].Path);
if (founds.Count == 1)
{
return first;
}
else
{
if (first.Lyrics.Exists())
{
return first;
}
else
{
foreach (var found in founds)
{
if (found.Path.EndsWith(".lrc"))
{
using (FileStream fs = File.OpenRead(found.Path))
{
_charsetDetector.Feed(fs);
_charsetDetector.DataEnd();
}
string content;
if (_charsetDetector.Charset != null)
{
Encoding encoding = Encoding.GetEncoding(
_charsetDetector.Charset
);
content = File.ReadAllText(found.Path, encoding);
}
else
{
content = File.ReadAllText(found.Path, Encoding.UTF8);
}
first.Lyrics.ParseLRC(content);
return first;
}
}
return first;
}
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
#region Interfaces
/// <summary>
/// Defines the <see cref="ILibWatcherService" />
/// </summary>
public interface ILibWatcherService
{
#region Events
/// <summary>
/// Defines the MusicLibraryFilesChanged
/// </summary>
event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
#endregion
#region Methods
/// <summary>
/// The UpdateWatchers
/// </summary>
/// <param name="folders">The folders<see cref="List{LocalLyricsFolder}"/></param>
public void UpdateWatchers(List<LocalLyricsFolder> folders);
#endregion
}
#endregion
}

View File

@@ -0,0 +1,47 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
#region Interfaces
/// <summary>
/// Defines the <see cref="IMusicSearchService" />
/// </summary>
public interface IMusicSearchService
{
#region Methods
/// <summary>
/// The SearchAlbumArtAsync
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <returns>The <see cref="byte[]?"/></returns>
byte[]? SearchAlbumArtAsync(string title, string artist);
/// <summary>
/// The SearchLyricsAsync
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="album">The album<see cref="string"/></param>
/// <param name="durationMs">The durationMs<see cref="double"/></param>
/// <param name="matchMode">The matchMode<see cref="MusicSearchMatchMode"/></param>
/// <returns>The <see cref="Task{(string?, LyricsFormat?)}"/></returns>
Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
);
#endregion
}
#endregion
}

View File

@@ -0,0 +1,56 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Services
{
#region Interfaces
/// <summary>
/// Defines the <see cref="IPlaybackService" />
/// </summary>
public interface IPlaybackService
{
#region Events
/// <summary>
/// Defines the IsPlayingChanged
/// </summary>
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
/// <summary>
/// Defines the PositionChanged
/// </summary>
event EventHandler<PositionChangedEventArgs>? PositionChanged;
/// <summary>
/// Defines the SongInfoChanged
/// </summary>
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
#endregion
#region Properties
/// <summary>
/// Gets a value indicating whether IsPlaying
/// </summary>
bool IsPlaying { get; }
/// <summary>
/// Gets the Position
/// </summary>
TimeSpan Position { get; }
/// <summary>
/// Gets the SongInfo
/// </summary>
SongInfo? SongInfo { get; }
#endregion
}
#endregion
}

View File

@@ -0,0 +1,147 @@
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Services
{
#region Interfaces
/// <summary>
/// Defines the <see cref="ISettingsService" />
/// </summary>
public interface ISettingsService
{
#region Properties
// App behavior
/// <summary>
/// Gets or sets the AutoStartWindowType
/// </summary>
AutoStartWindowType AutoStartWindowType { get; set; }
/// <summary>
/// Gets or sets the BackdropType
/// </summary>
BackdropType BackdropType { get; set; }
// Album art cover style
/// <summary>
/// Gets or sets the CoverImageRadius
/// </summary>
int CoverImageRadius { get; set; }
/// <summary>
/// Gets or sets the CoverOverlayBlurAmount
/// </summary>
int CoverOverlayBlurAmount { get; set; }
/// <summary>
/// Gets or sets the CoverOverlayOpacity
/// </summary>
int CoverOverlayOpacity { get; set; }
// Album art background
/// <summary>
/// Gets or sets a value indicating whether IsCoverOverlayEnabled
/// </summary>
bool IsCoverOverlayEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsDynamicCoverOverlayEnabled
/// </summary>
bool IsDynamicCoverOverlayEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsFirstRun
/// </summary>
bool IsFirstRun { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsLyricsGlowEffectEnabled
/// </summary>
bool IsLyricsGlowEffectEnabled { get; set; }
/// <summary>
/// Gets or sets the Language
/// </summary>
Language Language { get; set; }
// Lyrics lib
/// <summary>
/// Gets or sets the LocalLyricsFolders
/// </summary>
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
// Lyrics style and effetc
/// <summary>
/// Gets or sets the LyricsAlignmentType
/// </summary>
LyricsAlignmentType LyricsAlignmentType { get; set; }
/// <summary>
/// Gets or sets the LyricsBlurAmount
/// </summary>
int LyricsBlurAmount { get; set; }
/// <summary>
/// Gets or sets the LyricsFontColorType
/// </summary>
LyricsFontColorType LyricsFontColorType { get; set; }
/// <summary>
/// Gets or sets the LyricsFontSize
/// </summary>
int LyricsFontSize { get; set; }
/// <summary>
/// Gets or sets the LyricsFontWeight
/// </summary>
LyricsFontWeight LyricsFontWeight { get; set; }
/// <summary>
/// Gets or sets the LyricsGlowEffectScope
/// </summary>
LineRenderingType LyricsGlowEffectScope { get; set; }
/// <summary>
/// Gets or sets the LyricsLineSpacingFactor
/// </summary>
float LyricsLineSpacingFactor { get; set; }
/// <summary>
/// Gets or sets the LyricsSearchProvidersInfo
/// </summary>
List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
/// <summary>
/// Gets or sets the LyricsVerticalEdgeOpacity
/// </summary>
int LyricsVerticalEdgeOpacity { get; set; }
// App appearance
/// <summary>
/// Gets or sets the ThemeType
/// </summary>
ElementTheme ThemeType { get; set; }
/// <summary>
/// Gets or sets the TitleBarType
/// </summary>
TitleBarType TitleBarType { get; set; }
#endregion
}
#endregion
}

View File

@@ -0,0 +1,137 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
using global::BetterLyrics.WinUI3.Events;
using global::BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace BetterLyrics.WinUI3.Services
{
/// <summary>
/// Defines the <see cref="LibWatcherService" />
/// </summary>
public class LibWatcherService : IDisposable, ILibWatcherService
{
#region Fields
/// <summary>
/// Defines the _settingsService
/// </summary>
private readonly ISettingsService _settingsService;
/// <summary>
/// Defines the _watchers
/// </summary>
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LibWatcherService"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
public LibWatcherService(ISettingsService settingsService)
{
_settingsService = settingsService;
UpdateWatchers(_settingsService.LocalLyricsFolders);
}
#endregion
#region Events
/// <summary>
/// Defines the MusicLibraryFilesChanged
/// </summary>
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
#endregion
#region Methods
/// <summary>
/// The Dispose
/// </summary>
public void Dispose()
{
foreach (var watcher in _watchers.Values)
{
watcher.Dispose();
}
_watchers.Clear();
}
/// <summary>
/// The UpdateWatchers
/// </summary>
/// <param name="folders">The folders<see cref="List{LocalLyricsFolder}"/></param>
public void UpdateWatchers(List<LocalLyricsFolder> folders)
{
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
{
if (!folders.Any(x => x.Path == key && x.IsEnabled))
{
_watchers[key].Dispose();
_watchers.Remove(key);
}
}
// 添加新的监听
foreach (var folder in folders)
{
if (
!_watchers.ContainsKey(folder.Path)
&& Directory.Exists(folder.Path)
&& folder.IsEnabled
)
{
var watcher = new FileSystemWatcher(folder.Path)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true,
};
watcher.Created += (s, e) => OnChanged(folder.Path, e);
watcher.Changed += (s, e) => OnChanged(folder.Path, e);
watcher.Deleted += (s, e) => OnChanged(folder.Path, e);
watcher.Renamed += (s, e) => OnChanged(folder.Path, e);
_watchers[folder.Path] = watcher;
}
}
}
/// <summary>
/// The OnChanged
/// </summary>
/// <param name="folder">The folder<see cref="string"/></param>
/// <param name="e">The e<see cref="FileSystemEventArgs"/></param>
private void OnChanged(string folder, FileSystemEventArgs e)
{
App.DispatcherQueue!.TryEnqueue(
Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
MusicLibraryFilesChanged?.Invoke(
this,
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
);
}
);
}
#endregion
}
}
}

View File

@@ -0,0 +1,671 @@
// 2025/6/23 by Zhe Fang
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace BetterLyrics.WinUI3.Services
{
/// <summary>
/// Defines the <see cref="MusicSearchService" />
/// </summary>
public class MusicSearchService : IMusicSearchService
{
#region Fields
/// <summary>
/// Defines the _httpClient
/// </summary>
private readonly HttpClient _lrcLibHttpClient;
private readonly HttpClient _amllTtmlDbHttpClient;
/// <summary>
/// Defines the _settingsService
/// </summary>
private readonly ISettingsService _settingsService;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="MusicSearchService"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
public MusicSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_lrcLibHttpClient = new HttpClient();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
);
_amllTtmlDbHttpClient = new HttpClient();
}
#endregion
#region Methods
/// <summary>
/// The SearchAlbumArtAsync
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <returns>The <see cref="byte[]?"/></returns>
public byte[]? SearchAlbumArtAsync(string title, string artist)
{
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), title, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
}
}
}
}
return null;
}
/// <summary>
/// The SearchLyricsAsync
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="album">The album<see cref="string"/></param>
/// <param name="durationMs">The durationMs<see cref="double"/></param>
/// <param name="matchMode">The matchMode<see cref="MusicSearchMatchMode"/></param>
/// <returns>The <see cref="Task{(string?, LyricsFormat?)}"/></returns>
public async Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
)
{
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
string? cachedLyrics;
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
// Check cache first
if (provider.Provider.IsRemote())
{
cachedLyrics = ReadCache(
title,
artist,
lyricsFormat,
provider.Provider.GetCacheDirectory()
);
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, lyricsFormat);
}
}
string? searchedLyrics = null;
if (provider.Provider.IsLocal())
{
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = LocalLyricsSearchInMusicFiles(title, artist);
}
else
{
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
lyricsFormat
);
}
}
else
{
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLibAsync(
title,
artist,
album,
(int)(durationMs / 1000),
matchMode
);
break;
case LyricsSearchProvider.QQ:
searchedLyrics = await SearchQQAsync(
title,
artist,
album,
(int)durationMs,
matchMode
);
break;
case LyricsSearchProvider.Kugou:
searchedLyrics = await SearchKugouAsync(
title,
artist,
album,
(int)durationMs,
matchMode
);
break;
case LyricsSearchProvider.Netease:
searchedLyrics = await SearchNeteaseAsync(
title,
artist,
album,
(int)durationMs,
matchMode
);
break;
case LyricsSearchProvider.AmllTtmlDb:
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
break;
default:
break;
}
}
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
if (provider.Provider.IsRemote())
{
WriteCache(
title,
artist,
searchedLyrics,
lyricsFormat,
provider.Provider.GetCacheDirectory()
);
}
return (
searchedLyrics,
lyricsFormat == LyricsFormat.NotSpecified
? searchedLyrics.DetectFormat()
: lyricsFormat
);
}
}
return (null, null);
}
private static bool MusicMatch(string fileName, string title, string artist)
{
return fileName.Contains(title) && fileName.Contains(artist);
}
/// <summary>
/// The SanitizeFileName
/// </summary>
/// <param name="fileName">The fileName<see cref="string"/></param>
/// <param name="replacement">The replacement<see cref="char"/></param>
/// <returns>The <see cref="string"/></returns>
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();
}
/// <summary>
/// The LocalLyricsSearchInLyricsFiles
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="format">The format<see cref="LyricsFormat"/></param>
/// <returns>The <see cref="Task{string?}"/></returns>
private async Task<string?> LocalLyricsSearchInLyricsFiles(
string title,
string artist,
LyricsFormat format
)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (
var file in Directory.GetFiles(
folder.Path,
$"*{format.ToFileExtension()}",
SearchOption.AllDirectories
)
)
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(
file,
FileHelper.GetEncoding(file)
);
if (raw != null)
{
return raw;
}
}
}
}
}
return null;
}
/// <summary>
/// The LocalLyricsSearchInMusicFiles
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <returns>The <see cref="string?"/></returns>
private string? LocalLyricsSearchInMusicFiles(string title, string artist)
{
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), title, artist))
{
//Track track = new(file);
//var test1 = track.Lyrics.SynchronizedLyrics;
//var test2 = track.Lyrics.UnsynchronizedLyrics;
try
{
var plain = TagLib.File.Create(file).Tag.Lyrics;
if (plain != null && plain != string.Empty)
{
return plain;
}
}
catch (Exception e)
{
throw e;
}
}
}
}
}
return null;
}
/// <summary>
/// The ReadCache
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="format">The format<see cref="LyricsFormat"/></param>
/// <returns>The <see cref="string?"/></returns>
private string? ReadCache(
string title,
string artist,
LyricsFormat format,
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;
}
/// <summary>
/// The SearchLrcLib
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="album">The album<see cref="string"/></param>
/// <param name="duration">The duration<see cref="int"/></param>
/// <param name="matchMode">The matchMode<see cref="MusicSearchMatchMode"/></param>
/// <returns>The <see cref="Task{string?}"/></returns>
private async Task<string?> SearchLrcLibAsync(
string title,
string artist,
string album,
int duration,
MusicSearchMatchMode matchMode
)
{
// Build API query URL
var url =
$"https://lrclib.net/api/search?"
+ $"track_name={Uri.EscapeDataString(title)}&"
+ $"artist_name={Uri.EscapeDataString(artist)}";
if (matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration)
{
url +=
$"&album_name={Uri.EscapeDataString(album)}"
+ $"&durationMs={Uri.EscapeDataString(duration.ToString())}";
}
var response = await _lrcLibHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
var json = await response.Content.ReadAsStringAsync();
var jArr = JsonSerializer.Deserialize(
json,
Serialization.SourceGenerationContext.Default.JsonElement
);
if (jArr.ValueKind == JsonValueKind.Array && jArr.GetArrayLength() > 0)
{
var first = jArr[0];
var syncedLyrics = first.GetProperty("syncedLyrics").GetString();
var result = string.IsNullOrWhiteSpace(syncedLyrics) ? null : syncedLyrics;
if (!string.IsNullOrWhiteSpace(result))
{
return result;
}
}
return null;
}
private async Task<string?> SearchQQAsync(
string title,
string artist,
string album,
int durationMs,
MusicSearchMatchMode matchMode
)
{
string? queryId = (
(
await new Lyricify.Lyrics.Searchers.QQMusicSearcher().SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? durationMs
: null,
Album =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? album
: null,
AlbumArtists = [artist],
Artists = [artist],
Title = title,
}
)
) as Lyricify.Lyrics.Searchers.QQMusicSearchResult
)?.Id;
if (queryId is string id)
{
return (await Lyricify.Lyrics.Decrypter.Qrc.Helper.GetLyricsAsync(id))?.Lyrics;
}
return null;
}
private async Task<string?> SearchKugouAsync(
string title,
string artist,
string album,
int durationMs,
MusicSearchMatchMode matchMode
)
{
string? queryHash = (
(
await new Lyricify.Lyrics.Searchers.KugouSearcher().SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? durationMs
: null,
Album =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? album
: null,
AlbumArtists = [artist],
Artists = [artist],
Title = title,
}
)
) as Lyricify.Lyrics.Searchers.KugouSearchResult
)?.Hash;
if (queryHash != null)
{
var candidate = (
await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(
hash: queryHash
)
)?.Candidates.FirstOrDefault();
if (candidate != null)
{
return await Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyricsAsync(
candidate.Id,
candidate.AccessKey
);
}
}
return null;
}
private async Task<string?> SearchNeteaseAsync(
string title,
string artist,
string album,
int durationMs,
MusicSearchMatchMode matchMode
)
{
string? queryId = (
(
await new Lyricify.Lyrics.Searchers.NeteaseSearcher().SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? durationMs
: null,
Album =
matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration
? album
: null,
AlbumArtists = [artist],
Artists = [artist],
Title = title,
}
)
) as Lyricify.Lyrics.Searchers.NeteaseSearchResult
)?.Id;
if (queryId != null)
{
return (await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(queryId))
?.Lrc
.Lyric;
}
return null;
}
/// <summary>
/// 本地检索 amll-ttml-db 索引并下载歌词内容
/// </summary>
/// <param name="title">歌曲名</param>
/// <param name="artist">歌手名</param>
/// <returns>歌词内容字符串,找不到返回 null</returns>
private async Task<string?> SearchAmllTtmlDbAsync(string title, string artist)
{
// 检索本地 JSONL 索引文件,查找 rawLyricFile
if (!File.Exists(AppInfo.AmllTtmlDbIndexPath))
{
var downloadOk = await DownloadAmllTtmlDbIndexAsync();
if (!downloadOk || !File.Exists(AppInfo.AmllTtmlDbIndexPath))
return null;
}
string? rawLyricFile = null;
foreach (var line in File.ReadLines(AppInfo.AmllTtmlDbIndexPath))
{
if (string.IsNullOrWhiteSpace(line))
continue;
try
{
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
if (!root.TryGetProperty("metadata", out var metadataArr))
continue;
string? musicName = null;
string? artists = null;
foreach (var meta in metadataArr.EnumerateArray())
{
if (meta.GetArrayLength() != 2)
continue;
var key = meta[0].GetString();
var valueArr = meta[1];
if (key == "musicName" && valueArr.GetArrayLength() > 0)
musicName = valueArr[0].GetString();
if (key == "artists" && valueArr.GetArrayLength() > 0)
artists = valueArr[0].GetString();
}
if (musicName == null || artists == null)
continue;
if (MusicMatch($"{artists} - {musicName}", title, artist))
{
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
{
rawLyricFile = rawLyricFileProp.GetString();
break;
}
}
}
catch { }
}
if (string.IsNullOrWhiteSpace(rawLyricFile))
return null;
// 下载歌词内容
var url =
$"https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/raw-lyrics/{rawLyricFile}";
try
{
var response = await _amllTtmlDbHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStringAsync();
}
catch
{
return null;
}
}
/// <summary>
/// 下载 amll-ttml-db 的 JSONL 索引文件到本地缓存目录
/// </summary>
/// <returns>下载成功返回 true否则 false</returns>
public async Task<bool> DownloadAmllTtmlDbIndexAsync()
{
const string url =
"https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/metadata/raw-lyrics-index.jsonl";
try
{
using var response = await _amllTtmlDbHttpClient.GetAsync(
url,
HttpCompletionOption.ResponseHeadersRead
);
if (!response.IsSuccessStatusCode)
return false;
await using var stream = await response.Content.ReadAsStreamAsync();
await using var fs = new FileStream(
AppInfo.AmllTtmlDbIndexPath,
FileMode.Create,
FileAccess.Write,
FileShare.None
);
await stream.CopyToAsync(fs);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// The WriteCache
/// </summary>
/// <param name="title">The title<see cref="string"/></param>
/// <param name="artist">The artist<see cref="string"/></param>
/// <param name="lyrics">The lyrics<see cref="string"/></param>
/// <param name="format">The format<see cref="LyricsFormat"/></param>
private void WriteCache(
string title,
string artist,
string lyrics,
LyricsFormat format,
string cacheFolderPath
)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
cacheFolderPath,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
}
#endregion
}
}

View File

@@ -0,0 +1,309 @@
// 2025/6/23 by Zhe Fang
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Media.Control;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services
{
/// <summary>
/// Defines the <see cref="PlaybackService" />
/// </summary>
public partial class PlaybackService : IPlaybackService
{
#region Fields
/// <summary>
/// Defines the _dispatcherQueue
/// </summary>
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
/// <summary>
/// Defines the _musicSearchService
/// </summary>
private readonly IMusicSearchService _musicSearchService;
/// <summary>
/// Defines the _currentSession
/// </summary>
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
/// <summary>
/// Defines the _sessionManager
/// </summary>
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="PlaybackService"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
/// <param name="musicSearchService">The musicSearchService<see cref="IMusicSearchService"/></param>
public PlaybackService(
ISettingsService settingsService,
IMusicSearchService musicSearchService
)
{
_musicSearchService = musicSearchService;
InitMediaManager().ConfigureAwait(true);
}
#endregion
#region Events
/// <summary>
/// Defines the IsPlayingChanged
/// </summary>
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
/// <summary>
/// Defines the PositionChanged
/// </summary>
public event EventHandler<PositionChangedEventArgs>? PositionChanged;
/// <summary>
/// Defines the SongInfoChanged
/// </summary>
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
#endregion
#region Properties
/// <summary>
/// Gets a value indicating whether IsPlaying
/// </summary>
public bool IsPlaying { get; private set; }
/// <summary>
/// Gets the Position
/// </summary>
public TimeSpan Position { get; private set; }
/// <summary>
/// Gets the SongInfo
/// </summary>
public SongInfo? SongInfo { get; private set; }
#endregion
#region Methods
/// <summary>
/// Note: this func is invoked by non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private async void CurrentSession_MediaPropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
MediaPropertiesChangedEventArgs? args
)
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
if (sender == null)
{
SongInfo = null;
}
else
{
try
{
mediaProps = await sender.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
if (mediaProps == null)
{
SongInfo = null;
}
else
{
SongInfo = new SongInfo
{
Title = mediaProps.Title,
Artist = mediaProps.Artist,
Album = mediaProps?.AlbumTitle ?? string.Empty,
DurationMs = _currentSession
?.GetTimelineProperties()
.EndTime.TotalMilliseconds,
SourceAppUserModelId = _currentSession?.SourceAppUserModelId,
};
if (
SongInfo.SourceAppUserModelId?.Contains(Package.Current.Id.FamilyName)
?? false
)
{
SongInfo.Title = "甜度爆表";
SongInfo.Artist = "AI";
}
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
SongInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);
}
else
{
SongInfo.AlbumArt = _musicSearchService.SearchAlbumArtAsync(
SongInfo.Title,
SongInfo.Artist
);
if (SongInfo.AlbumArt == null)
{
SongInfo.AlbumArt = await ImageHelper.CreateTextPlaceholderBytesAsync(
$"{SongInfo.Artist} - {SongInfo.Title}",
400,
400
);
}
}
}
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
}
);
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_PlaybackInfoChanged(
GlobalSystemMediaTransportControlsSession? sender,
PlaybackInfoChangedEventArgs? args
)
{
if (sender == null)
{
IsPlaying = false;
}
else
{
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
{
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
IsPlaying = false;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
IsPlaying = true;
break;
default:
break;
}
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(IsPlaying));
}
);
}
/// <summary>
/// The CurrentSession_TimelinePropertiesChanged
/// </summary>
/// <param name="sender">The sender<see cref="GlobalSystemMediaTransportControlsSession?"/></param>
/// <param name="args">The args<see cref="TimelinePropertiesChangedEventArgs?"/></param>
private void CurrentSession_TimelinePropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
TimelinePropertiesChangedEventArgs? args
)
{
if (sender == null)
{
Position = TimeSpan.Zero;
}
else
{
Position = sender.GetTimelineProperties().Position;
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(Position));
}
);
}
/// <summary>
/// The InitMediaManager
/// </summary>
/// <returns>The <see cref="Task"/></returns>
private async Task InitMediaManager()
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
/// <summary>
/// The SessionManager_CurrentSessionChanged
/// </summary>
/// <param name="sender">The sender<see cref="GlobalSystemMediaTransportControlsSessionManager"/></param>
/// <param name="args">The args<see cref="CurrentSessionChangedEventArgs?"/></param>
private void SessionManager_CurrentSessionChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
CurrentSessionChangedEventArgs? args
)
{
// _logger.LogDebug("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged -=
CurrentSession_TimelinePropertiesChanged;
}
// Record and register events for current session
_currentSession = sender.GetCurrentSession();
if (_currentSession != null)
{
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged +=
CurrentSession_TimelinePropertiesChanged;
}
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
#endregion
}
}

View File

@@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services.Settings
{
public static class SettingsDefaultValues
{
public const bool IsFirstRun = true;
// Theme
public const int ThemeType = 0; // Follow system
// Language
public const int Language = 0; // Default
// Music
public const string MusicLibraries = "[]";
// Backdrop
public const int BackdropType = 5; // Acrylic Base
public const bool IsCoverOverlayEnabled = true;
public const bool IsDynamicCoverOverlay = true;
public const int CoverOverlayOpacity = 100; // 1.0
public const int CoverOverlayBlurAmount = 200;
// Title bar
public const int TitleBarType = 0;
// Album art
public const int CoverImageRadius = 24;
// Lyrics
public const int LyricsAlignmentType = 1; // Center
public const int LyricsBlurAmount = 0;
public const int LyricsVerticalEdgeOpacity = 0; // 0.0
public const float LyricsLineSpacingFactor = 0.5f;
public const int LyricsFontSize = 28;
public const bool IsLyricsGlowEffectEnabled = false;
public const bool IsLyricsDynamicGlowEffectEnabled = false;
public const int LyricsFontColorType = 0; // Default
public const int LyricsFontSelectedAccentColorIndex = 0;
// Notification
public const bool NeverShowEnterFullScreenMessage = false;
public const bool NeverShowEnterImmersiveModeMessage = false;
}
}

View File

@@ -1,52 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.Settings
{
public static class SettingsKeys
{
public const string IsFirstRun = "IsFirstRun";
// Theme
public const string ThemeType = "ThemeType";
// Language
public const string Language = "Language";
// Music
public const string MusicLibraries = "MusicLibraries";
// Backdrop
public const string BackdropType = "BackdropType";
public const string IsCoverOverlayEnabled = "IsCoverOverlayEnabled";
public const string IsDynamicCoverOverlay = "IsDynamicCoverOverlay";
public const string CoverOverlayOpacity = "CoverOverlayOpacity";
public const string CoverOverlayBlurAmount = "CoverOverlayBlurAmount";
// Title bar
public const string TitleBarType = "TitleBarType";
// Album art
public const string CoverImageRadius = "CoverImageRadius";
// Lyrics
public const string LyricsAlignmentType = "LyricsAlignmentType";
public const string LyricsBlurAmount = "LyricsBlurAmount";
public const string LyricsVerticalEdgeOpacity = "LyricsVerticalEdgeOpacity";
public const string LyricsLineSpacingFactor = "LyricsLineSpacingFactor";
public const string LyricsFontSize = "LyricsFontSize";
public const string IsLyricsGlowEffectEnabled = "IsLyricsGlowEffectEnabled";
public const string IsLyricsDynamicGlowEffectEnabled = "IsLyricsDynamicGlowEffectEnabled";
public const string LyricsFontColorType = "LyricsFontColorType";
public const string LyricsFontSelectedAccentColorIndex =
"LyricsFontSelectedAccentColorIndex";
// Notification
public const string NeverShowEnterFullScreenMessage = "NeverShowEnterFullScreenMessage";
public const string NeverShowEnterImmersiveModeMessage =
"NeverShowEnterImmersiveModeMessage";
}
}

View File

@@ -1,279 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using BetterLyrics.WinUI3.Messages;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.UI.Xaml;
using Newtonsoft.Json;
using Windows.Globalization;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services.Settings
{
public partial class SettingsService : ObservableObject
{
private readonly ApplicationDataContainer _localSettings;
public SettingsService()
{
_localSettings = ApplicationData.Current.LocalSettings;
_musicLibraries =
[
.. JsonConvert.DeserializeObject<List<string>>(
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)!
)!,
];
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
public bool IsFirstRun
{
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
set => Set(SettingsKeys.IsFirstRun, value);
}
[ObservableProperty]
private bool _isRebuildingLyricsIndexDatabase = false;
[ObservableProperty]
private bool _isImmersiveMode = false;
// Theme
public int Theme
{
get => Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
set
{
Set(SettingsKeys.ThemeType, value);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage((ElementTheme)value));
}
}
// Music
private ObservableCollection<string> _musicLibraries;
public ObservableCollection<string> MusicLibraries
{
get { return _musicLibraries; }
set
{
if (_musicLibraries != null)
{
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
}
_musicLibraries = value;
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
SaveMusicLibraries();
OnPropertyChanged();
}
}
private void SaveMusicLibraries()
{
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
}
// Language
public int Language
{
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
set
{
Set(SettingsKeys.Language, value);
switch ((Models.Language)Language)
{
case Models.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Models.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Models.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Models.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
default:
break;
}
}
}
// Backdrop
public int BackdropType
{
get => Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
set
{
Set(SettingsKeys.BackdropType, value);
WeakReferenceMessenger.Default.Send(
new SystemBackdropChangedMessage((BackdropType)value)
);
}
}
public bool IsCoverOverlayEnabled
{
get =>
Get(
SettingsKeys.IsCoverOverlayEnabled,
SettingsDefaultValues.IsCoverOverlayEnabled
);
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
}
public bool IsDynamicCoverOverlay
{
get =>
Get(
SettingsKeys.IsDynamicCoverOverlay,
SettingsDefaultValues.IsDynamicCoverOverlay
);
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
}
public int CoverOverlayOpacity
{
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
set => Set(SettingsKeys.CoverOverlayOpacity, value);
}
public int CoverOverlayBlurAmount
{
get =>
Get(
SettingsKeys.CoverOverlayBlurAmount,
SettingsDefaultValues.CoverOverlayBlurAmount
);
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
}
// Title bar
public int TitleBarType
{
get => Get(SettingsKeys.TitleBarType, SettingsDefaultValues.TitleBarType);
set => Set(SettingsKeys.TitleBarType, value);
}
// Album art
public int CoverImageRadius
{
get => Get(SettingsKeys.CoverImageRadius, SettingsDefaultValues.CoverImageRadius);
set => Set(SettingsKeys.CoverImageRadius, value);
}
// Lyrics
public int LyricsAlignmentType
{
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
set => Set(SettingsKeys.LyricsAlignmentType, value);
}
public int LyricsBlurAmount
{
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
set => Set(SettingsKeys.LyricsBlurAmount, value);
}
public int LyricsVerticalEdgeOpacity
{
get =>
Get(
SettingsKeys.LyricsVerticalEdgeOpacity,
SettingsDefaultValues.LyricsVerticalEdgeOpacity
);
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
}
public float LyricsLineSpacingFactor
{
get =>
Get(
SettingsKeys.LyricsLineSpacingFactor,
SettingsDefaultValues.LyricsLineSpacingFactor
);
set => Set(SettingsKeys.LyricsLineSpacingFactor, value);
}
public int LyricsFontSize
{
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
set => Set(SettingsKeys.LyricsFontSize, value);
}
public bool IsLyricsGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsGlowEffectEnabled,
SettingsDefaultValues.IsLyricsGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
}
public bool IsLyricsDynamicGlowEffectEnabled
{
get =>
Get(
SettingsKeys.IsLyricsDynamicGlowEffectEnabled,
SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled
);
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
}
public int LyricsFontColorType
{
get => Get(SettingsKeys.LyricsFontColorType, SettingsDefaultValues.LyricsFontColorType);
set => Set(SettingsKeys.LyricsFontColorType, value);
}
public int LyricsFontSelectedAccentColorIndex
{
get =>
Get(
SettingsKeys.LyricsFontSelectedAccentColorIndex,
SettingsDefaultValues.LyricsFontSelectedAccentColorIndex
);
set
{
if (value >= 0)
Set(SettingsKeys.LyricsFontSelectedAccentColorIndex, value);
}
}
//Notification
public bool NeverShowEnterFullScreenMessage
{
get =>
Get(
SettingsKeys.NeverShowEnterFullScreenMessage,
SettingsDefaultValues.NeverShowEnterFullScreenMessage
);
set => Set(SettingsKeys.NeverShowEnterFullScreenMessage, value);
}
public bool NeverShowEnterImmersiveModeMessage
{
get =>
Get(
SettingsKeys.NeverShowEnterImmersiveModeMessage,
SettingsDefaultValues.NeverShowEnterImmersiveModeMessage
);
set => Set(SettingsKeys.NeverShowEnterImmersiveModeMessage, value);
}
// Utils
private T? Get<T>(string key, T? defaultValue = default)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
{
return (T)Convert.ChangeType(value, typeof(T));
}
return defaultValue;
}
private void Set<T>(string key, T value, [CallerMemberName] string? propertyName = null)
{
_localSettings.Values[key] = value;
OnPropertyChanged(propertyName);
}
}
}

View File

@@ -0,0 +1,477 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using Microsoft.UI.Xaml;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services
{
/// <summary>
/// Defines the <see cref="SettingsService" />
/// </summary>
public class SettingsService : ISettingsService
{
#region Constants
// App behavior
/// <summary>
/// Defines the AutoStartWindowTypeKey
/// </summary>
private const string AutoStartWindowTypeKey = "AutoStartWindowType";
/// <summary>
/// Defines the BackdropTypeKey
/// </summary>
private const string BackdropTypeKey = "BackdropType";
/// <summary>
/// Defines the CoverImageRadiusKey
/// </summary>
private const string CoverImageRadiusKey = "CoverImageRadius";
/// <summary>
/// Defines the CoverOverlayBlurAmountKey
/// </summary>
private const string CoverOverlayBlurAmountKey = "CoverOverlayBlurAmount";
/// <summary>
/// Defines the CoverOverlayOpacityKey
/// </summary>
private const string CoverOverlayOpacityKey = "CoverOverlayOpacity";
// Album art
/// <summary>
/// Defines the IsCoverOverlayEnabledKey
/// </summary>
private const string IsCoverOverlayEnabledKey = "IsCoverOverlayEnabled";
/// <summary>
/// Defines the IsDynamicCoverOverlayEnabledKey
/// </summary>
private const string IsDynamicCoverOverlayEnabledKey = "IsDynamicCoverOverlayEnabled";
/// <summary>
/// Defines the IsFirstRunKey
/// </summary>
private const string IsFirstRunKey = "IsFirstRun";
/// <summary>
/// Defines the IsLyricsGlowEffectEnabledKey
/// </summary>
private const string IsLyricsGlowEffectEnabledKey = "IsLyricsGlowEffectEnabled";
/// <summary>
/// Defines the LanguageKey
/// </summary>
private const string LanguageKey = "Language";
// Lyrics lib
/// <summary>
/// Defines the LocalLyricsFoldersKey
/// </summary>
private const string LocalLyricsFoldersKey = "LocalLyricsFolders";
/// <summary>
/// Defines the LyricsAlignmentTypeKey
/// </summary>
private const string LyricsAlignmentTypeKey = "LyricsAlignmentType";
/// <summary>
/// Defines the LyricsBlurAmountKey
/// </summary>
private const string LyricsBlurAmountKey = "LyricsBlurAmount";
/// <summary>
/// Defines the LyricsFontColorTypeKey
/// </summary>
private const string LyricsFontColorTypeKey = "LyricsFontColorType";
/// <summary>
/// Defines the LyricsFontSizeKey
/// </summary>
private const string LyricsFontSizeKey = "LyricsFontSize";
/// <summary>
/// Defines the LyricsFontWeightKey
/// </summary>
private const string LyricsFontWeightKey = "LyricsFontWeightKey";
/// <summary>
/// Defines the LyricsGlowEffectScopeKey
/// </summary>
private const string LyricsGlowEffectScopeKey = "LyricsGlowEffectScope";
/// <summary>
/// Defines the LyricsLineSpacingFactorKey
/// </summary>
private const string LyricsLineSpacingFactorKey = "LyricsLineSpacingFactor";
/// <summary>
/// Defines the LyricsSearchProvidersInfoKey
/// </summary>
private const string LyricsSearchProvidersInfoKey = "LyricsSearchProvidersInfo";
/// <summary>
/// Defines the LyricsVerticalEdgeOpacityKey
/// </summary>
private const string LyricsVerticalEdgeOpacityKey = "LyricsVerticalEdgeOpacity";
// App appearance
/// <summary>
/// Defines the ThemeTypeKey
/// </summary>
private const string ThemeTypeKey = "ThemeType";
/// <summary>
/// Defines the TitleBarTypeKey
/// </summary>
private const string TitleBarTypeKey = "TitleBarType";
#endregion
#region Fields
/// <summary>
/// Defines the _localSettings
/// </summary>
private readonly ApplicationDataContainer _localSettings;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SettingsService"/> class.
/// </summary>
public SettingsService()
{
_localSettings = ApplicationData.Current.LocalSettings;
SetDefault(IsFirstRunKey, true);
// Lyrics lib
SetDefault(LocalLyricsFoldersKey, "[]");
SetDefault(
LyricsSearchProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
Enum.GetValues<LyricsSearchProvider>()
.Select(p => new LyricsSearchProviderInfo(p, true))
.ToList(),
SourceGenerationContext.Default.ListLyricsSearchProviderInfo
)
);
if (LyricsSearchProvidersInfo.Count != Enum.GetValues<LyricsSearchProvider>().Length)
{
LyricsSearchProvidersInfo = Enum.GetValues<LyricsSearchProvider>()
.Select(p => new LyricsSearchProviderInfo(
p,
LyricsSearchProvidersInfo
.Where(x => x.Provider == p)
.FirstOrDefault()
?.IsEnabled ?? true
))
.ToList();
}
// App appearance
SetDefault(ThemeTypeKey, (int)ElementTheme.Default);
SetDefault(LanguageKey, (int)Language.FollowSystem);
SetDefault(BackdropTypeKey, (int)BackdropType.DesktopAcrylic);
// App behavior
SetDefault(AutoStartWindowTypeKey, (int)AutoStartWindowType.StandardMode);
// Album art
SetDefault(IsCoverOverlayEnabledKey, true);
SetDefault(IsDynamicCoverOverlayEnabledKey, true);
SetDefault(CoverOverlayOpacityKey, 75); // 100 % = 1.0
SetDefault(CoverOverlayBlurAmountKey, 200);
SetDefault(TitleBarTypeKey, (int)TitleBarType.Compact);
SetDefault(CoverImageRadiusKey, 24); // 24 %
// Lyrics
SetDefault(LyricsAlignmentTypeKey, (int)LyricsAlignmentType.Center);
SetDefault(LyricsFontWeightKey, (int)LyricsFontWeight.Bold);
SetDefault(LyricsBlurAmountKey, 5);
SetDefault(LyricsFontColorTypeKey, (int)LyricsFontColorType.Default);
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
SetDefault(LyricsGlowEffectScopeKey, (int)LineRenderingType.UntilCurrentChar);
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the AutoStartWindowType
/// </summary>
public AutoStartWindowType AutoStartWindowType
{
get => (AutoStartWindowType)GetValue<int>(AutoStartWindowTypeKey);
set => SetValue(AutoStartWindowTypeKey, (int)value);
}
/// <summary>
/// Gets or sets the BackdropType
/// </summary>
public BackdropType BackdropType
{
get => (BackdropType)GetValue<int>(BackdropTypeKey);
set => SetValue(BackdropTypeKey, (int)value);
}
/// <summary>
/// Gets or sets the CoverImageRadius
/// </summary>
public int CoverImageRadius
{
get => GetValue<int>(CoverImageRadiusKey);
set => SetValue(CoverImageRadiusKey, value);
}
/// <summary>
/// Gets or sets the CoverOverlayBlurAmount
/// </summary>
public int CoverOverlayBlurAmount
{
get => GetValue<int>(CoverOverlayBlurAmountKey);
set => SetValue(CoverOverlayBlurAmountKey, value);
}
/// <summary>
/// Gets or sets the CoverOverlayOpacity
/// </summary>
public int CoverOverlayOpacity
{
get => GetValue<int>(CoverOverlayOpacityKey);
set => SetValue(CoverOverlayOpacityKey, value);
}
/// <summary>
/// Gets or sets a value indicating whether IsCoverOverlayEnabled
/// </summary>
public bool IsCoverOverlayEnabled
{
get => GetValue<bool>(IsCoverOverlayEnabledKey);
set => SetValue(IsCoverOverlayEnabledKey, value);
}
/// <summary>
/// Gets or sets a value indicating whether IsDynamicCoverOverlayEnabled
/// </summary>
public bool IsDynamicCoverOverlayEnabled
{
get => GetValue<bool>(IsDynamicCoverOverlayEnabledKey);
set => SetValue(IsDynamicCoverOverlayEnabledKey, value);
}
/// <summary>
/// Gets or sets a value indicating whether IsFirstRun
/// </summary>
public bool IsFirstRun
{
get => GetValue<bool>(IsFirstRunKey);
set => SetValue(IsFirstRunKey, value);
}
/// <summary>
/// Gets or sets a value indicating whether IsLyricsGlowEffectEnabled
/// </summary>
public bool IsLyricsGlowEffectEnabled
{
get => GetValue<bool>(IsLyricsGlowEffectEnabledKey);
set => SetValue(IsLyricsGlowEffectEnabledKey, value);
}
/// <summary>
/// Gets or sets the Language
/// </summary>
public Language Language
{
get => (Language)GetValue<int>(LanguageKey);
set => SetValue(LanguageKey, (int)value);
}
/// <summary>
/// Gets or sets the LocalLyricsFolders
/// </summary>
public List<LocalLyricsFolder> LocalLyricsFolders
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LocalLyricsFoldersKey) ?? "[]",
SourceGenerationContext.Default.ListLocalLyricsFolder
)!;
set =>
SetValue(
LocalLyricsFoldersKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLocalLyricsFolder
)
);
}
/// <summary>
/// Gets or sets the LyricsAlignmentType
/// </summary>
public LyricsAlignmentType LyricsAlignmentType
{
get => (LyricsAlignmentType)GetValue<int>(LyricsAlignmentTypeKey);
set => SetValue(LyricsAlignmentTypeKey, (int)value);
}
/// <summary>
/// Gets or sets the LyricsBlurAmount
/// </summary>
public int LyricsBlurAmount
{
get => GetValue<int>(LyricsBlurAmountKey);
set => SetValue(LyricsBlurAmountKey, value);
}
/// <summary>
/// Gets or sets the LyricsFontColorType
/// </summary>
public LyricsFontColorType LyricsFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsFontColorTypeKey);
set => SetValue(LyricsFontColorTypeKey, (int)value);
}
/// <summary>
/// Gets or sets the LyricsFontSize
/// </summary>
public int LyricsFontSize
{
get => GetValue<int>(LyricsFontSizeKey);
set => SetValue(LyricsFontSizeKey, value);
}
/// <summary>
/// Gets or sets the LyricsFontWeight
/// </summary>
public LyricsFontWeight LyricsFontWeight
{
get => (LyricsFontWeight)GetValue<int>(LyricsFontWeightKey);
set => SetValue(LyricsFontWeightKey, (int)value);
}
/// <summary>
/// Gets or sets the LyricsGlowEffectScope
/// </summary>
public LineRenderingType LyricsGlowEffectScope
{
get => (LineRenderingType)GetValue<int>(LyricsGlowEffectScopeKey);
set => SetValue(LyricsGlowEffectScopeKey, (int)value);
}
/// <summary>
/// Gets or sets the LyricsLineSpacingFactor
/// </summary>
public float LyricsLineSpacingFactor
{
get => GetValue<float>(LyricsLineSpacingFactorKey);
set => SetValue(LyricsLineSpacingFactorKey, value);
}
/// <summary>
/// Gets or sets the LyricsSearchProvidersInfo
/// </summary>
public List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LyricsSearchProvidersInfoKey) ?? "[]",
SourceGenerationContext.Default.ListLyricsSearchProviderInfo
)!;
set =>
SetValue(
LyricsSearchProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLyricsSearchProviderInfo
)
);
}
/// <summary>
/// Gets or sets the LyricsVerticalEdgeOpacity
/// </summary>
public int LyricsVerticalEdgeOpacity
{
get => GetValue<int>(LyricsVerticalEdgeOpacityKey);
set => SetValue(LyricsVerticalEdgeOpacityKey, value);
}
/// <summary>
/// Gets or sets the ThemeType
/// </summary>
public ElementTheme ThemeType
{
get => (ElementTheme)GetValue<int>(ThemeTypeKey);
set => SetValue(ThemeTypeKey, (int)value);
}
/// <summary>
/// Gets or sets the TitleBarType
/// </summary>
public TitleBarType TitleBarType
{
get => (TitleBarType)GetValue<int>(TitleBarTypeKey);
set => SetValue(TitleBarTypeKey, (int)value);
}
#endregion
#region Methods
/// <summary>
/// The GetValue
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">The key<see cref="string"/></param>
/// <returns>The <see cref="T?"/></returns>
private T? GetValue<T>(string key)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
{
return (T)value;
}
return default;
}
/// <summary>
/// The SetDefault
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">The key<see cref="string"/></param>
/// <param name="value">The value<see cref="T"/></param>
private void SetDefault<T>(string key, T value)
{
if (_localSettings.Values.ContainsKey(key) && _localSettings.Values[key] is T)
return;
_localSettings.Values[key] = value;
}
/// <summary>
/// The SetValue
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">The key<see cref="string"/></param>
/// <param name="value">The value<see cref="T"/></param>
private void SetValue<T>(string key, T value)
{
_localSettings.Values[key] = value;
}
#endregion
}
}

View File

@@ -121,10 +121,7 @@
<value>Local music libraries</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>Add folders storing music or lyrics to build lyrics index database</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>Open in file explorer</value>
<value>Add folders storing music or lyrics</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>Open in file explorer</value>
@@ -223,14 +220,11 @@
<value>Album art background opacity</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>Settings</value>
<value>Settings - BetterLyrics</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Lyrics style</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>Alignment</value>
</data>
@@ -273,45 +267,39 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>Font size</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>Show lyrics only</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Lyrics only</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
</data>
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>Album background</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>About</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>Lyrics library</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>App appearance</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Glow effect</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>Dynamic glow effect</value>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>Glow effect scope</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>Rebuild lyrics index database</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>Configure lyrics search providers</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>Rebuild</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>Drag to sort, the lyrics search order will be in the following order</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Add</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>Rebuilding the database, please wait...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Welcome to BetterLyrics</value>
</data>
@@ -321,7 +309,7 @@
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>No music playing now</value>
</data>
<data name="SettingsPageDev.Text" xml:space="preserve">
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>Developer options</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
@@ -330,8 +318,11 @@
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>Play using system player</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Log</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Including log files, network lyrics cache</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>Font color</value>
@@ -342,7 +333,7 @@
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>Album art accent color</value>
</data>
<data name="SettingsPageAlbumStyle.Text" xml:space="preserve">
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>Album art style</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
@@ -372,4 +363,172 @@
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>No local files matched</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Album art only</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Split view</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>Change display type</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>Switch to desktop lyrics mode</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-picture mode</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Exit picture-in-picture mode</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>Lyrics not found</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>Lyrics effect</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>Lyrics style</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>This folder is already included in the existing folder and does not need to be added again</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>App behavior</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>When starting the app</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>Activate standard mode</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>Activate dock mode</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>Lyrics not found</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>System tray - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>Dock mode</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>Desktop mode</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>Font weight</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>Thin</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>Extra Light</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Light</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Semi Light</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>Normal</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Medium</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>Semi Bold</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>Bold</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>Extra Bold</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>Black</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>Extra Black</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>Whole lyrics</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>Current line</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>Current char</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>Loading lyrics...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>Local .LRC files</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>Local music files</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>Local .ESLRC files</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Local .TTML files</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains added folders, please delete these folders to add the folder</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Show debug overlay</value>
</data>
<data name="DependenciesSettingsExpander.Header" xml:space="preserve">
<value>Dependencies</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Lock</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Exit</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>Unlock the window</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Lock</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>To unlock after locking, go to the system tray to unlock</value>
</data>
</root>

View File

@@ -0,0 +1,534 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>地元の音楽図書館</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>音楽や歌詞を保存するフォルダーを追加します</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>ファイルエクスプローラーで開きます</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>アプリから削除します</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>次のアイテムを削除しても安全です</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>このパスの元のファイルとフォルダーは、このアプリから削除するときに削除されません</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>フォルダーを追加します</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>テーマ</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>言語</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>システムをフォローします</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>ライト</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>暗い</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>これはオープンソースアプリです</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>新しいウィンドウで開きます</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>GitHubのソースコードを参照してください</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>バージョン</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>なし</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>雲母</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>マイカル</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>デスクトップアクリル</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>アクリルベース</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>アクリル薄い</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>デフォルト</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>変更を適用するためのアプリを再起動します</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>パスはコンピューターでは見つかりません</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>フォルダーが追加されました。二度と追加しないでください。</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>オーバーレイアルバムアートの背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>ダイナミックアルバムアートの背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>アルバムアートの背景不透明</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定 - BetterLyrics</value>
</data>
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>アライメント</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>中心</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>左</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>アルバムアートバックグラウンドブラー量</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>ぼやけの量</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>この値を調整すると、アルバム画像のバックグラウンドブラー強度も増加します。</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>現在の値:</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>ぼかしが有効になっている場合のGPU使用量が大幅に高くなります&gt; 0</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>この機能を有効にすると、GPUの使用率がわずかに増加します</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>上端と下端の不透明度</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>ライン間隔</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>x線の高さ</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>フォントサイズ</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>歌詞のみ</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>没入モード</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>アルバムの背景</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>について</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞ライブラリ</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>アプリの外観</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>グロー効果</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>グローエフェクトスコープ</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>歌詞検索プロバイダーを構成します</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>ドラッグしてソートすると、歌詞の検索注文は次の順序で行われます</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>追加</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>BetterLyrics へようこそ</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>今すぐ歌詞データベースをセットアップしましょう</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>今は音楽が再生されていません</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>開発者オプション</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>テスト音楽を再生します</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>システムプレーヤーを使用して再生します</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>キャッシュ</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>ログファイル、ネットワーク歌詞キャッシュを含む</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>フォントカラー</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>デフォルト</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>アルバムアートアクセントカラー</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>アルバムアートスタイル</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>コーナー半径</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>タイトルバーサイズ</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>コンパクト</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>拡張</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>常にトップ</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>全画面表示</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>ESCを押して、フルスクリーンモードを終了します</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>再びホバリングして、トグルボタンを表示します</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>このメッセージを二度と見せないでください</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>一致するローカルファイルはありません</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>アルバムアートのみ</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分割ビュー</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>表示タイプを変更します</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>デスクトップ歌詞モードに切り替えます</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモード</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモードを終了します</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>歌詞が見つかりません</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>歌詞効果</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>歌詞スタイル</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>このフォルダーは既存のフォルダーに既に含まれており、再度追加する必要はありません</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>アプリの動作</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>アプリを起動するとき</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>標準モードをアクティブにします</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>ドックモードをアクティブにします</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>歌詞が見つかりません</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>システムトレイ - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>ドックモード</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>デスクトップモード</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>フォント重量</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>薄い</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>余分な光</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>ライト</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>半光</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>普通</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>中くらい</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>セミボールド</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>大胆な</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>余分な太字</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>黒</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>余分な黒</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>歌詞全体</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>現在の行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>現在の文字</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>歌詞の読み込み...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>ローカル.LRCファイル</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>ローカル音楽ファイル</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>ローカル.ESLRCファイル</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>ローカル.TTMLファイル</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>このフォルダーには追加されたフォルダーが含まれています。これらのフォルダを削除してフォルダーを追加してください</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>デバッグオーバーレイを表示します</value>
</data>
<data name="DependenciesSettingsExpander.Header" xml:space="preserve">
<value>依存関係</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>ロック</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>設定を開く</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>プログラムを終了します</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>ウィンドウのロックを解除します</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>ロック</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>ロック後にロックを解除するには、システムトレイに移動してロックを解除します</value>
</data>
</root>

View File

@@ -0,0 +1,534 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>로컬 음악 도서관</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>음악이나 가사를 저장하는 폴더 추가</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>파일 탐색기에서 열립니다</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>앱에서 제거하십시오</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>다음 항목을 제거하는 것이 안전합니다</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>이 경로의 원본 파일과 폴더는이 앱에서 제거 할 때 삭제되지 않습니다.</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>폴더를 추가하십시오</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>주제</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>언어</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>시스템을 따르십시오</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>빛</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>어두운</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>이것은 오픈 소스 앱입니다</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>새 창에서 열립니다</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>GitHub의 소스 코드를 참조하십시오</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>버전</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>없음</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>운모</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>운모 대체</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>데스크탑 아크릴</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>아크릴베이스</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>아크릴 얇은</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>투명한</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>배경</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>기본</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>변경 사항을 적용하려면 앱을 다시 시작하십시오</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>경로는 컴퓨터에서 찾을 수 없습니다</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>폴더가 추가되었습니다. 다시 추가하지 마십시오.</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>오버레이 앨범 아트 배경</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>동적 앨범 아트 배경</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>앨범 아트 배경 불투명도</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>설정 - BetterLyrics</value>
</data>
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>조정</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>센터</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>왼쪽</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>오른쪽</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>앨범 아트 배경 흐림 금액</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>흐림 금액</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>이 값을 조정하면 앨범 이미지의 배경 흐림 강도가 증가합니다.</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>현재 가치 :</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>Blur가 활성화 될 때 상당히 높은 GPU 사용량 (&gt; 0)</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>이 기능을 활성화하면 GPU 사용률이 약간 증가합니다</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>상단 및 하단 가장자리 불투명도</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>라인 간격</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>X 라인 높이</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>글꼴 크기</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>가사 만</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>몰입 형 모드</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>앨범 배경</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>에 대한</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>가사 도서관</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>앱 모양</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>글로우 효과</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>글로우 효과 범위</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>가사 검색 제공 업체를 구성하십시오</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>정렬하기 위해 드래그하면 가사 검색 순서는 다음 순서로됩니다.</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>추가하다</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Betterlyrics에 오신 것을 환영합니다</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>지금 가사 데이터베이스를 설정합시다</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>지금 음악이 재생되지 않습니다</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>개발자 옵션</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>테스트 음악을 재생하십시오</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>시스템 플레이어를 사용하여 재생하십시오</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>은닉처</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>로그 파일, 네트워크 가사 캐시 포함</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>글꼴 색상</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>기본</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>앨범 아트 악센트 색상</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>앨범 아트 스타일</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>코너 반경</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>제목 바 크기</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>콤팩트</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>펼친</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>항상 위에</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>전체 화면</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>ESC를 눌러 전체 화면 모드를 종료하십시오</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>토글 버튼을 표시하려면 다시 다시 가져옵니다</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>이 메시지를 다시 표시하지 마십시오</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>로컬 파일이 일치하지 않습니다</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>앨범 아트 만</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>분할보기</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>디스플레이 유형을 변경하십시오</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>데스크탑 가사 모드로 전환하십시오</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>사진 인당 모드</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-Picture 모드 종료</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>가사 효과</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>가사 스타일</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>이 폴더는 이미 기존 폴더에 포함되어 있으며 다시 추가 할 필요가 없습니다.</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>앱 동작</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>앱을 시작할 때</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>표준 모드를 ​​활성화합니다</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>도크 모드를 활성화하십시오</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>시스템 트레이 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>도크 모드</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>데스크탑 모드</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>글꼴 무게</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>얇은</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>여분의 빛</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>빛</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>반 빛</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>정상</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>중간</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>반 대담한</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>용감한</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>추가 대담한</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>검은색</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>여분의 검은 색</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>전체 가사</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>현재 라인</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>현재 숯</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>설정</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>가사로드 ...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>로컬 .LRC 파일</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>로컬 음악 파일</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>로컬 .ESLRC 파일</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>로컬 .TTML 파일</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>이 폴더에는 추가 된 폴더가 포함되어 있습니다. 폴더를 추가하려면이 폴더를 삭제하십시오.</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>디버그 오버레이를 표시하십시오</value>
</data>
<data name="DependenciesSettingsExpander.Header" xml:space="preserve">
<value>의존성</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>잠금</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>열기 설정</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>프로그램을 종료하십시오</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>창을 잠금 해제하십시오</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>잠금</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>잠금 잠금을 해제하려면 시스템 트레이로 이동하여 잠금을 해제하십시오.</value>
</data>
</root>

View File

@@ -121,10 +121,7 @@
<value>本地音乐媒体库</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>添加存放音乐或歌词的文件夹以构建歌词索引数据库</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value>
<value>添加存放音乐或歌词的文件夹</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value>
@@ -223,14 +220,11 @@
<value>专辑图片背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>设置</value>
<value>设置 - BetterLyrics</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌词样式</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>对齐方式</value>
</data>
@@ -273,45 +267,39 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字体大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>仅示歌词</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>仅示歌词</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌词效果</value>
</data>
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>专辑背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>关于</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌词库</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>应用外观</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>辉光效果</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>动态辉光效果</value>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>辉光效果作用范围</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>重构歌词索引数据库</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌词搜索服务</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>重构</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>拖动排序,歌词搜索顺序将按以下顺序</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重构数据库中,请稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>欢迎使用 BetterLyrics</value>
</data>
@@ -321,7 +309,7 @@
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>当前没有正在播放的音乐</value>
</data>
<data name="SettingsPageDev.Text" xml:space="preserve">
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>开发者选项</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
@@ -330,8 +318,11 @@
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>使用系统播放器播放</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日志</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>缓存</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>包括日志文件,网络歌词缓存</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>字体颜色</value>
@@ -342,7 +333,7 @@
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>专辑强调色</value>
</data>
<data name="SettingsPageAlbumStyle.Text" xml:space="preserve">
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>专辑封面样式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
@@ -370,6 +361,174 @@
<value>再次悬停以显示切换按钮</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value> 不再显示此消息</value>
<value>不再显示此消息</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>仅显示专辑封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分屏视图</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>切换显示模式</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>切换到桌面歌词模式</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>画中画模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出画中画模式</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>未找到歌词</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>歌词动效</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>歌词样式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>该文件夹已包含在已有文件夹中,无需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>应用行为</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>启动应用时</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>启动标准模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>启动停靠模式</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>未找到歌词</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>系统托盘 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字体粗细</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>极细</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>超细</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>细体</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>次细</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>常规</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>中等</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>次粗</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>粗体</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>特粗</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>黑体</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>全部歌词</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>当前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>当前字符</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>设置</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>加载歌词中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>本地 .LRC 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音乐文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>显示调试覆盖层</value>
</data>
<data name="DependenciesSettingsExpander.Header" xml:space="preserve">
<value>依赖</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>锁定</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>打开设置</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>解锁窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>锁定</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>锁定后解锁,请转到系统托盘解锁</value>
</data>
</root>

View File

@@ -121,10 +121,7 @@
<value>本地音樂媒體庫</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>新增存放音樂或歌詞的資料夾以建立歌詞索引資料庫</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>在檔案總管中開啟</value>
<value>新增存放音樂或歌詞的資料夾</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>在檔案總管中開啟</value>
@@ -223,14 +220,11 @@
<value>專輯圖片背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定</value>
<value>設定 - BetterLyrics</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞樣式</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>對齊方式</value>
</data>
@@ -273,45 +267,39 @@
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字體大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>僅示歌詞</value>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>僅示歌詞</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞效果</value>
</data>
<data name="SettingsPageAlbumOverlay.Text" xml:space="preserve">
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>專輯背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>關於</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞庫</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<value>應用外觀</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>輝光效果</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>動態輝光效果</value>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>輝光效果作用範圍</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>重構歌詞索引資料庫</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌詞搜尋服務</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>重構</value>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>拖動排序,歌詞搜索順序將按以下順序</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重構資料庫中,請稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>歡迎使用 BetterLyrics</value>
</data>
@@ -321,7 +309,7 @@
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>目前沒有正在播放的音樂</value>
</data>
<data name="SettingsPageDev.Text" xml:space="preserve">
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>開發者選項</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
@@ -330,8 +318,11 @@
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>使用系統播放器播放</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>紀錄</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>快取</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>包括日誌文件,網絡歌詞緩存</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>字體顏色</value>
@@ -342,7 +333,7 @@
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>專輯強調色</value>
</data>
<data name="SettingsPageAlbumStyle.Text" xml:space="preserve">
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>專輯封面樣式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
@@ -372,4 +363,172 @@
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>不再顯示此訊息</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>僅顯示專輯封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>分割畫面視圖</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>切換顯示模式</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>切換到桌面歌詞模式</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>畫中畫模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出畫中畫模式</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<value>找不到歌詞</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<value>歌詞動效</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<value>歌詞樣式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>該資料夾已包含在已有資料夾中,無需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<value>應用行為</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>啟動應用程式時</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>啟動標準模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>啟動停靠模式</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>找不到歌詞</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>系統托盤 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字體粗細</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>極細</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>超細</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>細體</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>次細</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>常規</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>中等</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>次粗</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>粗體</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>特粗</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>黑體</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>全部歌詞</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>目前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>目前字元</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<value>載入歌詞中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>本地 .lRC 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音樂文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
<data name="SettingsPageKO.Content" xml:space="preserve">
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>該文件夾包含已添加文件夾,請刪除這些文件夾以添加該文件夾</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>顯示調試覆蓋層</value>
</data>
<data name="DependenciesSettingsExpander.Header" xml:space="preserve">
<value>依賴</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>鎖定</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>打開設置</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>解鎖窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>鎖定</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>鎖定後解鎖,請轉到系統托盤解鎖</value>
</data>
</root>

View File

@@ -0,0 +1,57 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using System;
using System.Runtime.CompilerServices;
namespace BetterLyrics.WinUI3.ViewModels
{
/// <summary>
/// Defines the <see cref="BaseViewModel" />
/// </summary>
public partial class BaseViewModel : ObservableRecipient, IDisposable
{
#region Fields
/// <summary>
/// Defines the _dispatcherQueue
/// </summary>
private protected readonly DispatcherQueue _dispatcherQueue =
DispatcherQueue.GetForCurrentThread();
/// <summary>
/// Defines the _settingsService
/// </summary>
private protected readonly ISettingsService _settingsService;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="BaseViewModel"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
public BaseViewModel(ISettingsService settingsService)
{
IsActive = true;
_settingsService = settingsService;
}
#endregion
#region Methods
/// <summary>
/// The Dispose
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseWindowModel : ObservableObject
{
public SettingsService SettingsService { get; private set; }
[ObservableProperty]
private int _titleBarFontSize = 11;
[ObservableProperty]
private Notification _notification = new();
[ObservableProperty]
private bool _showInfoBar = false;
public BaseWindowModel(SettingsService settingsService)
{
SettingsService = settingsService;
WeakReferenceMessenger.Default.Register<ShowNotificatonMessage>(
this,
async (r, m) =>
{
Notification = m.Value;
if (
!Notification.IsForeverDismissable
|| AlreadyForeverDismissedThisMessage() == false
)
{
Notification.Visibility = Notification.IsForeverDismissable
? Visibility.Visible
: Visibility.Collapsed;
ShowInfoBar = true;
await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
ShowInfoBar = false;
}
}
);
}
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
switch (Notification.RelatedSettingsKeyName)
{
case SettingsKeys.NeverShowEnterFullScreenMessage:
SettingsService.NeverShowEnterFullScreenMessage = value;
break;
case SettingsKeys.NeverShowEnterImmersiveModeMessage:
SettingsService.NeverShowEnterImmersiveModeMessage = value;
break;
default:
break;
}
}
private bool? AlreadyForeverDismissedThisMessage() =>
Notification.RelatedSettingsKeyName switch
{
SettingsKeys.NeverShowEnterFullScreenMessage =>
SettingsService.NeverShowEnterFullScreenMessage,
SettingsKeys.NeverShowEnterImmersiveModeMessage =>
SettingsService.NeverShowEnterImmersiveModeMessage,
_ => null,
};
}
}

View File

@@ -0,0 +1,353 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
using WinRT.Interop;
using WinUIEx;
using WinUIEx.Messaging;
namespace BetterLyrics.WinUI3
{
/// <summary>
/// Defines the <see cref="HostWindowViewModel" />
/// </summary>
public partial class HostWindowViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<TitleBarType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<BackdropType>>,
IRecipient<PropertyChangedMessage<int>>
{
#region Fields
/// <summary>
/// Defines the _watcherHelper
/// </summary>
private ForegroundWindowWatcherHelper? _watcherHelper = null;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="HostWindowViewModel"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
public HostWindowViewModel(ISettingsService settingsService)
: base(settingsService)
{
TitleBarType = _settingsService.TitleBarType;
ThemeType = _settingsService.ThemeType;
OnTitleBarTypeChanged(TitleBarType);
WeakReferenceMessenger.Default.Register<ShowNotificatonMessage>(
this,
async (r, m) =>
{
Notification = m.Value;
if (
!Notification.IsForeverDismissable
|| AlreadyForeverDismissedThisMessage() == false
)
{
Notification.Visibility = Notification.IsForeverDismissable
? Visibility.Visible
: Visibility.Collapsed;
ShowInfoBar = true;
await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
ShowInfoBar = false;
}
}
);
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the ActivatedWindowAccentColor
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color ActivatedWindowAccentColor { get; set; }
/// <summary>
/// Gets or sets the AppLogoImageIconHeight
/// </summary>
[ObservableProperty]
public partial double AppLogoImageIconHeight { get; set; }
/// <summary>
/// Gets or sets the FramePageType
/// </summary>
[ObservableProperty]
public partial Type FramePageType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsDockMode
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDockMode { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDesktopMode { get; set; } = false;
/// <summary>
/// Gets or sets the Notification
/// </summary>
[ObservableProperty]
public partial Notification Notification { get; set; } = new();
/// <summary>
/// Gets or sets a value indicating whether ShowInfoBar
/// </summary>
[ObservableProperty]
public partial bool ShowInfoBar { get; set; } = false;
/// <summary>
/// Gets or sets the ThemeType
/// </summary>
[ObservableProperty]
public partial ElementTheme ThemeType { get; set; }
/// <summary>
/// Gets or sets the TitleBarFontSize
/// </summary>
[ObservableProperty]
public partial double TitleBarFontSize { get; set; }
/// <summary>
/// Gets or sets the TitleBarHeight
/// </summary>
[ObservableProperty]
public partial double TitleBarHeight { get; set; }
/// <summary>
/// Gets or sets the TitleBarType
/// </summary>
[ObservableProperty]
public partial TitleBarType TitleBarType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsWindowLocked { get; set; } = false;
#endregion
#region Methods
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{BackdropType}"/></param>
public void Receive(PropertyChangedMessage<BackdropType> message)
{
WindowHelper.GetWindowByFramePageType(FramePageType).SystemBackdrop =
SystemBackdropHelper.CreateSystemBackdrop(message.NewValue);
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ElementTheme}"/></param>
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
ThemeType = message.NewValue;
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{int}"/></param>
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
if (IsDockMode)
{
DockModeHelper.UpdateAppBarHeight(
WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
),
message.NewValue * 3
);
}
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{TitleBarType}"/></param>
public void Receive(PropertyChangedMessage<TitleBarType> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.TitleBarType))
{
TitleBarType = message.NewValue;
}
}
}
/// <summary>
/// The UpdateAccentColor
/// </summary>
/// <param name="hwnd">The hwnd<see cref="nint"/></param>
public void UpdateAccentColor(nint hwnd)
{
ActivatedWindowAccentColor = WindowColorHelper
.GetDominantColorBelow(hwnd)
.ToWindowsUIColor();
}
/// <summary>
/// The AlreadyForeverDismissedThisMessage
/// </summary>
/// <returns>The <see cref="bool?"/></returns>
private bool? AlreadyForeverDismissedThisMessage()
{
//if (Notification.RelatedSettingsKeyName is string key)
// return _settingsService.Get(key, SettingsDefaultValues.NeverShowMessage);
//return null;
return null;
}
/// <summary>
/// The StartWatchWindowColorChange
/// </summary>
private void StartWatchWindowColorChange()
{
var hwnd = WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
);
_watcherHelper = new ForegroundWindowWatcherHelper(
hwnd,
onWindowChanged =>
{
UpdateAccentColor(hwnd);
}
);
_watcherHelper.Start();
UpdateAccentColor(hwnd);
}
/// <summary>
/// The StopWatchWindowColorChange
/// </summary>
private void StopWatchWindowColorChange()
{
_watcherHelper?.Stop();
_watcherHelper = null;
}
/// <summary>
/// The ToggleDockMode
/// </summary>
[RelayCommand]
private void ToggleDockMode()
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
IsDockMode = !IsDockMode;
if (IsDockMode)
{
DockModeHelper.Enable(window, _settingsService.LyricsFontSize * 3);
StartWatchWindowColorChange();
}
else
{
DockModeHelper.Disable(window);
StopWatchWindowColorChange();
}
}
[RelayCommand]
private void ToggleDesktopMode()
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
IsDesktopMode = !IsDesktopMode;
if (IsDesktopMode)
{
DesktopModeHelper.Enable(window);
WindowHelper.GetWindowByFramePageType(typeof(LyricsPage)).SystemBackdrop =
SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
}
else
{
DesktopModeHelper.Disable(window);
WindowHelper.GetWindowByFramePageType(typeof(LyricsPage)).SystemBackdrop =
SystemBackdropHelper.CreateSystemBackdrop(_settingsService.BackdropType);
}
}
[RelayCommand]
private void LockWindow()
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
DesktopModeHelper.Lock(window);
IsLyricsWindowLocked = true;
}
/// <summary>
/// The OnFramePageTypeChanged
/// </summary>
/// <param name="value">The value<see cref="Type"/></param>
partial void OnFramePageTypeChanged(Type value)
{
if (value != null)
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
_settingsService.BackdropType
);
}
}
/// <summary>
/// The OnTitleBarTypeChanged
/// </summary>
/// <param name="value">The value<see cref="TitleBarType"/></param>
partial void OnTitleBarTypeChanged(TitleBarType value)
{
switch (value)
{
case TitleBarType.Compact:
AppLogoImageIconHeight = 18;
TitleBarFontSize = 11;
break;
case TitleBarType.Extended:
AppLogoImageIconHeight = 20;
TitleBarFontSize = 14;
break;
default:
break;
}
TitleBarHeight = value.GetHeight();
}
#endregion
}
}

View File

@@ -0,0 +1,358 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using WinUIEx.Messaging;
namespace BetterLyrics.WinUI3.ViewModels
{
/// <summary>
/// Defines the <see cref="LyricsPageViewModel" />
/// </summary>
public partial class LyricsPageViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<LyricsStatus>>
{
#region Fields
/// <summary>
/// Defines the _playbackService
/// </summary>
private readonly IPlaybackService _playbackService;
/// <summary>
/// Defines the _preferredDisplayTypeBeforeSwitchToDockMode
/// </summary>
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToNonStandardMode;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LyricsPageViewModel"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
/// <param name="playbackService">The playbackService<see cref="IPlaybackService"/></param>
public LyricsPageViewModel(
ISettingsService settingsService,
IPlaybackService playbackService
)
: base(settingsService)
{
LyricsFontSize = _settingsService.LyricsFontSize;
CoverImageRadius = _settingsService.CoverImageRadius;
_playbackService = playbackService;
_playbackService.SongInfoChanged += async (_, args) =>
await UpdateSongInfoUI(args.SongInfo).ConfigureAwait(true);
IsFirstRun = _settingsService.IsFirstRun;
UpdateSongInfoUI(_playbackService.SongInfo).ConfigureAwait(true);
}
#endregion
#region Properties
/// <summary>
/// Gets or sets a value indicating whether AboutToUpdateUI
/// </summary>
[ObservableProperty]
public partial bool AboutToUpdateUI { get; set; }
/// <summary>
/// Gets or sets the CoverImage
/// </summary>
[ObservableProperty]
public partial BitmapImage? CoverImage { get; set; }
/// <summary>
/// Gets or sets the CoverImageGridActualHeight
/// </summary>
[ObservableProperty]
public partial double CoverImageGridActualHeight { get; set; }
/// <summary>
/// Gets or sets the CoverImageGridCornerRadius
/// </summary>
[ObservableProperty]
public partial CornerRadius CoverImageGridCornerRadius { get; set; }
/// <summary>
/// Gets or sets the CoverImageRadius
/// </summary>
[ObservableProperty]
public partial int CoverImageRadius { get; set; }
/// <summary>
/// Gets or sets the DisplayType
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsDisplayType DisplayType { get; set; } =
LyricsDisplayType.PlaceholderOnly;
/// <summary>
/// Gets or sets a value indicating whether IsFirstRun
/// </summary>
[ObservableProperty]
public partial bool IsFirstRun { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsNotMockMode
/// </summary>
[ObservableProperty]
public partial bool IsNotMockMode { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether IsWelcomeTeachingTipOpen
/// </summary>
[ObservableProperty]
public partial bool IsWelcomeTeachingTipOpen { get; set; }
/// <summary>
/// Gets or sets the LimitedLineWidth
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial double MaxLyricsWidth { get; set; } = 0.0;
/// <summary>
/// Gets or sets the LyricsFontSize
/// </summary>
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
/// <summary>
/// Gets or sets the LyricsStatus
/// </summary>
[ObservableProperty]
public partial LyricsStatus LyricsStatus { get; set; } = LyricsStatus.Loading;
/// <summary>
/// Gets or sets the PreferredDisplayType
/// </summary>
[ObservableProperty]
public partial LyricsDisplayType? PreferredDisplayType { get; set; } =
LyricsDisplayType.SplitView;
/// <summary>
/// Gets or sets the SongInfo
/// </summary>
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; } = null;
#endregion
#region Methods
/// <summary>
/// The OpenMatchedFileFolderInFileExplorer
/// </summary>
/// <param name="path">The path<see cref="string"/></param>
public void OpenMatchedFileFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
}
);
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{bool}"/></param>
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode))
{
IsNotMockMode = !message.NewValue;
SetNonStandardModePreferredDisplayType(message.NewValue);
TrySwitchToPreferredDisplayType(SongInfo);
}
else if (message.PropertyName == nameof(HostWindowViewModel.IsDesktopMode))
{
SetNonStandardModePreferredDisplayType(message.NewValue);
TrySwitchToPreferredDisplayType(SongInfo);
}
}
}
private void SetNonStandardModePreferredDisplayType(bool isEnabled)
{
if (isEnabled)
{
_preferredDisplayTypeBeforeSwitchToNonStandardMode = PreferredDisplayType;
PreferredDisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
PreferredDisplayType = _preferredDisplayTypeBeforeSwitchToNonStandardMode;
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{int}"/></param>
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
}
}
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsStatus}"/></param>
public void Receive(PropertyChangedMessage<LyricsStatus> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsStatus))
{
LyricsStatus = message.NewValue;
}
}
}
/// <summary>
/// The UpdateSongInfoUI
/// </summary>
/// <param name="songInfo">The songInfo<see cref="SongInfo?"/></param>
/// <returns>The <see cref="Task"/></returns>
public async Task UpdateSongInfoUI(SongInfo? songInfo)
{
AboutToUpdateUI = true;
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
SongInfo = songInfo;
CoverImage =
(songInfo?.AlbumArt == null)
? null
: await ImageHelper.GetBitmapImageFromBytesAsync(songInfo.AlbumArt);
TrySwitchToPreferredDisplayType(songInfo);
AboutToUpdateUI = false;
}
/// <summary>
/// The OnDisplayTypeChanged
/// </summary>
/// <param name="value">The value<see cref="object"/></param>
[RelayCommand]
private void OnDisplayTypeChanged(object value)
{
int index = Convert.ToInt32(value);
PreferredDisplayType = (LyricsDisplayType)index;
DisplayType = (LyricsDisplayType)index;
}
/// <summary>
/// The OpenSettingsWindow
/// </summary>
[RelayCommand]
private void OpenSettingsWindow()
{
WindowHelper.OpenSettingsWindow();
}
/// <summary>
/// The TrySwitchToPreferredDisplayType
/// </summary>
/// <param name="songInfo">The songInfo<see cref="SongInfo?"/></param>
private void TrySwitchToPreferredDisplayType(SongInfo? songInfo)
{
LyricsDisplayType displayType;
if (songInfo == null)
{
displayType = LyricsDisplayType.PlaceholderOnly;
}
else if (PreferredDisplayType is LyricsDisplayType preferredDisplayType)
{
displayType = preferredDisplayType;
}
else
{
displayType = LyricsDisplayType.SplitView;
}
DisplayType = displayType;
}
/// <summary>
/// The OnCoverImageGridActualHeightChanged
/// </summary>
/// <param name="value">The value<see cref="double"/></param>
partial void OnCoverImageGridActualHeightChanged(double value)
{
if (double.IsNaN(value))
return;
CoverImageGridCornerRadius = new CornerRadius(CoverImageRadius / 100f * value / 2);
}
/// <summary>
/// The OnCoverImageRadiusChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnCoverImageRadiusChanged(int value)
{
if (double.IsNaN(CoverImageGridActualHeight))
return;
CoverImageGridCornerRadius = new CornerRadius(
value / 100f * CoverImageGridActualHeight / 2
);
}
/// <summary>
/// The OnIsFirstRunChanged
/// </summary>
/// <param name="value">The value<see cref="bool"/></param>
partial void OnIsFirstRunChanged(bool value)
{
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
}
#endregion
}
}

View File

@@ -0,0 +1,322 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
/// <summary>
/// The Update
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="args">The args<see cref="CanvasAnimatedUpdateEventArgs"/></param>
public void Update(ICanvasAnimatedControl control, CanvasAnimatedUpdateEventArgs args)
{
if (_isPlaying)
{
TotalTime += args.Timing.ElapsedTime;
}
ElapsedTime = args.Timing.ElapsedTime;
if (_immersiveBgTransition.IsTransitioning)
{
_immersiveBgTransition.Update(ElapsedTime);
}
if (_albumArtBgTransition.IsTransitioning)
{
_albumArtBgTransition.Update(ElapsedTime);
}
if (IsDynamicCoverOverlayEnabled)
{
_rotateAngle += _coverRotateSpeed;
_rotateAngle %= MathF.PI * 2;
}
if (_maxLyricsWidthTransition.IsTransitioning)
{
_maxLyricsWidthTransition.Update(ElapsedTime);
_isRelayoutNeeded = true;
}
if (_isRelayoutNeeded)
{
ReLayout(control);
_isRelayoutNeeded = false;
UpdateCanvasYScrollOffset(control, false);
}
else
{
UpdateCanvasYScrollOffset(control, true);
}
UpdateLinesProps();
}
/// <summary>
/// The UpdateCanvasYScrollOffset
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
private void UpdateCanvasYScrollOffset(ICanvasAnimatedControl control, bool withAnimation)
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0)
{
return;
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(currentPlayingLineIndex);
var playingTextLayout = currentPlayingLine?.CanvasTextLayout;
if (currentPlayingLine == null || playingTextLayout == null)
{
return;
}
float targetYScrollOffset =
(float?)(
-currentPlayingLine.Position.Y
+ _multiLangLyrics.SafeGet(_langIndex)?[0].Position.Y
- playingTextLayout.LayoutBounds.Height / 2
) ?? 0f;
if (withAnimation && !_canvasYScrollTransition.IsTransitioning)
{
_canvasYScrollTransition.StartTransition(targetYScrollOffset);
}
else if (!withAnimation)
{
_canvasYScrollTransition.JumpTo(targetYScrollOffset);
}
if (_canvasYScrollTransition.IsTransitioning)
{
_canvasYScrollTransition.Update(ElapsedTime);
}
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update visible line indices
for (int i = startLineIndex; i <= endLineIndex; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null || line.CanvasTextLayout == null)
{
continue;
}
var textLayout = line.CanvasTextLayout;
if (
_canvasYScrollTransition.Value
+ (float)(control.Size.Height / 2)
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= 0
)
{
if (_startVisibleLineIndex == -1)
{
_startVisibleLineIndex = i;
}
}
if (
_canvasYScrollTransition.Value
+ (float)(control.Size.Height / 2)
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (_endVisibleLineIndex == -1)
{
_endVisibleLineIndex = i;
}
}
}
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
{
_endVisibleLineIndex = endLineIndex;
}
}
/// <summary>
/// The UpdateFontColor
/// </summary>
private protected void UpdateFontColor()
{
Color fallback = Colors.Transparent;
switch (Theme)
{
case ElementTheme.Default:
switch (Application.Current.RequestedTheme)
{
case ApplicationTheme.Light:
fallback = _darkFontColor;
break;
case ApplicationTheme.Dark:
fallback = _lightFontColor;
break;
default:
break;
}
break;
case ElementTheme.Light:
fallback = _darkFontColor;
break;
case ElementTheme.Dark:
fallback = _lightFontColor;
break;
default:
break;
}
switch (LyricsFontColorType)
{
case Enums.LyricsFontColorType.Default:
_fontColor = fallback;
break;
case Enums.LyricsFontColorType.Dominant:
_fontColor = _albumArtAccentColor ?? fallback;
break;
default:
break;
}
}
/// <summary>
/// The UpdateLinesProps
/// </summary>
/// <param name="source">The source<see cref="List{LyricsLine}?"/></param>
/// <param name="defaultOpacity">The defaultOpacity<see cref="float"/></param>
private void UpdateLinesProps()
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
int halfVisibleLineCount =
Math.Max(
currentPlayingLineIndex - _startVisibleLineIndex,
_endVisibleLineIndex - currentPlayingLineIndex
) + 1;
if (halfVisibleLineCount < 1)
{
return;
}
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null)
{
return;
}
int distanceFromPlayingLine = Math.Abs(i - currentPlayingLineIndex);
if (distanceFromPlayingLine > halfVisibleLineCount)
{
return;
}
float distanceZoomFactor = distanceFromPlayingLine / (float)halfVisibleLineCount;
line.BlurAmountTransition.StartTransition(LyricsBlurAmount * distanceZoomFactor);
line.ScaleTransition.StartTransition(
_highlightedScale - distanceZoomFactor * (_highlightedScale - _defaultScale)
);
// Only calculate highlight opacity for the current line and the two lines around it
// to avoid unnecessary calculations
if (distanceFromPlayingLine <= 1)
{
line.HighlightOpacityTransition.StartTransition(
distanceFromPlayingLine == 0 ? 1 : 0
);
}
if (line.ScaleTransition.IsTransitioning)
{
line.ScaleTransition.Update(ElapsedTime);
}
if (line.BlurAmountTransition.IsTransitioning)
{
line.BlurAmountTransition.Update(ElapsedTime);
}
// Only update highlight opacity for the current line and the two lines around it
if (distanceFromPlayingLine <= 1)
{
if (line.HighlightOpacityTransition.IsTransitioning)
{
line.HighlightOpacityTransition.Update(ElapsedTime);
}
}
}
}
/// <summary>
/// Reassigns positions (x,y) to lyrics lines based on the current control size and font size
/// </summary>
/// <param name="control"></param>
private void ReLayout(ICanvasAnimatedControl control)
{
if (control == null)
return;
_textFormat.FontSize = LyricsFontSize;
float y = _topMargin;
// Init Positions
for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
{
var line = _multiLangLyrics[_langIndex].SafeGet(i);
if (line == null)
{
continue;
}
if (line.CanvasTextLayout != null)
{
line.CanvasTextLayout.Dispose();
line.CanvasTextLayout = null;
}
// Calculate layout bounds
line.CanvasTextLayout = new CanvasTextLayout(
control,
line.Text,
_textFormat,
(float)_maxLyricsWidthTransition.Value,
(float)control.Size.Height
);
line.Position = new Vector2(0, y);
y +=
(float)line.CanvasTextLayout.LayoutBounds.Height
/ line.CanvasTextLayout.LineCount
* (line.CanvasTextLayout.LineCount + LyricsLineSpacingFactor);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More