Compare commits

..

24 Commits

Author SHA1 Message Date
Zhe Fang
509079e8c7 Merge pull request #10 from jayfunc/dev
1.0.9.0
2025-07-07 17:32:48 -04:00
Zhe Fang
a29e5c98f8 fix 2025-07-07 17:30:44 -04:00
Zhe Fang
78a6ba8e1f add local machine translation func 2025-07-05 15:16:34 -04:00
Zhe Fang
352ceca81d fix 2025-07-04 07:25:38 -04:00
Zhe Fang
c50c180aa0 Merge pull request #9 from jayfunc/dev
v1.0.7.0 release
2025-06-30 20:56:34 -04:00
Zhe Fang
2f99d44b86 update readme 2025-06-30 20:50:45 -04:00
Zhe Fang
03386e72b2 fix display error for windows 10; improve desktop mode experience; improve album art transition animation; fix ttml parse issue 2025-06-30 20:08:39 -04:00
Zhe Fang
54ba0a0c85 fix missing ResourceLoader in App.xaml.cs 2025-06-29 21:31:42 -04:00
Zhe Fang
7bf8b2894d Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-06-29 21:25:41 -04:00
Zhe Fang
875da76e6b add adaptive font color settings and custom color settings 2025-06-29 21:25:39 -04:00
Zhe Fang
547ca6d631 update README.CN.md 2025-06-28 07:10:36 -04:00
Zhe Fang
60fb088bea update README.md 2025-06-28 07:09:30 -04:00
Zhe Fang
3a89236af0 update readme 2025-06-27 09:52:29 -04:00
Zhe Fang
7d16bdbc88 Update README.md 2025-06-27 09:21:02 -04:00
Zhe Fang
812d23a101 Update README.md 2025-06-27 09:18:55 -04:00
Zhe Fang
4381a34191 update README.md 2025-06-27 07:42:12 -04:00
Zhe Fang
6e21e5636b update README.md 2025-06-27 06:38:17 -04:00
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
111 changed files with 7842 additions and 6455 deletions

View File

@@ -5,52 +5,62 @@
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:uap18="http://schemas.microsoft.com/appx/manifest/uap/windows10/18"
IgnorableNamespaces="uap rescap uap18">
<Identity
Name="37412.BetterLyrics"
Publisher="CN=Zhe"
Version="1.0.5.0" />
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.9.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Resources>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="BetterLyrics"
Description="BetterLyrics.WinUI3 (Package)"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="BetterLyrics"
Description="BetterLyrics.WinUI3 (Package)"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<uap5:Extension
Category="windows.startupTask">
<uap5:StartupTask
TaskId="AutoStartup"
Enabled="false"
DisplayName="BetterLyrics" />
</uap5:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -45,6 +45,7 @@
<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" />
@@ -52,7 +53,7 @@
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style (inc. the correct spacing) of a section header -->
<!-- Style -->
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
@@ -62,14 +63,16 @@
</Style.Setters>
</Style>
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Margin" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Padding" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
@@ -85,11 +88,14 @@
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8" />
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- Dimensions -->
<!-- Fonts -->
<FontFamily x:Key="IconFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,11 +1,10 @@
using System;
using System.Text;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Services.BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -13,30 +12,27 @@ using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Serilog;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
using ShadowViewer.Controls;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3
{
/// <summary>
/// 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; }
public static ResourceLoader? ResourceLoader { get; private set; }
public NotificationPanel? LyricsWindowNotificationPanel { get; set; }
public NotificationPanel? SettingsWindowNotificationPanel { get; set; }
/// <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().
/// </summary>
public App()
{
this.InitializeComponent();
@@ -49,7 +45,7 @@ namespace BetterLyrics.WinUI3
AppInfo.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetService<ILogger<App>>()!;
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
@@ -57,34 +53,29 @@ namespace BetterLyrics.WinUI3
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void CurrentDomain_FirstChanceException(
object? sender,
System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e
)
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
WindowHelper.OpenOrShowWindow<LyricsWindow>();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow == null) return;
private void TaskScheduler_UnobservedTaskException(
object? sender,
UnobservedTaskExceptionEventArgs e
)
{
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
private void CurrentDomain_UnhandledException(
object sender,
System.UnhandledExceptionEventArgs e
)
{
_logger.LogError(e.ExceptionObject.ToString(), "CurrentDomain_UnhandledException");
string[] commandLineArguments = Environment.GetCommandLineArgs();
if (commandLineArguments.Length > 1)
{
commandLineArguments = commandLineArguments.Skip(1).ToArray();
if (commandLineArguments.First() == AppInfo.UnlockWindowTag)
{
lyricsWindow.AutoSelectLyricsMode(AutoStartWindowType.DesktopMode, false);
return;
}
}
lyricsWindow.AutoSelectLyricsMode();
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.WriteTo.File(AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
@@ -101,32 +92,37 @@ namespace BetterLyrics.WinUI3
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ILibreTranslateService, LibreTranslateService>()
// ViewModels
.AddTransient<HostWindowViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsPageViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<LyricsSettingsControlViewModel>()
.BuildServiceProvider()
);
}
private void App_UnhandledException(
object sender,
Microsoft.UI.Xaml.UnhandledExceptionEventArgs e
)
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
_logger.LogError(e.Exception, "App_UnhandledException");
e.Handled = true;
}
/// <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)
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
WindowHelper.OpenLyricsWindow();
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
_logger.LogError(e.ExceptionObject.ToString(), "CurrentDomain_UnhandledException");
}
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
}
}

View File

@@ -10,6 +10,18 @@
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="ViewModels\Lyrics\**" />
<Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
@@ -18,41 +30,38 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference
Include="CommunityToolkit.Labs.WinUI.OpacityMaskView"
Version="0.1.250513-build.2126"
/>
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference
Include="CommunityToolkit.WinUI.Controls.SettingsControls"
Version="8.2.250402"
/>
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="iTunesSearch" Version="1.0.44" />
<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="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<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.26.0" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
@@ -64,14 +73,20 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\" />
<Folder Include="ViewModels\Lyrics\" />
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\SystemTray.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>

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,17 @@
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
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,15 +1,13 @@
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
public partial class ColorToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -0,0 +1,22 @@
using System;
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)
{
return (double)cornerRadius.TopLeft;
}
return .0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,14 +1,11 @@
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
internal partial class EnumToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,14 +1,11 @@
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
public partial class IntToCornerRadius : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,14 +1,12 @@
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 BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public class LyricsSearchProviderToDisplayNameConverter : IValueConverter
public partial class LyricsSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
@@ -16,22 +14,16 @@ namespace BetterLyrics.WinUI3.Converter
{
return provider switch
{
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalLrcFile"
),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderLocalMusicFile"
),
LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString(
"LyricsSearchProviderLrcLib"
),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderEslrcFile"
),
LyricsSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString(
"LyricsSearchProviderTtmlFile"
),
_ => throw new ArgumentOutOfRangeException(nameof(provider), provider, null),
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 "";

View File

@@ -1,10 +1,12 @@
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
public partial class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,8 +1,4 @@
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
{
@@ -10,5 +6,6 @@ namespace BetterLyrics.WinUI3.Enums
{
StandardMode,
DockMode,
DesktopMode,
}
}

View File

@@ -1,8 +1,4 @@
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
{

View File

@@ -0,0 +1,20 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum EasingType
{
Linear,
SmoothStep,
EaseInOutSine,
EaseInOutQuad,
EaseInOutCubic,
EaseInOutQuart,
EaseInOutQuint,
EaseInOutExpo,
EaseInOutCirc,
EaseInOutBack,
EaseInOutElastic,
EaseInOutBounce,
}
}

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;

View File

@@ -6,9 +6,9 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsType
public enum LineMaskType
{
InAppLyrics,
DesktopLyrics,
Glow,
Highlight,
}
}

View File

@@ -0,0 +1,10 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LineRenderingType
{
UntilCurrentChar,
CurrentCharOnly,
}
}

View File

@@ -1,8 +1,4 @@
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
{

View File

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

View File

@@ -1,4 +1,6 @@
namespace BetterLyrics.WinUI3.Enums
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsDisplayType
{

View File

@@ -1,14 +1,11 @@
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
{
public enum LyricsFontColorType
{
Default,
Dominant,
AdaptiveColored,
AdaptiveGrayed,
Custom,
}
}

View File

@@ -1,8 +1,6 @@
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.Text;
using Windows.UI.Text;
@@ -40,11 +38,7 @@ namespace BetterLyrics.WinUI3.Enums
LyricsFontWeight.ExtraBold => FontWeights.ExtraBold,
LyricsFontWeight.Black => FontWeights.Black,
LyricsFontWeight.ExtraBlack => FontWeights.ExtraBlack,
LyricsFontWeight _ => throw new ArgumentOutOfRangeException(
nameof(weight),
weight,
null
),
LyricsFontWeight _ => throw new ArgumentOutOfRangeException(nameof(weight), weight, null),
};
}
}

View File

@@ -1,8 +1,4 @@
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
{
@@ -11,38 +7,42 @@ namespace BetterLyrics.WinUI3.Enums
Lrc,
Eslrc,
Ttml,
Qrc,
Krc,
NotSpecified,
}
public static class LyricsFormatExtensions
{
public static string ToFileExtension(this LyricsFormat format)
public static LyricsFormat? DetectFormat(this string content)
{
return format switch
{
LyricsFormat.Lrc => ".lrc",
LyricsFormat.Eslrc => ".eslrc",
LyricsFormat.Ttml => ".ttml",
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null),
};
}
if (string.IsNullOrWhiteSpace(content))
return null;
public static LyricsFormat? Detect(string content)
{
if (
content.StartsWith("<?xml")
&& System.Text.RegularExpressions.Regex.IsMatch(content, @"<tt(:\w+)?\b")
)
// TTML
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}>"
)
)
// KRC: 检测主内容格式 [start,duration]<offset,duration,0>字...
else if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"^\[\d+,\d+\](<\d+,\d+,0>.+)+",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Krc;
}
// QRC: 检测主内容格式 [start,duration]字(offset,duration)
else if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"^\[\d+,\d+\].*?\(\d+,\d+\)",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Qrc;
}
// 标准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;
}
@@ -51,5 +51,18 @@ namespace BetterLyrics.WinUI3.Enums
return null;
}
}
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",
_ => ".*",
};
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsGlowEffectScope
{
WholeLyrics,
CurrentLine,
CurrentChar,
}
}

View File

@@ -1,8 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsHighlightType
{
LineByLine,
CharByChar,
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsPlayingState
{
/// <summary>
/// Not played yet, will be playing in the future
/// </summary>
NotPlayed,
/// <summary>
/// Playing
/// </summary>
Playing,
/// <summary>
/// Has already played
/// </summary>
Played,
}
}

View File

@@ -1,11 +1,65 @@
namespace BetterLyrics.WinUI3.Enums
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchProvider
{
QQ,
Kugou,
Netease,
LrcLib,
AmllTtmlDb,
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
}
public static class LyricsSearchProviderExtensions
{
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,
};
}
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();
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsStatus
{
NotFound,
Found,
Loading,
}
}

View File

@@ -1,4 +1,6 @@
namespace BetterLyrics.WinUI3.Enums
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum MusicSearchMatchMode
{

View File

@@ -0,0 +1,28 @@
// 2025/6/23 by Zhe Fang
using Microsoft.Graphics.Canvas.Text;
using System;
namespace BetterLyrics.WinUI3.Enums
{
public enum TextAlignmentType
{
Left,
Center,
Right,
}
public static class LyricsAlignmentTypeExtensions
{
public static CanvasHorizontalAlignment ToCanvasHorizontalAlignment(this TextAlignmentType alignmentType)
{
return alignmentType switch
{
TextAlignmentType.Left => CanvasHorizontalAlignment.Left,
TextAlignmentType.Center => CanvasHorizontalAlignment.Center,
TextAlignmentType.Right => CanvasHorizontalAlignment.Right,
_ => throw new ArgumentOutOfRangeException(nameof(alignmentType), alignmentType, null),
};
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum TitleBarType
{
Compact,
Extended,
}
public static class TitleBarTypeExtensions
{
public static double GetHeight(this TitleBarType titleBarType)
{
return titleBarType switch
{
TitleBarType.Compact => 32.0,
TitleBarType.Extended => 48.0,
_ => throw new ArgumentOutOfRangeException(
nameof(titleBarType),
titleBarType,
null
),
};
}
}
}

View File

@@ -0,0 +1,9 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum WindowColorSampleMode
{
BelowWindow,
WindowArea,
WindowEdge,
}
}

View File

@@ -1,13 +1,11 @@
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
{
public class IsPlayingChangedEventArgs(bool isPlaying) : EventArgs
{
public bool IsPlaying { get; set; } = isPlaying;
}
}
}

View File

@@ -1,4 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -7,17 +9,10 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class LibChangedEventArgs : EventArgs
public class LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType) : EventArgs
{
public string Folder { get; }
public string FilePath { get; }
public WatcherChangeTypes ChangeType { get; }
public LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType)
{
Folder = folder;
FilePath = filePath;
ChangeType = changeType;
}
public WatcherChangeTypes ChangeType { get; } = changeType;
public string FilePath { get; } = filePath;
public string Folder { get; } = folder;
}
}

View File

@@ -0,0 +1,14 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class MediaSourceProvidersInfoEventArgs(List<MediaSourceProviderInfo> sessionIds):EventArgs
{
public List<MediaSourceProviderInfo> MediaSourceProviersInfo { get; set; } = sessionIds;
}
}

View File

@@ -1,13 +1,11 @@
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
{
public class PositionChangedEventArgs(TimeSpan position) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
}
}
}

View File

@@ -1,8 +1,6 @@
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 BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Events
@@ -10,5 +8,5 @@ namespace BetterLyrics.WinUI3.Events
public class SongInfoChangedEventArgs(SongInfo? songInfo) : EventArgs
{
public SongInfo? SongInfo { get; set; } = songInfo;
}
}
}

View File

@@ -1,30 +1,37 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Diagnostics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public class AnimationHelper
{
public const int DebounceDefaultDuration = 200;
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
public const int DebounceDefaultDuration = 200;
}
public class ValueTransition<T>
where T : struct
{
private T _currentValue;
private float _durationSeconds;
private EasingType? _easingType;
private Func<T, T, float, T> _interpolator;
private bool _isTransitioning;
private float _progress;
private T _startValue;
private T _targetValue;
private float _progress;
private float _durationSeconds;
private bool _isTransitioning;
private Func<T, T, float, T> _interpolator;
public ValueTransition(
T initialValue,
float durationSeconds,
Func<T, T, float, T> interpolator
)
public float DurationSeconds => _durationSeconds;
public bool IsTransitioning => _isTransitioning;
public T Value => _currentValue;
public T TargetValue => _targetValue;
public ValueTransition(T initialValue, float durationSeconds, Func<T, T, float, T>? interpolator = null, EasingType? easingType = null)
{
_currentValue = initialValue;
_startValue = initialValue;
@@ -32,14 +39,50 @@ namespace BetterLyrics.WinUI3.Helper
_durationSeconds = durationSeconds;
_progress = 1f;
_isTransitioning = false;
_interpolator = interpolator;
if (interpolator != null)
{
_interpolator = interpolator;
_easingType = null;
}
else if (easingType.HasValue)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
else
{
_easingType = EasingType.SmoothStep;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
}
public T Value => _currentValue;
public bool IsTransitioning => _isTransitioning;
public void StartTransition(T targetValue)
private void JumpTo(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 1f;
_isTransitioning = false;
}
public void Reset(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 0f;
_isTransitioning = false;
}
public void StartTransition(T targetValue, bool jumpTo = false)
{
if (jumpTo)
{
JumpTo(targetValue);
return;
}
if (!targetValue.Equals(_currentValue))
{
_startValue = _currentValue;
@@ -49,10 +92,15 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static bool Equals(double x, double y, double tolerance)
{
var diff = Math.Abs(x - y);
return diff <= tolerance || diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
}
public void Update(TimeSpan elapsedTime)
{
if (!_isTransitioning)
return;
if (!_isTransitioning) return;
_progress += (float)elapsedTime.TotalSeconds / _durationSeconds;
if (_progress >= 1f)
@@ -67,13 +115,66 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public void Reset(T value)
private Func<T, T, float, T> GetInterpolatorByEasingType(EasingType type)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 0f;
_isTransitioning = false;
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.EaseInOutSine:
t = EasingHelper.EaseInOutSine(t);
break;
case EasingType.EaseInOutQuad:
t = EasingHelper.EaseInOutQuad(t);
break;
case EasingType.EaseInOutCubic:
t = EasingHelper.EaseInOutCubic(t);
break;
case EasingType.EaseInOutQuart:
t = EasingHelper.EaseInOutQuart(t);
break;
case EasingType.EaseInOutQuint:
t = EasingHelper.EaseInOutQuint(t);
break;
case EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
break;
}
return (T)(object)(s + (e - s) * t);
};
}
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
}
public void SetEasingType(EasingType easingType)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
}
}

View File

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

View File

@@ -1,8 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
@@ -10,8 +8,7 @@ namespace BetterLyrics.WinUI3.Helper
{
public static T? SafeGet<T>(this IList<T> list, int index)
{
if (list == null || index < 0 || index >= list.Count)
return default;
if (list == null || index < 0 || index >= list.Count) return default;
return list[index];
}
}

View File

@@ -1,24 +1,51 @@
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;
using Microsoft.UI.Xaml;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
{
public static Windows.UI.Color ToWindowsUIColor(this System.Drawing.Color color)
public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor)
{
return Windows.UI.Color.FromArgb(color.A, color.R, color.G, color.B);
// 计算亮度YIQ公式
double yiq =
((backgroundColor.R * 299) + (backgroundColor.G * 587) + (backgroundColor.B * 114))
/ 1000.0;
return yiq >= 128 ? ElementTheme.Light : ElementTheme.Dark;
}
public static Color GetInterpolatedColor(
float progress,
Color startColor,
Color targetColor
)
public static Color GetForegroundColor(Color background)
{
// 转为 HSL
var hsl = CommunityToolkit.WinUI.Helpers.ColorHelper.ToHsl(background);
double h = hsl.H;
double s = hsl.S;
double l = hsl.L;
// 目标亮度与背景错开,但不极端
double targetL;
if (l >= 0.7)
targetL = 0.35; // 背景很亮,前景适中偏暗
else if (l <= 0.3)
targetL = 0.75; // 背景很暗,前景适中偏亮
else
targetL = l > 0.5 ? l - 0.35 : l + 0.35; // 其余情况适度错开
// 保持色相,适当提升饱和度
double targetS = Math.Min(1.0, s + 0.2);
// 转回 Color
var fg = CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, targetS, targetL);
// 保持不透明
return Color.FromArgb(255, fg.R, fg.G, fg.B);
}
public static Color GetInterpolatedColor(float progress, Color startColor, Color targetColor)
{
byte Lerp(byte a, byte b) => (byte)(a + (progress * (b - a)));
return Color.FromArgb(
@@ -28,5 +55,48 @@ namespace BetterLyrics.WinUI3.Helper
Lerp(startColor.B, targetColor.B)
);
}
public static Color ToColor(this int argb)
{
byte a = (byte)(argb >> 24);
byte r = (byte)(argb >> 16);
byte g = (byte)(argb >> 8);
byte b = (byte)argb;
// 还原非预乘分量
if (a == 0)
return Color.FromArgb(0, 0, 0, 0);
// 预乘解码
// 这里 a+1 是编码时的分母
int ap1 = a + 1;
r = (byte)Math.Min(255, (r * 255 + (ap1 / 2)) / ap1);
g = (byte)Math.Min(255, (g * 255 + (ap1 / 2)) / ap1);
b = (byte)Math.Min(255, (b * 255 + (ap1 / 2)) / ap1);
return Color.FromArgb(a, r, g, b);
}
public static Color ToColor(this System.Drawing.Color color)
{
return Color.FromArgb(color.A, color.R, color.G, color.B);
}
public static Color WithAlpha(this Color color, byte alpha)
{
return Color.FromArgb(alpha, color.R, color.G, color.B);
}
public static Color WithBrightness(this Color color, double brightness)
{
// 确保亮度因子在合理范围内
brightness = Math.Max(0, Math.Min(1, brightness));
var hsl = CommunityToolkit.WinUI.Helpers.ColorHelper.ToHsl(color);
double h = hsl.H;
double s = hsl.S;
return CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, s, brightness);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Vanara.PInvoke;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DesktopModeHelper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private static readonly Dictionary<IntPtr, bool> _clickThroughStates = [];
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<IntPtr, (double X, double Y, double Width, double Height)> _originalWindowBounds = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyles = [];
private delegate nint WndProcDelegate(nint hWnd, uint msg, nint wParam, nint lParam);
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>С
if (_originalWindowBounds.TryGetValue(hwnd, out var bounds))
{
window.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 Enable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20><>¼ԭʼ<D4AD><CABC><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
if (!_originalWindowBounds.ContainsKey(hwnd))
{
_originalWindowBounds[hwnd] = (
window.AppWindow.Position.X,
window.AppWindow.Position.Y,
window.AppWindow.Size.Width,
window.AppWindow.Size.Height
);
}
// <20>Ӵ洢<D3B4><E6B4A2><EFBFBD><EFBFBD>ȡĿ<C8A1><C4BF><EFBFBD><EFBFBD><EFBFBD>ߺ<EFBFBD>λ<EFBFBD><CEBB>
int targetWidth = _settingsService.DesktopWindowWidth;
int targetHeight = _settingsService.DesktopWindowHeight;
int targetX = _settingsService.DesktopWindowLeft;
int targetY = _settingsService.DesktopWindowTop;
// <20><><EFBFBD>ô<EFBFBD><C3B4>ڴ<EFBFBD>С<EFBFBD><D0A1>λ<EFBFBD><CEBB>
window.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 Lock(Window window)
{
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
// <20><><EFBFBD><EFBFBD><EFBFBD>ޱ߿<DEB1><DFBF><EFBFBD>͸<EFBFBD><CDB8>
window.ToggleWindowStyle(true, WindowStyle.Popup | WindowStyle.Visible);
window.ExtendsContentIntoTitleBar = false;
SetClickThrough(window, true);
}
public static void SetClickThrough(Window window, bool enable)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
int exStyle = User32.GetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE);
if (enable)
{
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT | (int)User32.WindowStylesEx.WS_EX_LAYERED);
_clickThroughStates[hwnd] = true;
}
else
{
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle & ~(int)User32.WindowStylesEx.WS_EX_TRANSPARENT);
_clickThroughStates[hwnd] = false;
}
}
public static void Unlock(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƴ<EFBFBD><C6B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD> Disable ʱ<><CAB1><EFBFBD>Ƴ<EFBFBD><C6B3><EFBFBD>
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
}
window.ExtendsContentIntoTitleBar = true;
SetClickThrough(window, false);
// To recover the system backdrop, we need to reopen the window
WindowHelper.RestartApp(AppInfo.UnlockWindowTag);
}
}
}

