Compare commits

...

37 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
7bca1d1205 Merge pull request #5 from jayfunc/dev
Add dock mode, improve glow effect, fix bugs ...
2025-06-17 21:50:22 -04:00
104 changed files with 11059 additions and 3829 deletions

View File

@@ -10,8 +10,8 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=Zhe"
Version="1.0.4.0" />
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
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

@@ -45,6 +45,8 @@
<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,12 +1,13 @@
using System.Text;
// 2025/6/23 by Zhe Fang
using System;
using System.Text;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
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;
@@ -14,7 +15,9 @@ using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Serilog;
using WinUIEx;
// 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.
@@ -22,22 +25,23 @@ using WinUIEx;
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 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()
{
@@ -48,19 +52,62 @@ namespace BetterLyrics.WinUI3
ResourceLoader = new ResourceLoader();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Helper.AppInfo.EnsureDirectories();
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
@@ -73,10 +120,12 @@ namespace BetterLyrics.WinUI3
})
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IDatabaseService, DatabaseService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
// ViewModels
.AddTransient<HostWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<LyricsRendererViewModel>()
@@ -85,6 +134,11 @@ namespace BetterLyrics.WinUI3
);
}
/// <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
@@ -95,12 +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(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
)
{
WindowHelper.OpenLyricsWindow();
_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
}
}

View File

@@ -11,7 +11,16 @@
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\CustomTransform.bin" />
<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" />
@@ -32,21 +41,21 @@
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<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.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.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="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.25.0" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="6.26.0" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">
@@ -63,9 +72,19 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<Folder Include="Controls\" />
<Folder Include="ViewModels\Lyrics\" />
<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>

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

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

View File

@@ -1,15 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
internal class EnumToIntConverter : IValueConverter
/// <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)
@@ -19,6 +29,14 @@ namespace BetterLyrics.WinUI3.Converter
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)
@@ -27,5 +45,7 @@ namespace BetterLyrics.WinUI3.Converter
}
return Enum.ToObject(targetType, 0);
}
#endregion
}
}

View File

@@ -1,15 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.ViewModels;
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class IntToCornerRadius : IValueConverter
/// <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)
@@ -19,9 +29,19 @@ namespace BetterLyrics.WinUI3.Converter
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

@@ -1,11 +1,26 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
/// <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)
@@ -22,9 +37,19 @@ namespace BetterLyrics.WinUI3.Converter
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

@@ -1,14 +1,24 @@
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.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

@@ -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,12 +8,38 @@ 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

@@ -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,11 +8,43 @@ 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

@@ -6,10 +6,9 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsGlowEffectScope
public enum LineMaskType
{
WholeLyrics,
CurrentLine,
CurrentChar,
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

@@ -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,10 +8,28 @@ 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

@@ -1,10 +1,34 @@
namespace BetterLyrics.WinUI3.Enums
// 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

@@ -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,9 +8,23 @@ 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

@@ -1,30 +1,92 @@
using System;
// 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 Microsoft.UI.Text;
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
@@ -47,5 +109,7 @@ namespace BetterLyrics.WinUI3.Enums
),
};
}
#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,8 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsHighlightType
{
LineByLine,
CharByChar,
}
}

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,21 +8,28 @@ using System.Threading.Tasks;
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

@@ -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,9 +8,23 @@ 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

@@ -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,14 +8,38 @@ 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
@@ -27,5 +53,7 @@ namespace BetterLyrics.WinUI3.Enums
),
};
}
#endregion
}
}

View File

@@ -1,13 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 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

@@ -1,13 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 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

@@ -1,14 +1,26 @@
using System;
// 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;
using BetterLyrics.WinUI3.Models;
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,36 +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);
private static string CustomShaderFileName => "CustomTransform.bin";
public static string CustomShaderPath => Path.Combine(AssetsFolder, CustomShaderFileName);
// 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,19 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="ColorHelper" />
/// </summary>
public static class ColorHelper
{
public static Windows.UI.Color ToWindowsUIColor(this System.Drawing.Color color)
{
return Windows.UI.Color.FromArgb(color.A, color.R, color.G, color.B);
}
#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,
@@ -28,5 +32,17 @@ namespace BetterLyrics.WinUI3.Helper
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

@@ -11,7 +11,7 @@ using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockHelper
public static class DockModeHelper
{
private static readonly HashSet<IntPtr> _registered = [];
@@ -23,6 +23,7 @@ namespace BetterLyrics.WinUI3.Helper
{
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
window.SetIsAlwaysOnTop(false);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
@@ -43,8 +44,6 @@ namespace BetterLyrics.WinUI3.Helper
_originalPositions.Remove(hwnd);
}
window.SetIsAlwaysOnTop(false);
UnregisterAppBar(hwnd);
}
@@ -52,6 +51,7 @@ namespace BetterLyrics.WinUI3.Helper
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
@@ -82,8 +82,6 @@ namespace BetterLyrics.WinUI3.Helper
appBarHeight,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
window.SetIsAlwaysOnTop(true);
}
[DllImport("user32.dll", SetLastError = true)]

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

@@ -1,11 +1,18 @@
using System;
// 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 Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
@@ -13,11 +20,172 @@ using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Defines the <see cref="ImageHelper" />
/// </summary>
public class ImageHelper
{
private static readonly ColorThief _colorThief = new();
#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
)
@@ -31,27 +199,11 @@ namespace BetterLyrics.WinUI3.Helper
return stream;
}
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;
}
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.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();
@@ -60,16 +212,6 @@ namespace BetterLyrics.WinUI3.Helper
return memoryStream.ToArray();
}
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)
),
];
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
#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,11 +1,23 @@
using BetterLyrics.WinUI3.Enums;
// 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
@@ -18,5 +30,7 @@ namespace BetterLyrics.WinUI3.Helper
_ => null,
};
}
#endregion
}
}

View File

@@ -1,4 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
@@ -8,16 +10,91 @@ 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
@@ -27,16 +104,87 @@ namespace BetterLyrics.WinUI3.Helper
window.SetTitleBar(titleBar);
}
public static void OpenSettingsWindow()
{
OpenOrShowWindow(typeof(SettingsPage));
}
/// <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))
@@ -53,33 +201,13 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static void TrackWindow(Window window, Type pageType = null)
{
if (pageType != null)
{
_windowCache[pageType] = window;
}
if (!_activeWindows.Contains(window))
_activeWindows.Add(window);
}
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;
}
// 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)
@@ -95,39 +223,6 @@ namespace BetterLyrics.WinUI3.Helper
return 0.0;
}
public static List<Window> ActiveWindows
{
get { return _activeWindows; }
}
private static List<Window> _activeWindows = new List<Window>();
public static void TryShow(this Window window)
{
if (window is not null)
{
window.Activate();
}
}
public static void TryHide(this Window window)
{
if (window is not null)
{
window.Hide();
}
}
public static Window GetWindowByFramePageType(Type type)
{
foreach (var cachedWindow in _windowCache)
{
if (cachedWindow.Key == type)
{
return cachedWindow.Value;
}
}
return null;
}
#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

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

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

@@ -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,55 +1,74 @@
using System.Collections.Generic;
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Numerics;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Text;
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 LyricsLine Clone()
{
return new LyricsLine
{
Texts = new List<string>(this.Texts),
LanguageIndex = this.LanguageIndex,
StartPlayingTimestampMs = this.StartPlayingTimestampMs,
EndPlayingTimestampMs = this.EndPlayingTimestampMs,
PlayingState = this.PlayingState,
EnteringProgress = this.EnteringProgress,
ExitingProgress = this.ExitingProgress,
PlayingProgress = this.PlayingProgress,
Position = this.Position,
CenterPosition = this.CenterPosition,
Scale = this.Scale,
Opacity = this.Opacity,
};
}
/// <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

@@ -1,106 +1,63 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using ATL;
using BetterLyrics.WinUI3.Helper;
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI;
using Windows.UI;
using static ATL.LyricsInfo;
namespace BetterLyrics.WinUI3.Models
{
/// <summary>
/// Defines the <see cref="SongInfo" />
/// </summary>
public partial class SongInfo : ObservableObject
{
[ObservableProperty]
public partial string? Title { get; set; }
#region Constructors
[ObservableProperty]
public partial string? Artist { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="SongInfo"/> class.
/// </summary>
public SongInfo()
{
}
[ObservableProperty]
public partial ObservableCollection<string>? FilesFound { get; set; }
#endregion
[ObservableProperty]
public partial bool IsLyricsExisted { get; set; } = false;
#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;
[ObservableProperty]
public partial List<LyricsLine>? LyricsLines { get; set; } = null;
public byte[]? AlbumArt { get; set; } = null;
[ObservableProperty]
public partial List<Color>? CoverImageDominantColors { get; set; } = null;
public SongInfo() { }
/// <summary>
/// Try to parse lyrics from the track, optionally override the raw lyrics string.
/// Gets or sets the Title
/// </summary>
/// <param name="track"></param>
/// <param name="overrideRaw"></param>
public void ParseLyrics(Track track, string? overrideRaw = null)
{
List<LyricsLine>? result = null;
[ObservableProperty]
public partial string Title { get; set; }
if (overrideRaw != null)
track.Lyrics.ParseLRC(overrideRaw);
var lyricsPhrases = track.Lyrics.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0)
{
if (lyricsPhrases[0].TimestampMs > 0)
{
var placeholder = new LyricsPhrase(0, $"{track.Artist} - {track.Title}");
lyricsPhrases.Insert(0, placeholder);
lyricsPhrases.Insert(0, placeholder);
}
}
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++)
{
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count)
{
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
}
else
{
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine { StartPlayingTimestampMs = startTimestampMs };
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs)
{
continue;
}
else
{
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
result ??= [];
result.Add(lyricsLine);
lyricsLine = null;
}
}
if (result != null && result.Count == 0)
{
result = null;
}
LyricsLines = result;
IsLyricsExisted = result != null;
}
#endregion
}
}

View File

@@ -13,8 +13,6 @@
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Loaded="LyricsCanvas_Loaded"
Paused="{x:Bind ViewModel.IsPlaying, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
Update="LyricsCanvas_Update" />
</Grid>
</UserControl>

View File

@@ -1,3 +1,5 @@
// 2025/6/23 by Zhe Fang
using System.Diagnostics;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
@@ -7,18 +9,45 @@ 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
{
public LyricsRendererViewModel ViewModel { get; set; }
#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
@@ -27,17 +56,19 @@ namespace BetterLyrics.WinUI3.Renderer
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.Calculate(sender, args);
ViewModel.Update(sender, args);
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.RequestRelayout();
}
#endregion
}
}

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,176 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using SQLite;
using Ude;
using Windows.Media.Control;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.Database
{
public class DatabaseService : IDatabaseService
{
private readonly SQLiteConnection _connection;
private readonly CharsetDetector _charsetDetector = new();
public DatabaseService()
{
_connection = new SQLiteConnection(AppInfo.DatabasePath);
if (_connection.GetTableInfo("MetadataIndex").Count == 0)
{
_connection.CreateTable<MetadataIndex>();
}
}
public async Task RebuildDatabaseAsync(IList<string> paths)
{
await Task.Run(() =>
{
_connection.DeleteAll<MetadataIndex>();
HashSet<string> insertedPaths = new();
foreach (var path in paths)
{
if (Directory.Exists(path))
{
foreach (
var file in Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)
)
{
if (!insertedPaths.Contains(file))
{
var track = new Track(file);
_connection.Insert(
new MetadataIndex
{
Path = file,
Title = track.Title,
Artist = track.Artist,
}
);
insertedPaths.Add(file);
}
}
}
}
});
}
public async Task<SongInfo> FindSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
)
{
if (mediaProps == null || mediaProps.Title == null || mediaProps.Artist == null)
return new();
var songInfo = new SongInfo { Title = mediaProps?.Title, Artist = mediaProps?.Artist };
// App.ResourceLoader!.GetString("MainPageNoLocalFilesMatched");
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
songInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);
}
return await FindSongInfoAsync(songInfo, mediaProps!.Title, mediaProps!.Artist);
}
public async Task<SongInfo> FindSongInfoAsync(
SongInfo initSongInfo,
string searchTitle,
string searchArtist
)
{
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(searchTitle)
&& m.Artist.Contains(searchArtist)
)
|| (
m.Path != null
&& m.Path.Contains(searchTitle)
&& m.Path.Contains(searchArtist)
)
)
.ToList();
foreach (var found in founds)
{
initSongInfo.FilesFound ??= [];
initSongInfo.FilesFound.Add(found.Path!);
if (initSongInfo.LyricsLines == null || initSongInfo.AlbumArt == null)
{
Track track = new(found.Path);
initSongInfo.ParseLyrics(track);
// Successfully parse lyrics info from metadata in music file
if (initSongInfo.LyricsLines != null)
{
// Used as lyrics source
}
// Find lyrics file
if (initSongInfo.LyricsLines == null && found?.Path?.EndsWith(".lrc") == true)
{
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);
}
initSongInfo.ParseLyrics(track, content);
// Used as lyrics source
}
// Finf album art
if (initSongInfo.AlbumArt == null)
{
if (track.EmbeddedPictures.Count > 0)
{
initSongInfo.AlbumArt = track.EmbeddedPictures[0].PictureData;
// Used as album art source
}
}
}
else
break;
}
if (initSongInfo.AlbumArt == null)
{
initSongInfo.CoverImageDominantColors = null;
}
else
{
initSongInfo.CoverImageDominantColors = await ImageHelper.GetAccentColorsFromByte(
initSongInfo.AlbumArt
);
}
if (initSongInfo.LyricsLines == null) { }
return initSongInfo;
}
}
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
using Windows.Media.Control;
namespace BetterLyrics.WinUI3.Services.Database
{
public interface IDatabaseService
{
Task RebuildDatabaseAsync(IList<string> paths);
Task<SongInfo> FindSongInfoAsync(
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps
);
Task<SongInfo> FindSongInfoAsync(
SongInfo initSongInfo,
string searchTitle,
string searchArtist
);
}
}

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