View File

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

View File

@@ -0,0 +1,158 @@
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockModeHelper
{
private static readonly HashSet<IntPtr> _registered = [];
private static readonly Dictionary<IntPtr, RECT> _originalPositions = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyle = [];
public static void Disable(Window window)
{
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
window.SetIsAlwaysOnTop(false);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
rect.Left,
rect.Top,
rect.Right - rect.Left,
rect.Bottom - rect.Top,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
_originalPositions.Remove(hwnd);
}
UnregisterAppBar(hwnd);
}
public static void Enable(Window window, int appBarHeight)
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_originalWindowStyle.ContainsKey(hwnd))
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
if (!_originalPositions.ContainsKey(hwnd))
{
if (User32.GetWindowRect(hwnd, out var rect))
{
_originalPositions[hwnd] = rect;
}
}
RegisterAppBar(hwnd, appBarHeight);
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int screenHeight = User32.GetSystemMetrics(User32.SystemMetric.SM_CYSCREEN);
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
screenWidth,
appBarHeight,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
}
private static void RegisterAppBar(IntPtr hwnd, int height)
{
if (_registered.Contains(hwnd)) return;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = Shell32.ABE.ABE_TOP,
rc = new RECT
{
Left = 0,
Top = 0,
Right = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
Bottom = height,
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_NEW, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
_registered.Add(hwnd);
}
private static void UnregisterAppBar(IntPtr hwnd)
{
if (!_registered.Contains(hwnd))
return;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_REMOVE, ref abd);
_registered.Remove(hwnd);
}
public static void UpdateAppBarHeight(IntPtr hwnd, int newHeight)
{
if (!_registered.Contains(hwnd))
return;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = Shell32.ABE.ABE_TOP,
rc = new RECT
{
Left = 0,
Top = 0,
Right = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
Bottom = newHeight,
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
// 同步窗口实际高度
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
newHeight,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
}
}
}

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;
@@ -8,43 +10,103 @@ namespace BetterLyrics.WinUI3.Helper
{
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);
/// <summary>
/// Acceleration until halfway then deceleration
/// </summary>
public static float EaseInOutSine(float t)
{
return -(MathF.Cos(MathF.PI * t) - 1f) / 2f;
}
public static float EaseInOutQuad(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
/// <summary>
/// Smoother transition than linear
/// </summary>
public static float SmoothStep(float t)
public static float EaseInOutCubic(float t)
{
return t * t * (3 - 2 * t);
return t < 0.5f ? 4 * t * t * t : 1 - MathF.Pow(-2 * t + 2, 3) / 2;
}
public static float EaseInOutQuart(float t)
{
return t < 0.5f ? 8 * t * t * t * t : 1 - MathF.Pow(-2 * t + 2, 4) / 2;
}
/// <summary>
/// Even smoother transition with continuous first and second derivatives
/// </summary>
public static float SmootherStep(float t)
public static float EaseInOutQuint(float t)
{
return t * t * t * (t * (6 * t - 15) + 10);
return t < 0.5f ? 16 * t * t * t * t * t : 1 - MathF.Pow(-2 * t + 2, 5) / 2;
}
public static float EaseInOutExpo(float t)
{
return t == 0
? 0
: t == 1
? 1
: t < 0.5 ? MathF.Pow(2, 20 * t - 10) / 2
: (2 - MathF.Pow(2, -20 * t + 10)) / 2;
}
public static float EaseInOutCirc(float t)
{
return t < 0.5f
? (1 - MathF.Sqrt(1 - MathF.Pow(2 * t, 2))) / 2
: (MathF.Sqrt(1 - MathF.Pow(-2 * t + 2, 2)) + 1) / 2;
}
public static float EaseInOutBack(float t)
{
float c1 = 1.70158f;
float c2 = c1 * 1.525f;
return t < 0.5
? (MathF.Pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (MathF.Pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
public static float EaseInOutElastic(float t)
{
if (t == 0 || t == 1) return t;
float p = 0.3f;
float s = p / 4;
return t < 0.5f
? -(MathF.Pow(2, 20 * t - 10) * MathF.Sin((20 * t - 11.125f) * (2 * MathF.PI) / p)) / 2
: (MathF.Pow(2, -20 * t + 10) * MathF.Sin((20 * t - 11.125f) * (2 * MathF.PI) / p)) / 2 + 1;
}
private static float EaseOutBounce(float t)
{
if (t < 4 / 11f)
{
return (121 * t * t) / 16f;
}
else if (t < 8 / 11f)
{
return (363 / 40f * t * t) - (99 / 10f * t) + 17 / 5f;
}
else if (t < 9 / 10f)
{
return (4356 / 361f * t * t) - (35442 / 1805f * t) + 16061 / 1805f;
}
else
{
return (54 / 5f * t * t) - (513 / 25f * t) + 268 / 25f;
}
}
public static float EaseInOutBounce(float t)
{
if (t < 0.5f)
{
return (1 - EaseOutBounce(1 - 2 * t)) / 2;
}
else
{
return (1 + EaseOutBounce(2 * t - 1)) / 2;
}
}
public static float SmoothStep(float t)
{
return t * t * (3f - 2f * t);
}
public static float Linear(float t) => t;
}
}

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
// 2025/6/23 by Zhe Fang
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper

View File

@@ -5,32 +5,29 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
using Windows.System;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcherHelper
{
private readonly WinEventDelegate _winEventDelegate;
private readonly List<IntPtr> _hooks = new();
private IntPtr _currentForeground = IntPtr.Zero;
private readonly User32.WinEventProc _winEventDelegate;
private readonly List<User32.HWINEVENTHOOK> _hooks = new();
private HWND _currentForeground = HWND.NULL;
private readonly IntPtr _selfHwnd;
private readonly DispatcherTimer _pollingTimer;
private DateTime _lastEventTime = DateTime.MinValue;
private const int ThrottleIntervalMs = 100;
private const int ThrottleIntervalMs = 1000;
public delegate void WindowChangedHandler(IntPtr hwnd);
public delegate void WindowChangedHandler(HWND hwnd);
private readonly WindowChangedHandler _onWindowChanged;
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new WinEventDelegate(WinEventProc);
_winEventDelegate = new User32.WinEventProc(WinEventProc);
_pollingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_pollingTimer.Tick += (_, _) =>
@@ -44,27 +41,27 @@ namespace BetterLyrics.WinUI3.Helper
{
// Hook: foreground changes and minimize end
_hooks.Add(
SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_MINIMIZEEND,
IntPtr.Zero,
User32.SetWinEventHook(
User32.EventConstants.EVENT_SYSTEM_FOREGROUND,
User32.EventConstants.EVENT_SYSTEM_MINIMIZEEND,
HINSTANCE.NULL,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
User32.WINEVENT.WINEVENT_OUTOFCONTEXT
)
);
// Hook: window move/resize (location change)
_hooks.Add(
SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_OBJECT_LOCATIONCHANGE,
IntPtr.Zero,
User32.SetWinEventHook(
User32.EventConstants.EVENT_OBJECT_LOCATIONCHANGE,
User32.EventConstants.EVENT_OBJECT_LOCATIONCHANGE,
HINSTANCE.NULL,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
User32.WINEVENT.WINEVENT_OUTOFCONTEXT
)
);
@@ -74,16 +71,16 @@ namespace BetterLyrics.WinUI3.Helper
public void Stop()
{
foreach (var hook in _hooks)
UnhookWinEvent(hook);
User32.UnhookWinEvent(hook);
_hooks.Clear();
_pollingTimer.Stop();
}
private void WinEventProc(
IntPtr hWinEventHook,
User32.HWINEVENTHOOK hWinEventHook,
uint eventType,
IntPtr hwnd,
HWND hwnd,
int idObject,
int idChild,
uint dwEventThread,
@@ -99,44 +96,15 @@ namespace BetterLyrics.WinUI3.Helper
_lastEventTime = now;
if (eventType == EVENT_SYSTEM_FOREGROUND)
if (eventType == User32.EventConstants.EVENT_SYSTEM_FOREGROUND)
{
_currentForeground = hwnd;
_onWindowChanged?.Invoke(hwnd);
}
else if (
(eventType == EVENT_OBJECT_LOCATIONCHANGE || eventType == EVENT_SYSTEM_MINIMIZEEND)
&& hwnd == _currentForeground
)
else if ((eventType == User32.EventConstants.EVENT_OBJECT_LOCATIONCHANGE || eventType == User32.EventConstants.EVENT_SYSTEM_MINIMIZEEND) && hwnd == _currentForeground)
{
_onWindowChanged?.Invoke(hwnd);
}
}
#region WinAPI
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags
);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endregion
}
}

View File

@@ -1,4 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -8,11 +10,8 @@ using System.Threading.Tasks;
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 Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI;
@@ -20,34 +19,8 @@ namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
private static readonly ColorThief _colorThief = new();
public const int AccentColorCount = 3;
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(
byte[] imageBytes
)
{
if (imageBytes == null || imageBytes.Length == 0)
return null;
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
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();
@@ -57,31 +30,7 @@ namespace BetterLyrics.WinUI3.Helper
return stream;
}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
using var memoryStream = new MemoryStream();
await stream.AsStreamForRead().CopyToAsync(memoryStream);
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));
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(
string text,
int width,
int height
)
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(string text, int width, int height)
{
var device = CanvasDevice.GetSharedDevice();
var renderTarget = new CanvasRenderTarget(device, width, height, 96);
@@ -152,5 +101,92 @@ namespace BetterLyrics.WinUI3.Helper
return buffer;
}
}
public static List<Windows.UI.Color> GetAccentColorsFromByte(byte[] bytes)
{
// 使用 ImageSharp 读取图片
using var image = SixLabors.ImageSharp.Image.Load<SixLabors.ImageSharp.PixelFormats.Rgba32>(bytes);
// 简单聚类法:统计所有像素出现频率,取出现最多的前 AccentColorCount 个颜色
var colorCount = new Dictionary<SixLabors.ImageSharp.PixelFormats.Rgba32, int>();
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
var color = image[x, y];
// 可选:忽略透明像素
if (color.A < 32) continue;
if (colorCount.ContainsKey(color))
colorCount[color]++;
else
colorCount[color] = 1;
}
}
// 按出现次数排序,取前 AccentColorCount 个
var topColors = colorCount
.OrderByDescending(kv => kv.Value)
.Take(AccentColorCount)
.Select(kv => kv.Key)
.ToList();
// 转换为 Windows.UI.Color
return topColors
.Select(c => Windows.UI.Color.FromArgb(c.A, c.R, c.G, c.B))
.ToList();
}
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<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
{
if (imageBytes == null || imageBytes.Length == 0)
return null;
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
return stream;
}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
using var memoryStream = new MemoryStream();
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
public static float GetAverageLuminance(CanvasBitmap bitmap)
{
var pixels = bitmap.GetPixelBytes();
double sum = 0;
for (int i = 0; i < pixels.Length; i += 4)
{
// BGRA
byte b = pixels[i];
byte g = pixels[i + 1];
byte r = pixels[i + 2];
// 忽略A
double y = 0.299 * r + 0.587 * g + 0.114 * b;
sum += y / 255.0;
}
return (float)(sum / (pixels.Length / 4));
}
}
}

View File

@@ -1,11 +1,14 @@
using System;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Windows.Globalization.Fonts;
namespace BetterLyrics.WinUI3.Helper
{
@@ -13,50 +16,51 @@ namespace BetterLyrics.WinUI3.Helper
{
private List<List<LyricsLine>> _multiLangLyricsLines = [];
public List<List<LyricsLine>> Parse(
string raw,
LyricsFormat? lyricsFormat = null,
string? title = null,
string? artist = null,
int durationMs = 0
)
public List<List<LyricsLine>> Parse(string? raw, int durationMs)
{
_multiLangLyricsLines = [];
switch (lyricsFormat)
if (raw == null)
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw, durationMs);
break;
case LyricsFormat.Ttml:
ParseTtml(raw, durationMs);
break;
default:
break;
_multiLangLyricsLines.Add(
[
new LyricsLine
{
StartMs = 0,
EndMs = durationMs,
OriginalText = App.ResourceLoader!.GetString("LyricsNotFound"),
CharTimings = [],
},
]
);
}
else
{
switch (raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw);
break;
case LyricsFormat.Qrc:
ParseUsingLyricify(Lyricify.Lyrics.Parsers.QrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Krc:
ParseUsingLyricify(Lyricify.Lyrics.Parsers.KrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(raw);
break;
default:
break;
}
}
PostProcessLyricsLines(durationMs);
return _multiLangLyricsLines;
}
private void PostProcessLyricsLines(List<LyricsLine> lines)
private void ParseLrc(string raw)
{
if (lines.Count > 0 && lines[0].StartMs > 0)
{
lines.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = lines[0].StartMs,
Text = "",
CharTimings = [],
}
);
}
}
private void ParseLrc(string raw, int durationMs)
{
var lines = raw.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
new List<(int time, string text, List<(int time, string text)> syllables)>();
@@ -104,7 +108,7 @@ namespace BetterLyrics.WinUI3.Helper
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[3].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex.Replace(line, "").Trim();
content = bracketRegex.Replace(line, "");
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
@@ -132,61 +136,41 @@ namespace BetterLyrics.WinUI3.Helper
{
StartMs = start,
EndMs = 0, // 稍后统一修正
Text = text,
OriginalText = 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 charEnd = (j + 1 < syllables.Count) ? syllables[j + 1].Item1 : 0;
int startIndex = currentIndex;
line.CharTimings.Add(
new CharTiming { StartMs = charStart, EndMs = charEnd }
new CharTiming
{
StartMs = charStart,
EndMs = 0, // Fixed later
Text = charText ?? "",
StartIndex = startIndex,
}
);
currentIndex += charText?.Length ?? 0;
}
}
_multiLangLyricsLines[langIdx].Add(line);
}
}
// 修正 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 ParseTtml(string raw, int durationMs)
private void ParseTtml(string raw)
{
try
{
List<LyricsLine> singleLangLyricsLine = [];
var xdoc = XDocument.Parse(raw);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null)
return;
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
@@ -197,17 +181,13 @@ namespace BetterLyrics.WinUI3.Helper
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();
var spans = p.Elements().Where(s => s.Name.LocalName == "span").ToList();
string text = string.Concat(spans.Select(s => s.Value));
var charTimings = new List<CharTiming>();
int startIndex = 0;
for (int i = 0; i < spans.Count; i++)
{
var span = spans[i];
@@ -225,23 +205,23 @@ namespace BetterLyrics.WinUI3.Helper
? ParseTtmlTime(spans[i + 1].Attribute("begin")?.Value)
: pEndMs;
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = sEndMs });
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = 0, StartIndex = startIndex, Text = span.Value });
startIndex += span.Value.Length;
}
if (spans.Count == 0)
text = p.Value.Trim();
text = p.Value;
singleLangLyricsLine.Add(
new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
Text = text,
EndMs = 0,
OriginalText = text,
CharTimings = charTimings,
}
);
}
PostProcessLyricsLines(singleLangLyricsLine);
_multiLangLyricsLines.Add(singleLangLyricsLine);
}
catch
@@ -310,5 +290,111 @@ namespace BetterLyrics.WinUI3.Helper
}
return 0;
}
private void ParseUsingLyricify(List<ILineInfo>? lines)
{
lines = lines?.Where(x => x.Text != string.Empty).ToList();
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,
EndMs = 0,
OriginalText = lineRead.Text,
CharTimings = [],
};
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,
EndMs = 0,
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);
}
private void PostProcessLyricsLines(int durationMs)
{
for (int langIdx = 0; langIdx < _multiLangLyricsLines.Count; 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;
}
}
}
}
if (linesInSingleLang.Count > 0 && linesInSingleLang[0].StartMs > 0)
{
linesInSingleLang.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = linesInSingleLang[0].StartMs,
OriginalText = "● ● ●",
CharTimings = [],
}
);
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
using BetterLyrics.WinUI3.Enums;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml.Media;

View File

@@ -1,22 +1,111 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowColorHelper
{
public static Color GetDominantColorBelow(IntPtr myHwnd)
public static Color GetDominantColor(IntPtr myHwnd, WindowColorSampleMode mode)
{
if (!GetWindowRect(myHwnd, out RECT myRect))
return Color.Transparent;
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return Color.Transparent;
int screenWidth = GetSystemMetrics(SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
switch (mode)
{
case WindowColorSampleMode.BelowWindow:
{
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
case WindowColorSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return Color.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
case WindowColorSampleMode.WindowEdge:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return Color.Transparent;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
var edgeThickness = new Thickness(36, 0, 36, 0);
List<Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// 合并四边平均色
if (edgeColors.Count == 0)
return Color.Transparent;
long r = 0,
g = 0,
b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
);
}
default:
return Color.Transparent;
}
}
private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
@@ -25,21 +114,19 @@ namespace BetterLyrics.WinUI3.Helper
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = GetDC(IntPtr.Zero); // Entire screen
IntPtr hdcSrc = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, SRCCOPY);
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, Gdi32.RasterOperationMode.SRCCOPY);
gDest.ReleaseHdc(hdcDest);
ReleaseDC(IntPtr.Zero, hdcSrc);
User32.ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static Color ComputeAverageColor(Bitmap bmp)
{
long r = 0,
g = 0,
b = 0;
long r = 0, g = 0, b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
@@ -54,53 +141,8 @@ namespace BetterLyrics.WinUI3.Helper
}
}
if (count == 0)
return Color.Transparent;
if (count == 0) return Color.Transparent;
return Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
#region Win32 Imports & Structs
private const int SRCCOPY = 0x00CC0020;
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
private static extern bool BitBlt(
IntPtr hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
IntPtr hdcSrc,
int nXSrc,
int nYSrc,
int dwRop
);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(SystemMetric smIndex);
private enum SystemMetric
{
SM_CXSCREEN = 0,
SM_CYSCREEN = 1,
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
}
}

View File

@@ -1,8 +1,11 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.ApplicationModel.Core;
using WinRT.Interop;
using WinUIEx;
@@ -10,124 +13,92 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class WindowHelper
{
private static readonly Dictionary<Type, Window> _windowCache = new();
private static List<object> _activeWindows = [];
public static void HideSystemTitleBar(this Window window)
public static void CloseWindow<T>()
{
window.ExtendsContentIntoTitleBar = true;
window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
}
public static void HideSystemTitleBarAndSetCustomTitleBar(
this Window window,
UIElement titleBar
)
{
window.HideSystemTitleBar();
window.SetTitleBar(titleBar);
}
public static void OpenSettingsWindow()
{
OpenOrShowWindow(typeof(SettingsPage));
}
public static void OpenLyricsWindow()
{
OpenOrShowWindow(typeof(LyricsPage));
}
private static void OpenOrShowWindow(Type pageType)
{
if (_windowCache.TryGetValue(pageType, out var window))
var window = _activeWindows.Find(w => w is T);
if (window is Window w)
{
window.TryShow();
w.Close();
_activeWindows.Remove(w);
}
}
public static void ExitAllWindows()
{
while (_activeWindows.Count > 0)
{
var window = _activeWindows[0];
((Window)window).Close();
_activeWindows.Remove(window);
}
App.Current.Exit();
}
public static T? GetWindowByWindowType<T>()
{
foreach (var window in _activeWindows)
{
if (window is T castedWindow)
{
return castedWindow;
}
}
return default;
}
public static void OpenOrShowWindow<T>()
{
var window = _activeWindows.Find(w => w is T);
if (window != null)
{
var castedWindow = (Window)window;
castedWindow.Restore();
}
else
{
var newWindow = new HostWindow();
TrackWindow(newWindow, pageType);
newWindow.ViewModel.FramePageType = pageType;
newWindow.Navigate(pageType);
newWindow.Activate();
object newWindow;
if (typeof(T) == typeof(LyricsWindow))
{
newWindow = new LyricsWindow();
}
else if (typeof(T) == typeof(SettingsWindow))
{
newWindow = new SettingsWindow();
}
else
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
((Window)newWindow).Activate();
TrackWindow(newWindow);
}
}
public static void TrackWindow(Window window, Type pageType = null)
public static void RestartApp(string args = "")
{
if (pageType != null)
{
_windowCache[pageType] = window;
}
// The restart will be executed immediately.
AppRestartFailureReason failureReason =
Microsoft.Windows.AppLifecycle.AppInstance.Restart(args);
// If the restart fails, handle it here.
switch (failureReason)
{
case AppRestartFailureReason.RestartPending:
break;
case AppRestartFailureReason.NotInForeground:
break;
case AppRestartFailureReason.InvalidUser:
break;
default: //AppRestartFailureReason.Other
break;
}
}
private static void TrackWindow(object 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
static public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return element.XamlRoot.RasterizationScale;
}
}
}
return 0.0;
}
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;
}
}
}

View File

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

View File

@@ -1,14 +1,12 @@
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.Models
{
public class CharTiming
{
public int StartMs { get; set; }
public int EndMs { get; set; }
public int StartIndex { get; set; }
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty;
}
}

View File

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

View File

@@ -0,0 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class LanguageInfo : ObservableObject
{
[ObservableProperty]
public partial string Code { get; set; }
[ObservableProperty]
public partial string Name { get; set; }
public LanguageInfo(string code, string name)
{
Code = code;
Name = name;
}
}
}

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
@@ -10,10 +7,10 @@ namespace BetterLyrics.WinUI3.Models
public partial class LocalLyricsFolder : ObservableObject
{
[ObservableProperty]
public partial string Path { get; set; }
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial bool IsEnabled { get; set; }
public partial string Path { get; set; }
public LocalLyricsFolder() { }

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsData
{
public int LanguageIndex { get; set; } = 0;
public List<LyricsLine> LyricsLines => MultiLangLyricsLines[LanguageIndex];
public List<List<LyricsLine>> MultiLangLyricsLines { get; set; } = [];
}
}

View File

@@ -1,54 +1,33 @@
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
{
public class LyricsLine
{
public string Text { get; set; } = "";
private const float _animationDuration = 0.3f;
public ValueTransition<float> AngleTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> BlurAmountTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> HighlightOpacityTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> OpacityTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> ScaleTransition { get; set; } = new(initialValue: 0.95f, durationSeconds: _animationDuration);
public CanvasTextLayout? CanvasTextLayout { get; set; }
public Vector2 CenterPosition { get; set; }
public Vector2 Position { get; set; }
public List<CharTiming> CharTimings { get; set; } = [];
public int StartMs { get; set; }
public int EndMs { get; set; }
public LyricsPlayingState PlayingState { get; set; }
public int DurationMs => EndMs - StartMs;
public int EndMs { get; set; }
public int StartMs { get; set; }
public float EnteringProgress { get; set; }
public float ExitingProgress { get; set; }
public float PlayingProgress { get; set; }
public Vector2 Position { get; set; }
public Vector2 CenterPosition { get; set; }
public float Scale { get; set; }
public float Opacity { get; set; }
public LyricsLine Clone()
{
return new LyricsLine
{
Text = this.Text,
CharTimings = this.CharTimings,
StartMs = this.StartMs,
EndMs = this.EndMs,
PlayingState = this.PlayingState,
EnteringProgress = this.EnteringProgress,
ExitingProgress = this.ExitingProgress,
PlayingProgress = this.PlayingProgress,
Position = this.Position,
CenterPosition = this.CenterPosition,
Scale = this.Scale,
Opacity = this.Opacity,
};
}
public string DisplayedText { get; set; } = "";
public string OriginalText { get; set; } = "";
}
}

View File

@@ -1,22 +1,25 @@
using BetterLyrics.WinUI3.Enums;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LyricsSearchProviderInfo : ObservableObject
public partial class MediaSourceProviderInfo : ObservableObject
{
[ObservableProperty]
public partial LyricsSearchProvider Provider { get; set; }
[ObservableProperty]
public partial bool IsEnabled { get; set; }
public LyricsSearchProviderInfo() { }
[ObservableProperty]
public partial string Provider { get; set; }
public LyricsSearchProviderInfo(LyricsSearchProvider provider, bool isEnabled)
public MediaSourceProviderInfo() { }
public MediaSourceProviderInfo(string provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -0,0 +1,25 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LyricsSearchProviderInfo : ObservableObject
{
[ObservableProperty]
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial LyricsSearchProvider Provider { get; set; }
public LyricsSearchProviderInfo() { }
public LyricsSearchProviderInfo(LyricsSearchProvider provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -12,26 +9,21 @@ namespace BetterLyrics.WinUI3.Models
public partial class Notification : ObservableObject
{
[ObservableProperty]
public partial InfoBarSeverity Severity { get; set; }
public partial bool IsForeverDismissable { get; set; }
[ObservableProperty]
public partial string? Message { get; set; }
[ObservableProperty]
public partial bool IsForeverDismissable { get; set; }
public partial string? RelatedSettingsKeyName { get; set; }
[ObservableProperty]
public partial InfoBarSeverity Severity { get; set; }
[ObservableProperty]
public partial Visibility Visibility { get; set; }
[ObservableProperty]
public partial string? RelatedSettingsKeyName { get; set; }
public Notification(
string? message = null,
InfoBarSeverity severity = InfoBarSeverity.Informational,
bool isForeverDismissable = false,
string? relatedSettingsKeyName = null
)
public Notification(string? message = null, InfoBarSeverity severity = InfoBarSeverity.Informational, bool isForeverDismissable = false, string? relatedSettingsKeyName = null)
{
Message = message;
Severity = severity;

View File

@@ -1,28 +1,31 @@
using CommunityToolkit.Mvvm.ComponentModel;
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Models
{
public partial class SongInfo : ObservableObject
{
[ObservableProperty]
public partial string Title { get; set; }
public partial string? Album { get; set; }
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = null;
public Color? AlbumArtAccentColor { get; set; } = null;
[ObservableProperty]
public partial string Artist { get; set; }
[ObservableProperty]
public partial string? Album { get; set; }
/// <summary>
/// In milliseconds
/// </summary>
[ObservableProperty]
public partial double? DurationMs { get; set; }
[ObservableProperty]
public partial string? SourceAppUserModelId { get; set; } = null;
public byte[]? AlbumArt { get; set; } = null;
[ObservableProperty]
public partial string Title { get; set; }
public SongInfo() { }
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class TranslateResponse
{
[JsonPropertyName("translatedText")]
public string TranslatedText { get; set; }
}
}

View File

@@ -5,6 +5,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Renderer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
@@ -13,7 +14,16 @@
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Loaded="LyricsCanvas_Loaded"
Update="LyricsCanvas_Update" />
<Grid
Margin="36"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Visibility="{x:Bind ViewModel.IsTranslating, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<FontIcon
x:Name="RotatingIcon"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE8C1;" />
</Grid>
</Grid>
</UserControl>

View File

@@ -1,11 +1,9 @@
using System.Diagnostics;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
using Microsoft.UI.Xaml.Media.Animation;
namespace BetterLyrics.WinUI3.Renderer
{
@@ -19,25 +17,14 @@ namespace BetterLyrics.WinUI3.Renderer
ViewModel = Ioc.Default.GetRequiredService<LyricsRendererViewModel>();
}
private void LyricsCanvas_Draw(
Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs args
)
private void LyricsCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs args)
{
ViewModel.Draw(sender, args.DrawingSession);
}
private void LyricsCanvas_Update(
Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender,
Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs args
)
private void LyricsCanvas_Update(Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs args)
{
ViewModel.Update(sender, args);
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.RequestRelayout();
}
}
}

View File

@@ -1,17 +1,19 @@
using System;
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Serialization
{
[JsonSerializable(typeof(List<LyricsSearchProviderInfo>))]
[JsonSerializable(typeof(List<MediaSourceProviderInfo>))]
[JsonSerializable(typeof(List<LocalLyricsFolder>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<DetectLanguageResult>))]
[JsonSerializable(typeof(TranslateResponse))]
[JsonSerializable(typeof(JsonElement))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class SourceGenerationContext : JsonSerializerContext { }

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;
@@ -11,6 +13,7 @@ namespace BetterLyrics.WinUI3.Services
public interface ILibWatcherService
{
event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public void UpdateWatchers(List<LocalLyricsFolder> folders);
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public interface ILibreTranslateService
{
Task<string> TranslateAsync(string text, CancellationToken? token);
}
}

View File

@@ -1,4 +1,7 @@
using System.Collections.Generic;
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
@@ -6,14 +9,14 @@ namespace BetterLyrics.WinUI3.Services
{
public interface IMusicSearchService
{
Task<(string?, LyricsFormat?)> SearchLyricsAsync(
Task <byte[]?> SearchAlbumArtAsync(string title, string artist, string album);
Task<string?> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
string album,
double durationMs,
CancellationToken token
);
byte[]? SearchAlbumArtAsync(string title, string artist);
}
}

View File

@@ -1,4 +1,6 @@
using System;
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
@@ -6,12 +8,12 @@ namespace BetterLyrics.WinUI3.Services
{
public interface IPlaybackService
{
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<PositionChangedEventArgs>? PositionChanged;
SongInfo? SongInfo { get; }
bool IsPlaying { get; }
TimeSpan Position { get; }
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
}
}

View File

@@ -1,48 +1,85 @@
using System.Collections.Generic;
// 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;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Services
{
public interface ISettingsService
{
bool IsFirstRun { get; set; }
// Lyrics lib
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { 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; }
int CoverOverlayBlurAmount { get; set; }
int CoverOverlayOpacity { get; set; }
bool IsDynamicCoverOverlayEnabled { get; set; }
bool IsFanLyricsEnabled { get; set; }
bool IsFirstRun { get; set; }
bool IsLyricsGlowEffectEnabled { get; set; }
LyricsGlowEffectScope LyricsGlowEffectScope { get; set; }
LyricsFontColorType LyricsFontColorType { get; set; }
Language Language { get; set; }
int DesktopWindowLeft { get; set; }
int DesktopWindowTop { get; set; }
int DesktopWindowWidth { get; set; }
int DesktopWindowHeight { get; set; }
int StandardWindowWidth { get; set; }
int StandardWindowHeight { get; set; }
int StandardWindowLeft { get; set; }
int StandardWindowTop { get; set; }
bool AutoLockOnDesktopMode { get; set; }
string LibreTranslateServer { get; set; }
int SelectedTargetLanguageIndex { get; set; }
// Lyrics lib
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
// Lyrics style and effetc
TextAlignmentType LyricsAlignmentType { get; set; }
TextAlignmentType SongInfoAlignmentType { get; set; }
int LyricsBlurAmount { get; set; }
Color LyricsCustomBgFontColor { get; set; }
Color LyricsCustomFgFontColor { get; set; }
Color LyricsCustomStrokeFontColor { get; set; }
LyricsFontColorType LyricsBgFontColorType { get; set; }
LyricsFontColorType LyricsFgFontColorType { get; set; }
LyricsFontColorType LyricsStrokeFontColorType { get; set; }
int LyricsFontSize { get; set; }
ElementTheme LyricsBackgroundTheme { get; set; }
int LyricsFontStrokeWidth { get; set; }
LyricsFontWeight LyricsFontWeight { get; set; }
LineRenderingType LyricsGlowEffectScope { get; set; }
float LyricsLineSpacingFactor { get; set; }
List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
List<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; }
int LyricsVerticalEdgeOpacity { get; set; }
bool IgnoreFullscreenWindow { get; set; }
bool IsTranslationEnabled { get; set; }
LyricsDisplayType PreferredDisplayType { get; set; }
}
}

View File

@@ -1,90 +1,84 @@
using System;
// 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 BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using global::BetterLyrics.WinUI3.Events;
using global::BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services
public class LibWatcherService : IDisposable, ILibWatcherService
{
public class LibWatcherService : IDisposable, ILibWatcherService
private readonly ISettingsService _settingsService;
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
public LibWatcherService(ISettingsService settingsService)
{
private readonly ISettingsService _settingsService;
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
_settingsService = settingsService;
UpdateWatchers(_settingsService.LocalLyricsFolders);
}
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public LibWatcherService(ISettingsService settingsService)
public void Dispose()
{
foreach (var watcher in _watchers.Values)
{
_settingsService = settingsService;
UpdateWatchers(_settingsService.LocalLyricsFolders);
watcher.Dispose();
}
_watchers.Clear();
}
public void UpdateWatchers(List<LocalLyricsFolder> folders)
public void UpdateWatchers(List<LocalLyricsFolder> folders)
{
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
{
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
if (!folders.Any(x => x.Path == key && x.IsEnabled))
{
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;
}
_watchers[key].Dispose();
_watchers.Remove(key);
}
}
private void OnChanged(string folder, FileSystemEventArgs e)
// 添加新的监听
foreach (var folder in folders)
{
App.DispatcherQueue!.TryEnqueue(
Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
MusicLibraryFilesChanged?.Invoke(
this,
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
);
}
);
}
public void Dispose()
{
foreach (var watcher in _watchers.Values)
if (
!_watchers.ContainsKey(folder.Path)
&& Directory.Exists(folder.Path)
&& folder.IsEnabled
)
{
watcher.Dispose();
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;
}
_watchers.Clear();
}
}
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)
);
}
);
}
}
}

View File

@@ -0,0 +1,78 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class LibreTranslateService : ILibreTranslateService
{
private readonly ISettingsService _settingsService;
private readonly HttpClient _httpClient;
public LibreTranslateService(ISettingsService settingsService)
{
_settingsService = settingsService;
_httpClient = new HttpClient();
}
public async Task<string> TranslateAsync(string text, CancellationToken? token)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Text and target language must be provided.");
}
string targetLangCode = AppInfo.GetAllTranslationLanguagesInfo()[_settingsService.SelectedTargetLanguageIndex].Code;
string originalLangCode = await DetectLanguageCode(text);
token?.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(originalLangCode) || originalLangCode == targetLangCode)
{
return text; // No translation needed
}
var url = $"{_settingsService.LibreTranslateServer}/translate";
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(
[
new("q", text),
new("source", originalLangCode),
new("target", targetLangCode),
]));
token?.ThrowIfCancellationRequested();
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
token?.ThrowIfCancellationRequested();
var result = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.TranslateResponse);
return result?.TranslatedText ?? string.Empty;
}
private async Task<string> DetectLanguageCode(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Text must be provided.");
}
var url = $"{_settingsService.LibreTranslateServer}/detect";
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(
[
new("q", text),
]));
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var resultList = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.ListDetectLanguageResult);
return resultList?.OrderByDescending(x => x.Confidence).FirstOrDefault()?.Language ?? string.Empty;
}
}
}

View File

@@ -1,48 +1,96 @@
using System;
// 2025/6/23 by Zhe Fang
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.DependencyInjection;
using iTunesSearch.Library;
using Lyricify.Lyrics.Providers.Web.Kugou;
using Lyricify.Lyrics.Searchers;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
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
{
public class MusicSearchService : IMusicSearchService
{
private readonly HttpClient _httpClient;
private readonly HttpClient _amllTtmlDbHttpClient;
private readonly HttpClient _lrcLibHttpClient;
private readonly HttpClient _iTunesHttpClinet;
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public MusicSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add(
_logger = Ioc.Default.GetRequiredService<ILogger<MusicSearchService>>();
_lrcLibHttpClient = new();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
);
_amllTtmlDbHttpClient = new();
_iTunesHttpClinet = new();
}
public byte[]? SearchAlbumArtAsync(string title, string artist)
public async Task<bool> DownloadAmllTtmlDbIndexAsync()
{
const string url = "https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/metadata/raw-img-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;
}
}
private static string GuessCountryCode(string album, string artist)
{
string s = album + artist;
if (s.Any(c => c >= 0x4e00 && c <= 0x9fff)) // 中文
return "cn";
if (s.Any(c => (c >= 0x3040 && c <= 0x30ff) || (c >= 0x31f0 && c <= 0x31ff))) // 日文
return "jp";
if (s.Any(c => c >= 0xac00 && c <= 0xd7af)) // 韩文
return "kr";
if (s.Any(c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) // 英文
return "us";
// 其他情况
return "us";
}
public async Task<byte[]?> SearchAlbumArtAsync(string title, string artist, string album)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (
var file in Directory.GetFiles(
folder.Path,
$"*.*",
SearchOption.AllDirectories
)
)
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
@@ -55,17 +103,63 @@ namespace BetterLyrics.WinUI3.Services
}
}
return await SearchiTunesAlbumArtAsync(artist, album);
}
private async Task<byte[]?> SearchiTunesAlbumArtAsync(string artist, string album)
{
// Source: https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce
try
{
string format = ".jpg";
var cachedAlbumArt = ReadAlbumArtCache(artist, album, format, AppInfo.iTunesAlbumArtCacheDirectory);
if (cachedAlbumArt != null)
{
return cachedAlbumArt;
}
// Build the iTunes API URL
string url = $"https://itunes.apple.com/search?term=" + artist + "+" + album + "&country=" + GuessCountryCode(album, artist) + "&entity=album";
url.Replace(" ", "-");
// Make a request to the API
HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
// Parse the JSON response
var data = JsonSerializer.Deserialize(responseBody, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
{
// Get the first result
var result = results[0];
if (result.TryGetProperty("artworkUrl100", out var artworkUrlProp))
{
string artworkUrl = artworkUrlProp.GetString()?.Replace("100x100bb.jpg", "1200x1200bb.jpg") ?? string.Empty;
var fetched = await _iTunesHttpClinet.GetByteArrayAsync(artworkUrl);
if (fetched != null && fetched.Length > 0)
{
// Write to cache
WriteAlbumArtCache(artist, album, fetched, format, AppInfo.iTunesAlbumArtCacheDirectory);
return fetched;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching iTunes album art for {Artist} - {Album}", artist, album);
}
return null;
}
public async Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
)
public async Task<string?> SearchLyricsAsync(string title, string artist, string album, double durationMs, CancellationToken token)
{
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
if (!provider.IsEnabled)
@@ -73,150 +167,133 @@ namespace BetterLyrics.WinUI3.Services
continue;
}
switch (provider.Provider)
string? cachedLyrics;
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
// Check cache first
if (provider.Provider.IsRemote())
{
case LyricsSearchProvider.LrcLib:
// Check cache first
var cachedLyrics = ReadCache(title, artist, LyricsFormat.Lrc);
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, LyricsFormat.Lrc);
}
break;
default:
break;
cachedLyrics = ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return cachedLyrics;
}
}
string? searchedLyrics = null;
switch (provider.Provider)
if (provider.Provider.IsLocal())
{
case LyricsSearchProvider.LocalMusicFile:
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = LocalLyricsSearchInMusicFiles(title, artist);
break;
case LyricsSearchProvider.LocalLrcFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Lrc
);
break;
case LyricsSearchProvider.LocalEslrcFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Eslrc
);
break;
case LyricsSearchProvider.LocalTtmlFile:
searchedLyrics = await LocalLyricsSearchInLyricsFiles(
title,
artist,
LyricsFormat.Ttml
);
break;
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLib(
title,
artist,
album,
(int)(durationMs / 1000),
matchMode
);
break;
default:
break;
}
else
{
searchedLyrics = await LocalLyricsSearchInLyricsFiles(title, artist, lyricsFormat);
}
}
if (!string.IsNullOrWhiteSpace(searchedLyrics))
else
{
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
WriteCache(title, artist, searchedLyrics, LyricsFormat.Lrc);
return (searchedLyrics, LyricsFormat.Lrc);
case LyricsSearchProvider.LocalMusicFile:
return (searchedLyrics, LyricsFormatExtensions.Detect(searchedLyrics));
case LyricsSearchProvider.LocalLrcFile:
return (searchedLyrics, LyricsFormat.Lrc);
case LyricsSearchProvider.LocalEslrcFile:
return (searchedLyrics, LyricsFormat.Eslrc);
case LyricsSearchProvider.LocalTtmlFile:
return (searchedLyrics, LyricsFormat.Ttml);
searchedLyrics = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
break;
case LyricsSearchProvider.QQ:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
break;
default:
break;
}
}
token.ThrowIfCancellationRequested();
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
if (provider.Provider.IsRemote())
{
WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
}
return searchedLyrics;
}
}
return (null, null);
return null;
}
private static int LevenshteinDistance(string a, string b)
private static bool MusicMatch(string fileName, string title, string artist)
{
if (string.IsNullOrEmpty(a))
return b.Length;
if (string.IsNullOrEmpty(b))
return a.Length;
int[,] d = new int[a.Length + 1, b.Length + 1];
for (int i = 0; i <= a.Length; i++)
d[i, 0] = i;
for (int j = 0; j <= b.Length; j++)
d[0, j] = j;
for (int i = 1; i <= a.Length; i++)
for (int j = 1; j <= b.Length; j++)
d[i, j] = Math.Min(
Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
d[i - 1, j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1)
);
return d[a.Length, b.Length];
var normFileName = Normalize(fileName);
var normTitle = Normalize(title);
var normArtist = Normalize(artist);
// 常见两种顺序
return normFileName == normTitle + normArtist
|| normFileName == normArtist + normTitle;
}
// 判断相似度
private static bool FuzzyMatch(string fileName, string title, string artist)
// 预处理:去除空格、括号、下划线、横杠、点、大小写等
static string Normalize(string s) =>
new string(s
.Where(c => char.IsLetterOrDigit(c))
.ToArray())
.ToLowerInvariant();
private static string SanitizeFileName(string fileName, char replacement = '_')
{
var normFile = Normalize(fileName);
var normTarget1 = Normalize(title + artist);
var normTarget2 = Normalize(artist + title);
int dist1 = LevenshteinDistance(normFile, normTarget1);
int dist2 = LevenshteinDistance(normFile, normTarget2);
return dist1 <= 3 || dist2 <= 3; // 阈值可调整
}
private static string Normalize(string s)
{
if (string.IsNullOrWhiteSpace(s))
return "";
var sb = new StringBuilder();
foreach (var c in s.ToLowerInvariant())
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
if (char.IsLetterOrDigit(c))
sb.Append(c);
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
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;
}
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
)
)
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
//Track track = new(file);
//var plain = track.Lyrics.UnsynchronizedLyrics;
try
{
var plain = TagLib.File.Create(file).Tag.Lyrics;
@@ -234,63 +311,108 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private async Task<string?> LocalLyricsSearchInLyricsFiles(
string title,
string artist,
LyricsFormat format
)
private string? ReadLyricsCache(string title, string artist, LyricsFormat format, string cacheFolderPath)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(cacheFolderPath, $"{safeArtist} - {safeTitle}{format.ToFileExtension()}");
if (File.Exists(cacheFilePath))
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (
var file in Directory.GetFiles(
folder.Path,
$"*{format.ToFileExtension()}",
SearchOption.AllDirectories
)
)
{
if (FuzzyMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(
file,
FileHelper.GetEncoding(file)
);
if (raw != null)
{
return raw;
}
}
}
}
return File.ReadAllText(cacheFilePath);
}
return null;
}
private async Task<string?> SearchLrcLib(
string title,
string artist,
string album,
int duration,
MusicSearchMatchMode matchMode
)
private byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var safeArtist = SanitizeFileName(artist);
var safeAlbum = SanitizeFileName(album);
var cacheFilePath = Path.Combine(cacheFolderPath, $"{safeArtist} - {safeAlbum}{format}");
if (File.Exists(cacheFilePath))
{
return File.ReadAllBytes(cacheFilePath);
}
return null;
}
private async Task<string?> SearchAmllTtmlDbAsync(string title, string artist)
{
// 检索本地 JSONL 索引文件,查找 rawLyricFile
if (!File.Exists(AppInfo.AmllTtmlDbIndexPath))
{
var downloadOk = await DownloadAmllTtmlDbIndexAsync();
if (!downloadOk || !File.Exists(AppInfo.AmllTtmlDbIndexPath))
return null;
}
string? rawLyricFile = null;
await foreach (var line in File.ReadLinesAsync(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-img/{rawLyricFile}";
try
{
var response = await _amllTtmlDbHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStringAsync();
}
catch
{
return null;
}
}
private async Task<string?> SearchLrcLibAsync(string title, string artist, string album, int duration)
{
// Build API query URL
var url =
$"https://lrclib.net/api/search?"
+ $"track_name={Uri.EscapeDataString(title)}&"
+ $"artist_name={Uri.EscapeDataString(artist)}";
$"https://lrclib.net/api/search?" +
$"track_name={Uri.EscapeDataString(title)}&" +
$"artist_name={Uri.EscapeDataString(artist)}&" +
$"&album_name={Uri.EscapeDataString(album)}" +
$"&durationMs={Uri.EscapeDataString(duration.ToString())}";
if (matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration)
{
url +=
$"&album_name={Uri.EscapeDataString(album)}"
+ $"&durationMs={Uri.EscapeDataString(duration.ToString())}";
}
var response = await _httpClient.GetAsync(url);
var response = await _lrcLibHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
@@ -314,41 +436,82 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private void WriteCache(string title, string artist, string lyrics, LyricsFormat format)
private async Task<string?> SearchUsingLyricifyAsync(
string title,
string artist,
string album,
int durationMs,
Searchers searchers
)
{
var result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs = durationMs,
Album = album,
Artists = [artist],
Title = title,
}
);
if (result is QQMusicSearchResult qqResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.QQMusicApi.GetLyricsAsync(qqResult.Id);
var original = response?.Lyrics;
return original;
}
else if (result is NeteaseSearchResult neteaseResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(neteaseResult.Id);
return response?.Lrc.Lyric;
}
else if (result is KugouSearchResult kugouResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(hash: kugouResult.Hash);
if (response?.Candidates.FirstOrDefault() is SearchLyricsResponse.Candidate candidate)
{
return Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyrics(
candidate.Id,
candidate.AccessKey
);
}
}
return null;
}
private void WriteLyricsCache(
string title,
string artist,
string lyrics,
LyricsFormat format,
string cacheFolderPath
)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
AppInfo.OnlineLyricsCacheDirectory,
cacheFolderPath,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
}
private string? ReadCache(string title, string artist, LyricsFormat format)
private void WriteAlbumArtCache(
string album,
string artist,
byte[] img,
string format,
string cacheFolderPath
)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var safeAlbum = SanitizeFileName(album);
var cacheFilePath = Path.Combine(
AppInfo.OnlineLyricsCacheDirectory,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
cacheFolderPath,
$"{safeArtist} - {safeAlbum}{format}"
);
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
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();
File.WriteAllBytes(cacheFilePath, img);
}
}
}