@@ -1,18 +0,0 @@
using System;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services.Playback
{
public interface IPlaybackService
{
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<PositionChangedEventArgs>? PositionChanged;
void ReSendingMessages();
SongInfo? SongInfo { get; }
bool IsPlaying { get; }
TimeSpan Position { get; }
}
}

View File

@@ -1,185 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Windows.Media.Control;
namespace BetterLyrics.WinUI3.Services.Playback
{
public partial class PlaybackService : IPlaybackService
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<PositionChangedEventArgs>? PositionChanged;
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
public SongInfo? SongInfo { get; private set; }
public bool IsPlaying { get; private set; }
public TimeSpan Position { get; private set; }
private readonly IDatabaseService _databaseService;
public PlaybackService(IDatabaseService databaseService)
{
_databaseService = databaseService;
InitMediaManager().ConfigureAwait(true);
}
private async Task<SongInfo> GetSongInfoAsync()
{
var songInfo = await _databaseService.FindSongInfoAsync(
await _currentSession?.TryGetMediaPropertiesAsync()
);
songInfo.SourceAppUserModelId = _currentSession?.SourceAppUserModelId;
return songInfo;
}
private async Task InitMediaManager()
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
public void ReSendingMessages()
{
// Re-send messages to update UI
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
/// <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(() =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(IsPlaying));
});
}
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;
}
ReSendingMessages();
}
/// <summary>
/// Note: this func is invoked by non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_MediaPropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
MediaPropertiesChangedEventArgs? args
)
{
App.DispatcherQueueTimer!.Debounce(
async () =>
{
// _logger.LogDebug("CurrentSession_MediaPropertiesChanged");
if (sender == null)
SongInfo = null;
else
{
try
{
SongInfo = await GetSongInfoAsync();
}
catch (Exception) { }
}
_dispatcherQueue.TryEnqueue(() =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
});
},
TimeSpan.FromMilliseconds(AnimationHelper.DebounceDefaultDuration)
);
}
private void CurrentSession_TimelinePropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
TimelinePropertiesChangedEventArgs? args
)
{
if (sender == null)
{
Position = TimeSpan.Zero;
}
else
{
Position = sender.GetTimelineProperties().Position;
}
_dispatcherQueue.TryEnqueue(() =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(Position));
});
// _logger.LogDebug(_currentTime);
}
}
}

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,46 +0,0 @@
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Services.Settings
{
public interface ISettingsService
{
bool IsFirstRun { get; set; }
// Lyrics lib
List<string> MusicLibraries { get; set; }
// App appearance
ElementTheme ThemeType { get; set; }
BackdropType BackdropType { get; set; }
TitleBarType TitleBarType { get; set; }
Language Language { get; set; }
// App behavior
AutoStartWindowType AutoStartWindowType { get; set; }
// Album art background
bool IsCoverOverlayEnabled { get; set; }
bool IsDynamicCoverOverlayEnabled { get; set; }
int CoverOverlayOpacity { get; set; }
int CoverOverlayBlurAmount { get; set; }
// Album art cover style
int CoverImageRadius { get; set; }
// Lyrics style and effetc
LyricsAlignmentType LyricsAlignmentType { get; set; }
LyricsFontWeight LyricsFontWeight { get; set; }
int LyricsBlurAmount { get; set; }
int LyricsVerticalEdgeOpacity { get; set; }
float LyricsLineSpacingFactor { get; set; }
int LyricsFontSize { get; set; }
bool IsLyricsGlowEffectEnabled { get; set; }
LyricsGlowEffectScope LyricsGlowEffectScope { get; set; }
LyricsFontColorType LyricsFontColorType { get; set; }
}
}

View File

@@ -1,228 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Newtonsoft.Json;
using Windows.Media;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services.Settings
{
public class SettingsService : ISettingsService
{
private readonly ApplicationDataContainer _localSettings;
private const string IsFirstRunKey = "IsFirstRun";
// App appearance
private const string ThemeTypeKey = "ThemeType";
private const string LanguageKey = "Language";
private const string MusicLibrariesKey = "MusicLibraries";
private const string BackdropTypeKey = "BackdropType";
// App behavior
private const string AutoStartWindowTypeKey = "AutoStartWindowType";
// Album art
private const string IsCoverOverlayEnabledKey = "IsCoverOverlayEnabled";
private const string IsDynamicCoverOverlayEnabledKey = "IsDynamicCoverOverlayEnabled";
private const string CoverOverlayOpacityKey = "CoverOverlayOpacity";
private const string CoverOverlayBlurAmountKey = "CoverOverlayBlurAmount";
private const string TitleBarTypeKey = "TitleBarType";
private const string CoverImageRadiusKey = "CoverImageRadius";
private const string LyricsAlignmentTypeKey = "LyricsAlignmentType";
private const string LyricsFontWeightKey = "LyricsFontWeightKey";
private const string LyricsBlurAmountKey = "LyricsBlurAmount";
private const string LyricsVerticalEdgeOpacityKey = "LyricsVerticalEdgeOpacity";
private const string LyricsLineSpacingFactorKey = "LyricsLineSpacingFactor";
private const string LyricsFontSizeKey = "LyricsFontSize";
private const string IsLyricsGlowEffectEnabledKey = "IsLyricsGlowEffectEnabled";
private const string LyricsFontColorTypeKey = "LyricsFontColorType";
private const string LyricsGlowEffectScopeKey = "LyricsGlowEffectScope";
public bool IsFirstRun
{
get => GetValue<bool>(IsFirstRunKey);
set => SetValue(IsFirstRunKey, value);
}
public ElementTheme ThemeType
{
get => (ElementTheme)GetValue<int>(ThemeTypeKey);
set => SetValue(ThemeTypeKey, (int)value);
}
public Language Language
{
get => (Language)GetValue<int>(LanguageKey);
set => SetValue(LanguageKey, (int)value);
}
public BackdropType BackdropType
{
get => (BackdropType)GetValue<int>(BackdropTypeKey);
set => SetValue(BackdropTypeKey, (int)value);
}
public AutoStartWindowType AutoStartWindowType
{
get => (AutoStartWindowType)GetValue<int>(AutoStartWindowTypeKey);
set => SetValue(AutoStartWindowTypeKey, (int)value);
}
public List<string> MusicLibraries
{
get =>
JsonConvert.DeserializeObject<List<string>>(
GetValue<string>(MusicLibrariesKey) ?? "[]"
)!;
set => SetValue(MusicLibrariesKey, JsonConvert.SerializeObject(value));
}
public bool IsCoverOverlayEnabled
{
get => GetValue<bool>(IsCoverOverlayEnabledKey);
set => SetValue(IsCoverOverlayEnabledKey, value);
}
public bool IsDynamicCoverOverlayEnabled
{
get => GetValue<bool>(IsDynamicCoverOverlayEnabledKey);
set => SetValue(IsDynamicCoverOverlayEnabledKey, value);
}
public int CoverOverlayOpacity
{
get => GetValue<int>(CoverOverlayOpacityKey);
set => SetValue(CoverOverlayOpacityKey, value);
}
public int CoverOverlayBlurAmount
{
get => GetValue<int>(CoverOverlayBlurAmountKey);
set => SetValue(CoverOverlayBlurAmountKey, value);
}
public TitleBarType TitleBarType
{
get => (TitleBarType)GetValue<int>(TitleBarTypeKey);
set => SetValue(TitleBarTypeKey, (int)value);
}
public int CoverImageRadius
{
get => GetValue<int>(CoverImageRadiusKey);
set => SetValue(CoverImageRadiusKey, value);
}
public LyricsAlignmentType LyricsAlignmentType
{
get => (LyricsAlignmentType)GetValue<int>(LyricsAlignmentTypeKey);
set => SetValue(LyricsAlignmentTypeKey, (int)value);
}
public LyricsFontWeight LyricsFontWeight
{
get => (LyricsFontWeight)GetValue<int>(LyricsFontWeightKey);
set => SetValue(LyricsFontWeightKey, (int)value);
}
public int LyricsBlurAmount
{
get => GetValue<int>(LyricsBlurAmountKey);
set => SetValue(LyricsBlurAmountKey, value);
}
public int LyricsVerticalEdgeOpacity
{
get => GetValue<int>(LyricsVerticalEdgeOpacityKey);
set => SetValue(LyricsVerticalEdgeOpacityKey, value);
}
public float LyricsLineSpacingFactor
{
get => GetValue<float>(LyricsLineSpacingFactorKey);
set => SetValue(LyricsLineSpacingFactorKey, value);
}
public int LyricsFontSize
{
get => GetValue<int>(LyricsFontSizeKey);
set => SetValue(LyricsFontSizeKey, value);
}
public bool IsLyricsGlowEffectEnabled
{
get => GetValue<bool>(IsLyricsGlowEffectEnabledKey);
set => SetValue(IsLyricsGlowEffectEnabledKey, value);
}
public LyricsGlowEffectScope LyricsGlowEffectScope
{
get => (LyricsGlowEffectScope)GetValue<int>(LyricsGlowEffectScopeKey);
set => SetValue(LyricsGlowEffectScopeKey, (int)value);
}
public LyricsFontColorType LyricsFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsFontColorTypeKey);
set => SetValue(LyricsFontColorTypeKey, (int)value);
}
public SettingsService()
{
_localSettings = ApplicationData.Current.LocalSettings;
SetDefault(IsFirstRunKey, true);
// App appearance
SetDefault(ThemeTypeKey, (int)ElementTheme.Default);
SetDefault(LanguageKey, (int)Language.FollowSystem);
SetDefault(MusicLibrariesKey, "[]");
SetDefault(BackdropTypeKey, (int)BackdropType.DesktopAcrylic);
// App behavior
SetDefault(AutoStartWindowTypeKey, (int)AutoStartWindowType.StandardMode);
// Album art
SetDefault(IsCoverOverlayEnabledKey, true);
SetDefault(IsDynamicCoverOverlayEnabledKey, true);
SetDefault(CoverOverlayOpacityKey, 100); // 100 % = 1.1
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, 0);
SetDefault(LyricsFontColorTypeKey, (int)LyricsFontColorType.Default);
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
SetDefault(LyricsGlowEffectScopeKey, (int)LyricsGlowEffectScope.CurrentChar);
}
private T? GetValue<T>(string key)
{
if (_localSettings.Values.TryGetValue(key, out object? value))
{
return (T)value;
}
return default;
}
private void SetValue<T>(string key, T value)
{
_localSettings.Values[key] = value;
}
private void SetDefault<T>(string key, T value)
{
if (_localSettings.Values.ContainsKey(key) && _localSettings.Values[key] is T)
return;
_localSettings.Values[key] = value;
}
}
}

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>
@@ -294,18 +291,15 @@
<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>
@@ -324,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>
@@ -420,6 +417,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>Dock mode</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>Desktop mode</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>Font weight</value>
</data>
@@ -468,4 +468,67 @@
<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>
@@ -294,18 +291,15 @@
<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>
@@ -324,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>
@@ -420,6 +417,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字体粗细</value>
</data>
@@ -468,4 +468,67 @@
<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>
@@ -294,18 +291,15 @@
<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>
@@ -324,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>
@@ -420,6 +417,9 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字體粗細</value>
</data>
@@ -468,4 +468,67 @@
<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

@@ -1,23 +0,0 @@
using System;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.Graphics.Canvas.UI.Xaml;
namespace BetterLyrics.WinUI3.Rendering
{
public partial class BaseRendererViewModel(ISettingsService settingsService)
: BaseViewModel(settingsService)
{
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public TimeSpan ElapsedTime { get; set; } = TimeSpan.Zero;
public virtual void Calculate(
ICanvasAnimatedControl control,
CanvasAnimatedUpdateEventArgs args
)
{
TotalTime += args.Timing.ElapsedTime;
ElapsedTime = args.Timing.ElapsedTime;
}
}
}

View File

@@ -1,27 +1,57 @@
using System;
using System.Runtime.CompilerServices;
using BetterLyrics.WinUI3.Services.Settings;
// 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
{
private protected readonly ISettingsService _settingsService;
#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,25 +1,31 @@
using System;
// 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.Settings;
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 H.NotifyIcon.Interop;
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>>,
@@ -27,40 +33,21 @@ namespace BetterLyrics.WinUI3
IRecipient<PropertyChangedMessage<BackdropType>>,
IRecipient<PropertyChangedMessage<int>>
{
#region Fields
/// <summary>
/// Defines the _watcherHelper
/// </summary>
private ForegroundWindowWatcherHelper? _watcherHelper = null;
[ObservableProperty]
public partial Type FramePageType { get; set; }
#endregion
[ObservableProperty]
public partial ElementTheme ThemeType { get; set; }
[ObservableProperty]
public partial double AppLogoImageIconHeight { get; set; }
[ObservableProperty]
public partial double TitleBarFontSize { get; set; }
[ObservableProperty]
public partial double TitleBarHeight { get; set; }
[ObservableProperty]
public partial Notification Notification { get; set; } = new();
[ObservableProperty]
public partial bool ShowInfoBar { get; set; } = false;
[ObservableProperty]
public partial TitleBarType TitleBarType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDockMode { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color ActivatedWindowAccentColor { get; set; }
#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)
{
@@ -89,6 +76,167 @@ namespace BetterLyrics.WinUI3
);
}
#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(
@@ -105,12 +253,68 @@ namespace BetterLyrics.WinUI3
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)
@@ -122,13 +326,10 @@ namespace BetterLyrics.WinUI3
}
}
public void UpdateAccentColor(nint hwnd)
{
ActivatedWindowAccentColor = WindowColorHelper
.GetDominantColorBelow(hwnd)
.ToWindowsUIColor();
}
/// <summary>
/// The OnTitleBarTypeChanged
/// </summary>
/// <param name="value">The value<see cref="TitleBarType"/></param>
partial void OnTitleBarTypeChanged(TitleBarType value)
{
switch (value)
@@ -147,78 +348,6 @@ namespace BetterLyrics.WinUI3
TitleBarHeight = value.GetHeight();
}
[RelayCommand]
private void SwitchInfoBarNeverShowItAgainCheckBox(bool value)
{
//if (Notification.RelatedSettingsKeyName is string key)
// _settingsService.SetValue(key, value);
}
private bool? AlreadyForeverDismissedThisMessage()
{
//if (Notification.RelatedSettingsKeyName is string key)
// return _settingsService.Get(key, SettingsDefaultValues.NeverShowMessage);
//return null;
return null;
}
[RelayCommand]
private void ToggleDockMode()
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
IsDockMode = !IsDockMode;
if (IsDockMode)
{
DockHelper.Enable(window, _settingsService.LyricsFontSize * 3);
StartWatchWindowColorChange();
}
else
{
DockHelper.Disable(window);
StopWatchWindowColorChange();
}
}
public void Receive(PropertyChangedMessage<TitleBarType> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.TitleBarType))
{
TitleBarType = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
ThemeType = message.NewValue;
}
public void Receive(PropertyChangedMessage<BackdropType> message)
{
WindowHelper.GetWindowByFramePageType(FramePageType).SystemBackdrop =
SystemBackdropHelper.CreateSystemBackdrop(message.NewValue);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
if (IsDockMode)
{
DockHelper.UpdateAppBarHeight(
WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
),
message.NewValue * 3
);
}
}
}
}
#endregion
}
}

View File

@@ -1,13 +1,13 @@
using System;
// 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.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -18,61 +18,43 @@ 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<bool>>,
IRecipient<PropertyChangedMessage<LyricsStatus>>
{
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToDockMode;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial double LimitedLineWidth { get; set; } = 0.0;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsDisplayType DisplayType { get; set; } =
LyricsDisplayType.PlaceholderOnly;
[ObservableProperty]
public partial BitmapImage? CoverImage { get; set; }
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; } = null;
[ObservableProperty]
public partial LyricsDisplayType? PreferredDisplayType { get; set; } =
LyricsDisplayType.SplitView;
[ObservableProperty]
public partial bool AboutToUpdateUI { get; set; }
[ObservableProperty]
public partial double CoverImageGridActualHeight { get; set; }
[ObservableProperty]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
public partial CornerRadius CoverImageGridCornerRadius { get; set; }
[ObservableProperty]
public partial bool IsWelcomeTeachingTipOpen { get; set; }
[ObservableProperty]
public partial bool IsFirstRun { get; set; }
[ObservableProperty]
public partial bool IsNotMockMode { get; set; } = true;
#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;
@@ -84,44 +66,195 @@ namespace BetterLyrics.WinUI3.ViewModels
UpdateSongInfoUI(_playbackService.SongInfo).ConfigureAwait(true);
}
partial void OnCoverImageRadiusChanged(int value)
{
if (double.IsNaN(CoverImageGridActualHeight))
return;
#endregion
CoverImageGridCornerRadius = new CornerRadius(
value / 100f * CoverImageGridActualHeight / 2
#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,
}
);
}
partial void OnCoverImageGridActualHeightChanged(double value)
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{bool}"/></param>
public void Receive(PropertyChangedMessage<bool> message)
{
if (double.IsNaN(value))
return;
CoverImageGridCornerRadius = new CornerRadius(CoverImageRadius / 100f * value / 2);
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);
}
}
}
partial void OnIsFirstRunChanged(bool value)
private void SetNonStandardModePreferredDisplayType(bool isEnabled)
{
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
if (isEnabled)
{
_preferredDisplayTypeBeforeSwitchToNonStandardMode = PreferredDisplayType;
PreferredDisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
PreferredDisplayType = _preferredDisplayTypeBeforeSwitchToNonStandardMode;
}
}
[RelayCommand]
private void OnDisplayTypeChanged(object value)
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{int}"/></param>
public void Receive(PropertyChangedMessage<int> message)
{
int index = Convert.ToInt32(value);
PreferredDisplayType = (LyricsDisplayType)index;
DisplayType = (LyricsDisplayType)index;
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;
}
}
}
[RelayCommand]
private void OpenSettingsWindow()
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsStatus}"/></param>
public void Receive(PropertyChangedMessage<LyricsStatus> message)
{
WindowHelper.OpenSettingsWindow();
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;
@@ -139,6 +272,31 @@ namespace BetterLyrics.WinUI3.ViewModels
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;
@@ -159,61 +317,42 @@ namespace BetterLyrics.WinUI3.ViewModels
DisplayType = displayType;
}
public void OpenMatchedFileFolderInFileExplorer(string path)
/// <summary>
/// The OnCoverImageGridActualHeightChanged
/// </summary>
/// <param name="value">The value<see cref="double"/></param>
partial void OnCoverImageGridActualHeightChanged(double value)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
}
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
);
}
public void Receive(PropertyChangedMessage<int> message)
/// <summary>
/// The OnIsFirstRunChanged
/// </summary>
/// <param name="value">The value<see cref="bool"/></param>
partial void OnIsFirstRunChanged(bool value)
{
if (message.Sender.GetType() == typeof(SettingsViewModel))
{
if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
}
}
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode))
{
IsNotMockMode = !message.NewValue;
if (message.NewValue)
{
_preferredDisplayTypeBeforeSwitchToDockMode = PreferredDisplayType;
PreferredDisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
PreferredDisplayType = _preferredDisplayTypeBeforeSwitchToDockMode;
}
TrySwitchToPreferredDisplayType(SongInfo);
}
}
else if (message.Sender is SettingsViewModel)
{
if (
message.PropertyName
== nameof(SettingsViewModel.IsRebuildingLyricsIndexDatabase)
)
{
if (!message.NewValue)
{
_playbackService.ReSendingMessages();
}
}
}
}
#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);
}
}
}
}

View File

@@ -0,0 +1,380 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
: IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<float>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<LyricsFontColorType>>,
IRecipient<PropertyChangedMessage<LyricsAlignmentType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<LineRenderingType>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
{
/// <summary>
/// The OnLyricsFontColorTypeChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsFontColorType"/></param>
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
{
UpdateFontColor();
}
/// <summary>
/// The OnLyricsFontSizeChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnLyricsFontSizeChanged(int value)
{
_isRelayoutNeeded = true;
}
/// <summary>
/// The OnLyricsFontWeightChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsFontWeight"/></param>
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_textFormat.FontWeight = value.ToFontWeight();
_isRelayoutNeeded = true;
}
/// <summary>
/// The OnLyricsLineSpacingFactorChanged
/// </summary>
/// <param name="value">The value<see cref="float"/></param>
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_isRelayoutNeeded = true;
}
/// <summary>
/// The OnSongInfoChanged
/// </summary>
/// <param name="oldValue">The oldValue<see cref="SongInfo?"/></param>
/// <param name="newValue">The newValue<see cref="SongInfo?"/></param>
async partial void OnSongInfoChanged(SongInfo? oldValue, SongInfo? newValue)
{
TotalTime = TimeSpan.Zero;
_lastAlbumArtBitmap = _albumArtBitmap;
if (newValue?.AlbumArt is byte[] bytes)
{
_albumArtBitmap = await (
await ImageHelper.GetDecoderFromByte(bytes)
).GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
_albumArtAccentColor = (
await ImageHelper.GetAccentColorsFromByte(bytes)
).FirstOrDefault();
}
else
{
_albumArtBitmap = null;
_albumArtAccentColor = null;
}
UpdateFontColor();
_albumArtBgTransition.Reset(0f);
_albumArtBgTransition.StartTransition(1f);
await RefreshLyricsAsync();
}
/// <summary>
/// The OnThemeChanged
/// </summary>
/// <param name="value">The value<see cref="ElementTheme"/></param>
partial void OnThemeChanged(ElementTheme value)
{
UpdateFontColor();
}
// Receive methods for handling messages from other view models
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{bool}"/></param>
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.IsDynamicCoverOverlayEnabled))
{
IsDynamicCoverOverlayEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsViewModel.IsCoverOverlayEnabled))
{
IsCoverOverlayEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsViewModel.IsDebugOverlayEnabled))
{
_isDebugOverlayEnabled = message.NewValue;
}
}
else if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled)
)
{
IsLyricsGlowEffectEnabled = message.NewValue;
}
}
else if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.IsDockMode))
{
_isDockMode = message.NewValue;
}
else if (message.PropertyName == nameof(HostWindowViewModel.IsDesktopMode))
{
_isDesktopMode = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{Color}"/></param>
public void Receive(PropertyChangedMessage<Color> message)
{
if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.ActivatedWindowAccentColor))
{
_immersiveBgTransition.StartTransition(message.NewValue);
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{double}"/></param>
public void Receive(PropertyChangedMessage<double> message)
{
if (message.Sender is LyricsPageViewModel)
{
if (message.PropertyName == nameof(LyricsPageViewModel.MaxLyricsWidth))
{
_maxLyricsWidthTransition.StartTransition((float)message.NewValue);
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ElementTheme}"/></param>
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.ThemeType))
{
Theme = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{float}"/></param>
public void Receive(PropertyChangedMessage<float> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsLineSpacingFactor)
)
{
LyricsLineSpacingFactor = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{int}"/></param>
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayOpacity))
{
CoverOverlayOpacity = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsViewModel.CoverOverlayBlurAmount))
{
CoverOverlayBlurAmount = message.NewValue;
}
}
else if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsVerticalEdgeOpacity)
)
{
LyricsVerticalEdgeOpacity = message.NewValue;
}
else if (
message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsBlurAmount)
)
{
LyricsBlurAmount = message.NewValue;
}
else if (
message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize)
)
{
LyricsFontSize = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsAlignmentType}"/></param>
public void Receive(PropertyChangedMessage<LyricsAlignmentType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsAlignmentType)
)
{
LyricsAlignmentType = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsDisplayType}"/></param>
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
DisplayType = message.NewValue;
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsFontColorType}"/></param>
public void Receive(PropertyChangedMessage<LyricsFontColorType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsFontColorType)
)
{
LyricsFontColorType = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsFontWeight}"/></param>
public void Receive(PropertyChangedMessage<LyricsFontWeight> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontWeight))
{
LyricsFontWeight = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{LyricsGlowEffectScope}"/></param>
public void Receive(PropertyChangedMessage<LineRenderingType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsGlowEffectScope)
)
{
LyricsGlowEffectScope = message.NewValue;
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ObservableCollection{LocalLyricsFolder}}"/></param>
public void Receive(PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.LocalLyricsFolders))
{
// Music lib changed, re-fetch lyrics
RefreshLyricsAsync().ConfigureAwait(true);
}
}
}
/// <summary>
/// The Receive
/// </summary>
/// <param name="message">The message<see cref="PropertyChangedMessage{ObservableCollection{LyricsSearchProviderInfo}}"/></param>
public void Receive(
PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>> message
)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.LyricsSearchProvidersInfo))
{
// Lyrics search providers info changed, re-fetch lyrics
RefreshLyricsAsync().ConfigureAwait(true);
}
}
}
}
}