View File

@@ -1,231 +1,257 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Windows.ApplicationModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using WindowsMediaController;
namespace BetterLyrics.WinUI3.Services
{
public partial class PlaybackService : IPlaybackService
public partial class PlaybackService : BaseViewModel, IPlaybackService, IRecipient<PropertyChangedMessage<ObservableCollection<MediaSourceProviderInfo>>>
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly IMusicSearchService _musicSearchService;
private readonly ILogger<PlaybackService> _logger;
private readonly MediaManager _mediaManager = new();
private CancellationTokenSource? _mediaPropsCts;
private List<MediaSourceProviderInfo> _mediaSourceProvidersInfo;
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<PositionChangedEventArgs>? PositionChanged;
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
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 IMusicSearchService _musicSearchService;
public PlaybackService(
ISettingsService settingsService,
IMusicSearchService musicSearchService
)
public PlaybackService(ISettingsService settingsService, IMusicSearchService musicSearchService) : base(settingsService)
{
_musicSearchService = musicSearchService;
InitMediaManager().ConfigureAwait(true);
_logger = Ioc.Default.GetRequiredService<ILogger<PlaybackService>>();
_mediaSourceProvidersInfo = _settingsService.MediaSourceProvidersInfo;
InitMediaManager();
}
private async Task InitMediaManager()
private bool IsMediaSourceEnabled(string id)
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
return _mediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id)?.IsEnabled ?? true;
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_PlaybackInfoChanged(
GlobalSystemMediaTransportControlsSession? sender,
PlaybackInfoChangedEventArgs? args
)
private void InitMediaManager()
{
if (sender == null)
_mediaManager.Start();
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
_mediaManager.OnAnySessionClosed += MediaManager_OnAnySessionClosed;
_mediaManager.OnFocusedSessionChanged += MediaManager_OnFocusedSessionChanged;
_mediaManager.OnAnyMediaPropertyChanged += MediaManager_OnAnyMediaPropertyChanged;
_mediaManager.OnAnyPlaybackStateChanged += MediaManager_OnAnyPlaybackStateChanged;
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
MediaManager_OnFocusedSessionChanged(_mediaManager.GetFocusedSession());
}
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession mediaSession)
{
if (mediaSession == null || !IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId))
{
IsPlaying = false;
SendNullMessages();
}
else
{
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
_dispatcherQueue.TryEnqueue(async () =>
{
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;
}
try
{
var props = await mediaSession.ControlSession.TryGetMediaPropertiesAsync();
MediaManager_OnAnyMediaPropertyChanged(mediaSession, props);
MediaManager_OnAnyPlaybackStateChanged(mediaSession, mediaSession.ControlSession.GetPlaybackInfo());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "TryGetMediaPropertiesAsync failed");
SendNullMessages();
}
});
}
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties timelineProperties)
{
if (!IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId) || mediaSession != _mediaManager.GetFocusedSession()) return;
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(IsPlaying));
PositionChanged?.Invoke(this, new PositionChangedEventArgs(timelineProperties.Position));
}
);
}
private void SessionManager_CurrentSessionChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
CurrentSessionChangedEventArgs? args
)
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo playbackInfo)
{
// _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;
}
RecordMediaSourceProviderInfo(mediaSession);
if (!IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId) || mediaSession != _mediaManager.GetFocusedSession()) return;
// 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);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(playbackInfo.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
}));
}
);
}
/// <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
)
private async void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties mediaProperties)
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
if (sender == null)
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties.Title, mediaProperties.Artist, mediaProperties.AlbumTitle);
RecordMediaSourceProviderInfo(mediaSession);
string id = mediaSession.ControlSession.SourceAppUserModelId;
if (!IsMediaSourceEnabled(id) || mediaSession != _mediaManager.GetFocusedSession()) return;
_mediaPropsCts?.Cancel();
var cts = new CancellationTokenSource();
_mediaPropsCts = cts;
var token = cts.Token;
try
{
SongInfo = null;
}
else
{
try
{
mediaProps = await sender.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
SongInfo? songInfo;
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,
};
token.ThrowIfCancellationRequested();
if (
SongInfo.SourceAppUserModelId?.Contains(Package.Current.Id.FamilyName)
?? false
)
{
SongInfo.Title = "甜度爆表";
SongInfo.Artist = "AI";
}
songInfo = new SongInfo
{
Title = mediaProperties.Title,
Artist = mediaProperties.Artist,
Album = mediaProperties.AlbumTitle,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
SourceAppUserModelId = id,
};
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
byte[]? bytes;
bytes = await _musicSearchService.SearchAlbumArtAsync(
songInfo.Title,
songInfo.Artist,
songInfo.Album
);
token.ThrowIfCancellationRequested();
if (bytes == null)
{
if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
SongInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(streamReference);
bytes = await ImageHelper.ToByteArrayAsync(streamReference);
token.ThrowIfCancellationRequested();
}
else
{
SongInfo.AlbumArt = _musicSearchService.SearchAlbumArtAsync(
SongInfo.Title,
SongInfo.Artist
);
if (SongInfo.AlbumArt == null)
{
SongInfo.AlbumArt = await ImageHelper.CreateTextPlaceholderBytesAsync(
$"{SongInfo.Artist} - {SongInfo.Title}",
400,
400
);
}
bytes = await ImageHelper.CreateTextPlaceholderBytesAsync($"{songInfo.Artist} - {songInfo.Title}", 400, 400);
token.ThrowIfCancellationRequested();
}
}
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
token.ThrowIfCancellationRequested();
var decoder = await BitmapDecoder.CreateAsync(stream);
token.ThrowIfCancellationRequested();
songInfo.AlbumArtSwBitmap?.Dispose();
songInfo.AlbumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
token.ThrowIfCancellationRequested();
songInfo.AlbumArtAccentColor = ImageHelper.GetAccentColorsFromByte(bytes).FirstOrDefault();
if (!token.IsCancellationRequested)
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(songInfo));
});
}
);
}
catch (OperationCanceledException) { }
catch (Exception) { }
}
private void CurrentSession_TimelinePropertiesChanged(
GlobalSystemMediaTransportControlsSession? sender,
TimelinePropertiesChangedEventArgs? args
)
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
{
if (sender == null)
if (_mediaManager.CurrentMediaSessions.Count == 0)
{
Position = TimeSpan.Zero;
SendNullMessages();
}
else
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
RecordMediaSourceProviderInfo(mediaSession);
}
private void RecordMediaSourceProviderInfo(MediaManager.MediaSession mediaSession)
{
var id = mediaSession?.ControlSession?.SourceAppUserModelId;
if (string.IsNullOrEmpty(id)) return;
var found = _mediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
if (found == null)
{
Position = sender.GetTimelineProperties().Position;
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
_mediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, true));
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(Position));
MediaSourceProvidersInfoChanged?.Invoke(this, new MediaSourceProvidersInfoEventArgs(_mediaSourceProvidersInfo));
});
}
}
private void SendNullMessages()
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(null));
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(false));
PositionChanged?.Invoke(this, new PositionChangedEventArgs(TimeSpan.Zero));
});
}
public void Receive(PropertyChangedMessage<ObservableCollection<MediaSourceProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.MediaSourceProvidersInfo))
{
_mediaSourceProvidersInfo = [.. message.NewValue];
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
MediaManager_OnFocusedSessionChanged(_mediaManager.GetFocusedSession());
}
);
// _logger.LogDebug(_currentTime);
}
}
}
}

View File

@@ -1,55 +1,290 @@
using System;
using System.Collections.Generic;
using System.Linq;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using CommunityToolkit.WinUI.Helpers;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Storage;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services
{
public class SettingsService : ISettingsService
{
private readonly ApplicationDataContainer _localSettings;
private const string IsFirstRunKey = "IsFirstRun";
// Lyrics lib
private const string LocalLyricsFoldersKey = "LocalLyricsFolders";
private const string LyricsSearchProvidersInfoKey = "LyricsSearchProvidersInfo";
// App appearance
private const string ThemeTypeKey = "ThemeType";
private const string LanguageKey = "Language";
private const string BackdropTypeKey = "BackdropType";
public const string LyricsCustomBgFontColorKey = "LyricsCustomBgFontColor";
public const string LyricsCustomFgFontColorKey = "LyricsCustomFgFontColor";
public const string LyricsCustomStrokeFontColorKey = "LyricsCustomStrokeFontColor";
// 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 CoverImageRadiusKey = "AlbumArtCornerRadius";
private const string CoverOverlayBlurAmountKey = "CoverOverlayBlurAmount";
private const string TitleBarTypeKey = "TitleBarType";
private const string CoverImageRadiusKey = "CoverImageRadius";
private const string CoverOverlayOpacityKey = "CoverOverlayOpacity";
private const string IsCoverOverlayEnabledKey = "IsCoverOverlayEnabled";
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 DesktopWindowLeftKey = "DesktopWindowLeft";
private const string DesktopWindowTopKey = "DesktopWindowTop";
private const string DesktopWindowWidthKey = "DesktopWindowWidth";
private const string DesktopWindowHeightKey = "DesktopWindowHeight";
private const string StandardWindowLeftKey = "StandardWindowLeft";
private const string StandardWindowTopKey = "StandardWindowTop";
private const string StandardWindowWidthKey = "StandardWindowWidth";
private const string StandardWindowHeightKey = "StandardWindowHeight";
private const string AutoLockOnDesktopModeKey = "AutoLockOnDesktopMode";
private const string IsDynamicCoverOverlayEnabledKey = "IsDynamicCoverOverlayEnabled";
private const string IsFanLyricsEnabledKey = "IsFanLyricsEnabled";
private const string IsFirstRunKey = "IsFirstRun";
private const string IsLyricsGlowEffectEnabledKey = "IsLyricsGlowEffectEnabled";
private const string LyricsFontColorTypeKey = "LyricsFontColorType";
private const string LanguageKey = "Language";
private const string LocalLyricsFoldersKey = "LocalLyricsFolders";
private const string LyricsAlignmentTypeKey = "TextAlignmentType";
private const string SongInfoAlignmentTypeKey = "SongInfoAlignmentType";
private const string LyricsBlurAmountKey = "LyricsBlurAmount";
private const string LyricsBgFontColorTypeKey = "_lyricsBgFontColorType";
private const string LyricsFgFontColorTypeKey = "LyricsFgFontColorType";
private const string LyricsStrokeFontColorTypeKey = "LyricsStrokeFontColorType";
private const string LyricsFontStrokeWidthKey = "LyricsFontStrokeWidth";
private const string LyricsFontSizeKey = "LyricsFontSize";
private const string LyricsFontWeightKey = "LyricsFontWeightKey";
private const string LyricsGlowEffectScopeKey = "LyricsGlowEffectScope";
private const string LyricsLineSpacingFactorKey = "LyricsLineSpacingFactor";
private const string LyricsSearchProvidersInfoKey = "LyricsSearchProvidersInfo";
private const string LyricsVerticalEdgeOpacityKey = "LyricsVerticalEdgeOpacity";
private const string MediaSourceProvidersInfoKey = "MediaSourceProvidersInfo";
private const string IsTranslationEnabledKey = "IsTranslationEnabled";
private const string LibreTranslateServerKey = "LibreTranslateServer";
private const string SelectedTargetLanguageIndexKey = "SelectedTargetLanguageIndex";
private const string LyricsBackgroundThemeKey = "LyricsBackgroundTheme";
private const string IgnoreFullscreenWindowKey = "IgnoreFullscreenWindow";
private const string PreferredDisplayTypeKey = "PreferredDisplayTypeKey";
private readonly ApplicationDataContainer _localSettings;
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
)
);
SetDefault(MediaSourceProvidersInfoKey, "[]");
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(LanguageKey, (int)Language.FollowSystem);
SetDefault(DesktopWindowHeightKey, 600);
SetDefault(DesktopWindowLeftKey, 200);
SetDefault(DesktopWindowTopKey, 200);
SetDefault(DesktopWindowWidthKey, 1200);
SetDefault(StandardWindowHeightKey, 800);
SetDefault(StandardWindowLeftKey, 200);
SetDefault(StandardWindowTopKey, 200);
SetDefault(StandardWindowWidthKey, 1600);
SetDefault(AutoLockOnDesktopModeKey, false);
// App behavior
SetDefault(AutoStartWindowTypeKey, (int)AutoStartWindowType.StandardMode);
// Album art
SetDefault(IsCoverOverlayEnabledKey, true);
SetDefault(IsDynamicCoverOverlayEnabledKey, true);
SetDefault(CoverOverlayOpacityKey, 100); // 100 % = 1.0
SetDefault(CoverOverlayBlurAmountKey, 200);
SetDefault(CoverImageRadiusKey, 12); // 12 %
// Lyrics
SetDefault(LyricsAlignmentTypeKey, (int)TextAlignmentType.Center);
SetDefault(SongInfoAlignmentTypeKey, (int)TextAlignmentType.Left);
SetDefault(LyricsFontWeightKey, (int)LyricsFontWeight.Bold);
SetDefault(LyricsBlurAmountKey, 5);
SetDefault(LyricsBackgroundThemeKey, (int)ElementTheme.Default);
SetDefault(LyricsBgFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsFgFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsStrokeFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsCustomBgFontColorKey, Colors.White.ToInt());
SetDefault(LyricsCustomFgFontColorKey, Colors.White.ToInt());
SetDefault(LyricsCustomStrokeFontColorKey, Colors.White.ToInt());
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
SetDefault(LyricsGlowEffectScopeKey, (int)LineRenderingType.CurrentCharOnly);
SetDefault(IsFanLyricsEnabledKey, false);
SetDefault(LibreTranslateServerKey, "");
SetDefault(IsTranslationEnabledKey, false);
SetDefault(SelectedTargetLanguageIndexKey, 6);
SetDefault(LyricsFontStrokeWidthKey, 3);
SetDefault(IgnoreFullscreenWindowKey, false);
SetDefault(PreferredDisplayTypeKey, (int)LyricsDisplayType.SplitView);
}
public LyricsDisplayType PreferredDisplayType
{
get => (LyricsDisplayType)GetValue<int>(PreferredDisplayTypeKey);
set => SetValue(PreferredDisplayTypeKey, (int)value);
}
public ElementTheme LyricsBackgroundTheme
{
get => (ElementTheme)GetValue<int>(LyricsBackgroundThemeKey);
set => SetValue(LyricsBackgroundThemeKey, (int)value);
}
public AutoStartWindowType AutoStartWindowType
{
get => (AutoStartWindowType)GetValue<int>(AutoStartWindowTypeKey);
set => SetValue(AutoStartWindowTypeKey, (int)value);
}
public int DesktopWindowLeft
{
get => GetValue<int>(DesktopWindowLeftKey);
set => SetValue(DesktopWindowLeftKey, value);
}
public int DesktopWindowTop
{
get => GetValue<int>(DesktopWindowTopKey);
set => SetValue(DesktopWindowTopKey, value);
}
public int DesktopWindowWidth
{
get => GetValue<int>(DesktopWindowWidthKey);
set => SetValue(DesktopWindowWidthKey, value);
}
public int DesktopWindowHeight
{
get => GetValue<int>(DesktopWindowHeightKey);
set => SetValue(DesktopWindowHeightKey, value);
}
public int StandardWindowLeft
{
get => GetValue<int>(StandardWindowLeftKey);
set => SetValue(StandardWindowLeftKey, value);
}
public int StandardWindowTop
{
get => GetValue<int>(StandardWindowTopKey);
set => SetValue(StandardWindowTopKey, value);
}
public int StandardWindowWidth
{
get => GetValue<int>(StandardWindowWidthKey);
set => SetValue(StandardWindowWidthKey, value);
}
public int StandardWindowHeight
{
get => GetValue<int>(StandardWindowHeightKey);
set => SetValue(StandardWindowHeightKey, value);
}
public bool AutoLockOnDesktopMode
{
get => GetValue<bool>(AutoLockOnDesktopModeKey);
set => SetValue(AutoLockOnDesktopModeKey, value);
}
public int CoverImageRadius
{
get => GetValue<int>(CoverImageRadiusKey);
set => SetValue(CoverImageRadiusKey, value);
}
public int CoverOverlayBlurAmount
{
get => GetValue<int>(CoverOverlayBlurAmountKey);
set => SetValue(CoverOverlayBlurAmountKey, value);
}
public int CoverOverlayOpacity
{
get => GetValue<int>(CoverOverlayOpacityKey);
set => SetValue(CoverOverlayOpacityKey, value);
}
public bool IsDynamicCoverOverlayEnabled
{
get => GetValue<bool>(IsDynamicCoverOverlayEnabledKey);
set => SetValue(IsDynamicCoverOverlayEnabledKey, value);
}
public bool IsFanLyricsEnabled
{
get => GetValue<bool>(IsFanLyricsEnabledKey);
set => SetValue(IsFanLyricsEnabledKey, value);
}
public bool IsFirstRun
{
get => GetValue<bool>(IsFirstRunKey);
set => SetValue(IsFirstRunKey, value);
}
public bool IsLyricsGlowEffectEnabled
{
get => GetValue<bool>(IsLyricsGlowEffectEnabledKey);
set => SetValue(IsLyricsGlowEffectEnabledKey, value);
}
public Language Language
{
get => (Language)GetValue<int>(LanguageKey);
set => SetValue(LanguageKey, (int)value);
}
public List<LocalLyricsFolder> LocalLyricsFolders
{
get =>
@@ -67,6 +302,90 @@ namespace BetterLyrics.WinUI3.Services
);
}
public TextAlignmentType LyricsAlignmentType
{
get => (TextAlignmentType)GetValue<int>(LyricsAlignmentTypeKey);
set => SetValue(LyricsAlignmentTypeKey, (int)value);
}
public TextAlignmentType SongInfoAlignmentType
{
get => (TextAlignmentType)GetValue<int>(SongInfoAlignmentTypeKey);
set => SetValue(SongInfoAlignmentTypeKey, (int)value);
}
public int LyricsBlurAmount
{
get => GetValue<int>(LyricsBlurAmountKey);
set => SetValue(LyricsBlurAmountKey, value);
}
public Color LyricsCustomBgFontColor
{
get => GetValue<int>(LyricsCustomBgFontColorKey)!.ToColor();
set => SetValue(LyricsCustomBgFontColorKey, value.ToInt());
}
public Color LyricsCustomFgFontColor
{
get => GetValue<int>(LyricsCustomFgFontColorKey)!.ToColor();
set => SetValue(LyricsCustomFgFontColorKey, value.ToInt());
}
public Color LyricsCustomStrokeFontColor
{
get => GetValue<int>(LyricsCustomStrokeFontColorKey)!.ToColor();
set => SetValue(LyricsCustomStrokeFontColorKey, value.ToInt());
}
public LyricsFontColorType LyricsBgFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsBgFontColorTypeKey);
set => SetValue(LyricsBgFontColorTypeKey, (int)value);
}
public LyricsFontColorType LyricsFgFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsFgFontColorTypeKey);
set => SetValue(LyricsFgFontColorTypeKey, (int)value);
}
public LyricsFontColorType LyricsStrokeFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsStrokeFontColorTypeKey);
set => SetValue(LyricsStrokeFontColorTypeKey, (int)value);
}
public int LyricsFontStrokeWidth
{
get => GetValue<int>(LyricsFontStrokeWidthKey);
set => SetValue(LyricsFontStrokeWidthKey, value);
}
public int LyricsFontSize
{
get => GetValue<int>(LyricsFontSizeKey);
set => SetValue(LyricsFontSizeKey, value);
}
public LyricsFontWeight LyricsFontWeight
{
get => (LyricsFontWeight)GetValue<int>(LyricsFontWeightKey);
set => SetValue(LyricsFontWeightKey, (int)value);
}
public LineRenderingType LyricsGlowEffectScope
{
get => (LineRenderingType)GetValue<int>(LyricsGlowEffectScopeKey);
set => SetValue(LyricsGlowEffectScopeKey, (int)value);
}
public float LyricsLineSpacingFactor
{
get => GetValue<float>(LyricsLineSpacingFactorKey);
set => SetValue(LyricsLineSpacingFactorKey, value);
}
public List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo
{
get =>
@@ -84,82 +403,21 @@ namespace BetterLyrics.WinUI3.Services
);
}
public ElementTheme ThemeType
public List<MediaSourceProviderInfo> MediaSourceProvidersInfo
{
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 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);
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(MediaSourceProvidersInfoKey) ?? "[]",
SourceGenerationContext.Default.ListMediaSourceProviderInfo
)!;
set =>
SetValue(
MediaSourceProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListMediaSourceProviderInfo
)
);
}
public int LyricsVerticalEdgeOpacity
@@ -168,87 +426,28 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(LyricsVerticalEdgeOpacityKey, value);
}
public float LyricsLineSpacingFactor
public string LibreTranslateServer
{
get => GetValue<float>(LyricsLineSpacingFactorKey);
set => SetValue(LyricsLineSpacingFactorKey, value);
get => GetValue<string>(LibreTranslateServerKey)!;
set => SetValue(LibreTranslateServerKey, value);
}
public int LyricsFontSize
public bool IsTranslationEnabled
{
get => GetValue<int>(LyricsFontSizeKey);
set => SetValue(LyricsFontSizeKey, value);
get => GetValue<bool>(IsTranslationEnabledKey);
set => SetValue(IsTranslationEnabledKey, value);
}
public bool IsLyricsGlowEffectEnabled
public int SelectedTargetLanguageIndex
{
get => GetValue<bool>(IsLyricsGlowEffectEnabledKey);
set => SetValue(IsLyricsGlowEffectEnabledKey, value);
get => GetValue<int>(SelectedTargetLanguageIndexKey);
set => SetValue(SelectedTargetLanguageIndexKey, value);
}
public LyricsGlowEffectScope LyricsGlowEffectScope
public bool IgnoreFullscreenWindow
{
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);
// 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, 0);
SetDefault(LyricsFontColorTypeKey, (int)LyricsFontColorType.Default);
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
SetDefault(LyricsGlowEffectScopeKey, (int)LyricsGlowEffectScope.CurrentChar);
get => GetValue<bool>(IgnoreFullscreenWindowKey);
set => SetValue(IgnoreFullscreenWindowKey, value);
}
private T? GetValue<T>(string key)
@@ -260,16 +459,16 @@ namespace BetterLyrics.WinUI3.Services
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;
}
private void SetValue<T>(string key, T value)
{
_localSettings.Values[key] = value;
}
}
}