View File

@@ -0,0 +1,565 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
/// <summary>
/// The Draw
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
// Blurred lyrics layer
using var blurredLyrics = new CanvasCommandList(control);
using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
{
switch (DisplayType)
{
case LyricsDisplayType.AlbumArtOnly:
case LyricsDisplayType.PlaceholderOnly:
break;
case LyricsDisplayType.LyricsOnly:
case LyricsDisplayType.SplitView:
DrawBlurredLyrics(control, blurredLyricsDs);
break;
default:
break;
}
}
// Masked mock gradient blurred lyrics layer
using var maskedBlurredLyrics = new CanvasCommandList(control);
using (var maskedBlurredLyricsDs = maskedBlurredLyrics.CreateDrawingSession())
{
if (LyricsVerticalEdgeOpacity == 100)
{
maskedBlurredLyricsDs.DrawImage(blurredLyrics);
}
else
{
using var mask = new CanvasCommandList(control);
using (var maskDs = mask.CreateDrawingSession())
{
DrawGradientOpacityMask(control, maskDs);
}
maskedBlurredLyricsDs.DrawImage(
new AlphaMaskEffect { Source = blurredLyrics, AlphaMask = mask }
);
}
}
using var combined = new CanvasCommandList(control);
using var combinedDs = combined.CreateDrawingSession();
if (IsCoverOverlayEnabled)
{
DrawAlbumArtBackground(control, combinedDs);
}
if (_isDockMode)
{
DrawImmersiveBackground(control, combinedDs, IsCoverOverlayEnabled);
}
combinedDs.DrawImage(maskedBlurredLyrics);
if (_isDesktopMode)
{
float w = (float)control.Size.Width;
float h = (float)control.Size.Height;
float maskThickness = Math.Min(18f, Math.Min(w / 2, h / 2)); // 遮罩宽度
float cornerRadius = maskThickness / 2; // 圆角半径
float blurAmount = maskThickness / 2; // 高斯模糊强度
using var mask = new CanvasCommandList(control);
using (var maskDs = mask.CreateDrawingSession())
{
// 画一个比窗口小一圈的圆角矩形
var rect = new Rect(
maskThickness,
maskThickness,
w - maskThickness * 2,
h - maskThickness * 2
);
maskDs.FillRoundedRectangle(rect, cornerRadius, cornerRadius, Colors.White);
}
// 对圆角矩形做高斯模糊
var blurredMask = new GaussianBlurEffect
{
Source = mask,
BlurAmount = blurAmount,
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
};
ds.DrawImage(new AlphaMaskEffect { Source = combined, AlphaMask = blurredMask });
}
else
{
ds.DrawImage(combined);
}
if (_isDebugOverlayEnabled)
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
var currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(currentPlayingLineIndex);
if (currentPlayingLine != null)
{
GetLinePlayingProgress(
currentPlayingLine,
out int charStartIndex,
out int charLength,
out float charProgress
);
ds.DrawText(
$"DEBUG: "
+ $"播放行 {currentPlayingLineIndex}, 字符 {charStartIndex}, 长度 {charLength}, 进度 {charProgress}\n"
+ $"可见行 [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n"
+ $"当前时刻 {TotalTime}",
new Vector2(10, 10),
Colors.Red
);
}
}
}
/// <summary>
/// The DrawImgae
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
/// <param name="softwareBitmap">The softwareBitmap<see cref="SoftwareBitmap"/></param>
/// <param name="opacity">The opacity<see cref="float"/></param>
private static void DrawImgae(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
SoftwareBitmap softwareBitmap,
float opacity
)
{
using var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap);
float imageWidth = (float)canvasBitmap.Size.Width;
float imageHeight = (float)canvasBitmap.Size.Height;
var scaleFactor =
(float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2))
/ Math.Min(imageWidth, imageHeight);
ds.DrawImage(
new OpacityEffect
{
Source = new ScaleEffect
{
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
BorderMode = EffectBorderMode.Hard,
Scale = new Vector2(scaleFactor),
Source = canvasBitmap,
},
Opacity = opacity,
},
(float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
(float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
);
}
/// <summary>
/// The DrawAlbumArtBackground
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
private void DrawAlbumArtBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
var overlappedCovers = new CanvasCommandList(control.Device);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
if (_albumArtBgTransition.IsTransitioning)
{
if (_lastAlbumArtBitmap != null)
{
DrawImgae(
control,
overlappedCoversDs,
_lastAlbumArtBitmap,
1 - _albumArtBgTransition.Value
);
}
if (_albumArtBitmap != null)
{
DrawImgae(
control,
overlappedCoversDs,
_albumArtBitmap,
_albumArtBgTransition.Value
);
}
}
else if (_albumArtBitmap != null)
{
DrawImgae(control, overlappedCoversDs, _albumArtBitmap, 1f);
}
using var coverOverlayEffect = new OpacityEffect
{
Opacity = CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = CoverOverlayBlurAmount,
Source = overlappedCovers,
},
};
ds.DrawImage(coverOverlayEffect);
ds.Transform = Matrix3x2.Identity;
}
/// <summary>
/// The DrawGradientOpacityMask
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
private void DrawGradientOpacityMask(
ICanvasAnimatedControl control,
CanvasDrawingSession ds
)
{
byte verticalEdgeAlpha = (byte)(255 * LyricsVerticalEdgeOpacity / 100f);
using var maskBrush = new CanvasLinearGradientBrush(
control,
[
new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
new() { Position = 0.5f, Color = Color.FromArgb(255, 0, 0, 0) },
new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, 0, 0, 0) },
]
)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, (float)control.Size.Height),
};
ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
}
/// <summary>
/// The DrawImmersiveBackground
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
/// <param name="withGradient">The withGradient<see cref="bool"/></param>
private void DrawImmersiveBackground(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
bool withGradient
)
{
ds.FillRectangle(
new Rect(0, 0, control.Size.Width, control.Size.Height),
new CanvasLinearGradientBrush(
control,
[
new CanvasGradientStop
{
Position = 0f,
Color = withGradient
? Color.FromArgb(
211,
_immersiveBgTransition.Value.R,
_immersiveBgTransition.Value.G,
_immersiveBgTransition.Value.B
)
: _immersiveBgTransition.Value,
},
new CanvasGradientStop
{
Position = 1,
Color = _immersiveBgTransition.Value,
},
]
)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, (float)control.Size.Height),
}
);
}
/// <summary>
/// The DrawLyrics
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="ds">The ds<see cref="CanvasDrawingSession"/></param>
/// <param name="currentLineHighlightType">The currentLineHighlightType<see cref="LyricsHighlightType"/></param>
private void DrawBlurredLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null)
{
continue;
}
var textLayout = line.CanvasTextLayout;
if (textLayout == null)
{
continue;
}
var position = new Vector2(line.Position.X, line.Position.Y);
float layoutWidth = (float)textLayout.LayoutBounds.Width;
float layoutHeight = (float)textLayout.LayoutBounds.Height;
if (layoutWidth <= 0 || layoutHeight <= 0)
{
continue;
}
float centerX = position.X;
float centerY = position.Y + layoutHeight / 2;
switch (LyricsAlignmentType)
{
case LyricsAlignmentType.Left:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case LyricsAlignmentType.Center:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += (float)_maxLyricsWidthTransition.Value / 2;
break;
case LyricsAlignmentType.Right:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += (float)_maxLyricsWidthTransition.Value;
break;
default:
break;
}
float offsetToLeft =
(float)control.Size.Width - _rightMargin - _maxLyricsWidthTransition.Value;
// Scale
ds.Transform =
Matrix3x2.CreateScale(line.ScaleTransition.Value, new Vector2(centerX, centerY))
* Matrix3x2.CreateTranslation(
offsetToLeft,
_canvasYScrollTransition.Value + (float)(control.Size.Height / 2)
);
// Create the original lyrics line
using var lyrics = new CanvasCommandList(control.Device);
using var lyricsDs = lyrics.CreateDrawingSession();
lyricsDs.DrawTextLayout(textLayout, position, _fontColor);
// Mock gradient blurred lyrics layer
// 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层
// Current line will not be blurred
ds.DrawImage(
new GaussianBlurEffect
{
Source = new OpacityEffect { Source = lyrics, Opacity = _defaultOpacity },
BlurAmount = line.BlurAmountTransition.Value,
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
}
);
// 再叠加当前行歌词层
// Only draw the current line and the two lines around it
// This layer is to highlight the current line
// and for fade-in and fade-out effects, two lines around it is also drawn
if (Math.Abs(i - currentPlayingLineIndex) <= 1)
{
using var mask = new CanvasCommandList(control.Device);
using var maskDs = mask.CreateDrawingSession();
using var highlightMask = new CanvasCommandList(control.Device);
using var highlightMaskDs = highlightMask.CreateDrawingSession();
if (i == currentPlayingLineIndex)
{
GetLinePlayingProgress(
line,
out int charStartIndex,
out int charLength,
out float charProgress
);
var regions = textLayout.GetCharacterRegions(0, charStartIndex);
var highlightRegion = textLayout
.GetCharacterRegions(charStartIndex, charLength)
.FirstOrDefault();
if (regions.Length > 0)
{
// Draw the mask for the current line
for (int j = 0; j < regions.Length; j++)
{
var region = regions[j];
var rect = new Rect(
region.LayoutBounds.X,
region.LayoutBounds.Y + position.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
maskDs.FillRectangle(rect, Colors.Black);
}
}
float highlightTotalWidth = (float)highlightRegion.LayoutBounds.Width;
// Draw the highlight for the current character
float highlightWidth = highlightTotalWidth * charProgress;
float fadingWidth = (float)highlightRegion.LayoutBounds.Height / 2;
// Rects
var highlightRect = new Rect(
highlightRegion.LayoutBounds.X,
highlightRegion.LayoutBounds.Y + position.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + position.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
var fadeOutRect = new Rect(
highlightRect.Right,
highlightRegion.LayoutBounds.Y + position.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
// Brushes
using var fadeInBrush = GetHorizontalFillBrush(
control,
[(0f, 0f), (1f, 1f)],
(float)highlightRect.Right - fadingWidth,
fadingWidth
);
using var fadeOutBrush = GetHorizontalFillBrush(
control,
[(0f, 1f), (1f, 0f)],
(float)highlightRect.Right,
fadingWidth
);
maskDs.FillRectangle(highlightRect, Colors.White);
maskDs.FillRectangle(fadeOutRect, fadeOutBrush);
highlightMaskDs.FillRectangle(fadeInRect, fadeInBrush);
highlightMaskDs.FillRectangle(fadeOutRect, fadeOutBrush);
}
else
{
maskDs.FillRectangle(
new Rect(
textLayout.LayoutBounds.X,
position.Y,
textLayout.LayoutBounds.Width,
textLayout.LayoutBounds.Height
),
Colors.White
);
}
ds.DrawImage(
new OpacityEffect
{
Source = new BlendEffect
{
Background = IsLyricsGlowEffectEnabled
? new GaussianBlurEffect
{
Source = new AlphaMaskEffect
{
Source = lyrics,
AlphaMask = LyricsGlowEffectScope switch
{
LineRenderingType.UntilCurrentChar => mask,
LineRenderingType.CurrentCharOnly => highlightMask,
_ => mask,
},
},
BlurAmount = _lyricsGlowEffectAmount,
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
}
: new CanvasCommandList(control.Device),
Foreground = new AlphaMaskEffect
{
Source = lyrics,
AlphaMask = mask,
},
},
Opacity = line.HighlightOpacityTransition.Value,
}
);
}
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
/// <summary>
/// The GetHorizontalFillBrush
/// </summary>
/// <param name="control">The control<see cref="ICanvasAnimatedControl"/></param>
/// <param name="stopPosition">The stopPosition<see cref="float[]"/></param>
/// <param name="stopOpacity">The stopOpacity<see cref="float[]"/></param>
/// <param name="startX">The startX<see cref="float"/></param>
/// <param name="endX">The endX<see cref="float"/></param>
/// <returns>The <see cref="CanvasLinearGradientBrush"/></returns>
private CanvasLinearGradientBrush GetHorizontalFillBrush(
ICanvasAnimatedControl control,
List<(float position, float opacity)> stops,
float startX,
float width
)
{
return new CanvasLinearGradientBrush(
control,
stops
.Select(stops => new CanvasGradientStop
{
Position = stops.position,
Color = Color.FromArgb((byte)(stops.opacity * 255), 0, 0, 0),
})
.ToArray()
)
{
StartPoint = new Vector2(startX, 0),
EndPoint = new Vector2(startX + width, 0),
};
}
}
}

View File

@@ -1,55 +1,23 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI;
using Windows.UI;
namespace BetterInAppLyrics.WinUI3.ViewModels
{
/// <summary>
/// Defines the <see cref="LyricsSettingsControlViewModel" />
/// </summary>
public partial class LyricsSettingsControlViewModel : BaseViewModel
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsAlignmentType LyricsAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsVerticalEdgeOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial float LyricsLineSpacingFactor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsGlowEffectEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsGlowEffectScope LyricsGlowEffectScope { get; set; }
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LyricsSettingsControlViewModel"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
public LyricsSettingsControlViewModel(ISettingsService settingsService)
: base(settingsService)
{
@@ -66,49 +34,158 @@ namespace BetterInAppLyrics.WinUI3.ViewModels
LyricsFontColorType = _settingsService.LyricsFontColorType;
}
partial void OnLyricsAlignmentTypeChanged(LyricsAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
#endregion
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
#region Properties
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
/// <summary>
/// Gets or sets a value indicating whether IsLyricsGlowEffectEnabled
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsGlowEffectEnabled { get; set; }
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
/// <summary>
/// Gets or sets the LyricsAlignmentType
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsAlignmentType LyricsAlignmentType { get; set; }
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
/// <summary>
/// Gets or sets the LyricsBlurAmount
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBlurAmount { get; set; }
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
/// <summary>
/// Gets or sets the LyricsFontColorType
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsFontColorType { get; set; }
/// <summary>
/// Gets or sets the LyricsFontSize
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontSize { get; set; }
/// <summary>
/// Gets or sets the LyricsFontWeight
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
/// <summary>
/// Gets or sets the LyricsGlowEffectScope
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LineRenderingType LyricsGlowEffectScope { get; set; }
/// <summary>
/// Gets or sets the LyricsLineSpacingFactor
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial float LyricsLineSpacingFactor { get; set; }
/// <summary>
/// Gets or sets the LyricsVerticalEdgeOpacity
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsVerticalEdgeOpacity { get; set; }
#endregion
#region Methods
/// <summary>
/// The OnIsLyricsGlowEffectEnabledChanged
/// </summary>
/// <param name="value">The value<see cref="bool"/></param>
partial void OnIsLyricsGlowEffectEnabledChanged(bool value)
{
_settingsService.IsLyricsGlowEffectEnabled = value;
}
/// <summary>
/// The OnLyricsAlignmentTypeChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsAlignmentType"/></param>
partial void OnLyricsAlignmentTypeChanged(LyricsAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
/// <summary>
/// The OnLyricsBlurAmountChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
/// <summary>
/// The OnLyricsFontColorTypeChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsFontColorType"/></param>
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsFontColorType = value;
}
partial void OnLyricsGlowEffectScopeChanged(LyricsGlowEffectScope value)
/// <summary>
/// The OnLyricsFontSizeChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
/// <summary>
/// The OnLyricsFontWeightChanged
/// </summary>
/// <param name="value">The value<see cref="LyricsFontWeight"/></param>
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
/// <summary>
/// The OnLyricsGlowEffectScopeChanged
/// </summary>
/// <param name="value">The value<see tef="LyricsGlowEffectScope"/></param>
partial void OnLyricsGlowEffectScopeChanged(LineRenderingType value)
{
_settingsService?.LyricsGlowEffectScope = value;
}
/// <summary>
/// The OnLyricsLineSpacingFactorChanged
/// </summary>
/// <param name="value">The value<see cref="float"/></param>
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
/// <summary>
/// The OnLyricsVerticalEdgeOpacityChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
#endregion
}
}

View File

@@ -1,141 +1,85 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using Windows.Globalization;
using Windows.Media;
using Windows.Media.Playback;
using Windows.System;
using Windows.UI;
using WinRT.Interop;
using WinUIEx.Messaging;
namespace BetterLyrics.WinUI3.ViewModels
{
/// <summary>
/// Defines the <see cref="SettingsViewModel" />
/// </summary>
public partial class SettingsViewModel : ObservableRecipient
{
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsRebuildingLyricsIndexDatabase { get; set; } = false;
#region Fields
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial BackdropType BackdropType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TitleBarType TitleBarType { get; set; }
[ObservableProperty]
public partial AutoStartWindowType AutoStartWindowType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<string> MusicLibraries { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsCoverOverlayEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDynamicCoverOverlayEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
partial void OnMusicLibrariesChanged(
ObservableCollection<string> oldValue,
ObservableCollection<string> newValue
)
{
if (oldValue != null)
{
oldValue.CollectionChanged -= (_, _) =>
_settingsService.MusicLibraries = [.. MusicLibraries];
}
if (newValue != null)
{
newValue.CollectionChanged += (_, _) =>
_settingsService.MusicLibraries = [.. MusicLibraries];
}
}
[ObservableProperty]
public partial Enums.Language Language { get; set; }
partial void OnLanguageChanged(Enums.Language value)
{
switch (value)
{
case Enums.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Enums.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Enums.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Enums.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
default:
break;
}
_settingsService.Language = Language;
}
/// <summary>
/// Defines the _libWatcherService
/// </summary>
private readonly ILibWatcherService _libWatcherService;
/// <summary>
/// Defines the _mediaPlayer
/// </summary>
private readonly MediaPlayer _mediaPlayer = new();
private readonly IDatabaseService _databaseService;
/// <summary>
/// Defines the _playbackService
/// </summary>
private readonly IPlaybackService _playbackService;
/// <summary>
/// Defines the _settingsService
/// </summary>
private readonly ISettingsService _settingsService;
public string Version { get; set; } = AppInfo.AppVersion;
#endregion
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "LyricsLib";
#region Constructors
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
public SettingsViewModel(IDatabaseService databaseService, ISettingsService settingsService)
/// <summary>
/// Initializes a new instance of the <see cref="SettingsViewModel"/> class.
/// </summary>
/// <param name="settingsService">The settingsService<see cref="ISettingsService"/></param>
/// <param name="libWatcherService">The libWatcherService<see cref="ILibWatcherService"/></param>
/// <param name="playbackService">The playbackService<see cref="IPlaybackService"/></param>
public SettingsViewModel(
ISettingsService settingsService,
ILibWatcherService libWatcherService,
IPlaybackService playbackService
)
{
_databaseService = databaseService;
_settingsService = settingsService;
_libWatcherService = libWatcherService;
_playbackService = playbackService;
RootGridMargin = new Thickness(0, _settingsService.TitleBarType.GetHeight(), 0, 0);
MusicLibraries = [.. _settingsService.MusicLibraries];
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
ThemeType = _settingsService.ThemeType;
@@ -150,106 +94,194 @@ namespace BetterLyrics.WinUI3.ViewModels
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
}
partial void OnMusicLibrariesChanged(ObservableCollection<string> value)
#endregion
#region Properties
/// <summary>
/// Gets or sets the AutoStartWindowType
/// </summary>
[ObservableProperty]
public partial AutoStartWindowType AutoStartWindowType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDebugOverlayEnabled { get; set; } = false;
/// <summary>
/// Gets or sets the BackdropType
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial BackdropType BackdropType { get; set; }
/// <summary>
/// Gets or sets the CoverImageRadius
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
/// <summary>
/// Gets or sets the CoverOverlayBlurAmount
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
/// <summary>
/// Gets or sets the CoverOverlayOpacity
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayOpacity { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsCoverOverlayEnabled
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsCoverOverlayEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IsDynamicCoverOverlayEnabled
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDynamicCoverOverlayEnabled { get; set; }
/// <summary>
/// Gets or sets the Language
/// </summary>
[ObservableProperty]
public partial Enums.Language Language { get; set; }
/// <summary>
/// Gets or sets the LocalLyricsFolders
/// </summary>
[ObservableProperty]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
/// <summary>
/// Gets or sets the LyricsSearchProvidersInfo
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
/// <summary>
/// Gets or sets the NavViewSelectedItemTag
/// </summary>
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "LyricsLib";
/// <summary>
/// Gets or sets the RootGridMargin
/// </summary>
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
/// <summary>
/// Gets or sets the ThemeType
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeType { get; set; }
/// <summary>
/// Gets or sets the TitleBarType
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TitleBarType TitleBarType { get; set; }
/// <summary>
/// Gets or sets the Version
/// </summary>
public string Version { get; set; } = Helper.AppInfo.AppVersion;
#endregion
#region Methods
/// <summary>
/// The OnLyricsSearchProvidersReordered
/// </summary>
public void OnLyricsSearchProvidersReordered()
{
_settingsService.MusicLibraries = [.. value];
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
partial void OnThemeTypeChanged(ElementTheme value)
/// <summary>
/// The OpenMusicFolder
/// </summary>
/// <param name="folder">The folder<see cref="LocalLyricsFolder"/></param>
public void OpenMusicFolder(LocalLyricsFolder folder)
{
_settingsService.ThemeType = value;
OpenFolderInFileExplorer(folder.Path);
}
partial void OnBackdropTypeChanged(BackdropType value)
/// <summary>
/// The RemoveFolderAsync
/// </summary>
/// <param name="folder">The folder<see cref="LocalLyricsFolder"/></param>
public void RemoveFolderAsync(LocalLyricsFolder folder)
{
_settingsService.BackdropType = value;
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
partial void OnTitleBarTypeChanged(TitleBarType value)
/// <summary>
/// The ToggleLocalLyricsFolder
/// </summary>
/// <param name="folder">The folder<see cref="LocalLyricsFolder"/></param>
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
{
_settingsService.TitleBarType = value;
RootGridMargin = new Thickness(0, value.GetHeight(), 0, 0);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
partial void OnAutoStartWindowTypeChanged(AutoStartWindowType value)
/// <summary>
/// The ToggleLyricsSearchProvider
/// </summary>
/// <param name="providerInfo">The providerInfo<see cref="LyricsSearchProviderInfo"/></param>
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
{
_settingsService.AutoStartWindowType = value;
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
partial void OnCoverImageRadiusChanged(int value)
/// <summary>
/// The AddFolderAsync
/// </summary>
/// <param name="path">The path<see cref="string"/></param>
private void AddFolderAsync(string path)
{
_settingsService.CoverImageRadius = value;
}
var normalizedPath =
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
partial void OnIsCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsCoverOverlayEnabled = value;
}
partial void OnIsDynamicCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsDynamicCoverOverlayEnabled = value;
}
partial void OnCoverOverlayOpacityChanged(int value)
{
_settingsService.CoverOverlayOpacity = value;
}
partial void OnCoverOverlayBlurAmountChanged(int value)
{
_settingsService.CoverOverlayBlurAmount = value;
}
[RelayCommand]
private async Task RebuildLyricsIndexDatabaseAsync()
{
IsRebuildingLyricsIndexDatabase = true;
await _databaseService.RebuildDatabaseAsync(MusicLibraries);
IsRebuildingLyricsIndexDatabase = false;
}
public async Task RemoveFolderAsync(string path)
{
MusicLibraries.Remove(path);
await RebuildLyricsIndexDatabaseAsync();
}
[RelayCommand]
private async Task SelectAndAddFolderAsync(UIElement sender)
{
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.FileTypeFilter.Add("*");
var hwnd = WindowNative.GetWindowHandle(WindowHelper.GetWindowForElement(sender));
InitializeWithWindow.Initialize(picker, hwnd);
var folder = await picker.PickSingleFolderAsync();
if (folder != null)
{
if (MusicLibraries.Any((item) => folder.Path.StartsWith(item)))
{
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo")
)
if (
LocalLyricsFolders.Any(x =>
Path.GetFullPath(x.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.Equals(
normalizedPath.TrimEnd(Path.DirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase
)
);
}
else
{
await AddFolderAsync(folder.Path);
}
}
}
private async Task AddFolderAsync(string path)
{
bool existed = MusicLibraries.Any((x) => x == path);
if (existed)
)
)
{
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
@@ -259,19 +291,65 @@ namespace BetterLyrics.WinUI3.ViewModels
)
);
}
else if (
LocalLyricsFolders.Any(item =>
normalizedPath.StartsWith(
Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase
)
)
)
{
// 添加的文件夹是现有文件夹的子文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo")
)
)
);
}
else if (
LocalLyricsFolders.Any(item =>
Path.GetFullPath(item.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase)
)
)
{
// 添加的文件夹是现有文件夹的父文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathIncludingOthersInfo")
)
)
);
}
else
{
MusicLibraries.Add(path);
await RebuildLyricsIndexDatabaseAsync();
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
}
/// <summary>
/// The LaunchProjectGitHubPageAsync
/// </summary>
/// <returns>The <see cref="Task"/></returns>
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(Helper.AppInfo.GithubUrl));
}
/// <summary>
/// The OpenFolderInFileExplorer
/// </summary>
/// <param name="path">The path<see cref="string"/></param>
private void OpenFolderInFileExplorer(string path)
{
Process.Start(
@@ -284,11 +362,29 @@ namespace BetterLyrics.WinUI3.ViewModels
);
}
public void OpenMusicFolder(string path)
/// <summary>
/// The OpenLogFolder
/// </summary>
[RelayCommand]
private void OpenCacheFolder()
{
OpenFolderInFileExplorer(path);
OpenFolderInFileExplorer(Helper.AppInfo.CacheFolder);
}
/// <summary>
/// The PlayTestingMusicTask
/// </summary>
[RelayCommand]
private void PlayTestingMusicTask()
{
AddFolderAsync(Helper.AppInfo.AssetsFolder);
_mediaPlayer.SetUriSource(new Uri(Helper.AppInfo.TestMusicPath));
_mediaPlayer.Play();
}
/// <summary>
/// The RestartApp
/// </summary>
[RelayCommand]
private void RestartApp()
{
@@ -310,18 +406,143 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
/// <summary>
/// The SelectAndAddFolderAsync
/// </summary>
/// <param name="sender">The sender<see cref="UIElement"/></param>
/// <returns>The <see cref="Task"/></returns>
[RelayCommand]
private async Task PlayTestingMusicTask()
private async Task SelectAndAddFolderAsync(UIElement sender)
{
await AddFolderAsync(AppInfo.AssetsFolder);
_mediaPlayer.SetUriSource(new Uri(AppInfo.TestMusicPath));
_mediaPlayer.Play();
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.FileTypeFilter.Add("*");
var hwnd = WindowNative.GetWindowHandle(WindowHelper.GetWindowForElement(sender));
InitializeWithWindow.Initialize(picker, hwnd);
var folder = await picker.PickSingleFolderAsync();
if (folder != null)
{
AddFolderAsync(folder.Path);
}
}
[RelayCommand]
private void OpenLogFolder()
/// <summary>
/// The OnAutoStartWindowTypeChanged
/// </summary>
/// <param name="value">The value<see cref="AutoStartWindowType"/></param>
partial void OnAutoStartWindowTypeChanged(AutoStartWindowType value)
{
OpenFolderInFileExplorer(AppInfo.LogDirectory);
_settingsService.AutoStartWindowType = value;
}
/// <summary>
/// The OnBackdropTypeChanged
/// </summary>
/// <param name="value">The value<see cref="BackdropType"/></param>
partial void OnBackdropTypeChanged(BackdropType value)
{
_settingsService.BackdropType = value;
}
/// <summary>
/// The OnCoverImageRadiusChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnCoverImageRadiusChanged(int value)
{
_settingsService.CoverImageRadius = value;
}
/// <summary>
/// The OnCoverOverlayBlurAmountChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnCoverOverlayBlurAmountChanged(int value)
{
_settingsService.CoverOverlayBlurAmount = value;
}
/// <summary>
/// The OnCoverOverlayOpacityChanged
/// </summary>
/// <param name="value">The value<see cref="int"/></param>
partial void OnCoverOverlayOpacityChanged(int value)
{
_settingsService.CoverOverlayOpacity = value;
}
/// <summary>
/// The OnIsCoverOverlayEnabledChanged
/// </summary>
/// <param name="value">The value<see cref="bool"/></param>
partial void OnIsCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsCoverOverlayEnabled = value;
}
/// <summary>
/// The OnIsDynamicCoverOverlayEnabledChanged
/// </summary>
/// <param name="value">The value<see cref="bool"/></param>
partial void OnIsDynamicCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsDynamicCoverOverlayEnabled = value;
}
/// <summary>
/// The OnLanguageChanged
/// </summary>
/// <param name="value">The value<see cref="Enums.Language"/></param>
partial void OnLanguageChanged(Enums.Language value)
{
switch (value)
{
case Enums.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Enums.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Enums.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Enums.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
case Enums.Language.Japanese:
ApplicationLanguages.PrimaryLanguageOverride = "ja-JP";
break;
case Enums.Language.Korean:
ApplicationLanguages.PrimaryLanguageOverride = "ko-KR";
break;
default:
break;
}
_settingsService.Language = Language;
}
/// <summary>
/// The OnThemeTypeChanged
/// </summary>
/// <param name="value">The value<see cref="ElementTheme"/></param>
partial void OnThemeTypeChanged(ElementTheme value)
{
_settingsService.ThemeType = value;
}
/// <summary>
/// The OnTitleBarTypeChanged
/// </summary>
/// <param name="value">The value<see cref="TitleBarType"/></param>
partial void OnTitleBarTypeChanged(TitleBarType value)
{
_settingsService.TitleBarType = value;
RootGridMargin = new Thickness(0, value.GetHeight(), 0, 0);
}
#endregion
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
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;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SystemTrayViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<bool>>
{
[ObservableProperty]
public partial string ToolTipText { get; set; } = AppInfo.AppName;
[ObservableProperty]
public partial bool IsLyricsWindowLocked { get; set; } = false;
public SystemTrayViewModel(ISettingsService settingsService)
: base(settingsService) { }
[RelayCommand]
private void OpenSettings()
{
// 打开设置窗口
WindowHelper.OpenSettingsWindow();
}
[RelayCommand]
private void ExitApp()
{
// 退出应用程序
App.Current.Exit();
}
[RelayCommand]
private void UnlockWindow()
{
var window = WindowHelper.GetWindowByFramePageType(typeof(LyricsPage));
DesktopModeHelper.Unlock(window);
IsLyricsWindowLocked = false;
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is HostWindowViewModel)
{
if (message.PropertyName == nameof(HostWindowViewModel.IsLyricsWindowLocked))
{
IsLyricsWindowLocked = message.NewValue;
}
}
}
}
}

View File

@@ -12,7 +12,10 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid x:Name="RootGrid" RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<Grid
x:Name="RootGrid"
PointerMoved="RootGrid_PointerMoved"
RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
<Frame
x:Name="RootFrame"
@@ -25,8 +28,7 @@
VerticalAlignment="Top"
Background="Transparent"
Opacity="0"
PointerEntered="TopCommandGrid_PointerEntered"
PointerExited="TopCommandGrid_PointerExited">
PointerMoved="TopCommandGrid_PointerMoved">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
@@ -49,6 +51,30 @@
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
x:Name="ClickThroughButton"
x:Uid="HostWindowClickThroughButton"
Command="{x:Bind ViewModel.LockWindowCommand}"
Style="{StaticResource TitleBarButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="LockToolTip" x:Uid="HostWindowLockToolTip" />
</ToolTipService.ToolTip>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ChangePropertyAction
PropertyName="IsOpen"
TargetObject="{x:Bind LockToolTip}"
Value="True" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ChangePropertyAction
PropertyName="IsOpen"
TargetObject="{x:Bind LockToolTip}"
Value="False" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button x:Name="MoreButton" Style="{StaticResource TitleBarButtonStyle}">
<Grid>
<FontIcon
@@ -84,6 +110,11 @@
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="DesktopFlyoutItem"
x:Uid="HostWindowDesktopFlyoutItem"
Command="{x:Bind ViewModel.ToggleDesktopModeCommand}"
IsChecked="{x:Bind ViewModel.IsDesktopMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="MiniFlyoutItem"
x:Uid="BaseWindowMiniFlyoutItem"