View File

@@ -139,13 +139,16 @@
<value>Add a folder</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>Theme</value>
<value>Lyrics background theme</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>Language</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>Follow system</value>
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adaptive to lyrics background (Colored)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adaptive to lyrics background (Colored)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Light</value>
@@ -196,7 +199,7 @@
<value>Transparent</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Backdrop</value>
<value>Lyrics backdrop</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>Default</value>
@@ -210,14 +213,14 @@
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>The folder has been added. Please do not add it again.</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>Overlay album art background</value>
<data name="SettingsPageLyricsBackground.Header" xml:space="preserve">
<value>Lyrics background</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>Dynamic album art background</value>
<data name="SettingsPageDynamicLyricsBackground.Header" xml:space="preserve">
<value>Dynamic lyrics background</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>Album art background opacity</value>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>Lyrics background opacity</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>Settings - BetterLyrics</value>
@@ -237,8 +240,8 @@
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>Album art background blur amount</value>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>Lyrics background blur amount</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>Blur amount</value>
@@ -252,9 +255,6 @@
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>Significantly higher GPU usage when blur is enabled (&gt; 0)</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>Enabling this feature will slightly increase GPU utilization</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Top and bottom edge opacity</value>
</data>
@@ -273,8 +273,8 @@
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>Album background</value>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>Lyrics background</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>About</value>
@@ -282,7 +282,7 @@
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>Lyrics library</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>App appearance</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
@@ -294,6 +294,9 @@
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>Configure lyrics search providers</value>
</data>
<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>
@@ -301,7 +304,7 @@
<value>Welcome to BetterLyrics</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's setup lyrics database now</value>
<value>Hover the mouse over the top or bottom area of the app to display more function options</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>No music playing now</value>
@@ -313,22 +316,28 @@
<value>Play test music</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>Play using system player</value>
<value>Play "Cut To The Feeling" on "soundcloud.com"</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="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>Font color</value>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Including log files, network lyrics cache</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>Default</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>Font color (Non-current playback area)</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>Album art accent color</value>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>Font color (Current playback area)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adaptive to lyrics background (Grayed)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adaptive to lyrics background (Grayed)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>Album art style</value>
<value>Album art area style</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Corner radius</value>
@@ -378,19 +387,19 @@
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Exit picture-in-picture mode</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<data name="LyricsNotFound" xml:space="preserve">
<value>Lyrics not found</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Lyrics style</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>This folder is already included in the existing folder and does not need to be added again</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>App behavior</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
@@ -400,10 +409,10 @@
<value>Activate standard mode</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>Activate dock mode</value>
<value>Activate desktop mode</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>Lyrics not found</value>
<data name="SettingsPageAutoStartDockLyrics.Content" xml:space="preserve">
<value>Activate dock mode</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>System tray - BetterLyrics</value>
@@ -411,6 +420,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>
@@ -459,7 +471,7 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<data name="LyricsLoading" xml:space="preserve">
<value>Loading lyrics...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
@@ -486,4 +498,160 @@
<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 (Restart needed)</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>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Fan lyrics</value>
</data>
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>Custom</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>Custom</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>Lyrics style and effect</value>
</data>
<data name="SettingsPageApp.Content" xml:space="preserve">
<value>App appearance and behavior</value>
</data>
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>Auto-lock when activating desktop mode</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>Alignment</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>Center</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>Left</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>Album art</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>Song title &amp; artist</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Easing animation type</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>Media sources</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>Media source</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>Enable or disable lyrics display for a specified media source</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Log record</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>Lyrics translation</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>Lyrics timeline offset (ms)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>Configure translation services</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>Server address</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>Test server</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>Target language</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>Translation service powered by LibreTranslate</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>Visit https://github.com/LibreTranslate/LibreTranslate for installation instructions and more information (this software is not affiliated with this translation service in any way)</value>
</data>
<data name="SettingsPageLibreTranslateTestSuccessInfo" xml:space="preserve">
<value>Server test successful</value>
</data>
<data name="SettingsPageLibreTranslateTestFailedInfo" xml:space="preserve">
<value>Server test failed</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>Lyrics stroke width (Desktop mode only)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>Follow system</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Automatic startup</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>Lyrics stroke color (Desktop mode only)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>Always stay on top of fullscreen applications</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>Force this app to appear on top of full-screen apps when docked or desktop mode is enabled</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>More</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>Lyrics timeline offset</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>Translate</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>Display type</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>Settings</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>Translate server is not set, please configure it in settings first</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>Will automatically reset to 0 when switching songs</value>
</data>
</root>

View File