View File

@@ -1,9 +1,11 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -21,57 +23,48 @@ using WinUIEx;
// 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.Views
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// An empty window that can be used on its own or navigated to within a Frame
/// </summary>
public sealed partial class HostWindow : Window
{
public HostWindowViewModel ViewModel { get; private set; } =
Ioc.Default.GetRequiredService<HostWindowViewModel>();
#region Fields
/// <summary>
/// Defines the _settingsService
/// </summary>
private readonly ISettingsService _settingsService =
Ioc.Default.GetRequiredService<ISettingsService>();
public HostWindow(bool alwaysOnTop = false, bool clickThrough = false)
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="HostWindow"/> class.
/// </summary>
/// <param name="alwaysOnTop">The alwaysOnTop<see cref="bool"/></param>
/// <param name="clickThrough">The clickThrough<see cref="bool"/></param>
public HostWindow()
{
this.InitializeComponent();
AppWindow.Changed += AppWindow_Changed;
AppWindow.Closing += AppWindow_Closing;
this.HideSystemTitleBarAndSetCustomTitleBar(TopCommandGrid);
if (clickThrough)
this.SetExtendedWindowStyle(
ExtendedWindowStyle.Transparent | ExtendedWindowStyle.Layered
);
if (alwaysOnTop)
((OverlappedPresenter)AppWindow.Presenter).IsAlwaysOnTop = true;
}
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
if (args.DidPresenterChange)
UpdateTitleBarWindowButtonsVisibility();
}
public void Navigate(Type type)
{
RootFrame.Navigate(type);
}
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
private void CloseOrExit()
{
if (RootFrame.SourcePageType == typeof(LyricsPage))
{
Application.Current.Exit();
App.Current.Exit();
}
else
{
@@ -79,116 +72,72 @@ namespace BetterLyrics.WinUI3.Views
}
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Maximize();
}
args.Cancel = true;
CloseOrExit();
}
private void MinimiseButton_Click(object sender, RoutedEventArgs e)
#endregion
#region Properties
/// <summary>
/// Gets the ViewModel
/// </summary>
public HostWindowViewModel ViewModel { get; private set; } =
Ioc.Default.GetRequiredService<HostWindowViewModel>();
#endregion
#region Methods
/// <summary>
/// The Navigate
/// </summary>
/// <param name="type">The type<see cref="Type"/></param>
public void Navigate(Type type)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Minimize();
}
}
private void RestoreButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Restore();
}
}
private void UpdateTitleBarWindowButtonsVisibility()
{
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
break;
case AppWindowPresenterKind.CompactOverlay:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
Visibility.Collapsed;
break;
case AppWindowPresenterKind.FullScreen:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
Visibility.Collapsed;
FullScreenFlyoutItem.IsChecked = true;
break;
case AppWindowPresenterKind.Overlapped:
DockFlyoutItem.Visibility = Visibility.Visible;
var overlappedPresenter = (OverlappedPresenter)AppWindow.Presenter;
if (DockFlyoutItem.IsChecked)
{
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
Visibility.Collapsed;
}
else
{
MinimiseButton.Visibility =
AOTFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
Visibility.Visible;
FullScreenFlyoutItem.IsChecked = false;
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
}
else if (overlappedPresenter.State == OverlappedPresenterState.Restored)
{
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
}
break;
default:
break;
}
}
private void RootFrame_Navigated(object sender, NavigationEventArgs e)
{
AppWindow.Title = Title = App.ResourceLoader!.GetString(
$"{e.SourcePageType.Name}Title"
);
if (e.SourcePageType == typeof(LyricsPage))
{
if (_settingsService.AutoStartWindowType == AutoStartWindowType.DockMode)
{
DockFlyoutItem.IsChecked = true;
ViewModel.ToggleDockModeCommand.Execute(null);
}
}
RootFrame.Navigate(type);
}
/// <summary>
/// The AOTFlyoutItem_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
{
var overlappedPresenter = (OverlappedPresenter)AppWindow.Presenter;
overlappedPresenter.IsAlwaysOnTop = !overlappedPresenter.IsAlwaysOnTop;
}
/// <summary>
/// The AppWindow_Changed
/// </summary>
/// <param name="sender">The sender<see cref="AppWindow"/></param>
/// <param name="args">The args<see cref="AppWindowChangedEventArgs"/></param>
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
if (args.DidPresenterChange)
UpdateTitleBarWindowButtonsVisibility();
}
/// <summary>
/// The CloseButton_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
CloseOrExit();
}
/// <summary>
/// The FullScreenFlyoutItem_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void FullScreenFlyoutItem_Click(object sender, RoutedEventArgs e)
{
switch (AppWindow.Presenter.Kind)
@@ -208,6 +157,24 @@ namespace BetterLyrics.WinUI3.Views
}
}
/// <summary>
/// The MaximiseButton_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
{
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Maximize();
}
}
/// <summary>
/// The MiniFlyoutItem_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void MiniFlyoutItem_Click(object sender, RoutedEventArgs e)
{
if (MiniFlyoutItem.IsChecked)
@@ -220,21 +187,203 @@ namespace BetterLyrics.WinUI3.Views
}
}
private void TopCommandGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
/// <summary>
/// The MinimiseButton_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void MinimiseButton_Click(object sender, RoutedEventArgs e)
{
if (TopCommandGrid.Opacity == 0)
TopCommandGrid.Opacity = .5;
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Minimize();
}
}
private void TopCommandGrid_PointerExited(object sender, PointerRoutedEventArgs e)
/// <summary>
/// The RestoreButton_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void RestoreButton_Click(object sender, RoutedEventArgs e)
{
if (TopCommandGrid.Opacity == .5)
TopCommandGrid.Opacity = 0;
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.Restore();
}
}
/// <summary>
/// The RootFrame_Navigated
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="NavigationEventArgs"/></param>
private void RootFrame_Navigated(object sender, NavigationEventArgs e)
{
AppWindow.Title = Title = App.ResourceLoader!.GetString(
$"{e.SourcePageType.Name}Title"
);
if (e.SourcePageType == typeof(LyricsPage))
{
if (_settingsService.AutoStartWindowType == AutoStartWindowType.DockMode)
{
DockFlyoutItem.IsChecked = true;
ViewModel.ToggleDockModeCommand.Execute(null);
}
}
}
/// <summary>
/// The RootFrame_NavigationFailed
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="NavigationFailedEventArgs"/></param>
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// The RootGrid_PointerMoved
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="PointerRoutedEventArgs"/></param>
private void RootGrid_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(RootGrid);
double y = point.Position.Y;
if (y >= 0 && y <= TopCommandGrid.ActualHeight + 5)
{
if (TopCommandGrid.Opacity == 0)
{
TopCommandGrid.Opacity = .5;
}
}
else
{
if (TopCommandGrid.Opacity == .5)
{
TopCommandGrid.Opacity = 0;
}
}
}
/// <summary>
/// The SettingsMenuFlyoutItem_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void SettingsMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenSettingsWindow();
}
/// <summary>
/// The TopCommandGrid_PointerMoved
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="PointerRoutedEventArgs"/></param>
private void TopCommandGrid_PointerMoved(object sender, PointerRoutedEventArgs e) { }
/// <summary>
/// The UpdateTitleBarWindowButtonsVisibility
/// </summary>
private void UpdateTitleBarWindowButtonsVisibility()
{
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
break;
case AppWindowPresenterKind.CompactOverlay:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
DesktopFlyoutItem.Visibility =
ClickThroughButton.Visibility =
FullScreenFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
Visibility.Collapsed;
break;
case AppWindowPresenterKind.FullScreen:
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
ClickThroughButton.Visibility =
DesktopFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
Visibility.Collapsed;
FullScreenFlyoutItem.IsChecked = true;
break;
case AppWindowPresenterKind.Overlapped:
DockFlyoutItem.Visibility = Visibility.Visible;
var overlappedPresenter = (OverlappedPresenter)AppWindow.Presenter;
if (DockFlyoutItem.IsChecked)
{
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
AOTFlyoutItem.Visibility =
DesktopFlyoutItem.Visibility =
ClickThroughButton.Visibility =
FullScreenFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
Visibility.Collapsed;
}
else if (DesktopFlyoutItem.IsChecked)
{
MinimiseButton.Visibility =
MaximiseButton.Visibility =
RestoreButton.Visibility =
DockFlyoutItem.Visibility =
AOTFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
Visibility.Collapsed;
ClickThroughButton.Visibility = Visibility.Visible;
}
else
{
MinimiseButton.Visibility =
AOTFlyoutItem.Visibility =
DesktopFlyoutItem.Visibility =
DockFlyoutItem.Visibility =
MiniFlyoutItem.Visibility =
FullScreenFlyoutItem.Visibility =
Visibility.Visible;
FullScreenFlyoutItem.IsChecked = false;
ClickThroughButton.Visibility = Visibility.Collapsed;
AOTFlyoutItem.IsChecked = overlappedPresenter.IsAlwaysOnTop;
if (overlappedPresenter.State == OverlappedPresenterState.Maximized)
{
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
}
else if (overlappedPresenter.State == OverlappedPresenterState.Restored)
{
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
}
TopCommandGrid.Opacity = 0;
break;
default:
break;
}
}
#endregion
private void ClickThroughButton_Click(object sender, RoutedEventArgs e)
{
this.SetExtendedWindowStyle(
ExtendedWindowStyle.Transparent | ExtendedWindowStyle.Layered
);
}
}
}

View File

@@ -14,6 +14,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:renderer="using:BetterLyrics.WinUI3.Renderer"
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -26,7 +27,7 @@
<renderer:LyricsRenderer />
</Grid>
<Grid Margin="36">
<Grid Margin="36,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="36" />
@@ -45,24 +46,34 @@
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel
x:Name="LyricsPlaceholderStackPanel"
x:Name="LyricsNotFoundPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Opacity="0"
Orientation="Horizontal"
Spacing="12">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<FontIcon
FontFamily="Segoe Fluent Icons"
FontSize="{StaticResource DisplayTextBlockFontSize}"
Glyph="&#xE90B;" />
<TextBlock x:Uid="MainPageLyricsNotFound" FontSize="{StaticResource TitleTextBlockFontSize}" />
<TextBlock x:Uid="MainPageLyricsNotFound" FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
</StackPanel>
<StackPanel
x:Name="LyricsLoadingPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Opacity="0"
Orientation="Horizontal"
Spacing="12">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<TextBlock x:Uid="MainPageLyricsLoading" FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
</StackPanel>
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoInnerGrid">
<Grid x:Name="SongInfoInnerGrid" Margin="0,36">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<!-- Cover area -->
@@ -83,45 +94,52 @@
Grid.Row="1"
SizeChanged="CoverArea_SizeChanged">
<Grid
x:Name="CoverImageGrid"
CornerRadius="{x:Bind ViewModel.CoverImageGridCornerRadius, Mode=OneWay}"
SizeChanged="CoverImageGrid_SizeChanged">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
<Grid x:Name="CoverImageGrid" SizeChanged="CoverImageGrid_SizeChanged">
<Grid CornerRadius="{x:Bind ViewModel.CoverImageGridCornerRadius, Mode=OneWay}">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
<ui:Effects.Shadow>
<media:AttachedCardShadow
BlurRadius="32"
CornerRadius="{x:Bind ViewModel.CoverImageGridCornerRadius, Mode=OneWay, Converter={StaticResource CornerRadiusToDoubleConverter}}"
InnerContentClipMode="CompositionMaskBrush"
Opacity="0.1" />
</ui:Effects.Shadow>
</Grid>
</Grid>
<!-- Title and artist -->
@@ -169,7 +187,7 @@
x:Name="TitleTextBlock"
Behavior="Bouncing"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="SemiBold"
FontWeight="Bold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
@@ -212,7 +230,6 @@
<labs:MarqueeText
Behavior="Bouncing"
FontSize="{StaticResource SubtitleTextBlockFontSize}"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="0.5"
Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
@@ -311,42 +328,6 @@
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE77B;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF0E3;" />
<ListView
x:Name="MatchedFilesListView"
ItemsSource="{x:Bind ViewModel.SongInfo.FilesFound, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<HyperlinkButton
VerticalAlignment="Center"
Click="OpenMatchedFileButton_Click"
RelativePanel.AlignLeftWithPanel="True"
Tag="{Binding Mode=OneWay}">
<TextBlock
MaxWidth="300"
Text="{Binding Mode=OneWay}"
TextWrapping="Wrap" />
</HyperlinkButton>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock
x:Name="NoLocalFilesMatchedTextBlock"
x:Uid="MainPageNoLocalFilesMatched"
Visibility="Collapsed" />
</StackPanel>
</StackPanel>
</Flyout>
</Button.Flyout>
@@ -369,6 +350,8 @@
IsOpen="{x:Bind ViewModel.IsWelcomeTeachingTipOpen, Mode=OneWay}"
Target="{x:Bind SettingsButton}" />
<uc:SystemTray />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="LayoutStates">
@@ -449,34 +432,32 @@
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FilesMatchStates">
<VisualState x:Name="Matched" />
<VisualState x:Name="NotMatched">
<VisualStateGroup x:Name="LyricsStatus">
<VisualState x:Name="Loading">
<VisualState.StateTriggers>
<ui:IsNullOrEmptyStateTrigger Value="{x:Bind ViewModel.SongInfo.FilesFound, Mode=OneWay}" />
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="NoLocalFilesMatchedTextBlock.Visibility" Value="Visible" />
<Setter Target="MatchedFilesListView.Visibility" Value="Collapsed" />
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value="0" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value=".5" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="LyricsExistenceStates">
<VisualState x:Name="Existed">
<VisualState x:Name="Found">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.SongInfo.IsLyricsExisted, Mode=OneWay}" />
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsPlaceholderStackPanel.Opacity" Value="0" />
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value="0" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NotExisted">
<VisualState x:Name="NotFound">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.SongInfo.IsLyricsExisted, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}" />
<ui:IsEqualStateTrigger Value="{x:Bind ViewModel.LyricsStatus, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}" To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="LyricsPlaceholderStackPanel.Opacity" Value=".5" />
<Setter Target="LyricsNotFoundPlaceholder.Opacity" Value=".5" />
<Setter Target="LyricsLoadingPlaceholder.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

View File

@@ -1,13 +1,13 @@
using System;
using BetterLyrics.WinUI3.Helper;
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
// 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.
@@ -15,12 +15,15 @@ using WinUIEx;
namespace BetterLyrics.WinUI3.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// An empty page that can be used on its own or navigated to within a Frame
/// </summary>
public sealed partial class LyricsPage : Page
{
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="LyricsPage"/> class.
/// </summary>
public LyricsPage()
{
this.InitializeComponent();
@@ -28,19 +31,24 @@ namespace BetterLyrics.WinUI3.Views
DataContext = Ioc.Default.GetService<LyricsPageViewModel>();
}
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
{
ViewModel.IsFirstRun = false;
}
#endregion
private void CoverArea_SizeChanged(object sender, SizeChangedEventArgs e)
{
CoverImageGrid.Width = CoverImageGrid.Height = Math.Min(
CoverArea.ActualWidth,
CoverArea.ActualHeight
);
}
#region Properties
/// <summary>
/// Gets the ViewModel
/// </summary>
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
#endregion
#region Methods
/// <summary>
/// The BottomCommandGrid_PointerEntered
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="Microsoft.UI.Xaml.Input.PointerRoutedEventArgs"/></param>
private void BottomCommandGrid_PointerEntered(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
@@ -50,6 +58,11 @@ namespace BetterLyrics.WinUI3.Views
BottomCommandGrid.Opacity = .5;
}
/// <summary>
/// The BottomCommandGrid_PointerExited
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="Microsoft.UI.Xaml.Input.PointerRoutedEventArgs"/></param>
private void BottomCommandGrid_PointerExited(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
@@ -59,19 +72,49 @@ namespace BetterLyrics.WinUI3.Views
BottomCommandGrid.Opacity = 0;
}
private void LyricsPlaceholderGrid_SizeChanged(object sender, SizeChangedEventArgs e)
/// <summary>
/// The CoverArea_SizeChanged
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="SizeChangedEventArgs"/></param>
private void CoverArea_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.LimitedLineWidth = e.NewSize.Width;
}
private void OpenMatchedFileButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.OpenMatchedFileFolderInFileExplorer((string)(sender as HyperlinkButton)!.Tag);
CoverImageGrid.Width = CoverImageGrid.Height = Math.Min(
CoverArea.ActualWidth,
CoverArea.ActualHeight
);
}
/// <summary>
/// The CoverImageGrid_SizeChanged
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="SizeChangedEventArgs"/></param>
private void CoverImageGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.CoverImageGridActualHeight = e.NewSize.Height;
}
/// <summary>
/// The LyricsPlaceholderGrid_SizeChanged
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="SizeChangedEventArgs"/></param>
private void LyricsPlaceholderGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.MaxLyricsWidth = e.NewSize.Width;
}
/// <summary>
/// The WelcomeTeachingTip_Closed
/// </summary>
/// <param name="sender">The sender<see cref="TeachingTip"/></param>
/// <param name="args">The args<see cref="TeachingTipClosedEventArgs"/></param>
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
{
ViewModel.IsFirstRun = false;
}
#endregion
}
}

View File