@@ -139,13 +139,16 @@
<value>フォルダーを追加します</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>テーマ</value>
<value>歌詞の背景テーマ</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>言語</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>システムをフォローします</value>
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>歌詞の背景に適応する(色付き)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>歌詞の背景に適応する(色付き)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>ライト</value>
@@ -196,7 +199,7 @@
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景</value>
<value>歌詞の背景素材</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>デフォルト</value>
@@ -210,14 +213,14 @@
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>フォルダーが追加されました。二度と追加しないでください。</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>オーバーレイアルバムアートの背景</value>
<data name="SettingsPageLyricsBackground.Header" xml:space="preserve">
<value>歌詞の背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>ダイナミックアルバムアートの背景</value>
<data name="SettingsPageDynamicLyricsBackground.Header" xml:space="preserve">
<value>ダイナミックな歌詞の背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>アルバムアートの背景不透明</value>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>歌詞の背景不透明</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定 - BetterLyrics</value>
@@ -237,8 +240,8 @@
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>アルバムアートバックグラウンドブラー量</value>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>歌詞の背景ぼやけ</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>ぼやけの量</value>
@@ -252,9 +255,6 @@
<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>
@@ -273,8 +273,8 @@
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>没入モード</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>アルバムの背景</value>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>歌詞の背景</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>について</value>
@@ -282,7 +282,7 @@
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞ライブラリ</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>アプリの外観</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
@@ -294,6 +294,9 @@
<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>
@@ -301,7 +304,7 @@
<value>BetterLyrics へようこそ</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>今すぐ歌詞データベースをセットアップしましょう</value>
<value>マウスをアプリの上または下部の領域にホバリングして、より多くの機能オプションを表示します</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>今は音楽が再生されていません</value>
@@ -313,22 +316,28 @@
<value>テスト音楽を再生します</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>システムプレーヤーを使用して再生します</value>
<value>「SoundCloud.com」で「Cut to the Feeling」を再生する</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>ログ</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>キャッシュ</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>フォントカラー</value>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>ログファイル、ネットワーク歌詞キャッシュを含む</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>フォルト</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>フォントカラー(非電流再生エリア)</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>アルバムアートアクセントカラー</value>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>フォントカラー(現在の再生エリア)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>歌詞の背景に適応する(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>歌詞の背景に適応する(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>アルバムアートスタイル</value>
<value>アルバムエリアスタイル</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>コーナー半径</value>
@@ -378,19 +387,19 @@
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモードを終了します</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<data name="LyricsNotFound" xml:space="preserve">
<value>歌詞が見つかりません</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞効果</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞スタイル</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>このフォルダーは既存のフォルダーに既に含まれており、再度追加する必要はありません</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>アプリの動作</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
@@ -400,10 +409,10 @@
<value>標準モードをアクティブにします</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>ドックモードをアクティブにします</value>
<value>デスクトップモードを開始します</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>歌詞が見つかりません</value>
<data name="SettingsPageAutoStartDockLyrics.Content" xml:space="preserve">
<value>ドックモードをアクティブにします</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>システムトレイ - BetterLyrics</value>
@@ -411,6 +420,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>
@@ -459,7 +471,7 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<data name="LyricsLoading" xml:space="preserve">
<value>歌詞の読み込み...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
@@ -486,4 +498,160 @@
<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>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>ファンの歌詞</value>
</data>
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>カスタマイズ</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>カスタマイズ</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>歌詞のスタイルと効果</value>
</data>
<data name="SettingsPageApp.Content" xml:space="preserve">
<value>アプリの外観と動作</value>
</data>
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>デスクトップモードをアクティブにするときの自動ロック</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>アライメント</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>中心</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>アルバムアート</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>曲のタイトル&アーティスト</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>アニメーションタイプを緩和します</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアソース</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>メディアソース</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>指定されたメディアソースの歌詞ディスプレイを有効または無効にする</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>ログレコード</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌詞翻訳</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌詞タイムラインオフセット(ミリ秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>翻訳サービスを構成します</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>サーバーアドレス</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>テストサーバー</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>ターゲット言語</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>LibreTranslate を搭載した翻訳サービス</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>https://github.com/LibreTranslate/LibreTranslate にアクセスしてください。</value>
</data>
<data name="SettingsPageLibreTranslateTestSuccessInfo" xml:space="preserve">
<value>サーバーテストが成功しました</value>
</data>
<data name="SettingsPageLibreTranslateTestFailedInfo" xml:space="preserve">
<value>サーバーテストに失敗しました</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌詞ストローク幅(デスクトップモードのみ)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>システムをフォローします</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動起動</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌詞ストロークカラー(デスクトップモードのみ)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>常にフルスクリーンアプリケーションを常に把握してください</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>このアプリは、ドッキングまたはデスクトップモードが有効になっているときにフルスクリーンアプリの上に表示されます</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>もっと</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌詞タイムラインオフセット</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻訳する</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>表示タイプ</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>設定</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>翻訳サーバーは設定されていません。最初に設定で構成してください</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>曲を切り替えると、0 に自動的にリセットされます</value>
</data>
</root>

View File

@@ -139,13 +139,16 @@
<value>폴더를 추가하십시오</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>주제</value>
<value>가사 배경 테마</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>언어</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>시스템을 따르십시오</value>
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>가사 배경 (색상)에 적응</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>가사 배경 (색상)에 적응</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>빛</value>
@@ -196,7 +199,7 @@
<value>투명한</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>배경</value>
<value>가사 배경 자료</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>기본</value>
@@ -210,14 +213,14 @@
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>폴더가 추가되었습니다. 다시 추가하지 마십시오.</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>오버레이 앨범 아트 배경</value>
<data name="SettingsPageLyricsBackground.Header" xml:space="preserve">
<value>가사 배경</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>동적 앨범 아트 배경</value>
<data name="SettingsPageDynamicLyricsBackground.Header" xml:space="preserve">
<value>동적 인 가사 배경</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>앨범 아트 배경 불투명도</value>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>가사 배경 불투명도</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>설정 - BetterLyrics</value>
@@ -237,8 +240,8 @@
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>오른쪽</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>앨범 아트 배경 흐림 금액</value>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>가사 배경 블러</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>흐림 금액</value>
@@ -252,9 +255,6 @@
<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>
@@ -273,8 +273,8 @@
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>몰입 형 모드</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>앨범 배경</value>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>가사 배경</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>에 대한</value>
@@ -282,7 +282,7 @@
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>가사 도서관</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>앱 모양</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
@@ -294,6 +294,9 @@
<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>
@@ -301,7 +304,7 @@
<value>Betterlyrics에 오신 것을 환영합니다</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>지금 가사 데이터베이스를 설정합시다</value>
<value>더 많은 기능 옵션을 표시하려면 앱의 상단 또는 하단 영역 위로 마우스를 가져옵니다.</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>지금 음악이 재생되지 않습니다</value>
@@ -313,22 +316,28 @@
<value>테스트 음악을 재생하십시오</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>시스템 플레이어를 사용하여 재생하십시오</value>
<value>"soundcloud.com"에서 "Fut to the Feeling"을 재생하십시오.</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>통나무</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>은닉처</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>글꼴 색상</value>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>로그 파일, 네트워크 가사 캐시 포함</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>기본</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>글꼴 색상 (비 전류 재생 영역)</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>앨범 아트 악센트 색상</value>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>글꼴 색상 (현재 재생 영역)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>가사 배경 (회색)에 적응</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>가사 배경 (회색)에 적응</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>앨범 아트 스타일</value>
<value>앨범 영역 스타일</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>코너 반경</value>
@@ -378,19 +387,19 @@
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-Picture 모드 종료</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<data name="LyricsNotFound" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>가사 효과</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>가사 스타일</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>이 폴더는 이미 기존 폴더에 포함되어 있으며 다시 추가 할 필요가 없습니다.</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>앱 동작</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
@@ -400,10 +409,10 @@
<value>표준 모드를 ​​활성화합니다</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>도크 모드를 활성화하십시오</value>
<value>데스크탑 모드를 시작하십시오</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
<data name="SettingsPageAutoStartDockLyrics.Content" xml:space="preserve">
<value>도크 모드를 활성화하십시오</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>시스템 트레이 - BetterLyrics</value>
@@ -411,6 +420,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>
@@ -459,7 +471,7 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>설정</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<data name="LyricsLoading" xml:space="preserve">
<value>가사로드 ...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
@@ -486,4 +498,160 @@
<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>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>팬 가사</value>
</data>
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>사용자 정의하십시오</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>사용자 정의하십시오</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>가사 스타일과 효과</value>
</data>
<data name="SettingsPageApp.Content" xml:space="preserve">
<value>앱 외관과 행동</value>
</data>
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>데스크탑 모드를 활성화 할 때 자동 잠금</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>조정</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>센터</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>왼쪽</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>오른쪽</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>앨범 아트</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>노래 제목 및 아티스트</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>애니메이션 유형 완화</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>미디어 소스</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>미디어 소스</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>지정된 미디어 소스의 가사 디스플레이 활성화 또는 비활성화</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>로그 레코드</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>가사 번역</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>가사 타임 라인 오프셋 (밀리초)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>번역 서비스를 구성하십시오</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>서버 주소</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>테스트 서버</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>대상 언어</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>LibreTranslate 가 구동하는 번역 서비스</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>설치 지침 및 자세한 정보는 https://github.com/LibreTranslate/LibreTranslate 를 방문하십시오 (이 소프트웨어는이 번역 서비스와 제휴하지 않습니다).</value>
</data>
<data name="SettingsPageLibreTranslateTestSuccessInfo" xml:space="preserve">
<value>서버 테스트 성공</value>
</data>
<data name="SettingsPageLibreTranslateTestFailedInfo" xml:space="preserve">
<value>서버 테스트가 실패했습니다</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>가사 뇌졸중 너비 (데스크탑 모드 만)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>시스템을 따르십시오</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>자동 시작</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>가사 스트로크 컬러 (데스크탑 모드 만)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>항상 전체 화면 응용 프로그램 위에 머물러 있습니다</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>도킹 또는 데스크탑 모드가 활성화 될 때이 앱이 전체 화면 앱 위에 나타나도록 강요</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>더</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>가사 타임 라인 오프셋</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>번역하다</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>디스플레이 유형</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>설정</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>번역 서버가 설정되지 않았습니다. 먼저 설정으로 구성하십시오.</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>노래를 전환 할 때 자동으로 0 으로 재설정됩니다</value>
</data>
</root>

View File

@@ -139,13 +139,16 @@
<value>添加文件夹</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>主题</value>
<value>歌词背景主题</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>语言</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>适应歌词背景(彩色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>适应歌词背景(彩色)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>浅色</value>
@@ -196,7 +199,7 @@
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景材质</value>
<value>歌词背景材质</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>默认</value>
@@ -210,14 +213,14 @@
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>已添加过该文件夹,请勿重复添加</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>叠加专辑图片背景</value>
<data name="SettingsPageLyricsBackground.Header" xml:space="preserve">
<value>歌词背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>动态专辑图片背景</value>
<data name="SettingsPageDynamicLyricsBackground.Header" xml:space="preserve">
<value>动态歌词背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>专辑图片背景不透明度</value>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>歌词背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>设置 - BetterLyrics</value>
@@ -237,8 +240,8 @@
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>专辑图片背景模糊度</value>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>歌词背景模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>模糊度</value>
@@ -252,9 +255,6 @@
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>启用模糊(&gt; 0时将显著提升 GPU 占用率</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>启用该功能将略微提升 GPU 占用率</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>上下边缘不透明度</value>
</data>
@@ -273,8 +273,8 @@
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>专辑背景</value>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>歌词背景</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>关于</value>
@@ -282,7 +282,7 @@
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌词库</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>应用外观</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
@@ -294,6 +294,9 @@
<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>
@@ -301,7 +304,7 @@
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>现在就来初始化歌词数据库吧</value>
<value>将鼠标悬停在应用程序的顶部或底部区域以显示更多功能选项</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>当前没有正在播放的音乐</value>
@@ -313,22 +316,28 @@
<value>播放测试音乐</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>使用系统播放器播放</value>
<value>在 “soundcloud.com” 上播放 “Cut to the Feeling”</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日志</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>缓存</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>字体颜色</value>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>包括日志文件,网络歌词缓存</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>默认</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>字体颜色(非当前播放区域)</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>专辑强调色</value>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>字体颜色(当前播放区域)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>适应歌词背景(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>适应歌词背景(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>专辑封面样式</value>
<value>专辑区域样式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圆角半径</value>
@@ -378,19 +387,19 @@
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出画中画模式</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<data name="LyricsNotFound" xml:space="preserve">
<value>未找到歌词</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌词动效</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌词样式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>该文件夹已包含在已有文件夹中,无需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>应用行为</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
@@ -400,10 +409,10 @@
<value>启动标准模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>启动停靠模式</value>
<value>启动桌面模式</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>未找到歌词</value>
<data name="SettingsPageAutoStartDockLyrics.Content" xml:space="preserve">
<value>启动停靠模式</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>系统托盘 - BetterLyrics</value>
@@ -411,6 +420,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>
@@ -459,7 +471,7 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>设置</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<data name="LyricsLoading" xml:space="preserve">
<value>加载歌词中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
@@ -486,4 +498,160 @@
<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>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>扇形歌词</value>
</data>
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>歌词样式与动效</value>
</data>
<data name="SettingsPageApp.Content" xml:space="preserve">
<value>应用外观与行为</value>
</data>
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>启动桌面模式时随即锁定窗口</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>对齐方式</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>专辑</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>歌曲标题和艺术家</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>缓动动画类型</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒体来源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>媒体来源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>为指定媒体源启用或禁用歌词显示</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日志记录</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌词翻译</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌词时间轴偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻译服务</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服务器地址</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>测试服务器</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>目标语言</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>翻译服务由 LibreTranslate 驱动</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>访问 https://github.com/LibreTranslate/LibreTranslate 获取安装教程及更多信息(本软件与该翻译服务无任何联系)</value>
</data>
<data name="SettingsPageLibreTranslateTestSuccessInfo" xml:space="preserve">
<value>服务器测试成功</value>
</data>
<data name="SettingsPageLibreTranslateTestFailedInfo" xml:space="preserve">
<value>服务器测试失败</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌词描边宽度(仅桌面模式)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自动启动</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌词描边颜色(仅桌面模式)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>始终显示在全屏应用上方</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>当启用停靠模式或桌面模式时强制将本应用显示在全屏应用的上方</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>更多</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌词时间偏移</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻译</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>显示类型</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>设置</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未设置翻译服务器,请先在设置中进行配置</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>切换歌曲时将自动重置为 0</value>
</data>
</root>

View File

@@ -139,13 +139,16 @@
<value>新增資料夾</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>主題</value>
<value>歌詞背景主題</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>語言</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟隨系統</value>
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>適應歌詞背景(彩色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>適應歌詞背景(彩色)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>淺色</value>
@@ -196,7 +199,7 @@
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景材質</value>
<value>歌詞背景材質</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>預設</value>
@@ -210,14 +213,14 @@
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>已新增過該資料夾,請勿重複新增</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>疊加專輯圖片背景</value>
<data name="SettingsPageLyricsBackground.Header" xml:space="preserve">
<value>歌詞背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>動態專輯圖片背景</value>
<data name="SettingsPageDynamicLyricsBackground.Header" xml:space="preserve">
<value>動態歌詞背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>專輯圖片背景不透明度</value>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>歌詞背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定 - BetterLyrics</value>
@@ -237,8 +240,8 @@
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>專輯圖片背景模糊度</value>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>歌詞背景模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>模糊度</value>
@@ -252,9 +255,6 @@
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>啟用模糊(&gt; 0時將顯著提升 GPU 佔用率</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>啟用此功能將略微提升 GPU 佔用率</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>上下邊緣不透明度</value>
</data>
@@ -273,8 +273,8 @@
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>沉浸模式</value>
</data>
<data name="SettingsPageAlbumOverlay.Content" xml:space="preserve">
<value>專輯背景</value>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>歌詞背景</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>關於</value>
@@ -282,7 +282,7 @@
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞庫</value>
</data>
<data name="SettingsPageAppAppearance.Content" xml:space="preserve">
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>應用外觀</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
@@ -294,6 +294,9 @@
<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>
@@ -301,7 +304,7 @@
<value>歡迎使用 BetterLyrics</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>現在就來初始化歌詞資料庫吧</value>
<value>將鼠標懸停在應用程序的頂部或底部區域以顯示更多功能選項</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>目前沒有正在播放的音樂</value>
@@ -313,22 +316,28 @@
<value>播放測試音樂</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>使用系統播放器播放</value>
<value>在 “soundcloud.com” 上播放 “Cut to the Feeling”</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>紀錄</value>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>快取</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>字體顏色</value>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>包括日誌文件,網絡歌詞緩存</value>
</data>
<data name="SettingsPageLyricsFontColorDefault.Content" xml:space="preserve">
<value>預設</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>字體顏色(非當前播放區域)</value>
</data>
<data name="SettingsPageLyricsFontColorDominant.Content" xml:space="preserve">
<value>專輯強調色</value>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>字體顏色(當前播放區域)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>適應歌詞背景(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>適應歌詞背景(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>專輯封面樣式</value>
<value>专辑区域样式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圓角半徑</value>
@@ -378,19 +387,19 @@
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出畫中畫模式</value>
</data>
<data name="MainPageLyricsNotFound.Text" xml:space="preserve">
<data name="LyricsNotFound" xml:space="preserve">
<value>找不到歌詞</value>
</data>
<data name="SettingsPageLyricsEffect.Content" xml:space="preserve">
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞動效</value>
</data>
<data name="SettingsPageLyricsStyle.Content" xml:space="preserve">
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞樣式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>該資料夾已包含在已有資料夾中,無需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Content" xml:space="preserve">
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>應用行為</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
@@ -400,10 +409,10 @@
<value>啟動標準模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>啟動停靠模式</value>
<value>啟動桌面模式</value>
</data>
<data name="DesktopLyricsRendererPageLyricsNotFound.Text" xml:space="preserve">
<value>找不到歌詞</value>
<data name="SettingsPageAutoStartDockLyrics.Content" xml:space="preserve">
<value>啟動停靠模式</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>系統托盤 - BetterLyrics</value>
@@ -411,6 +420,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>
@@ -459,7 +471,7 @@
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageLyricsLoading.Text" xml:space="preserve">
<data name="LyricsLoading" xml:space="preserve">
<value>載入歌詞中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
@@ -486,4 +498,160 @@
<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>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>扇形歌詞</value>
</data>
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>自定義</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>自定義</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>歌詞樣式與動效</value>
</data>
<data name="SettingsPageApp.Content" xml:space="preserve">
<value>應用外觀與行為</value>
</data>
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>啟動桌面模式時隨即鎖定窗口</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>對齊方式</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>專輯</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>歌曲標題和藝術家</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>缓动动画类型</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒體來源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>媒體來源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>為指定媒體源啟用或禁用歌詞顯示</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日誌記錄</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌詞翻譯</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌詞時間軸偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻譯服務</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服務器地址</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>測試服務器</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>目標語言</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>翻译服务由 LibreTranslate 驱动</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>造訪 https://github.com/LibreTranslate/LibreTranslate 以取得安裝教學及更多資訊(本軟體與此翻譯服務無任何關聯)</value>
</data>
<data name="SettingsPageLibreTranslateTestSuccessInfo" xml:space="preserve">
<value>服務器測試成功</value>
</data>
<data name="SettingsPageLibreTranslateTestFailedInfo" xml:space="preserve">
<value>服務器測試失敗</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌詞描邊寬度(僅桌面模式)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動啟動</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌词描边颜色(仅桌面模式)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>始終顯示在全螢幕應用程式上方</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>啟用停靠模式或桌面模式時強制將本應用程式顯示在全螢幕應用程式的上方</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>更多</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌詞時間偏移</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻譯</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>顯示類型</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>設定</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未設定翻譯伺服器,請先在設定中進行配置</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>將在切換歌曲時自動重設為 0</value>
</data>
</root>

View File

@@ -1,5 +1,6 @@
using System;
using System.Runtime.CompilerServices;
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
@@ -8,11 +9,11 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseViewModel : ObservableRecipient, IDisposable
{
private protected readonly ISettingsService _settingsService;
private protected readonly DispatcherQueue _dispatcherQueue =
DispatcherQueue.GetForCurrentThread();
private protected readonly ISettingsService _settingsService;
public BaseViewModel(ISettingsService settingsService)
{
IsActive = true;

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseWindowViewModel : BaseViewModel
{
public BaseWindowViewModel(ISettingsService settingsService) : base(settingsService) { }
}
}

View File

@@ -1,223 +0,0 @@
using System;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3
{
public partial class HostWindowViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<TitleBarType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<BackdropType>>,
IRecipient<PropertyChangedMessage<int>>
{
private ForegroundWindowWatcherHelper? _watcherHelper = null;
[ObservableProperty]
public partial Type FramePageType { get; set; }
[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; }
public HostWindowViewModel(ISettingsService settingsService)
: base(settingsService)
{
TitleBarType = _settingsService.TitleBarType;
ThemeType = _settingsService.ThemeType;
OnTitleBarTypeChanged(TitleBarType);
WeakReferenceMessenger.Default.Register<ShowNotificatonMessage>(
this,
async (r, m) =>
{
Notification = m.Value;
if (
!Notification.IsForeverDismissable
|| AlreadyForeverDismissedThisMessage() == false
)
{
Notification.Visibility = Notification.IsForeverDismissable
? Visibility.Visible
: Visibility.Collapsed;
ShowInfoBar = true;
await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
ShowInfoBar = false;
}
}
);
}
private void StartWatchWindowColorChange()
{
var hwnd = WindowNative.GetWindowHandle(
WindowHelper.GetWindowByFramePageType(FramePageType)
);
_watcherHelper = new ForegroundWindowWatcherHelper(
hwnd,
onWindowChanged =>
{
UpdateAccentColor(hwnd);
}
);
_watcherHelper.Start();
UpdateAccentColor(hwnd);
}
private void StopWatchWindowColorChange()
{
_watcherHelper?.Stop();
_watcherHelper = null;
}
partial void OnFramePageTypeChanged(Type value)
{
if (value != null)
{
var window = WindowHelper.GetWindowByFramePageType(FramePageType);
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(
_settingsService.BackdropType
);
}
}
public void UpdateAccentColor(nint hwnd)
{
ActivatedWindowAccentColor = WindowColorHelper
.GetDominantColorBelow(hwnd)
.ToWindowsUIColor();
}
partial void OnTitleBarTypeChanged(TitleBarType value)
{
switch (value)
{
case TitleBarType.Compact:
AppLogoImageIconHeight = 18;
TitleBarFontSize = 11;
break;
case TitleBarType.Extended:
AppLogoImageIconHeight = 20;
TitleBarFontSize = 14;
break;
default:
break;
}
TitleBarHeight = value.GetHeight();
}
[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
);
}
}
}
}
}
}

View File

@@ -1,150 +1,131 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
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;
using Microsoft.UI.Xaml.Media.Imaging;
using WinUIEx.Messaging;
using System.Diagnostics;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsPageViewModel
: BaseViewModel,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<LyricsStatus>>
public partial class LyricsPageViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<int>>, IRecipient<PropertyChangedMessage<bool>>
{
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToDockMode;
private readonly IPlaybackService _playbackService;
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToNonStandardMode;
public LyricsPageViewModel(ISettingsService settingsService, IPlaybackService playbackService) : base(settingsService)
{
LyricsFontSize = _settingsService.LyricsFontSize;
IsTranslationEnabled = _settingsService.IsTranslationEnabled;
PreferredDisplayType = _settingsService.PreferredDisplayType;
_playbackService = playbackService;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
IsFirstRun = _settingsService.IsFirstRun;
}
private void PlaybackService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
PositionOffset = 0; // Reset position offset when song changes
TrySwitchToPreferredDisplayType(e.SongInfo);
}
[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 LyricsStatus LyricsStatus { get; set; } = LyricsStatus.Loading;
[ObservableProperty]
public partial LyricsDisplayType? PreferredDisplayType { get; set; } =
LyricsDisplayType.SplitView;
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[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; }
public partial LyricsDisplayType DisplayType { get; set; } = LyricsDisplayType.PlaceholderOnly;
[ObservableProperty]
public partial bool IsFirstRun { get; set; }
[ObservableProperty]
public partial bool IsNotMockMode { get; set; } = true;
public partial bool IsWelcomeTeachingTipOpen { get; set; }
private readonly IPlaybackService _playbackService;
[ObservableProperty]
public partial Visibility BottomCommandGridVisibility { get; set; } = Visibility.Visible;
public LyricsPageViewModel(
ISettingsService settingsService,
IPlaybackService playbackService
)
: base(settingsService)
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial LyricsDisplayType PreferredDisplayType { get; set; }
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; } = null;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int PositionOffset { get; set; } = 0;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsTranslationEnabled { get; set; } = false;
partial void OnIsTranslationEnabledChanged(bool value)
{
LyricsFontSize = _settingsService.LyricsFontSize;
CoverImageRadius = _settingsService.CoverImageRadius;
_playbackService = playbackService;
_playbackService.SongInfoChanged += async (_, args) =>
await UpdateSongInfoUI(args.SongInfo).ConfigureAwait(true);
IsFirstRun = _settingsService.IsFirstRun;
UpdateSongInfoUI(_playbackService.SongInfo).ConfigureAwait(true);
_settingsService.IsTranslationEnabled = value;
}
partial void OnCoverImageRadiusChanged(int value)
partial void OnPreferredDisplayTypeChanged(LyricsDisplayType value)
{
if (double.IsNaN(CoverImageGridActualHeight))
return;
CoverImageGridCornerRadius = new CornerRadius(
value / 100f * CoverImageGridActualHeight / 2
);
_settingsService.PreferredDisplayType = value;
}
partial void OnCoverImageGridActualHeightChanged(double value)
public void Receive(PropertyChangedMessage<bool> message)
{
if (double.IsNaN(value))
return;
CoverImageGridCornerRadius = new CornerRadius(CoverImageRadius / 100f * value / 2);
if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.IsDockMode))
{
SetNonStandardModePreferredDisplayType(message.NewValue);
TrySwitchToPreferredDisplayType(SongInfo);
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
SetNonStandardModePreferredDisplayType(message.NewValue);
TrySwitchToPreferredDisplayType(SongInfo);
}
}
}
partial void OnIsFirstRunChanged(bool value)
public void Receive(PropertyChangedMessage<int> message)
{
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
}
[RelayCommand]
private void OnDisplayTypeChanged(object value)
{
int index = Convert.ToInt32(value);
PreferredDisplayType = (LyricsDisplayType)index;
DisplayType = (LyricsDisplayType)index;
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
[RelayCommand]
private void OpenSettingsWindow()
{
WindowHelper.OpenSettingsWindow();
WindowHelper.OpenOrShowWindow<SettingsWindow>();
}
public async Task UpdateSongInfoUI(SongInfo? songInfo)
private void SetNonStandardModePreferredDisplayType(bool isEnabled)
{
AboutToUpdateUI = true;
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
SongInfo = songInfo;
CoverImage =
(songInfo?.AlbumArt == null)
? null
: await ImageHelper.GetBitmapImageFromBytesAsync(songInfo.AlbumArt);
TrySwitchToPreferredDisplayType(songInfo);
AboutToUpdateUI = false;
if (isEnabled)
{
_preferredDisplayTypeBeforeSwitchToNonStandardMode = PreferredDisplayType;
PreferredDisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
PreferredDisplayType = _preferredDisplayTypeBeforeSwitchToNonStandardMode ?? LyricsDisplayType.SplitView;
}
}
private void TrySwitchToPreferredDisplayType(SongInfo? songInfo)
@@ -165,68 +146,13 @@ namespace BetterLyrics.WinUI3.ViewModels
}
DisplayType = displayType;
}
public void OpenMatchedFileFolderInFileExplorer(string path)
partial void OnIsFirstRunChanged(bool value)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
}
);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsViewModel)
{
if (message.PropertyName == nameof(SettingsViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
}
}
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
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);
}
}
}
public void Receive(PropertyChangedMessage<LyricsStatus> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsStatus))
{
LyricsStatus = message.NewValue;
}
}
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
}
}
}

View File

@@ -0,0 +1,569 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.UI;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
// Blurred lyrics layer
using var blurredLyrics = new CanvasCommandList(control);
using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
{
DrawBlurredLyrics(control, blurredLyricsDs);
}
if (_lastAlbumArtSwBitmap != null && _lastAlbumArtCanvasBitmap == null)
{
_lastAlbumArtCanvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, _lastAlbumArtSwBitmap);
}
if (_albumArtSwBitmap != null && _albumArtCanvasBitmap == null)
{
_albumArtCanvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, _albumArtSwBitmap);
}
using var combined = new CanvasCommandList(control);
using var combinedDs = combined.CreateDrawingSession();
DrawAlbumArtBackground(control, combinedDs);
if (_isDockMode)
{
DrawImmersiveBackground(control, combinedDs);
}
combinedDs.DrawImage(blurredLyrics);
if (_isDesktopMode)
{
ds.DrawImage(blurredLyrics);
}
else
{
ds.DrawImage(combined);
}
DrawAlbumArt(control, ds);
DrawTitleAndArtist(control, ds);
if (_isDebugOverlayEnabled)
{
var currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(_playingLineIndex);
if (currentPlayingLine != null)
{
GetLinePlayingProgress(
currentPlayingLine,
out int charStartIndex,
out int charLength,
out float charProgress
);
//ds.DrawText(
// $"[DEBUG]\n" +
// $"Cur playing {_playingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n" +
// $"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
// $"Cur time {_totalTime + _positionOffset}\n" +
// $"Lang size {_multiLangLyrics.Count}\n" +
// $"Song duration {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}",
// new Vector2(10, 10),
// ThemeTypeSent == Microsoft.UI.Xaml.ElementTheme.Light ? Colors.Black : Colors.White
//);
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
LyricsLine? line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line != null)
{
ds.DrawText(
$"[{i}] {line.OriginalText} {line.HighlightOpacityTransition.Value}",
new Vector2(10, 30 + (i - _startVisibleLineIndex) * 20),
ThemeTypeSent == ElementTheme.Light ? Colors.Black : Colors.White
);
}
}
}
}
}
private void DrawBackgroundImgae(ICanvasAnimatedControl control, CanvasDrawingSession ds, CanvasBitmap canvasBitmap, float opacity)
{
float imageWidth = (float)canvasBitmap.Size.Width;
float imageHeight = (float)canvasBitmap.Size.Height;
float scaleFactor = MathF.Sqrt(MathF.Pow(_canvasWidth, 2) + MathF.Pow(_canvasHeight, 2)) / MathF.Min(imageWidth, imageHeight);
float x = _canvasWidth / 2 - imageWidth * scaleFactor / 2;
float y = _canvasHeight / 2 - imageHeight * scaleFactor / 2;
// Source: https://zhuanlan.zhihu.com/p/37178216
float bright = _lyricsBgBrightnessTransition.Value / 1f * 2f; // 明度参数范围在0.0f到2.0f之间
float whiteX = Math.Min(2 - bright, 1);
float whiteY = 1f;
float blackX = Math.Max(1 - bright, 0);
float blackY = 0f;
ds.DrawImage(new OpacityEffect
{
Source = new BrightnessEffect
{
Source = new ScaleEffect
{
Scale = new Vector2(scaleFactor),
Source = canvasBitmap,
},
WhitePoint = new Vector2(whiteX, whiteY),
BlackPoint = new Vector2(blackX, blackY),
},
Opacity = opacity,
}, new Vector2(x, y)
);
}
private void DrawForegroundImgae(ICanvasAnimatedControl control, CanvasDrawingSession ds, CanvasBitmap canvasBitmap, float opacity)
{
if (opacity == 0) return;
float imageWidth = (float)canvasBitmap.Size.Width;
float imageHeight = (float)canvasBitmap.Size.Height;
float scaleFactor = _albumArtSize / Math.Min(imageWidth, imageHeight);
if (scaleFactor < 0.01f) return;
float cornerRadius = _albumArtCornerRadius / 100f * _albumArtSize / 2;
using var cornerRadiusMask = new CanvasCommandList(control.Device);
using var cornerRadiusMaskDs = cornerRadiusMask.CreateDrawingSession();
cornerRadiusMaskDs.FillRoundedRectangle(
new Rect(0, 0, imageWidth * scaleFactor, imageHeight * scaleFactor),
cornerRadius, cornerRadius, Colors.White
);
ds.DrawImage(new OpacityEffect
{
Source = new AlphaMaskEffect
{
Source = new ScaleEffect
{
Scale = new Vector2(scaleFactor),
Source = canvasBitmap,
},
AlphaMask = cornerRadiusMask,
},
Opacity = opacity,
}, new Vector2(_albumArtXTransition.Value, _albumArtY)
);
}
private void DrawAlbumArtBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
using var overlappedCovers = new CanvasCommandList(control.Device);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
if (_lastAlbumArtCanvasBitmap != null)
{
DrawBackgroundImgae(control, overlappedCoversDs, _lastAlbumArtCanvasBitmap, 1 - _albumArtBgTransition.Value);
}
if (_albumArtCanvasBitmap != null)
{
DrawBackgroundImgae(control, overlappedCoversDs, _albumArtCanvasBitmap, _albumArtBgTransition.Value);
}
using var coverOverlayEffect = new OpacityEffect
{
Opacity = CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = CoverOverlayBlurAmount,
Source = overlappedCovers,
},
};
ds.DrawImage(coverOverlayEffect);
ds.Transform = Matrix3x2.Identity;
}
private void DrawAlbumArt(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
using var albumArt = new CanvasCommandList(control.Device);
using var albumArtDs = albumArt.CreateDrawingSession();
if (_albumArtCanvasBitmap != null)
{
DrawForegroundImgae(control, albumArtDs, _albumArtCanvasBitmap, _albumArtBgTransition.Value);
}
if (_lastAlbumArtCanvasBitmap != null)
{
DrawForegroundImgae(control, albumArtDs, _lastAlbumArtCanvasBitmap, 1 - _albumArtBgTransition.Value);
}
using var opacity = new CanvasCommandList(control.Device);
using var opacityDs = opacity.CreateDrawingSession();
opacityDs.DrawImage(new GaussianBlurEffect
{
Source = albumArt,
BlurAmount = 12f,
Optimization = EffectOptimization.Quality,
});
opacityDs.DrawImage(albumArt);
ds.DrawImage(new OpacityEffect
{
Source = opacity,
Opacity = _albumArtOpacityTransition.Value
});
}
private void DrawTitleAndArtist(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_lastSongTitle != null || _lastSongArtist != null)
{
DrawSingleTitleAndArtist(control, ds, _lastSongTitle, _lastSongArtist, 1 - _songInfoOpacityTransition.Value);
}
if (_songTitle != null || _songArtist != null)
{
DrawSingleTitleAndArtist(control, ds, _songTitle, _songArtist, _songInfoOpacityTransition.Value);
}
}
private void DrawSingleTitleAndArtist(ICanvasAnimatedControl control, CanvasDrawingSession ds, string? title, string? artist, float opacity)
{
CanvasTextLayout titleLayout = new(
control, title ?? string.Empty,
_titleTextFormat, _albumArtSize, _canvasHeight
);
CanvasTextLayout artistLayout = new(
control, artist ?? string.Empty,
_artistTextFormat, _albumArtSize, _canvasHeight
);
ds.DrawTextLayout(
titleLayout,
new Vector2(_albumArtXTransition.Value, _titleY),
_bgFontColor.WithAlpha((byte)(_albumArtOpacityTransition.Value * 255 * opacity)));
ds.DrawTextLayout(
artistLayout,
new Vector2(_albumArtXTransition.Value, _titleY + (float)titleLayout.LayoutBounds.Height),
_bgFontColor.WithAlpha((byte)(_albumArtOpacityTransition.Value * 128 * opacity)));
}
private void DrawBlurredLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(_playingLineIndex);
if (currentPlayingLine == null)
{
return;
}
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 TextAlignmentType.Left:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case TextAlignmentType.Center:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += _maxLyricsWidth / 2;
break;
case TextAlignmentType.Right:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += _maxLyricsWidth;
break;
default:
break;
}
// 组合变换:缩放 -> 旋转 -> 平移
ds.Transform =
Matrix3x2.CreateScale(line.ScaleTransition.Value, new Vector2(centerX, centerY))
* Matrix3x2.CreateRotation(line.AngleTransition.Value, currentPlayingLine.Position)
* Matrix3x2.CreateTranslation(_lyricsXTransition.Value, _canvasYScrollTransition.Value + _canvasHeight / 2);
// Create the background lyrics line with stroke and fill
using var bgLyrics = new CanvasCommandList(control.Device);
using var bgLyricsDs = bgLyrics.CreateDrawingSession();
// Create the foreground lyrics line with stroke and fill
using var fgLyrics = new CanvasCommandList(control.Device);
using var fgLyricsDs = fgLyrics.CreateDrawingSession();
// 创建文字几何体
using (var textGeometry = CanvasGeometry.CreateText(textLayout))
{
if (_isDesktopMode)
{
bgLyricsDs.DrawGeometry(textGeometry, position, _strokeFontColor, _lyricsFontStrokeWidth); // 背景描边
fgLyricsDs.DrawGeometry(textGeometry, position, _strokeFontColor, _lyricsFontStrokeWidth); // 前景描边
}
bgLyricsDs.FillGeometry(textGeometry, position, _bgFontColor); // 背景填充
fgLyricsDs.FillGeometry(textGeometry, position, _fgFontColor); // 前景填充
}
// Mock gradient blurred lyrics layer
// 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层(背景歌词层次)
// Current line will not be blurred
ds.DrawImage(
new GaussianBlurEffect
{
Source = new OpacityEffect { Source = bgLyrics, Opacity = line.OpacityTransition.Value * _lyricsOpacityTransition.Value },
BlurAmount = line.BlurAmountTransition.Value,
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
}
);
if (line.HighlightOpacityTransition.Value != 0)
{
// 再叠加高亮行歌词层(前景歌词层)
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 == _playingLineIndex)
{
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
{
float height = 0f;
var regions = textLayout.GetCharacterRegions(0, string.Join("", line.CharTimings.Select(x => x.Text)).Length);
if (regions.Length > 0)
{
height = (float)regions[^1].LayoutBounds.Bottom - (float)regions[0].LayoutBounds.Top;
}
maskDs.FillRectangle(
new Rect(
textLayout.LayoutBounds.X,
position.Y,
textLayout.LayoutBounds.Width,
height
),
Colors.White
);
}
ds.DrawImage(
new OpacityEffect
{
Source = new BlendEffect
{
Background = IsLyricsGlowEffectEnabled
? new GaussianBlurEffect
{
Source = new AlphaMaskEffect
{
Source = fgLyrics,
AlphaMask = LyricsGlowEffectScope switch
{
LineRenderingType.UntilCurrentChar => mask,
LineRenderingType.CurrentCharOnly => highlightMask,
_ => mask,
},
},
BlurAmount = _lyricsGlowEffectAmount,
Optimization = EffectOptimization.Quality,
}
: new CanvasCommandList(control.Device),
Foreground = new AlphaMaskEffect
{
Source = fgLyrics,
AlphaMask = mask,
},
},
Opacity = line.HighlightOpacityTransition.Value * _lyricsOpacityTransition.Value,
}
);
}
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
private void DrawImmersiveBackground(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
bool withGradient = true
)
{
ds.FillRectangle(
new Rect(0, 0, _canvasWidth, _canvasHeight),
new CanvasLinearGradientBrush(
control,
[
new CanvasGradientStop
{
Position = 0f,
Color = withGradient
? Color.FromArgb(
211,
_immersiveBgTransition.Value.R,
_immersiveBgTransition.Value.G,
_immersiveBgTransition.Value.B
)
: _immersiveBgTransition.Value,
},
new CanvasGradientStop
{
Position = 1,
Color = _immersiveBgTransition.Value,
},
]
)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, _canvasHeight),
}
);
}
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

@@ -0,0 +1,284 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using System;
using System.Collections.ObjectModel;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
: IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<float>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<LyricsFontColorType>>,
IRecipient<PropertyChangedMessage<TextAlignmentType>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<LineRenderingType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
{
public void Receive(PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalLyricsFolders))
{
// Music lib changed, re-fetch lyrics
_logger.LogInformation("Local lyrics folders changed, refreshing lyrics.");
RefreshLyricsAsync();
}
}
}
public void Receive(PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsSearchProvidersInfo))
{
// Lyrics search providers info changed, re-fetch lyrics
_logger.LogInformation("Lyrics search providers info changed, refreshing lyrics.");
RefreshLyricsAsync();
}
}
}
// Receive methods for handling messages from other view models
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.IsDynamicCoverOverlayEnabled))
{
IsDynamicCoverOverlayEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsDebugOverlayEnabled))
{
_isDebugOverlayEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsLyricsGlowEffectEnabled))
{
IsLyricsGlowEffectEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsFanLyricsEnabled))
{
_isFanLyricsEnabled = message.NewValue;
_isLayoutChanged = true;
}
}
else if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.IsDockMode))
{
_isDockMode = message.NewValue;
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
_isDesktopMode = message.NewValue;
}
}
else if (message.Sender is LyricsPageViewModel)
{
if (message.PropertyName == nameof(LyricsPageViewModel.IsTranslationEnabled))
{
_isTranslationEnabled = message.NewValue;
_logger.LogInformation("Translation enabled state changed: {IsEnabled}", _isTranslationEnabled);
UpdateTranslationsAsync();
}
}
}
public void Receive(PropertyChangedMessage<Color> message)
{
if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.ActivatedWindowAccentColor))
{
_immersiveBgTransition.StartTransition(message.NewValue);
_environmentalColor = message.NewValue;
UpdateFontColor();
}
}
else if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsCustomBgFontColor))
{
_customBgFontColor = message.NewValue;
UpdateFontColor();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsCustomFgFontColor))
{
_customFgFontColor = message.NewValue;
UpdateFontColor();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsCustomStrokeFontColor))
{
_customStrokeFontColor = message.NewValue;
UpdateFontColor();
}
}
}
public void Receive(PropertyChangedMessage<float> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsLineSpacingFactor))
{
LyricsLineSpacingFactor = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.CoverImageRadius))
{
_albumArtCornerRadius = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayOpacity))
{
CoverOverlayOpacity = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayBlurAmount))
{
CoverOverlayBlurAmount = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsVerticalEdgeOpacity))
{
LyricsVerticalEdgeOpacity = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBlurAmount))
{
LyricsBlurAmount = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SelectedTargetLanguageIndex))
{
_targetLanguageIndex = message.NewValue;
_logger.LogInformation("Target language index changed: {Index}", _targetLanguageIndex);
UpdateTranslationsAsync();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontStrokeWidth))
{
_lyricsFontStrokeWidth = message.NewValue;
}
}
else if (message.Sender is LyricsPageViewModel)
{
if (message.PropertyName == nameof(LyricsPageViewModel.PositionOffset))
{
_positionOffset = TimeSpan.FromMilliseconds(message.NewValue);
}
}
}
public void Receive(PropertyChangedMessage<LineRenderingType> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsGlowEffectScope))
{
LyricsGlowEffectScope = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<TextAlignmentType> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsAlignmentType))
{
LyricsAlignmentType = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SongInfoAlignmentType))
{
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment =
message.NewValue.ToCanvasHorizontalAlignment();
}
}
}
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
_displayTypeReceived = message.NewValue;
}
public void Receive(PropertyChangedMessage<LyricsFontColorType> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBgFontColorType))
{
_lyricsBgFontColorType = message.NewValue;
UpdateFontColor();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFgFontColorType))
{
_lyricsFgFontColorType = message.NewValue;
UpdateFontColor();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsStrokeFontColorType))
{
_lyricsStrokeFontColorType = message.NewValue;
UpdateFontColor();
}
}
}
public void Receive(PropertyChangedMessage<LyricsFontWeight> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontWeight))
{
LyricsFontWeight = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBackgroundTheme))
{
_lyricsBgTheme = message.NewValue;
UpdateFontColor();
}
}
}
partial void OnLyricsFontSizeChanged(int value)
{
_isLayoutChanged = true;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_lyricsTextFormat.FontWeight = value.ToFontWeight();
_isLayoutChanged = true;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_isLayoutChanged = true;
}
}
}

View File

@@ -0,0 +1,61 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
private readonly ValueTransition<float> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<Color> _immersiveBgTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<float> _lyricsXTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<float> _lyricsOpacityTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<float> _albumArtBgTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<float> _albumArtOpacityTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<float> _albumArtXTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<float> _songInfoOpacityTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<float> _lyricsBgBrightnessTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
}
}

View File

@@ -0,0 +1,401 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
private bool _isCanvasWidthChanged = false;
private bool _isCanvasHeightChanged = false;
private bool _isDisplayTypeChanged = false;
private bool _isPlayingLineChanged = false;
private bool _isVisibleLinesBoundaryChanged = false;
public void Update(ICanvasAnimatedControl control, CanvasAnimatedUpdateEventArgs args)
{
_elapsedTime = args.Timing.ElapsedTime;
if (_isPlaying)
{
_totalTime += _elapsedTime;
}
var playingLineIndex = GetCurrentPlayingLineIndex();
_isCanvasWidthChanged = _canvasWidth != control.Size.Width;
_isCanvasHeightChanged = _canvasHeight != control.Size.Height;
_isDisplayTypeChanged = _displayType != _displayTypeReceived;
_isPlayingLineChanged = _playingLineIndex != playingLineIndex;
_canvasWidth = (float)control.Size.Width;
_canvasHeight = (float)control.Size.Height;
_displayType = _displayTypeReceived;
_playingLineIndex = playingLineIndex;
_immersiveBgTransition.Update(_elapsedTime);
_albumArtBgTransition.Update(_elapsedTime);
_lyricsBgBrightnessTransition.Update(_elapsedTime);
_songInfoOpacityTransition.Update(_elapsedTime);
if (IsDynamicCoverOverlayEnabled)
{
_rotateAngle += _coverRotateSpeed;
_rotateAngle %= MathF.PI * 2;
}
if (_isCanvasHeightChanged)
{
_albumArtY = 36 + (_canvasHeight - 36 * 2) * 3 / 16f;
}
if (_isCanvasWidthChanged || _isCanvasHeightChanged)
{
_albumArtSize = MathF.Min(
(_canvasHeight - _topMargin - _bottomMargin) * 8.5f / 16,
(_canvasWidth - _leftMargin - _middleMargin - _rightMargin) / 2);
_albumArtSize = MathF.Max(0, _albumArtSize);
_titleY = _albumArtY + _albumArtSize * 1.05f;
}
if (_isDisplayTypeChanged || _isCanvasWidthChanged)
{
bool jumpTo = !_isDisplayTypeChanged && _isCanvasWidthChanged;
switch (_displayType)
{
case LyricsDisplayType.AlbumArtOnly:
_lyricsOpacityTransition.StartTransition(0f, jumpTo);
_albumArtOpacityTransition.StartTransition(1f, jumpTo);
_albumArtXTransition.StartTransition(_canvasWidth / 2 - _albumArtSize / 2, jumpTo);
break;
case LyricsDisplayType.LyricsOnly:
_lyricsOpacityTransition.StartTransition(1f, jumpTo);
_albumArtOpacityTransition.StartTransition(0f, jumpTo);
_lyricsXTransition.StartTransition(_leftMargin, jumpTo);
break;
case LyricsDisplayType.SplitView:
_lyricsOpacityTransition.StartTransition(1f, jumpTo);
_albumArtOpacityTransition.StartTransition(1f, jumpTo);
_lyricsXTransition.StartTransition((_canvasWidth - _leftMargin - _middleMargin - _rightMargin) / 2 + _leftMargin + _middleMargin, jumpTo);
_albumArtXTransition.StartTransition(_leftMargin + ((_canvasWidth - _leftMargin - _middleMargin - _rightMargin) / 2 - _albumArtSize) / 2, jumpTo);
break;
case LyricsDisplayType.PlaceholderOnly:
break;
default:
break;
}
}
_lyricsXTransition.Update(_elapsedTime);
_albumArtXTransition.Update(_elapsedTime);
_lyricsOpacityTransition.Update(_elapsedTime);
_albumArtOpacityTransition.Update(_elapsedTime);
if (_isCanvasWidthChanged || _lyricsXTransition.IsTransitioning)
{
_maxLyricsWidth = _canvasWidth - _lyricsXTransition.Value - _rightMargin;
_maxLyricsWidth = Math.Max(_maxLyricsWidth, 0);
_isLayoutChanged = true;
}
if (_isLayoutChanged)
{
ReLayout(control);
UpdateCanvasYScrollOffset(control, true, false);
}
else
{
UpdateCanvasYScrollOffset(control, false, true);
}
UpdateLinesProps();
_isLayoutChanged = false;
}
private void ReLayout(ICanvasAnimatedControl control)
{
if (control == null)
return;
_lyricsTextFormat.FontSize = LyricsFontSize;
float y = 0;
// 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.DisplayedText,
_lyricsTextFormat,
_maxLyricsWidth,
_canvasHeight
);
line.Position = new Vector2(0, y);
y +=
(float)line.CanvasTextLayout.LayoutBounds.Height
/ line.CanvasTextLayout.LineCount
* (line.CanvasTextLayout.LineCount + LyricsLineSpacingFactor);
}
}
private void UpdateCanvasYScrollOffset(ICanvasAnimatedControl control, bool forceScroll, bool withAnimation)
{
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0) return;
// Set _scrollOffsetY
if ((!_isPlayingLineChanged && forceScroll) || _isPlayingLineChanged)
{
LyricsLine? currentPlayingLine = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(_playingLineIndex);
if (currentPlayingLine == null) return;
var playingTextLayout = currentPlayingLine?.CanvasTextLayout;
if (playingTextLayout == null) return;
float? targetYScrollOffset = (float?)(-currentPlayingLine!.Position.Y + _multiLangLyrics.SafeGet(_langIndex)?[0].Position.Y - playingTextLayout.LayoutBounds.Height / 2);
if (!targetYScrollOffset.HasValue) return;
_canvasYScrollTransition.StartTransition(targetYScrollOffset.Value, !withAnimation);
}
_canvasYScrollTransition.Update(_elapsedTime);
int startVisibleLineIndex = -1;
int 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
+ _canvasHeight / 2
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= 0
)
{
if (startVisibleLineIndex == -1)
{
startVisibleLineIndex = i;
}
}
if (
_canvasYScrollTransition.Value
+ _canvasHeight / 2
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (endVisibleLineIndex == -1)
{
endVisibleLineIndex = i;
}
}
}
if (startVisibleLineIndex != -1 && endVisibleLineIndex == -1)
{
endVisibleLineIndex = endLineIndex;
}
_isVisibleLinesBoundaryChanged = _startVisibleLineIndex != startVisibleLineIndex || _endVisibleLineIndex != endVisibleLineIndex;
_startVisibleLineIndex = startVisibleLineIndex;
_endVisibleLineIndex = endVisibleLineIndex;
}
private void UpdateFontColor()
{
if (_isDesktopMode || _isDockMode)
{
ThemeTypeSent = Helper.ColorHelper.GetElementThemeFromBackgroundColor(_environmentalColor);
}
else
{
ThemeTypeSent = _lyricsBgTheme;
}
float brightness = 0f;
Color grayedEnvironmentalColor = Colors.Transparent;
switch (ThemeTypeSent)
{
case ElementTheme.Default:
switch (Application.Current.RequestedTheme)
{
case ApplicationTheme.Light:
_adaptiveGrayedFontColor = _darkColor;
brightness = 0.7f;
break;
case ApplicationTheme.Dark:
_adaptiveGrayedFontColor = _lightColor;
brightness = 0.3f;
break;
default:
break;
}
break;
case ElementTheme.Light:
_adaptiveGrayedFontColor = _darkColor;
brightness = 0.7f;
break;
case ElementTheme.Dark:
_adaptiveGrayedFontColor = _lightColor;
brightness = 0.3f;
break;
default:
break;
}
if (_adaptiveGrayedFontColor == _lightColor)
{
grayedEnvironmentalColor = _darkColor;
} else if (_adaptiveGrayedFontColor == _darkColor)
{
grayedEnvironmentalColor = _lightColor;
}
_lyricsBgBrightnessTransition.StartTransition(brightness);
if (_isDesktopMode || _isDockMode)
{
_adaptiveColoredFontColor = Helper.ColorHelper.GetForegroundColor(_environmentalColor);
}
else
{
_adaptiveColoredFontColor = Helper.ColorHelper.GetForegroundColor(_albumArtAccentColor?.WithBrightness(brightness) ?? Colors.Transparent);
}
switch (_lyricsBgFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
_bgFontColor = _adaptiveGrayedFontColor;
break;
case LyricsFontColorType.AdaptiveColored:
_bgFontColor = _adaptiveColoredFontColor ?? _adaptiveGrayedFontColor;
break;
case LyricsFontColorType.Custom:
_bgFontColor = _customBgFontColor ?? _adaptiveGrayedFontColor;
break;
default:
break;
}
switch (_lyricsFgFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
_fgFontColor = _adaptiveGrayedFontColor;
break;
case LyricsFontColorType.AdaptiveColored:
_fgFontColor = _adaptiveColoredFontColor ?? _adaptiveGrayedFontColor;
break;
case LyricsFontColorType.Custom:
_fgFontColor = _customFgFontColor ?? _adaptiveGrayedFontColor;
break;
default:
break;
}
switch (_lyricsStrokeFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
_strokeFontColor = grayedEnvironmentalColor.WithBrightness(0.7);
break;
case LyricsFontColorType.AdaptiveColored:
_strokeFontColor = _environmentalColor.WithBrightness(0.7);
break;
case LyricsFontColorType.Custom:
_strokeFontColor = _customStrokeFontColor ?? _environmentalColor;
break;
default:
break;
}
}
private void UpdateLinesProps()
{
var currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(_playingLineIndex);
if (currentPlayingLine == null) return;
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null) continue;
if (_isLayoutChanged || _isVisibleLinesBoundaryChanged || _isPlayingLineChanged)
{
float distanceFromPlayingLine = Math.Abs(line.Position.Y - currentPlayingLine.Position.Y);
float distanceFactor = Math.Clamp(distanceFromPlayingLine / (_canvasHeight / 2), 0, 1);
line.AngleTransition.StartTransition(_isFanLyricsEnabled
? (float)Math.PI
* (30f / 180f)
* distanceFactor
* (i > _playingLineIndex ? 1 : -1)
: 0
);
line.BlurAmountTransition.StartTransition(LyricsBlurAmount * distanceFactor);
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale));
line.OpacityTransition.StartTransition(_defaultOpacity - distanceFactor * _defaultOpacity * (1 - LyricsVerticalEdgeOpacity / 100f));
line.HighlightOpacityTransition.StartTransition(i == _playingLineIndex ? 1f : 0f);
}
line.AngleTransition.Update(_elapsedTime);
line.ScaleTransition.Update(_elapsedTime);
line.BlurAmountTransition.Update(_elapsedTime);
line.OpacityTransition.Update(_elapsedTime);
line.HighlightOpacityTransition.Update(_elapsedTime);
}
}
}
}