@@ -8,6 +8,8 @@
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:BetterLyrics.WinUI3.ViewModels"
mc:Ignorable="d">
@@ -73,21 +75,26 @@
<controls:SettingsExpander
x:Uid="SettingsPageMusicLib"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsEnabled="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.MusicLibraries, Mode=OneWay}">
ItemsSource="{x:Bind ViewModel.LocalLyricsFolders, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard Header="{Binding}">
<StackPanel Orientation="Horizontal">
<controls:SettingsCard>
<controls:SettingsCard.Header>
<HyperlinkButton
x:Uid="SettingsPageOpenPath"
Click="SettingsPageOpenPathButton_Click"
Content="{Binding Path, Mode=OneWay}"
Tag="{Binding}" />
</controls:SettingsCard.Header>
<StackPanel Orientation="Horizontal">
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
<ToggleSwitch
DataContext="{Binding}"
IsOn="{Binding IsEnabled, Mode=TwoWay}"
Toggled="LocalLyricsFolderToggleSwitch_Toggled" />
</StackPanel>
</controls:SettingsCard>
</DataTemplate>
@@ -104,13 +111,13 @@
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MusicLibraries.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MusicLibraries.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -131,21 +138,38 @@
</controls:SettingsExpander>
<controls:SettingsCard
x:Uid="SettingsPageRebuildDatabase"
HeaderIcon="{ui:FontIcon Glyph=&#xE621;}"
IsEnabled="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<controls:SettingsCard.Description>
<TextBlock x:Uid="SettingsPageRebuildDatabaseDesc" Visibility="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</controls:SettingsCard.Description>
<StackPanel>
<Button x:Uid="SettingsPageRebuildDatabaseButton" Command="{x:Bind ViewModel.RebuildLyricsIndexDatabaseCommand}" />
<ProgressBar
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</controls:SettingsCard>
x:Name="LyricsSearchProvidersSettingsExpander"
x:Uid="SettingsPageLyricsSearchProvidersConfig"
HeaderIcon="{ui:FontIcon Glyph=&#xF6FA;}" />
<ListView
x:Name="LyricsSearchProvidersListView"
Margin="0,-4,0,0"
AllowDrop="True"
CanDragItems="True"
CanReorderItems="True"
DragItemsCompleted="LyricsSearchProvidersListView_DragItemsCompleted"
ItemsSource="{x:Bind ViewModel.LyricsSearchProvidersInfo, Mode=OneWay}"
SelectionMode="None">
<ListView.OpacityTransition>
<ScalarTransition />
</ListView.OpacityTransition>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchProviderInfo">
<controls:SettingsCard Padding="60,0,48,0" Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" Toggled="LyricsSearchProviderToggleSwitch_Toggled" />
</controls:SettingsCard>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</controls:Case>
@@ -166,8 +190,6 @@
<ComboBoxItem x:Uid="SettingsPageMica" />
<ComboBoxItem x:Uid="SettingsPageMicaAlt" />
<ComboBoxItem x:Uid="SettingsPageDesktopAcrylic" />
<!--<ComboBoxItem x:Uid="SettingsPageAcrylicThin" />
<ComboBoxItem x:Uid="SettingsPageAcrylicBase" />-->
<ComboBoxItem x:Uid="SettingsPageTransparent" />
</ComboBox>
</controls:SettingsCard>
@@ -188,6 +210,8 @@
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
<ComboBoxItem x:Uid="SettingsPageTC" />
<ComboBoxItem x:Uid="SettingsPageJA" />
<ComboBoxItem x:Uid="SettingsPageKO" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard>
@@ -395,12 +419,6 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsBlurAmount" HeaderIcon="{ui:FontIcon Glyph=&#xE727;}">
<controls:SettingsCard.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageLyricsBlurHighGPUUsage" Foreground="{ThemeResource SystemFillColorCautionBrush}" />
<TextBlock x:Uid="SettingsPageLyricsBlurAmountSideEffect" />
</StackPanel>
</controls:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
@@ -427,7 +445,6 @@
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLyricsGlowEffectScope" IsEnabled="{x:Bind LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind LyricsSettingsControlViewModel.LyricsGlowEffectScope, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsGlowEffectScopeWholeLyrics" />
<ComboBoxItem x:Uid="SettingsPageLyricsGlowEffectScopeCurrentLine" />
<ComboBoxItem x:Uid="SettingsPageLyricsGlowEffectScopeCurrentChar" />
</ComboBox>
@@ -457,6 +474,9 @@
Command="{x:Bind ViewModel.LaunchProjectGitHubPageCommand}"
HeaderIcon="{ui:FontIcon Glyph=&#xE943;}"
IsClickEnabled="True" />
<uc:DependenciesSettingsExpander />
</StackPanel>
</controls:Case>
@@ -465,8 +485,11 @@
<controls:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<Button x:Uid="SettingsPagePlayingMockMusicButton" Command="{x:Bind ViewModel.PlayTestingMusicTaskCommand}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLog">
<Button x:Uid="SettingsPageOpenLogFolderButton" Command="{x:Bind ViewModel.OpenLogFolderCommand}" />
<controls:SettingsCard x:Uid="SettingsPageCache">
<Button x:Uid="SettingsPageOpenLogFolderButton" Command="{x:Bind ViewModel.OpenCacheFolderCommand}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDebugOverlay">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsDebugOverlayEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
</StackPanel>
</controls:Case>

View File

@@ -1,8 +1,17 @@
// 2025/6/23 by Zhe Fang
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Threading.Tasks;
// 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.
@@ -10,36 +19,90 @@ using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// An empty page that can be used on its own or navigated to within a Frame
/// </summary>
public sealed partial class SettingsPage : Page
{
public SettingsViewModel ViewModel => (SettingsViewModel)DataContext;
public LyricsSettingsControlViewModel LyricsSettingsControlViewModel =>
Ioc.Default.GetRequiredService<LyricsSettingsControlViewModel>();
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SettingsPage"/> class.
/// </summary>
public SettingsPage()
{
this.InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<SettingsViewModel>();
}
private void SettingsPageOpenPathButton_Click(
object sender,
Microsoft.UI.Xaml.RoutedEventArgs e
)
#endregion
#region Properties
/// <summary>
/// Gets the LyricsSettingsControlViewModel
/// </summary>
public LyricsSettingsControlViewModel LyricsSettingsControlViewModel =>
Ioc.Default.GetRequiredService<LyricsSettingsControlViewModel>();
/// <summary>
/// Gets the ViewModel
/// </summary>
public SettingsViewModel ViewModel => (SettingsViewModel)DataContext;
#endregion
#region Methods
/// <summary>
/// The LocalLyricsFolderToggleSwitch_Toggled
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void LocalLyricsFolderToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
ViewModel.OpenMusicFolder((string)(sender as HyperlinkButton)!.Tag);
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LocalLyricsFolder localLyricsFolder)
{
ViewModel.ToggleLocalLyricsFolder(localLyricsFolder);
}
}
}
private async void SettingsPageRemovePathButton_Click(
object sender,
Microsoft.UI.Xaml.RoutedEventArgs e
/// <summary>
/// The LyricsSearchProvidersListView_DragItemsCompleted
/// </summary>
/// <param name="sender">The sender<see cref="ListViewBase"/></param>
/// <param name="args">The args<see cref="DragItemsCompletedEventArgs"/></param>
private void LyricsSearchProvidersListView_DragItemsCompleted(
ListViewBase sender,
DragItemsCompletedEventArgs args
)
{
await ViewModel.RemoveFolderAsync((string)(sender as HyperlinkButton)!.Tag);
ViewModel.OnLyricsSearchProvidersReordered();
}
/// <summary>
/// The LyricsSearchProviderToggleSwitch_Toggled
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="RoutedEventArgs"/></param>
private void LyricsSearchProviderToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LyricsSearchProviderInfo providerInfo)
{
ViewModel.ToggleLyricsSearchProvider(providerInfo);
}
}
}
/// <summary>
/// The NavView_SelectionChanged
/// </summary>
/// <param name="sender">The sender<see cref="NavigationView"/></param>
/// <param name="args">The args<see cref="NavigationViewSelectionChangedEventArgs"/></param>
private void NavView_SelectionChanged(
NavigationView sender,
NavigationViewSelectionChangedEventArgs args
@@ -47,5 +110,33 @@ namespace BetterLyrics.WinUI3.Views
{
ViewModel.NavViewSelectedItemTag = (args.SelectedItem as NavigationViewItem)!.Tag;
}
/// <summary>
/// The SettingsPageOpenPathButton_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="Microsoft.UI.Xaml.RoutedEventArgs"/></param>
private void SettingsPageOpenPathButton_Click(
object sender,
Microsoft.UI.Xaml.RoutedEventArgs e
)
{
ViewModel.OpenMusicFolder((LocalLyricsFolder)(sender as HyperlinkButton)!.Tag);
}
/// <summary>
/// The SettingsPageRemovePathButton_Click
/// </summary>
/// <param name="sender">The sender<see cref="object"/></param>
/// <param name="e">The e<see cref="Microsoft.UI.Xaml.RoutedEventArgs"/></param>
private void SettingsPageRemovePathButton_Click(
object sender,
Microsoft.UI.Xaml.RoutedEventArgs e
)
{
ViewModel.RemoveFolderAsync((LocalLyricsFolder)(sender as HyperlinkButton)!.Tag);
}
#endregion
}
}

188
README.CN.md Normal file
View File

@@ -0,0 +1,188 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.md">_**Click here to see the English version**_</a>
<div align="center">
  <img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
</div>
<h2 align="center">
BetterLyrics
</h2>
<h3 align="center">
基于 WinUI 3 打造的流畅动态本地歌词显示工具
</h3>
---
## 亮点
- 支持将模糊专辑封面设为背景
- 歌词淡入淡出、缩放等动画流畅自然
- 切换歌曲时界面无缝过渡
- 支持每个字符的渐变卡拉OK发光效果
- 沉浸式桌面歌词Dock 模式)
> 项目仍在开发中,`dev` 分支可能存在 bug。
---
## 支持的歌词源
- 本地歌词:
- 音乐文件内嵌歌词(通过 [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) 读取和解析)
- `.lrc` 文件
- 在线歌词源:
- [LRCLIB](https://lrclib.net/)
- QQ 音乐(通过 [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) 获取和解码)
---
## 多种个性化设置选项
提供了丰富的自定义项:
- 主题模式(浅色、深色、跟随系统)
- 背景样式无、Mica 云母、Acrylic 亚克力、透明)
- 专辑封面背景(动态显示、模糊程度、透明度)
- 歌词样式(对齐方式、字体大小、颜色 **(从专辑封面中提取主题色)**、行间距、透明度、模糊强度、动态**发光**特效)
- 语言(英文、简体中文、繁体中文)
---
## 软件截图
![模式](Screenshots/mode.png)
![发光效果](Screenshots/glow.png)
![发光动画](Screenshots/glow.gif)
![Dock 模式](Screenshots/dock.png)
![沉浸式 Dock](Screenshots/immersive-dock.gif)
![歌词 Dock 动画](Screenshots/dock.gif)
![画中画](Screenshots/pip.png)
![设置界面](Screenshots/settings.png)
![全屏歌词](Screenshots/fs.png)
---
## 演示视频
观看我们的介绍视频「BetterLyrics 阶段性开发成果展示」(上传于 2025 年 5 月 31 日):
[点此观看 B 站视频](https://b23.tv/QjKkYmL)
---
## 立即体验
### 稳定版本
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
 <img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
> **推荐方式****永久免费试用或购买**(免费与付费功能上无差别,若喜欢可购买支持作者)
也可从 Google Drive 下载(详见 [release 页面](https://github.com/jayfunc/BetterLyrics/releases/latest)
> 注意:这是一个 `.zip` 压缩包,请参考[安装指南](How2Install/How2Install.md)进行安装。
### 最新开发版本
可通过 `git clone` 克隆本仓库后自行构建运行。
---
## 播放器适配说明
本项目通过监听 [SMTC](https://learn.microsoft.com/en-ca/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols) 获取当前播放歌曲信息。
理论上,**只要你的播放器支持 SMTC 控件**,加载本地音乐或歌词后即可使用。
兼容性良好的播放器包括但不限于:
- Spotify
- Groove 音乐
- Apple Music
- Windows 媒体播放器
- VLC
- QQ 音乐
- 酷狗音乐
- 酷我音乐
>(注:未测试全部播放器,如有异常欢迎反馈 issue
---
## 后续工作
敬请期待。
---
## 特别感谢
- [LRCLIB](https://lrclib.net/)
- 在线歌词 API 提供源
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- 本地音频元信息读取
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- 简化 Win32 窗口操作
- [TagLib#](https://github.com/mono/taglib-sharp)
- 曾用作元信息解析库
- [Stackoverflow - WPF 动画 Margin 属性](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Bilibili -【WinUI3】SystemBackdropController 教程](https://www.bilibili.com/video/BV1PY4FevEkS)
- [博客园 - .NET App 与 SMTC 交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 游戏循环教程](https://www.cnblogs.com/walterlv/p/10236395.html)
- [Win2D Iris Blur 示例](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 教程合集](https://mvvm.coldwind.top/)
---
## 灵感来源
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
---
## 使用的第三方库
```xml
<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.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.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="Lyricify.Lyrics.Helper" Version="0.1.4" />
<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.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="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.25.0" />
```
## Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date)
## 欢迎提出反馈或建议
感谢。

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