View File

@@ -1,107 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterInAppLyrics.WinUI3.ViewModels
{
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; }
public LyricsSettingsControlViewModel(ISettingsService settingsService)
: base(settingsService)
{
IsActive = true;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
LyricsFontColorType = _settingsService.LyricsFontColorType;
}
partial void OnLyricsAlignmentTypeChanged(LyricsAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
partial void OnIsLyricsGlowEffectEnabledChanged(bool value)
{
_settingsService.IsLyricsGlowEffectEnabled = value;
}
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsFontColorType = value;
}
partial void OnLyricsGlowEffectScopeChanged(LyricsGlowEffectScope value)
{
_settingsService?.LyricsGlowEffectScope = value;
}
}
}

View File

@@ -0,0 +1,199 @@
// 2025/6/23 by Zhe Fang
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.UI;
using WinRT.Interop;
namespace BetterLyrics.WinUI3
{
public partial class LyricsWindowViewModel
: BaseWindowViewModel,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<bool>>
{
private ForegroundWindowWatcherHelper? _watcherHelper = null;
public LyricsWindowViewModel(ISettingsService settingsService) : base(settingsService)
{
_ignoreFullscreenWindow = _settingsService.IgnoreFullscreenWindow;
}
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color ActivatedWindowAccentColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDesktopMode { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDockMode { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsWindowLocked { get; set; } = false;
[ObservableProperty]
public partial Notification Notification { get; set; } = new();
[ObservableProperty]
public partial bool ShowInfoBar { get; set; } = false;
[ObservableProperty]
public partial ElementTheme ThemeType { get; set; } = ElementTheme.Default;
[ObservableProperty]
public partial double TitleBarFontSize { get; set; } = 11;
[ObservableProperty]
public partial double TitleBarHeight { get; set; } = 36;
private bool _ignoreFullscreenWindow = false;
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is SystemTrayViewModel)
{
if (message.PropertyName == nameof(SystemTrayViewModel.IsLyricsWindowLocked))
{
if (IsLyricsWindowLocked != message.NewValue)
{
IsLyricsWindowLocked = message.NewValue;
}
}
}
else if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.IgnoreFullscreenWindow))
{
_ignoreFullscreenWindow = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.ThemeTypeSent))
{
ThemeType = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
if (IsDockMode)
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
DockModeHelper.UpdateAppBarHeight(WindowNative.GetWindowHandle(window), message.NewValue * 4);
}
}
}
}
public void StartWatchWindowColorChange(WindowColorSampleMode mode)
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
_watcherHelper = new ForegroundWindowWatcherHelper(
hwnd,
onWindowChanged =>
{
if (_ignoreFullscreenWindow && window.AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsAlwaysOnTop = true;
}
UpdateAccentColor(hwnd, mode);
}
);
_watcherHelper.Start();
UpdateAccentColor(hwnd, mode);
}
public void UpdateAccentColor(nint hwnd, WindowColorSampleMode mode)
{
ActivatedWindowAccentColor = WindowColorHelper.GetDominantColor(hwnd, mode).ToColor();
}
[RelayCommand]
private void LockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
DesktopModeHelper.Lock(window);
IsLyricsWindowLocked = true;
}
private void StopWatchWindowColorChange()
{
_watcherHelper?.Stop();
_watcherHelper = null;
}
[RelayCommand]
private void ToggleDesktopMode()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
StopWatchWindowColorChange();
IsDesktopMode = !IsDesktopMode;
if (IsDesktopMode)
{
StartWatchWindowColorChange(WindowColorSampleMode.WindowEdge);
DesktopModeHelper.Enable(window);
}
else
{
DesktopModeHelper.Disable(window);
}
}
[RelayCommand]
private void ToggleDockMode()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
StopWatchWindowColorChange();
IsDockMode = !IsDockMode;
if (IsDockMode)
{
StartWatchWindowColorChange(WindowColorSampleMode.BelowWindow);
DockModeHelper.Enable(window, _settingsService.LyricsFontSize * 4);
}
else
{
DockModeHelper.Disable(window);
}
}
}
}

View File

@@ -0,0 +1,591 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using ShadowViewer.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Globalization;
using Windows.Media.Playback;
using Windows.System;
using Windows.UI;
using Windows.UI.Popups;
using WinRT.Interop;
using AppInfo = BetterLyrics.WinUI3.Helper.AppInfo;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsPageViewModel : BaseViewModel
{
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
private readonly ILibreTranslateService _libreTranslateService;
private readonly string _autoStartupTaskId = "AutoStartup";
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, IPlaybackService playbackService, ILibreTranslateService libreTranslateService) : base(settingsService)
{
_libWatcherService = libWatcherService;
_playbackService = playbackService;
_libreTranslateService = libreTranslateService;
LibreTranslateServer = _settingsService.LibreTranslateServer;
SelectedTargetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
AutoStartWindowType = _settingsService.AutoStartWindowType;
AutoLockOnDesktopMode = _settingsService.AutoLockOnDesktopMode;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
SongInfoAlignmentType = _settingsService.SongInfoAlignmentType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
IsFanLyricsEnabled = _settingsService.IsFanLyricsEnabled;
LyricsBgFontColorType = _settingsService.LyricsBgFontColorType;
LyricsFgFontColorType = _settingsService.LyricsFgFontColorType;
LyricsStrokeFontColorType = _settingsService.LyricsStrokeFontColorType;
LyricsCustomBgFontColor = _settingsService.LyricsCustomBgFontColor;
LyricsCustomFgFontColor = _settingsService.LyricsCustomFgFontColor;
LyricsCustomStrokeFontColor = _settingsService.LyricsCustomStrokeFontColor;
LyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
LyricsBackgroundTheme = _settingsService.LyricsBackgroundTheme;
MediaSourceProvidersInfo = [.. _settingsService.MediaSourceProvidersInfo];
IgnoreFullscreenWindow = _settingsService.IgnoreFullscreenWindow;
_playbackService.MediaSourceProvidersInfoChanged += PlaybackService_SessionIdsChanged;
Task.Run(async () =>
{
BuildDate = (await Helper.AppInfo.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
});
}
private void PlaybackService_SessionIdsChanged(object? sender, Events.MediaSourceProvidersInfoEventArgs e)
{
MediaSourceProvidersInfo = [.. e.MediaSourceProviersInfo];
}
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme LyricsBackgroundTheme { get; set; }
[ObservableProperty]
public partial AutoStartWindowType AutoStartWindowType { get; set; }
[ObservableProperty]
public partial bool AutoLockOnDesktopMode { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDebugOverlayEnabled { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLogEnabled { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDynamicCoverOverlayEnabled { get; set; }
[ObservableProperty]
public partial Enums.Language Language { get; set; }
[ObservableProperty]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty]
public partial ObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsFanLyricsEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsGlowEffectEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TextAlignmentType LyricsAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TextAlignmentType SongInfoAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomBgFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomFgFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomStrokeFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsBgFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsFgFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsStrokeFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LineRenderingType LyricsGlowEffectScope { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial float LyricsLineSpacingFactor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsVerticalEdgeOpacity { get; set; }
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; }
public string Version { get; set; } = Helper.AppInfo.AppVersion;
public string BuildDate { get; set; } = string.Empty;
[ObservableProperty]
public partial string LibreTranslateServer { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int SelectedTargetLanguageIndex { get; set; } = 0;
[ObservableProperty]
public partial bool IsLibreTranslateServerTesting { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontStrokeWidth { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IgnoreFullscreenWindow { get; set; }
partial void OnLyricsBackgroundThemeChanged(ElementTheme value)
{
_settingsService.LyricsBackgroundTheme = value;
}
partial void OnLyricsFontStrokeWidthChanged(int value)
{
_settingsService.LyricsFontStrokeWidth = value;
}
public void OnLyricsSearchProvidersReordered()
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
public void OpenMusicFolder(LocalLyricsFolder folder)
{
OpenFolderInFileExplorer(folder.Path);
}
public void RemoveFolderAsync(LocalLyricsFolder folder)
{
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
{
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
public void ToggleMediaSourceProvider(MediaSourceProviderInfo providerInfo)
{
Broadcast(
MediaSourceProvidersInfo,
MediaSourceProvidersInfo,
nameof(MediaSourceProvidersInfo)
);
}
private void AddFolderAsync(string path)
{
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (LocalLyricsFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"));
}
else if (LocalLyricsFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
{
// 添加的文件夹是现有文件夹的子文件夹
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo"));
}
else if (LocalLyricsFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
)
{
// 添加的文件夹是现有文件夹的父文件夹
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathIncludingOthersInfo"));
}
else
{
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(Helper.AppInfo.GithubUrl));
}
[RelayCommand]
private void OpenCacheFolder()
{
OpenFolderInFileExplorer(Helper.AppInfo.CacheFolder);
}
private void OpenFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = path,
UseShellExecute = true,
}
);
}
[RelayCommand]
private void PlayTestingMusicTask()
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
}
[RelayCommand]
private void RestartApp()
{
WindowHelper.RestartApp();
}
[RelayCommand]
private async Task SelectAndAddFolderAsync(UIElement sender)
{
var window = WindowHelper.GetWindowByWindowType<SettingsWindow>();
if (window == null) return;
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.FileTypeFilter.Add("*");
var hwnd = WindowNative.GetWindowHandle(window);
InitializeWithWindow.Initialize(picker, hwnd);
var folder = await picker.PickSingleFolderAsync();
if (folder != null)
{
AddFolderAsync(folder.Path);
}
}
[RelayCommand]
private void LibreTranslateServerTest()
{
IsLibreTranslateServerTesting = true;
Task.Run(async () =>
{
try
{
string result = await _libreTranslateService.TranslateAsync("Hello, world!", null);
_dispatcherQueue.TryEnqueue(() =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageLibreTranslateTestSuccessInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Success);
IsLibreTranslateServerTesting = false;
});
}
catch (Exception)
{
_dispatcherQueue.TryEnqueue(() =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageLibreTranslateTestFailedInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
IsLibreTranslateServerTesting = false;
});
}
});
}
public async Task<bool> ToggleAutoStartupAsync(bool target)
{
StartupTask startupTask = await StartupTask.GetAsync(_autoStartupTaskId);
if (target)
{
await startupTask.RequestEnableAsync();
}
else
{
startupTask.Disable();
}
return await DetectIsAutoStartupEnabledAsync();
}
public async Task<bool> DetectIsAutoStartupEnabledAsync()
{
bool result = false;
var startupTask = await StartupTask.GetAsync(_autoStartupTaskId);
switch (startupTask.State)
{
case StartupTaskState.Disabled:
case StartupTaskState.DisabledByUser:
case StartupTaskState.DisabledByPolicy:
result = false;
break;
case StartupTaskState.Enabled:
result = true;
break;
}
return result;
}
partial void OnIgnoreFullscreenWindowChanged(bool value)
{
_settingsService.IgnoreFullscreenWindow = value;
}
partial void OnSelectedTargetLanguageIndexChanged(int value)
{
_settingsService.SelectedTargetLanguageIndex = value;
}
partial void OnLibreTranslateServerChanged(string value)
{
_settingsService.LibreTranslateServer = value;
}
partial void OnAutoStartWindowTypeChanged(AutoStartWindowType value)
{
_settingsService.AutoStartWindowType = value;
}
partial void OnAutoLockOnDesktopModeChanged(bool value)
{
_settingsService.AutoLockOnDesktopMode = value;
}
partial void OnCoverImageRadiusChanged(int value)
{
_settingsService.CoverImageRadius = value;
}
partial void OnCoverOverlayBlurAmountChanged(int value)
{
_settingsService.CoverOverlayBlurAmount = value;
}
partial void OnCoverOverlayOpacityChanged(int value)
{
_settingsService.CoverOverlayOpacity = value;
}
partial void OnIsDynamicCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsDynamicCoverOverlayEnabled = value;
}
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;
}
partial void OnIsFanLyricsEnabledChanged(bool value)
{
_settingsService.IsFanLyricsEnabled = value;
}
partial void OnIsLyricsGlowEffectEnabledChanged(bool value)
{
_settingsService.IsLyricsGlowEffectEnabled = value;
}
partial void OnLyricsAlignmentTypeChanged(TextAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
partial void OnSongInfoAlignmentTypeChanged(TextAlignmentType value)
{
_settingsService.SongInfoAlignmentType = value;
}
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
partial void OnLyricsCustomBgFontColorChanged(Color value)
{
_settingsService.LyricsCustomBgFontColor = value;
}
partial void OnLyricsCustomFgFontColorChanged(Color value)
{
_settingsService.LyricsCustomFgFontColor = value;
}
partial void OnLyricsCustomStrokeFontColorChanged(Color value)
{
_settingsService.LyricsCustomStrokeFontColor = value;
}
partial void OnLyricsBgFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsBgFontColorType = value;
}
partial void OnLyricsFgFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsFgFontColorType = value;
}
partial void OnLyricsStrokeFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsStrokeFontColorType = value;
}
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
partial void OnLyricsGlowEffectScopeChanged(LineRenderingType value)
{
_settingsService.LyricsGlowEffectScope = value;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
}
}

View File

@@ -1,370 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel.Core;
using Windows.Globalization;
using Windows.Media;
using Windows.Media.Playback;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsViewModel : ObservableRecipient
{
[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]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { 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; }
[ObservableProperty]
public partial Enums.Language Language { get; set; }
public string Version { get; set; } = AppInfo.AppVersion;
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "LyricsLib";
[ObservableProperty]
public partial Thickness RootGridMargin { get; set; } = new(0, 0, 0, 0);
private readonly MediaPlayer _mediaPlayer = new();
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
public SettingsViewModel(
ISettingsService settingsService,
ILibWatcherService libWatcherService,
IPlaybackService playbackService
)
{
_settingsService = settingsService;
_libWatcherService = libWatcherService;
_playbackService = playbackService;
RootGridMargin = new Thickness(0, _settingsService.TitleBarType.GetHeight(), 0, 0);
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
ThemeType = _settingsService.ThemeType;
BackdropType = _settingsService.BackdropType;
TitleBarType = _settingsService.TitleBarType;
AutoStartWindowType = _settingsService.AutoStartWindowType;
IsCoverOverlayEnabled = _settingsService.IsCoverOverlayEnabled;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
}
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;
}
partial void OnThemeTypeChanged(ElementTheme value)
{
_settingsService.ThemeType = value;
}
partial void OnBackdropTypeChanged(BackdropType value)
{
_settingsService.BackdropType = value;
}
partial void OnTitleBarTypeChanged(TitleBarType value)
{
_settingsService.TitleBarType = value;
RootGridMargin = new Thickness(0, value.GetHeight(), 0, 0);
}
partial void OnAutoStartWindowTypeChanged(AutoStartWindowType value)
{
_settingsService.AutoStartWindowType = value;
}
partial void OnCoverImageRadiusChanged(int value)
{
_settingsService.CoverImageRadius = value;
}
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;
}
public void RemoveFolderAsync(LocalLyricsFolder folder)
{
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
public void OnLyricsSearchProvidersReordered()
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
[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)
{
AddFolderAsync(folder.Path);
}
}
private void AddFolderAsync(string path)
{
var normalizedPath =
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
if (
LocalLyricsFolders.Any(x =>
Path.GetFullPath(x.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.Equals(
normalizedPath.TrimEnd(Path.DirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase
)
)
)
{
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
)
)
);
}
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
{
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(AppInfo.GithubUrl));
}
private void OpenFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = path,
UseShellExecute = true,
}
);
}
public void OpenMusicFolder(LocalLyricsFolder folder)
{
OpenFolderInFileExplorer(folder.Path);
}
[RelayCommand]
private void RestartApp()
{
// The restart will be executed immediately.
AppRestartFailureReason failureReason =
Microsoft.Windows.AppLifecycle.AppInstance.Restart("");
// If the restart fails, handle it here.
switch (failureReason)
{
case AppRestartFailureReason.RestartPending:
break;
case AppRestartFailureReason.NotInForeground:
break;
case AppRestartFailureReason.InvalidUser:
break;
default: //AppRestartFailureReason.Other
break;
}
}
[RelayCommand]
private void PlayTestingMusicTask()
{
AddFolderAsync(AppInfo.AssetsFolder);
_mediaPlayer.SetUriSource(new Uri(AppInfo.TestMusicPath));
_mediaPlayer.Play();
}
[RelayCommand]
private void OpenLogFolder()
{
OpenFolderInFileExplorer(AppInfo.LogDirectory);
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
LyricsSearchProvidersInfo,
LyricsSearchProvidersInfo,
nameof(LyricsSearchProvidersInfo)
);
}
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
{
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
}
}
}

View File

@@ -0,0 +1,9 @@
using BetterLyrics.WinUI3.Services;
namespace BetterLyrics.WinUI3.ViewModels
{
public class SettingsWindowViewModel : BaseWindowViewModel
{
public SettingsWindowViewModel(ISettingsService settingsService) : base(settingsService) { }
}
}

View File

@@ -0,0 +1,59 @@
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;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SystemTrayViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<bool>>
{
public SystemTrayViewModel(ISettingsService settingsService) : base(settingsService) { }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsWindowLocked { get; set; } = false;
[ObservableProperty]
public partial string ToolTipText { get; set; } = AppInfo.AppName;
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.IsLyricsWindowLocked))
{
if (IsLyricsWindowLocked != message.NewValue)
{
IsLyricsWindowLocked = message.NewValue;
}
}
}
}
[RelayCommand]
private void ExitApp()
{
WindowHelper.ExitAllWindows();
}
[RelayCommand]
private void OpenSettings()
{
// 打开设置窗口
WindowHelper.OpenOrShowWindow<SettingsWindow>();
}
[RelayCommand]
private void UnlockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
DesktopModeHelper.Unlock(window);
IsLyricsWindowLocked = false;
}
}
}

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