Compare commits

..

25 Commits

Author SHA1 Message Date
Zhe Fang
c0f2b4106a fix: change discord presence dep 2025-11-19 18:38:18 -05:00
Zhe Fang
c704edde19 chores: code cleanup 2025-11-19 17:50:59 -05:00
Zhe Fang
35de1ef7db add: roman parse for ncm and amll-ttml-db lyrics source 2025-11-19 17:49:24 -05:00
Zhe Fang
1c8840e053 fix: search control title missing 2025-11-19 11:40:58 -05:00
Zhe Fang
2d0839d777 fix: apple music metadata fetch missing 2025-11-19 11:32:05 -05:00
Zhe Fang
7e6ce08c56 fix: amll-ttml-db search rule 2025-11-18 20:38:27 -05:00
Zhe Fang
28f75fa751 chores: rollback media app icon and name fetch algo; add new lyrics search type: best match 2025-11-18 19:07:37 -05:00
Zhe Fang
051a7af21a chores: Change fallback name for playback source 2025-11-18 09:21:13 -05:00
Zhe Fang
c8b8853561 chores: Fix taskbarlist status and progress 2025-11-18 08:50:26 -05:00
Zhe Fang
baab2bcc7a chores: bump version code 2025-11-17 22:16:08 -05:00
Zhe Fang
9a3e4adc0c chores: Fix thread issue 2025-11-17 21:02:11 -05:00
Zhe Fang
8d909da139 chores: Add spliter hint for search control 2025-11-17 19:56:07 -05:00
Zhe Fang
d97f5fec37 chores 2025-11-17 19:22:24 -05:00
Zhe Fang
9a12b06b7e chores: Add fog layer 2025-11-17 17:23:28 -05:00
Zhe Fang
3fca90ca72 chores: Change static media session icon matching to dynamic 2025-11-17 16:10:24 -05:00
Zhe Fang
220e940bbb chores: Import SnowEffect from shadertoy 2025-11-16 20:26:31 -05:00
Zhe Fang
dc627bd1ef chores: Cleanup 2025-11-16 18:02:53 -05:00
Zhe Fang
3c852a9d2f chores: Move LyricsWindowStatusExtensions to Extensions folder 2025-11-16 17:58:42 -05:00
Zhe Fang
ecff788c6c chores 2025-11-16 17:54:37 -05:00
Zhe Fang
c471f128c1 chores: Add new rule for locally search lyrics (only compare file name) 2025-11-16 08:04:22 -05:00
Zhe Fang
4a389d5e33 chores: Improve 2025-11-15 08:30:06 -05:00
Zhe Fang
ae1fa1c54d chores: update docs 2025-11-14 15:21:00 -05:00
Zhe Fang
738a4a0bec chores: update docs 2025-11-14 15:20:10 -05:00
Zhe Fang
37ae278a11 chores: update docs 2025-11-14 15:16:10 -05:00
Zhe Fang
45094bde31 chores: Add discord presence and update docs 2025-11-14 15:15:49 -05:00
156 changed files with 4676 additions and 2574 deletions

1
.gitignore vendored
View File

@@ -407,3 +407,4 @@ FodyWeavers.xsd
*.sln.iml
/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package)_TemporaryKey.pfx
/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/LastFM.cs
/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/Discord.cs

View File

@@ -143,7 +143,7 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />

View File

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

View File

@@ -53,8 +53,6 @@
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converter:MillisecondsToFormattedTimeConverter x:Key="MillisecondsToFormattedTimeConverter" />
<converter:MediaSourceProviderToLogoUriConverter x:Key="MediaSourceProviderToLogoUriConverter" />
<converter:MediaSourceProviderToDisplayedNameConverter x:Key="MediaSourceProviderToDisplayedNameConverter" />
<converter:FPSToTimeSpanConverter x:Key="FPSToTimeSpanConverter" />
<converter:ShortcutToStringConverter x:Key="ShortcutToStringConverter" />
<converter:BoolNegationToVisibilityConverter x:Key="BoolNegationToVisibilityConverter" />
@@ -69,6 +67,8 @@
<converter:TrackToLyricsConverter x:Key="TrackToLyricsConverter" />
<converter:IntToBoolConverter x:Key="IntToBoolConverter" />
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
<converter:MillisecondsToSecondsConverter x:Key="MillisecondsToSecondsConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />

View File

@@ -1,7 +1,9 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
@@ -67,10 +69,10 @@ namespace BetterLyrics.WinUI3
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
WindowHook.OpenOrShowWindow<LyricsWindow>();
if (Ioc.Default.GetRequiredService<ISettingsService>().AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHelper.OpenOrShowWindow<MusicGalleryWindow>();
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
@@ -99,6 +101,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<ITranslateService, TranslateService>()
.AddSingleton<ILastFMService, LastFMService>()
.AddSingleton<IResourceService, ResourceService>()
.AddSingleton<IDiscordService, DiscordService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>()

View File

@@ -34,6 +34,7 @@
<None Remove="Controls\LyricsWindowSwitchControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
@@ -61,17 +62,19 @@
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.250402" />
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
<PackageReference Include="csharp-kana" Version="1.0.2" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="DevWinUI.Controls" Version="9.5.0" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="F23.StringSimilarity" Version="7.0.0" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.2" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
@@ -87,8 +90,10 @@
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.2.1" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.2.1" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
<PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" />
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
<PackageReference Include="z440.atl.core" Version="7.7.0" />
<PackageReference Include="z440.atl.core" Version="7.8.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
@@ -106,8 +111,13 @@
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
<TrimmerRootAssembly Include="Vanara.PInvoke.Gdi32" />
<TrimmerRootAssembly Include="Vanara.PInvoke.Shell32" />
<TrimmerRootAssembly Include="Vanara.PInvoke.User32" />
<TrimmerRootAssembly Include="Vanara.Windows.Shell" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AIMP.png">
@@ -116,6 +126,9 @@
<Content Update="Assets\AlbumArtPlaceholder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Alipay.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AMLLPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -206,6 +219,9 @@
<Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\WeChatReward.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -318,6 +334,11 @@
<ItemGroup>
<Folder Include="TemplateSelector\" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -4,7 +4,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace BetterLyrics.WinUI3.Extensions
namespace BetterLyrics.WinUI3.Collections
{
// https://stackoverflow.com/a/32013610/11048731
public class FullyObservableCollection<T> : ObservableCollection<T>

View File

@@ -0,0 +1,7 @@
namespace BetterLyrics.WinUI3.Constants
{
class Discord
{
public const string AppID = "Your Discord app ID here";
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Constants
{
public class ExtendedGenreFiled
{
public const string NetEaseCloudMusicTrackID = "NCM-";
public const string FileName = "FILENAME-";
}
}

View File

@@ -2,11 +2,14 @@
{
public static class Link
{
public const string MicrosoftStoreUrl = "https://apps.microsoft.com/detail/9p1wcd1p597r";
public const string GitHubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string ShareHubUrl = $"{GitHubUrl}/blob/dev/ShareHub/index.md";
public const string WikiUrl = $"{GitHubUrl}/wiki";
public const string AppleMusicCfgUrl = $"{WikiUrl}/%5BEN%5D-Lyrics-provider-configuration#apple-music";
public const string FAQUrl = $"{GitHubUrl}/blob/dev/FAQ/index.md";
public const string TermsOfServiceUrl = $"{GitHubUrl}/blob/dev/TermsofService.md";
public const string PrivacyPolicy = $"{GitHubUrl}/blob/dev/PrivacyPolicy.md";
public const string WikiUrl = "https://jayfunc.blog/work/betterlyrics";
public const string AppleMusicCfgUrl = $"{WikiUrl}#lyrics-sources-configuration";
public const string FAQUrl = $"{WikiUrl}#faq";
public const string QQGroupUrl = "https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info";
public const string DiscordUrl = "https://discord.gg/5yAQPnyCKv";
public const string TelegramUrl = "https://t.me/+svhSLZ7awPsxNGY1";

View File

@@ -50,10 +50,12 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageInstructions" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="GitHub" NavigateUri="{x:Bind const:Link.GitHubUrl}" />
<HyperlinkButton Content="Wiki" NavigateUri="{x:Bind const:Link.WikiUrl}" />
<HyperlinkButton Content="FAQ" NavigateUri="{x:Bind const:Link.FAQUrl}" />
<HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfServiceUrl}" />
</StackPanel>
</StackPanel>

View File

@@ -5,9 +5,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dev="using:DevWinUI"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -42,7 +42,6 @@
<dev:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEA3A;}">
<local:ExtendedSlider
Default="12"
Maximum="100"
Minimum="0"
Unit="%"
@@ -52,7 +51,6 @@
<dev:SettingsCard x:Uid="SettingsPageAlbumShadowAmount" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF5EF;}">
<local:ExtendedSlider
Default="12"
Maximum="64"
Minimum="0"
Value="{x:Bind AlbumArtLayoutSettings.CoverImageShadowAmount, Mode=TwoWay}" />

View File

@@ -71,6 +71,10 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8D8;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreCacheWhenSearching, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageShowHideHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.GeneralSettings.ShowOrHideLyricsWindowShortcut, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -128,18 +128,29 @@
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsSnowFlakeOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Maximum="100"
Minimum="10"
Maximum="10"
Minimum="0"
ResetButtonVisibility="Collapsed"
Value="{x:Bind LyricsBackgroundSettings.SnowFlakeOverlayAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageSpeed" IsEnabled="{x:Bind LyricsBackgroundSettings.IsSnowFlakeOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Maximum="10"
Minimum="1"
ResetButtonVisibility="Collapsed"
Value="{x:Bind LyricsBackgroundSettings.SnowFlakeOverlaySpeed, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageFogLayer" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEA91;}">
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsFogOverlayEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsExpander
x:Uid="SettingsPageSpectrumLayer"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE93E;}"
Glyph=&#xF61F;}"
IsExpanded="{x:Bind LyricsBackgroundSettings.IsSpectrumOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsSpectrumOverlayEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>

View File

@@ -69,6 +69,17 @@
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}" TextWrapping="Wrap" />
<RichTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap">
<Paragraph>
<Run x:Uid="ArtistsSplitHint" />
<Run Text=";" />
<Run Text="," />
<Run Text="/" />
<Run Text="" />
<Run Text="、" />
<Run Text="" />
</Paragraph>
</RichTextBlock>
<Button
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedArtistCommand}"
@@ -125,22 +136,27 @@
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchResult">
<ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="3,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<TextBlock Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" Text="{x:Bind Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<TextBlock
Text="{x:Bind Title}"
TextWrapping="Wrap"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Artist}"
TextWrapping="Wrap"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Album}"
TextWrapping="Wrap"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow
Margin="-12,0,0,0"
Link="{x:Bind Reference, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind Reference, TargetNullValue=N/A, Mode=OneWay}"
Value="{x:Bind Provider, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<!-- Lyrics search result -->
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind Duration, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind MatchPercentage, Mode=OneWay}" />
</StackPanel>
<!-- NOT FOUND -->
<TextBlock
x:Uid="LyricsSearchControlNotFound"
VerticalAlignment="Center"
@@ -209,23 +225,30 @@
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{Binding LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<DataTemplate x:DataType="models:LyricsData">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<InfoBadge
x:Uid="LyricsSearchControlAutoGenerated"
Margin="6,0,0,0"
Style="{StaticResource StringInfoBadgeStyle}"
Visibility="{x:Bind AutoGenerated, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding LyricsLines, Mode=OneWay}" SelectionChanged="ListView_SelectionChanged">
<DataTemplate x:DataType="models:LyricsData">
<ListView ItemsSource="{x:Bind LyricsLines, Mode=OneWay}" SelectionChanged="ListView_SelectionChanged">
<ListView.ItemTemplate>
<DataTemplate>
<DataTemplate x:DataType="models:LyricsLine">
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock
Margin="1,0"
Foreground="{ThemeResource SystemFillColorNeutralBrush}"
Text="-" />
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding EndMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock Margin="6,0" Text="{Binding OriginalText}" />
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind EndMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock Margin="6,0" Text="{x:Bind OriginalText, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>

View File

@@ -2,7 +2,6 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
@@ -27,7 +26,6 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public LyricsWindowSettingsControl()
{
@@ -87,7 +85,7 @@ namespace BetterLyrics.WinUI3.Controls
clonedData.IsDefault = false;
var json = System.Text.Json.JsonSerializer.Serialize(clonedData, SourceGenerationContext.Default.LyricsWindowStatus);
File.WriteAllText(file.Path, json);
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("ExportSettingsSuccess"));
ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success);
}
}
}
@@ -147,7 +145,7 @@ namespace BetterLyrics.WinUI3.Controls
if (data != null)
{
ViewModel.AppSettings.WindowBoundsRecords.Add(data);
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("ImportSettingsSuccess"));
ToastHelper.ShowToast("ImportSettingsSuccess", null, InfoBarSeverity.Success);
}
}
}

View File

@@ -11,66 +11,74 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
CornerRadius="12">
<FontIcon
Margin="20"
HorizontalAlignment="Left"
VerticalAlignment="Top"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE8AB;" />
<Button
Margin="12"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Click="Button_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE73C;}"
Style="{StaticResource GhostButtonStyle}">
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="Escape" />
</Button.KeyboardAccelerators>
</Button>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid x:Name="ShadowCastGrid" />
<Border
x:Name="ShadowRect"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="12"
Loaded="ShadowRect_Loaded"
Translation="0,0,64">
<Border.Shadow>
<ThemeShadow x:Name="Shadow" />
</Border.Shadow>
</Border>
<Grid Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}" CornerRadius="12">
<TextBlock
x:Uid="SystemTraySwitch"
Margin="20"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
<Button
Margin="12"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Click="Button_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE73C;}"
Style="{StaticResource GhostButtonStyle}">
<Button.KeyboardAccelerators>
<KeyboardAccelerator Key="Escape" />
</Button.KeyboardAccelerators>
</Button>
<ListView
Margin="48,56"
ItemsSource="{x:Bind ViewModel.AppSettings.WindowBoundsRecords, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.LiveStates.LyricsWindowStatus, Mode=TwoWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="0" VerticalSpacing="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid
Margin="0,10"
Padding="5"
AllowFocusOnInteraction="True"
CornerRadius="4"
Tapped="Grid_Tapped">
<uc:DemoWindowGrid LyricsWindowStatus="{Binding}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView
Margin="48,56"
ItemsSource="{x:Bind ViewModel.AppSettings.WindowBoundsRecords, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.LiveStates.LyricsWindowStatus, Mode=TwoWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="0" VerticalSpacing="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid
Margin="0,10"
Padding="5"
AllowFocusOnInteraction="True"
CornerRadius="4"
Tapped="Grid_Tapped">
<uc:DemoWindowGrid LyricsWindowStatus="{Binding}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Margin="20"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Orientation="Horizontal"
Spacing="6">
<FontIcon
Margin="0,1,0,0"
FontFamily="{StaticResource IconFontFamily}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE946;" />
<TextBlock x:Uid="LyricsWindowSwitchWindowHelp" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
<StackPanel
Margin="20"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Orientation="Horizontal"
Spacing="6">
<HyperlinkButton Click="SettingsHypelinkButton_Click">
<HyperlinkButton.Content>
<TextBlock x:Uid="LyricsWindowSwitchWindowHelp" />
</HyperlinkButton.Content>
</HyperlinkButton>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,4 +1,4 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
@@ -34,10 +34,21 @@ namespace BetterLyrics.WinUI3.Controls
private async Task HideAsync()
{
var lyricsWindowSwitchWindow = WindowHelper.GetWindowByWindowType<LyricsWindowSwitchWindow>();
var lyricsWindowSwitchWindow = WindowHook.GetWindow<LyricsWindowSwitchWindow>();
lyricsWindowSwitchWindow?.ViewModel.RootGridOpacity = 0;
await Task.Delay(300);
WindowHelper.HideWindow<LyricsWindowSwitchWindow>();
WindowHook.HideWindow<LyricsWindowSwitchWindow>();
}
private void ShadowRect_Loaded(object sender, RoutedEventArgs e)
{
Shadow.Receivers.Add(ShadowCastGrid);
}
private async void SettingsHypelinkButton_Click(object sender, RoutedEventArgs e)
{
await HideAsync();
WindowHook.OpenOrShowWindow<SettingsWindow>();
}
}
}

View File

@@ -64,14 +64,22 @@
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:MediaSourceProviderInfo">
<StackPanel Orientation="Horizontal" Spacing="6">
<ToolTipService.ToolTip>
<ToolTip Content="{Binding Provider, Mode=OneWay}" />
</ToolTipService.ToolTip>
<FontIcon
FontFamily="Segoe UI Symbol"
FontSize="12"
Glyph="&#x283F;" />
<ImageIcon Height="16" Source="{Binding Provider, Converter={StaticResource MediaSourceProviderToLogoUriConverter}, Mode=OneWay}" />
<Grid
Width="16"
Height="16"
CornerRadius="4">
<ImageIcon Source="{Binding LogoPath}" />
</Grid>
<TextBlock
MaxWidth="200"
Text="{Binding Provider, Converter={StaticResource MediaSourceProviderToDisplayedNameConverter}, Mode=OneWay}"
Text="{Binding DisplayName, Mode=OneWay}"
TextWrapping="Wrap" />
</StackPanel>
</DataTemplate>
@@ -90,6 +98,10 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.SelectedMediaSourceProvider.IsLastFMTrackEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageDiscordPresence">
<ToggleSwitch IsOn="{x:Bind ViewModel.SelectedMediaSourceProvider.IsDiscordPresenceEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- LX music server -->
<dev:SettingsCard x:Uid="SettingsPageLXMusicServer" Visibility="{x:Bind ViewModel.SelectedMediaSourceProvider.IsLXMusic, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid ColumnSpacing="6">
@@ -170,6 +182,13 @@
<!-- 歌词源配置 -->
<TextBlock x:Uid="SettingsPageLyricsSearchProvidersConfig" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageLyricsSearchType">
<ComboBox SelectedIndex="{x:Bind ViewModel.SelectedMediaSourceProvider.LyricsSearchType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsSearchSequential" />
<ComboBoxItem x:Uid="SettingsPageLyricsSearchBestMatch" />
</ComboBox>
</dev:SettingsCard>
<ListView
x:Name="LyricsSearchProvidersListView"
AllowDrop="True"
@@ -236,20 +255,75 @@
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Provider info -->
<!-- Realtime info -->
<TextBlock x:Uid="SettingsPageRealtimeStatus" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="LyricsPageLyricsProviderPrefix">
<HyperlinkButton
Content="{x:Bind ViewModel.LyricsSearchProvider, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}"
IsEnabled="False"
NavigateUri="{x:Bind ViewModel.OriginalLyricsRef, Mode=OneWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsPageTranslationProviderPrefix">
<HyperlinkButton
Content="{x:Bind ViewModel.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}"
IsEnabled="False"
NavigateUri="{x:Bind ViewModel.TranslatedLyricsRef, Mode=OneWay}" />
</dev:SettingsCard>
<!-- Playback source info -->
<Expander
x:Uid="SettingsPagePlaybackStatus"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<!-- Playback source -->
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<!-- Playback source ID -->
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
<!-- Song info -->
<Expander
x:Uid="SettingsPageSongStatus"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<!-- Song title -->
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Song artists -->
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Song album -->
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Song duration -->
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Duration, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
<!-- Search result info -->
<Expander
x:Uid="SettingsPageSearchResultStatus"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<!-- Searched title -->
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Searched artists -->
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Searched album -->
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Searched duration -->
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, TargetNullValue=N/A, Mode=OneWay}" />
<!-- Lyrics source -->
<local:PropertyRow
x:Uid="LyricsPageLyricsProviderPrefix"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay}"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<!-- Translation source -->
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
<!-- Match percentage -->
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
</StackPanel>
</Expander>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -272,12 +346,7 @@
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageTranslationConfig" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=OneWay}">
<dev:SettingsCard.Description>
<HyperlinkButton Margin="0,6,0,0" NavigateUri="https://github.com/LibreTranslate/LibreTranslate">
<TextBlock
x:Uid="SettingsPageTranslationInfoLink"
FontSize="14"
TextWrapping="Wrap" />
</HyperlinkButton>
<HyperlinkButton Content="https://github.com/LibreTranslate/LibreTranslate" NavigateUri="https://github.com/LibreTranslate/LibreTranslate" />
</dev:SettingsCard.Description>
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PropertyRow"
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:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid HorizontalAlignment="Left" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Top"
Text="{x:Bind Header, Mode=OneWay}" />
<Grid
Grid.Column="1"
VerticalAlignment="Top"
PointerEntered="OnPointerEntered"
PointerExited="OnPointerExited">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<RichTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Visibility="{x:Bind TextVisibility, Mode=OneWay}">
<Paragraph>
<Run Text="{x:Bind Value, Mode=OneWay}" />
<Run Text="{x:Bind Unit, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
<HyperlinkButton
Margin="0,-2,0,0"
Padding="0"
HorizontalContentAlignment="Left"
Click="OnLinkClicked"
Content="{x:Bind Value, Mode=OneWay}"
Visibility="{x:Bind LinkVisibility, Mode=OneWay}" />
</Grid>
<Button
x:Name="CopyButton"
Grid.Column="1"
Margin="8,-2,0,0"
Padding="4"
VerticalAlignment="Top"
Click="OnCopyClicked"
Opacity="0">
<ToolTipService.ToolTip>
<TextBlock x:Uid="Copy" />
</ToolTipService.ToolTip>
<Button.OpacityTransition>
<ScalarTransition />
</Button.OpacityTransition>
<Grid>
<FontIcon
x:Name="CopyIcon"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8C8;"
Opacity="1">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<FontIcon
x:Name="CheckIcon"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE73E;"
Opacity="0">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,115 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class PropertyRow : UserControl
{
public PropertyRow()
{
this.InitializeComponent();
}
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register(nameof(Header), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
public string Value
{
get => (string)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
public string Link
{
get => (string)GetValue(LinkProperty);
set => SetValue(LinkProperty, value);
}
public static readonly DependencyProperty LinkProperty =
DependencyProperty.Register(nameof(Link), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
public string Unit
{
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
public static readonly DependencyProperty UnitProperty =
DependencyProperty.Register(nameof(Unit), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
private Visibility TextVisibility => Link == string.Empty ? Visibility.Visible : Visibility.Collapsed;
private Visibility LinkVisibility => Link != string.Empty ? Visibility.Visible : Visibility.Collapsed;
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
if (!string.IsNullOrEmpty(Value))
{
CopyButton.Opacity = 1;
}
}
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
{
CopyButton.Opacity = 0;
}
private void OnCopyClicked(object sender, RoutedEventArgs e)
{
var targetValue = string.IsNullOrEmpty(Link) ? Value : Link;
if (string.IsNullOrEmpty(targetValue)) return;
try
{
DataPackage dataPackage = new DataPackage();
dataPackage.SetText(targetValue);
Clipboard.SetContent(dataPackage);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Copy failed: {ex.Message}");
return;
}
CheckIcon.Opacity = 1;
CopyIcon.Opacity = 0;
this.DispatcherQueue.TryEnqueue(async () =>
{
await Task.Delay(1000);
CheckIcon.Opacity = 0;
CopyIcon.Opacity = 1;
});
}
private async void OnLinkClicked(object sender, RoutedEventArgs e)
{
Uri.TryCreate(Link, UriKind.Absolute, out var uri);
if (uri != null)
{
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
await Windows.System.Launcher.LaunchUriAsync(uri);
}
else if (uri.Scheme == Uri.UriSchemeFile)
{
await LauncherHelper.SelectAndShowFile(uri.LocalPath);
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Input;
@@ -89,14 +90,14 @@ namespace BetterLyrics.WinUI3.Controls
private void CheckButton_Click(object sender, RoutedEventArgs e)
{
bool registered = GlobalHotKeyHelper.IsHotKeyRegistered(Shortcut);
bool registered = GlobalHotKeyHook.IsHotKeyRegistered(Shortcut);
if (registered)
{
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("SettingsPageShortcutRegSuccessInfo"));
ToastHelper.ShowToast("SettingsPageShortcutRegSuccessInfo", null, InfoBarSeverity.Success);
}
else
{
DevWinUI.Growl.Error(_resourceService.GetLocalizedString("SettingsPageShortcutRegFailInfo"));
ToastHelper.ShowToast("SettingsPageShortcutRegFailInfo", null, InfoBarSeverity.Error);
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public class IntToDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int intValue)
{
return (double)intValue;
}
return 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,47 +0,0 @@
using BetterLyrics.WinUI3.Constants;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public class MediaSourceProviderToDisplayedNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string provider)
{
return provider switch
{
PlayerID.Spotify => PlayerName.Spotify,
PlayerID.AppleMusic => PlayerName.AppleMusic,
PlayerID.iTunes => PlayerName.iTunes,
PlayerID.KugouMusic => PlayerName.KugouMusic,
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerID.QQMusic => PlayerName.QQMusic,
PlayerID.LXMusic => PlayerName.LXMusic,
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerID.AIMP => PlayerName.AIMP,
PlayerID.Foobar2000 => PlayerName.Foobar2000,
PlayerID.MusicBee => PlayerName.MusicBee,
PlayerID.PotPlayer => PlayerName.PotPlayer,
PlayerID.Chrome => PlayerName.Chrome,
PlayerID.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindows => PlayerName.SaltPlayerForWindows,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1,
_ => provider,
};
}
return value?.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,49 +0,0 @@
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public class MediaSourceProviderToLogoUriConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string provider)
{
return provider switch
{
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerID.QQMusic => PathHelper.QQMusicLogoPath,
PlayerID.LXMusic => PathHelper.LXMusicLogoPath,
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerID.AIMP => PathHelper.AIMPLogoPath,
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerID.Chrome => PathHelper.ChromeLogoPath,
PlayerID.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindows => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath,
_ => PathHelper.UnknownPlayerLogoPath,
};
}
return PathHelper.UnknownPlayerLogoPath;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public class MillisecondsToSecondsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int intValue)
{
return intValue / 1000.0;
}
else if (value is double doubleValue)
{
return doubleValue / 1000.0;
}
return 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,5 +1,5 @@
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Data;
using System;
@@ -11,7 +11,7 @@ namespace BetterLyrics.WinUI3.Converter
{
if (value is Track track)
{
return track.GetLyrics();
return track.GetRawLyrics();
}
return "";
}

View File

@@ -1,24 +1,8 @@
using BetterLyrics.WinUI3.Helper;
using System;
namespace BetterLyrics.WinUI3.Enums
namespace BetterLyrics.WinUI3.Enums
{
public enum ChineseRomanization
{
Pinyin,
Jyutping,
}
public static class ChineseRomanizationExtensions
{
public static string ToPhoneticCode(this ChineseRomanization chineseRomanization)
{
return chineseRomanization switch
{
ChineseRomanization.Pinyin => PhoneticHelper.PinyinCode,
ChineseRomanization.Jyutping => PhoneticHelper.JyutpingCode,
_ => throw new ArgumentOutOfRangeException(nameof(chineseRomanization))
};
}
}
}

View File

@@ -11,72 +11,4 @@ namespace BetterLyrics.WinUI3.Enums
Krc,
NotSpecified,
}
public static class LyricsFormatExtensions
{
public static LyricsFormat? DetectFormat(this string content)
{
if (string.IsNullOrWhiteSpace(content))
return null;
// TTML: 检查 <tt ... xmlns="http://www.w3.org/ns/ttml"
if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"<tt\b[^>]*\bxmlns\s*=\s*[""']http://www\.w3\.org/ns/ttml[""']",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
return LyricsFormat.Ttml;
}
// 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;
}
else
{
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",
_ => ".*",
};
}
public static LyricsSearchProvider? ToLyricsSearchProvider(this LyricsFormat format)
{
return format switch
{
LyricsFormat.Lrc => LyricsSearchProvider.LocalLrcFile,
LyricsFormat.Eslrc => LyricsSearchProvider.LocalEslrcFile,
LyricsFormat.Ttml => LyricsSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}

View File

@@ -1,7 +1,5 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchProvider
@@ -17,70 +15,4 @@ namespace BetterLyrics.WinUI3.Enums
LocalTtmlFile,
AppleMusic,
}
public static class LyricsSearchProviderExtensions
{
public static string GetCacheDirectory(this LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
_ => 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.AppleMusic => 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();
}
public static TranslationSearchProvider? ToTranslationSearchProvider(this LyricsSearchProvider? provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => TranslationSearchProvider.LrcLib,
LyricsSearchProvider.QQ => TranslationSearchProvider.QQ,
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
LyricsSearchProvider.AppleMusic => TranslationSearchProvider.AppleMusic,
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,
LyricsSearchProvider.LocalTtmlFile => TranslationSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchType
{
Sequential,
BestMatch
}
}

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
namespace BetterLyrics.WinUI3.Enums
{
public enum SpectrumPlacement
{

View File

@@ -5,9 +5,8 @@ using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs(byte[]? bytes, SoftwareBitmap? albumArtSwBitmap, List<Color> albumArtLightAccentColors, List<Color> albumArtDarkAccentColors) : EventArgs
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, List<Color> albumArtLightAccentColors, List<Color> albumArtDarkAccentColors) : EventArgs
{
public byte[]? Bytes { get; set; } = bytes;
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public List<Color> AlbumArtLightAccentColors { get; set; } = albumArtLightAccentColors;
public List<Color> AlbumArtDarkAccentColors { get; set; } = albumArtDarkAccentColors;

View File

@@ -1,11 +0,0 @@
// 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,12 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Events
{
public class SongInfoChangedEventArgs(SongInfo? songInfo) : EventArgs
{
public SongInfo? SongInfo { get; set; } = songInfo;
}
}

View File

@@ -1,12 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
namespace BetterLyrics.WinUI3.Events
{
public class TimelineChangedEventArgs(TimeSpan position, TimeSpan end) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
public TimeSpan End { get; set; } = end;
}
}

View File

@@ -0,0 +1,18 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Windowing;
namespace BetterLyrics.WinUI3.Extensions
{
public static class AppWindowExtensions
{
extension(AppWindow appWindow)
{
public void SetIcons()
{
appWindow.SetIcon(PathHelper.LogoPath);
appWindow.SetTaskbarIcon(PathHelper.LogoPath);
appWindow.SetTitleBarIcon(PathHelper.LogoPath);
}
}
}
}

View File

@@ -0,0 +1,22 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Text;
namespace BetterLyrics.WinUI3.Extensions
{
public static class CanvasTextLayoutExtensions
{
extension(CanvasTextLayout? canvasTextLayout)
{
public void SetFontFamily(string? text, string cjk, string latin)
{
if (canvasTextLayout == null) return;
if (text == null) return;
for (int i = 0; i < text.Length; i++)
{
canvasTextLayout.SetFontFamily(i, 1, LanguageHelper.IsCJK(text[i]) ? cjk : latin);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
namespace BetterLyrics.WinUI3.Extensions
{
public static class ChineseRomanizationExtensions
{
extension(ChineseRomanization chineseRomanization)
{
public string ToPhoneticCode() => chineseRomanization switch
{
ChineseRomanization.Pinyin => PhoneticHelper.PinyinCode,
ChineseRomanization.Jyutping => PhoneticHelper.JyutpingCode,
_ => throw new ArgumentOutOfRangeException(nameof(chineseRomanization))
};
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.Extensions
{
public static class ColorExtensions
{
extension(Color color)
{
public Color WithAlpha(byte alpha)
{
return Color.FromArgb(alpha, color.R, color.G, color.B);
}
public Color WithOpacity(float opacity)
{
return Color.FromArgb((byte)(opacity * 255), color.R, color.G, color.B);
}
public Color WithBrightness(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);
}
public Vector3 ToVector3RGB()
{
return new Vector3((float)color.R / 0xff, (float)color.G / 0xff, (float)color.B / 0xff);
}
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Reflection;
namespace BetterLyrics.WinUI3.Extensions
{
public static class DisposableObjectExtension
{
extension(IDisposable? obj)
{
// Credit/Copyright to https://gist.github.com/tcartwright/dab50ebaff7c59f05013de0fb349cabd
public bool IsDisposed()
{
/*
TIM C: This hacky code is because MSFT does not provide a standard way to interrogate if an object is disposed or not.
I wrote this based upon streams, but it should work for many other types of MSFT objects (maybe).
*/
if (obj == null) { return true; }
var objType = obj.GetType();
//var foo = new System.IO.BufferedStream();
// the _disposed pattern should catch a lot of msft objects.... hopefully
var isDisposedField = objType.GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance) ??
objType.GetField("disposed", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return Convert.ToBoolean(isDisposedField.GetValue(obj)); }
isDisposedField = objType.GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return !Convert.ToBoolean(isDisposedField.GetValue(obj)); }
// Windows.Graphics.Imaging.SoftwareBitmap
isDisposedField = objType.GetField("_objRef_global__System_IDisposable", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return !Convert.ToBoolean(isDisposedField.GetValue(obj)); }
// System.IO.FileStream
var strategyField = objType.GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance);
if (strategyField != null)
{
var strategy = strategyField.GetValue(obj);
var isClosedField = strategy.GetType().GetProperty("IsClosed", BindingFlags.NonPublic | BindingFlags.Instance);
if (isClosedField != null) { return Convert.ToBoolean(isClosedField.GetValue(strategy)); }
}
// other streams that use this pattern to determine if they are disposed
if (obj is Stream stream) { return !stream.CanRead && !stream.CanWrite; }
return false;
}
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace BetterLyrics.WinUI3.Extensions
{
public static class EnumExtensions
{
extension<T>(T value) where T : struct, Enum
{
public T GetNext()
{
T[] values = Enum.GetValues<T>();
int currentIndex = Array.IndexOf(values, value);
int nextIndex = (currentIndex + 1) % values.Length;
return values[nextIndex];
}
}
}
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace BetterLyrics.WinUI3.Extensions
{
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> items)
{
public ObservableCollection<GroupInfoList> GetGroupedBy(Func<T, object> groupKeySelector, Func<object, object>? orderSelector = null)
{
var query = from item in items
group item by groupKeySelector(item) into g
orderby g.Key
select new GroupInfoList(g.Cast<object>(), orderSelector) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
}
}
}

View File

@@ -0,0 +1,34 @@
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Extensions
{
public static class LyricsFormatExtensions
{
extension(LyricsFormat format)
{
public string ToFileExtension()
{
return format switch
{
LyricsFormat.Lrc => ".lrc",
LyricsFormat.Qrc => ".qrc",
LyricsFormat.Krc => ".krc",
LyricsFormat.Eslrc => ".eslrc",
LyricsFormat.Ttml => ".ttml",
_ => ".*",
};
}
public LyricsSearchProvider? ToLyricsSearchProvider()
{
return format switch
{
LyricsFormat.Lrc => LyricsSearchProvider.LocalLrcFile,
LyricsFormat.Eslrc => LyricsSearchProvider.LocalEslrcFile,
LyricsFormat.Ttml => LyricsSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}
}

View File

@@ -0,0 +1,60 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
namespace BetterLyrics.WinUI3.Extensions
{
public static class LyricsSearchProviderExtensions
{
extension(LyricsSearchProvider provider)
{
public string GetCacheDirectory() => provider switch
{
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
};
public LyricsFormat GetLyricsFormat() => provider switch
{
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
LyricsSearchProvider.QQ => LyricsFormat.Qrc,
LyricsSearchProvider.Kugou => LyricsFormat.Krc,
LyricsSearchProvider.Netease => LyricsFormat.Lrc,
LyricsSearchProvider.AmllTtmlDb => LyricsFormat.Ttml,
LyricsSearchProvider.AppleMusic => LyricsFormat.Ttml,
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
LyricsSearchProvider.LocalEslrcFile => LyricsFormat.Eslrc,
LyricsSearchProvider.LocalTtmlFile => LyricsFormat.Ttml,
_ => LyricsFormat.NotSpecified,
};
public bool IsLocal() => provider
is LyricsSearchProvider.LocalMusicFile
or LyricsSearchProvider.LocalLrcFile
or LyricsSearchProvider.LocalEslrcFile
or LyricsSearchProvider.LocalTtmlFile;
public bool IsRemote() => !provider.IsLocal();
public TranslationSearchProvider? ToTranslationSearchProvider() => provider switch
{
LyricsSearchProvider.LrcLib => TranslationSearchProvider.LrcLib,
LyricsSearchProvider.QQ => TranslationSearchProvider.QQ,
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
LyricsSearchProvider.AppleMusic => TranslationSearchProvider.AppleMusic,
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,
LyricsSearchProvider.LocalTtmlFile => TranslationSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}

View File

@@ -0,0 +1,108 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Extensions
{
public static class LyricsWindowStatusExtensions
{
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public static LyricsWindowStatus DesktopMode()
{
return new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("DesktopMode"),
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
WindowBounds = new Rect(100, 100, 600, 250),
IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true,
IsBorderless = true,
IsClickThrough = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
EnvironmentSampleMode = WindowPixelSampleMode.WindowEdge,
LyricsStyleSettings = new()
{
LyricsAlignmentType = TextAlignmentType.Center,
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsFluidOverlayEnabled = false,
}
};
}
public static LyricsWindowStatus DockedMode()
{
var status = new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("DockedMode"),
IsWorkArea = true,
IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true,
IsBorderless = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
EnvironmentSampleMode = WindowPixelSampleMode.BelowWindow,
TitleBarArea = TitleBarArea.None,
LyricsStyleSettings = new LyricsStyleSettings
{
LyricsAlignmentType = TextAlignmentType.Center,
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsFluidOverlayEnabled = false,
IsPureColorOverlayEnabled = true,
}
};
status.WindowBounds = status.GetWindowBoundsWhenWorkArea();
return status;
}
public static LyricsWindowStatus FullscreenMode()
{
var status = new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("FullscreenMode"),
IsBorderless = true,
IsAlwaysOnTop = false,
TitleBarArea = TitleBarArea.None,
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
LyricsStyleSettings = new LyricsStyleSettings
{
LyricsAlignmentType = TextAlignmentType.Center,
},
};
status.WindowBounds = new Rect(
status.MonitorBounds.X,
status.MonitorBounds.Y - 1,
status.MonitorBounds.Width,
status.MonitorBounds.Height + 1
);
return status;
}
public static LyricsWindowStatus StandardMode()
{
return new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("StandardMode"),
};
}
public static LyricsWindowStatus NarrowMode()
{
return new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("NarrowMode"),
WindowBounds = new Rect(100, 100, 400, 800),
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
};
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace BetterLyrics.WinUI3.Extensions
{
public static class ObservableCollectionExtensions
{
extension<T>(ObservableCollection<T> list)
{
public void InsertRange(int index, IEnumerable<T> items)
{
if (list == null) return;
if (items == null) return;
if (index < 0 || index > list.Count) return;
foreach (var item in items)
{
list.Insert(index++, item);
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
using Windows.Foundation;
using Windows.Graphics;
namespace BetterLyrics.WinUI3.Extensions
{
public static class PointExtensions
{
extension(Point point)
{
public PointInt32 ToPointInt32() => new((int)point.X, (int)point.Y);
}
}
}

View File

@@ -0,0 +1,46 @@
using Windows.Foundation;
using Windows.Graphics;
namespace BetterLyrics.WinUI3.Extensions
{
public static class RectExtensions
{
extension(Rect rect)
{
public RectInt32 ToRectInt32() => new(
(int)rect.X,
(int)rect.Y,
(int)rect.Width,
(int)rect.Height
);
public Rect WithHeight(double height) => new(
rect.X,
rect.Y,
rect.Width,
height
);
public Rect WithWidth(double width) => new(
rect.X,
rect.Y,
width,
rect.Height
);
public Rect WithX(double x) => new(
x,
rect.Y,
rect.Width,
rect.Height
);
public Rect WithY(double y) => new(
rect.X,
y,
rect.Width,
rect.Height
);
}
}
}

View File

@@ -0,0 +1,35 @@
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Extensions
{
public static class SongInfoExtensions
{
public static SongInfo Placeholder => new SongInfo
{
Title = "N/A",
Album = "N/A",
Artists = ["N/A"],
};
extension(SongInfo songInfo)
{
public SongInfo WithTitle(string value)
{
songInfo.Title = value;
return songInfo;
}
public SongInfo WithArtist(string[] value)
{
songInfo.Artists = value;
return songInfo;
}
public SongInfo WithAlbum(string value)
{
songInfo.Album = value;
return songInfo;
}
}
}
}

View File

@@ -0,0 +1,80 @@
using BetterLyrics.WinUI3.Enums;
using System.Linq;
namespace BetterLyrics.WinUI3.Extensions
{
public static class StringExtensions
{
private static readonly string[] _splitter =
[
";"
,
","
,
"/"
,
""
,
"、"
,
""
];
extension(string str)
{
public string[] SplitByCommonSplitter()
{
var splitter = _splitter.FirstOrDefault(str.Contains);
if (splitter != null)
{
return str.Split(splitter);
}
else
{
return [str];
}
}
public LyricsFormat? DetectFormat()
{
if (string.IsNullOrWhiteSpace(str))
return null;
// TTML: 检查 <tt ... xmlns="http://www.w3.org/ns/ttml"
if (System.Text.RegularExpressions.Regex.IsMatch(
str,
@"<tt\b[^>]*\bxmlns\s*=\s*[""']http://www\.w3\.org/ns/ttml[""']",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
return LyricsFormat.Ttml;
}
// KRC: 检测主内容格式 [start,duration]<offset,duration,0>字...
else if (System.Text.RegularExpressions.Regex.IsMatch(
str,
@"^\[\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(
str,
@"^\[\d+,\d+\].*?\(\d+,\d+\)",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Qrc;
}
// 标准LRC和增强型LRC
else if (System.Text.RegularExpressions.Regex.IsMatch(str, @"\[\d{1,2}:\d{2}") ||
System.Text.RegularExpressions.Regex.IsMatch(str, @"<\d{1,2}:\d{2}\.\d{2,3}>"))
{
return LyricsFormat.Lrc;
}
else
{
return null;
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
using ATL;
using System.IO;
namespace BetterLyrics.WinUI3.Extensions
{
public static class TrackExtensions
{
extension(Track track)
{
public string GetParentFolderName() => Directory.GetParent(track.Path)?.Name ?? "";
public string GetParentFolderPath() => Directory.GetParent(track.Path)?.FullName ?? "";
public string GetRawLyrics()
{
if (track.Path is string path)
{
return TagLib.File.Create(path).Tag.Lyrics;
}
return "";
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Numerics;
namespace BetterLyrics.WinUI3.Extensions
{
public static class VectorExtensions
{
extension(Vector2 vector2)
{
public Vector2 WithX(float x)
{
return new Vector2(x, vector2.Y);
}
public Vector2 WithY(float y)
{
return new Vector2(vector2.X, y);
}
}
}
}

View File

@@ -0,0 +1,33 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Extensions
{
public static class WindowExtensions
{
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
extension(Window window)
{
public void Init(
string titleKey,
TitleBarHeightOption titleBarHeightOption = TitleBarHeightOption.Standard,
BackdropType backdropType = BackdropType.DesktopAcrylic)
{
window.Title = _resourceService.GetLocalizedString(titleKey);
window.AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
window.AppWindow.SetIcons();
window.ExtendsContentIntoTitleBar = true;
window.AppWindow.TitleBar.PreferredHeightOption = titleBarHeightOption;
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(backdropType);
}
}
}
}

View File

@@ -1,14 +0,0 @@
using Microsoft.UI.Windowing;
namespace BetterLyrics.WinUI3.Helper
{
public static class AppWindowHelper
{
public static void SetIcons(this AppWindow appWindow)
{
appWindow.SetIcon(PathHelper.LogoPath);
appWindow.SetTaskbarIcon(PathHelper.LogoPath);
appWindow.SetTitleBarIcon(PathHelper.LogoPath);
}
}
}

View File

@@ -1,18 +0,0 @@
using Microsoft.Graphics.Canvas.Text;
namespace BetterLyrics.WinUI3.Helper
{
public static class CanvasTextLayoutExtensions
{
public static void SetFontFamily(this CanvasTextLayout? layout, string? text, string cjk, string latin)
{
if (layout == null) return;
if (text == null) return;
for (int i = 0; i < text.Length; i++)
{
layout.SetFontFamily(i, 1, LanguageHelper.IsCJK(text[i]) ? cjk : latin);
}
}
}
}

View File

@@ -1,46 +0,0 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace BetterLyrics.WinUI3.Helper
{
public static class CollectionHelper
{
public static ObservableCollection<GroupInfoList> GetGroupedBy<T>(
this IEnumerable<T> items,
Func<T, object> groupKeySelector,
Func<object, object>? orderSelector = null)
{
var query = from item in items
group item by groupKeySelector(item) into g
orderby g.Key
select new GroupInfoList(g.Cast<object>(), orderSelector) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
{
if (collection == null) return;
if (items == null) return;
foreach (var item in items)
{
collection.Add(item);
}
}
public static void InsertRange<T>(this IList<T> list, int index, IEnumerable<T> items)
{
if (list == null) return;
if (items == null) return;
if (index < 0 || index > list.Count) return;
foreach (var item in items)
{
list.Insert(index++, item);
}
}
}
}

View File

@@ -1,15 +1,15 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Numerics;
using Vanara.PInvoke;
using Color = Windows.UI.Color;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
{
@@ -62,86 +62,28 @@ namespace BetterLyrics.WinUI3.Helper
);
}
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 WithOpacity(this Color color, float opacity)
{
return Color.FromArgb((byte)(opacity * 255), 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);
}
public static Vector3 ToVector3RGB(this Color color)
{
return new Vector3((float)color.R / 0xff, (float)color.G / 0xff, (float)color.B / 0xff);
}
public static Color GetRandomColor()
{
return Color.FromArgb(255, (byte)Random.Shared.Next(0, 256), (byte)Random.Shared.Next(0, 256), (byte)Random.Shared.Next(0, 256));
}
public static System.Drawing.Color GetAccentColor(IntPtr myHwnd, string monitorDeviceName, WindowPixelSampleMode mode)
public static Color GetAccentColor(IntPtr myHwnd, string monitorDeviceName, WindowPixelSampleMode mode)
{
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return System.Drawing.Color.Transparent;
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return Colors.Transparent;
var monitorInfo = MonitorHelper.GetMonitorInfoExFromDeviceName(monitorDeviceName);
var monitorInfo = MonitorHook.GetMonitorInfoExFromDeviceName(monitorDeviceName);
int screenWidth = monitorInfo.rcMonitor.Width;
switch (mode)
{
case WindowPixelSampleMode.BelowWindow:
{
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Bottom + 2, screenWidth, 1);
}
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Bottom + 2, screenWidth, 1);
case WindowPixelSampleMode.AboveWindow:
{
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - 2, screenWidth, 1);
}
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - 2, screenWidth, 1);
case WindowPixelSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
if (width <= 0 || height <= 0) return Colors.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
@@ -150,10 +92,10 @@ namespace BetterLyrics.WinUI3.Helper
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
return Colors.Transparent;
var edgeThickness = new Thickness(36, 36, 36, 36);
List<System.Drawing.Color> edgeColors = [];
List<Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0)
@@ -169,33 +111,30 @@ namespace BetterLyrics.WinUI3.Helper
edgeColors.Add(GetAverageColorFromScreenRegion(myRect.Right, myRect.Top, (int)edgeThickness.Right, height));
// 合并四边平均色
if (edgeColors.Count == 0)
return System.Drawing.Color.Transparent;
long r = 0,
g = 0,
b = 0;
if (edgeColors.Count == 0) return Colors.Transparent;
long r = 0, g = 0, b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return System.Drawing.Color.FromArgb(
return Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
(byte)(r / edgeColors.Count),
(byte)(g / edgeColors.Count),
(byte)(b / edgeColors.Count)
);
}
default:
return System.Drawing.Color.Transparent;
return Colors.Transparent;
}
}
private static System.Drawing.Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
using System.Drawing.Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using var gDest = System.Drawing.Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
@@ -208,7 +147,7 @@ namespace BetterLyrics.WinUI3.Helper
return ComputeAverageColor(bmp);
}
private static System.Drawing.Color ComputeAverageColor(Bitmap bmp)
private static Color ComputeAverageColor(System.Drawing.Bitmap bmp)
{
long r = 0, g = 0, b = 0;
int count = 0;
@@ -225,8 +164,10 @@ namespace BetterLyrics.WinUI3.Helper
}
}
if (count == 0) return System.Drawing.Color.Transparent;
return System.Drawing.Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
if (count == 0) return Colors.Transparent;
return Color.FromArgb(255, (byte)(r / count), (byte)(g / count), (byte)(b / count));
}
public static Color FromVector3(Vector3 vector3) => Color.FromArgb(255, (byte)vector3.X, (byte)vector3.Y, (byte)vector3.Z);
}
}

View File

@@ -1,15 +0,0 @@
using System;
namespace BetterLyrics.WinUI3.Helper
{
public static class EnumExtensions
{
public static T GetNext<T>(this T value) where T : struct, Enum
{
T[] values = Enum.GetValues<T>();
int currentIndex = Array.IndexOf(values, value);
int nextIndex = (currentIndex + 1) % values.Length;
return values[nextIndex];
}
}
}

View File

@@ -1,6 +1,9 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using System;
using System.IO;
using System.Text;
@@ -35,12 +38,17 @@ namespace BetterLyrics.WinUI3.Helper
return sb.ToString();
}
public static string? ReadLyricsCache(string title, string artist, string album, LyricsFormat format, string cacheFolderPath)
public static LyricsSearchResult? ReadLyricsCache(SongInfo songInfo, LyricsSearchProvider lyricsSearchProvider)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title} - {album}{format.ToFileExtension()}"));
var cacheFilePath = Path.Combine(
lyricsSearchProvider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
var json = File.ReadAllText(cacheFilePath);
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
return data;
}
return null;
}
@@ -55,29 +63,21 @@ namespace BetterLyrics.WinUI3.Helper
return null;
}
public static void WriteLyricsCache(string title, string artist, string album, string lyrics, LyricsFormat format, string cacheFolderPath)
public static void WriteLyricsCache(SongInfo songInfo, LyricsSearchResult lyricsSearchResult)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title} - {album}{format.ToFileExtension()}"));
File.WriteAllText(cacheFilePath, lyrics);
var cacheFilePath = Path.Combine(
lyricsSearchResult.Provider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
File.WriteAllText(cacheFilePath, json);
}
public static void WriteAlbumArtCache(string album, string artist, byte[] img, string format, string cacheFolderPath)
public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.DisplayArtists} - {songInfo.Album}{format}"));
File.WriteAllBytes(cacheFilePath, img);
}
public static bool IsSwitchableNormalizedMatch(string fileName, string q1, string q2)
{
var normFileName = StringHelper.Normalize(fileName);
var normQ1 = StringHelper.Normalize(q1);
var normQ2 = StringHelper.Normalize(q2);
// 常见两种顺序
return normFileName == normQ1 + normQ2
|| normFileName == normQ2 + normQ1;
}
public static readonly string[] MusicExtensions = {
".mp3", ".aac", ".m4a", ".ogg", ".opus", ".wma", ".amr",
".flac", ".alac", ".ape", ".wv", ".tak",

View File

@@ -4,10 +4,9 @@ using BetterLyrics.WinUI3.Enums;
using Impressionist.Abstractions;
using Microsoft.Graphics.Canvas;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
@@ -18,29 +17,9 @@ namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
return stream;
}
public static RandomAccessStreamReference ByteArrayToRandomAccessStreamReference(byte[] bytes)
{
using var stream = new InMemoryRandomAccessStream();
using var writer = new DataWriter(stream);
writer.WriteBytes(bytes);
writer.StoreAsync().GetAwaiter().GetResult();
writer.FlushAsync().GetAwaiter().GetResult();
writer.DetachStream();
return RandomAccessStreamReference.CreateFromStream(stream);
}
public static async Task<IRandomAccessStream> GetAlbumArtPlaceholderAsync()
{
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(PathHelper.AlbumArtPlaceholderPath));
StorageFile file = await StorageFile.GetFileFromPathAsync(PathHelper.AlbumArtPlaceholderPath);
IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read);
return stream;
}
@@ -65,59 +44,6 @@ namespace BetterLyrics.WinUI3.Helper
};
}
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();
var count = bitmapDecoder.PixelWidth * bitmapDecoder.PixelHeight;
var vector = new Dictionary<Vector3, int>();
for (int i = 0; i < count; i += 10)
{
var offset = i * 4;
var b = pixels[offset];
var g = pixels[offset + 1];
var r = pixels[offset + 2];
var a = pixels[offset + 3];
if (a == 0) continue;
var color = new Vector3(r, g, b);
if (vector.ContainsKey(color))
{
vector[color]++;
}
else
{
vector[color] = 1;
}
}
return vector;
}
//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<IBuffer> ToBufferAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
@@ -127,26 +53,8 @@ namespace BetterLyrics.WinUI3.Helper
return buffer;
}
public static double 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 (double)(sum / (pixels.Length / 4));
}
public static async Task<BitmapDecoder> MakeSquareWithThemeColor(IBuffer buffer, PaletteGeneratorType generatorType)
{
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(buffer);
var decoder = await BitmapDecoder.CreateAsync(stream);
@@ -183,69 +91,6 @@ namespace BetterLyrics.WinUI3.Helper
}
public static async Task<IBuffer> Resize(IBuffer buffer, int size)
{
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(buffer);
var decoder = await BitmapDecoder.CreateAsync(stream);
var factor = Math.Max((double)size / decoder.PixelWidth, (double)size / decoder.PixelHeight);
var width = (uint)(decoder.PixelWidth * factor);
var height = (uint)(decoder.PixelHeight * factor);
if (factor > 1)
{
var transform = new BitmapTransform()
{
ScaledWidth = width,
ScaledHeight = height,
InterpolationMode = BitmapInterpolationMode.Fant
};
var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Rgba8,
BitmapAlphaMode.Straight,
transform, ExifOrientationMode.RespectExifOrientation,
ColorManagementMode.ColorManageToSRgb);
var pixels = pixelData.DetachPixelData();
stream.Seek(0);
stream.Size = 0;
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
encoder.SetPixelData(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Straight, width, height, 96, 96, pixels);
await encoder.FlushAsync();
var output = new Windows.Storage.Streams.Buffer((uint)stream.Size);
stream.Seek(0);
await stream.ReadAsync(output, (uint)stream.Size, InputStreamOptions.None);
return output;
}
else
{
var transform = new BitmapTransform()
{
ScaledWidth = (uint)width,
ScaledHeight = (uint)height,
InterpolationMode = BitmapInterpolationMode.NearestNeighbor
};
var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Rgba8,
BitmapAlphaMode.Straight,
transform, ExifOrientationMode.RespectExifOrientation,
ColorManagementMode.ColorManageToSRgb);
var pixels = pixelData.DetachPixelData();
stream.Seek(0);
stream.Size = 0;
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
encoder.SetPixelData(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Straight, width, height, 96, 96, pixels);
await encoder.FlushAsync();
var output = new Windows.Storage.Streams.Buffer((uint)stream.Size);
stream.Seek(0);
await stream.ReadAsync(output, (uint)stream.Size, InputStreamOptions.None);
return output;
}
}
public static byte[] GenerateNoiseBGRA(int width, int height)
{
var random = new Random();
@@ -335,5 +180,10 @@ namespace BetterLyrics.WinUI3.Helper
return null;
}
}
public static IRandomAccessStream ToIRandomAccessStream(IBuffer buffer)
{
return buffer.AsStream().AsRandomAccessStream();
}
}
}

View File

@@ -14,6 +14,9 @@ namespace BetterLyrics.WinUI3.Helper
private static readonly RankedLanguageIdentifier _identifier;
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public const string ChineseCode = "zh";
public const string JapaneseCode = "ja";
public static List<ExtendedLanguage> SupportedTranslationTargetLanguages { get; set; } =
[
new ExtendedLanguage("ar"),
@@ -105,7 +108,6 @@ namespace BetterLyrics.WinUI3.Helper
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
var guessList = _identifier.Identify(text);
string? code = guessList?.FirstOrDefault()?.Item1.Iso639_2T;
code = code switch

View File

@@ -1,517 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Parsers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace BetterLyrics.WinUI3.Helper
{
public partial class LyricsParser
{
public List<LyricsData> LyricsDataArr { get; private set; } = [];
public void Parse(List<MappedSongSearchQuery> mappedSongSearchQueries, string title, string artist, string album, string? raw, int? durationMs, LyricsSearchProvider? lyricsSearchProvider)
{
var overridenTitle = title;
var overridenArtist = artist;
var overridenAlbum = album;
var found = mappedSongSearchQueries
.Where(x => x.OriginalTitle == overridenTitle && x.OriginalArtist == overridenArtist && x.OriginalAlbum == overridenAlbum)
.FirstOrDefault();
if (found != null)
{
overridenTitle = found.MappedTitle;
overridenArtist = found.MappedArtist;
overridenAlbum = found.MappedAlbum;
}
LyricsDataArr = [];
durationMs ??= (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
if (raw == null)
{
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder(durationMs.Value));
}
else
{
switch (raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw);
break;
case LyricsFormat.Qrc:
ParseQrcKrc(QrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Krc:
ParseQrcKrc(KrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(raw);
break;
default:
break;
}
}
FillRomanizationLyricsData();
FillTranslationFromCache(overridenTitle, overridenArtist, overridenAlbum, lyricsSearchProvider);
}
private void FillTranslationFromCache(string title, string artist, string album, LyricsSearchProvider? provider)
{
string? translationRaw = null;
switch (provider)
{
case LyricsSearchProvider.QQ:
translationRaw = FileHelper.ReadLyricsCache(title, artist, album, LyricsFormat.Lrc, PathHelper.QQTranslationCacheDirectory);
break;
case LyricsSearchProvider.Kugou:
translationRaw = FileHelper.ReadLyricsCache(title, artist, album, LyricsFormat.Lrc, PathHelper.KugouTranslationCacheDirectory);
break;
case LyricsSearchProvider.Netease:
translationRaw = FileHelper.ReadLyricsCache(title, artist, album, LyricsFormat.Lrc, PathHelper.NeteaseTranslationCacheDirectory);
break;
case LyricsSearchProvider.LrcLib:
break;
case LyricsSearchProvider.AmllTtmlDb:
break;
case LyricsSearchProvider.LocalMusicFile:
break;
case LyricsSearchProvider.LocalLrcFile:
break;
case LyricsSearchProvider.LocalEslrcFile:
break;
case LyricsSearchProvider.LocalTtmlFile:
break;
default:
break;
}
if (translationRaw != null)
{
switch (provider)
{
case LyricsSearchProvider.QQ:
case LyricsSearchProvider.Kugou:
case LyricsSearchProvider.Netease:
ParseLrc(translationRaw);
break;
default:
break;
}
}
}
private void FillRomanizationLyricsData()
{
var chinese = LyricsDataArr.Where(x => x.LanguageCode == "zh").FirstOrDefault();
if (chinese != null)
{
LyricsDataArr.Add(new LyricsData
{
LanguageCode = PhoneticHelper.PinyinCode,
LyricsLines = chinese.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = PhoneticHelper.ToPinyin(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToPinyin(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
LyricsDataArr.Add(new LyricsData
{
LanguageCode = PhoneticHelper.JyutpingCode,
LyricsLines = chinese.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = PhoneticHelper.ToJyutping(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToJyutping(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
var japanese = LyricsDataArr.Where(x => x.LanguageCode == "ja").FirstOrDefault();
if (japanese != null)
{
LyricsDataArr.Add(new LyricsData
{
LanguageCode = PhoneticHelper.RomajiCode,
LyricsLines = japanese.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = PhoneticHelper.ToRomaji(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToRomaji(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
}
private void ParseLrc(string raw)
{
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
new List<(int time, string text, List<(int time, string text)> syllables)>();
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
var syllableRegex = SyllableRegex();
foreach (var line in lines)
{
var matches = syllableRegex.Matches(line);
var syllables = new List<(int, string)>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
int min = int.Parse(m.Groups[2].Value);
int sec = int.Parse(m.Groups[3].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
int totalMs = min * 60_000 + sec * 1000 + ms;
string text = m.Groups[6].Value;
syllables.Add((totalMs, text));
}
if (syllables.Count > 1)
{
lrcLines.Add(
(
syllables[0].Item1,
string.Concat(syllables.Select(s => s.Item2)),
syllables
)
);
}
else
{
// 普通LRC行
Regex? bracketRegex = LrcRegex();
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 0)
{
var m = bracketMatches![0];
int min = int.Parse(m.Groups[1].Value);
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex!.Replace(line, "");
if (content == "//") content = "";
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
}
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = 0;
if (grouped != null && grouped.Count > 0)
{
// 计算最大语言数量
languageCount = grouped.Max(g => g.Count());
}
// 初始化每种语言的歌词列表
//LyricsDataArr.Clear();
int langStartIndex = LyricsDataArr.Count;
for (int i = 0; i < languageCount; i++) LyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
if (grouped != null)
{
foreach (var group in grouped)
{
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
// 只添加有对应行的语言,否则跳过
if (langIdx < linesInGroup.Count)
{
var (start, text, syllables) = linesInGroup[langIdx];
var line = new LyricsLine
{
StartMs = start,
OriginalText = text,
LyricsChars = [],
};
if (syllables != null && syllables.Count > 0)
{
int currentIndex = 0;
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int startIndex = currentIndex;
line.LyricsChars.Add(
new LyricsChar
{
StartMs = charStart,
Text = charText ?? "",
StartIndex = startIndex,
}
);
currentIndex += charText?.Length ?? 0;
}
}
LyricsDataArr[langStartIndex + langIdx].LyricsLines.Add(line);
}
// 没有翻译行则不补原文,直接跳过
}
}
}
}
private void ParseTtml(string raw)
{
try
{
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 只获取一级span且排除 ttm:role="x-bg" 的 span 和 ttm:role="x-roman"
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-bg" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-roman")
.ToList();
// 原文和翻译分离
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-translation")
.ToList();
var translationTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
// 处理原文span后的空白
for (int i = 0; i < originalTextSpans.Count; i++)
{
var span = originalTextSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += textNode.Value;
}
}
// 拼接空白字符后的原文
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
var originalCharTimings = new List<LyricsChar>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
originalCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
originalText = p.Value;
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = originalText,
LyricsChars = originalCharTimings,
});
// 翻译
string translationText = string.Concat(translationTextSpans.Select(s => s.Value));
var translationCharTimings = new List<LyricsChar>();
int translationStartIndex = 0;
foreach (var span in translationTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
translationCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = translationStartIndex,
Text = span.Value
});
translationStartIndex += span.Value.Length;
}
if (translationTextSpans.Count > 0)
{
translationLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = translationText,
LyricsChars = translationCharTimings,
});
}
}
LyricsDataArr.Add(new LyricsData(originalLines));
if (translationLines.Count > 0)
LyricsDataArr.Add(new LyricsData(translationLines));
}
catch
{
// 解析失败,忽略
}
}
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
private void ParseQrcKrc(List<Lyricify.Lyrics.Models.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 = lineRead.EndTime ?? 0,
OriginalText = lineRead.Text,
LyricsChars = [],
};
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;
if (syllables != null)
{
int startIndex = 0;
for (
int syllableIndex = 0;
syllableIndex < syllables.Count;
syllableIndex++
)
{
var syllable = syllables[syllableIndex];
var charTiming = new LyricsChar
{
StartMs = syllable.StartTime,
EndMs = syllable.EndTime,
Text = syllable.Text,
StartIndex = startIndex,
};
lineWrite.LyricsChars.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
lyricsLines.Add(lineWrite);
}
}
LyricsDataArr.Add(new LyricsData(lyricsLines));
}
[GeneratedRegex(@"\[(\d*):(\d*)(\.|\:)(\d*)\]")]
private static partial Regex LrcRegex();
[GeneratedRegex(@"(\[|\<)(\d*):(\d*)\.(\d*)(\]|\>)([^\[\]\<\>]*)")]
private static partial Regex SyllableRegex();
}
}

View File

@@ -0,0 +1,121 @@
using BetterLyrics.WinUI3.Models;
using F23.StringSimilarity;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static partial class MetadataComparer
{
private const double WeightTitle = 0.40;
private const double WeightArtist = 0.40;
private const double WeightAlbum = 0.10;
private const double WeightDuration = 0.10;
// JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new JaroWinkler();
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
{
if (local == null || remote == null) return 0;
double titleScore = GetStringSimilarity(local.Title, remote.Title);
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
double albumScore = GetStringSimilarity(local.Album, remote.Album);
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration);
double totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
(albumScore * WeightAlbum) +
(durationScore * WeightDuration);
return (int)Math.Round(totalScore * 100);
}
public static int CalculateScore(SongInfo songInfo, string filePathOrName)
{
if (songInfo == null || string.IsNullOrWhiteSpace(filePathOrName)) return 0;
string fileName = Path.GetFileNameWithoutExtension(filePathOrName);
string fileFingerprint = CreateSortedFingerprint(fileName);
var infoParts = new List<string>();
if (!string.IsNullOrEmpty(songInfo.Title))
infoParts.Add(songInfo.Title);
if (songInfo.Artists != null)
infoParts.AddRange(songInfo.Artists);
string infoRaw = string.Join(" ", infoParts);
string infoFingerprint = CreateSortedFingerprint(infoRaw);
double score = _algo.Similarity(infoFingerprint, fileFingerprint);
return (int)Math.Round(score * 100);
}
private static double GetStringSimilarity(string? s1, string? s2)
{
s1 = s1?.Trim().ToLowerInvariant() ?? "";
s2 = s2?.Trim().ToLowerInvariant() ?? "";
if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 1.0; // 都是空,视为匹配
if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2)) return 0.0; // 其中一个为空
return _algo.Similarity(s1, s2);
}
private static double GetArtistSimilarity(string[]? localArtists, string[]? remoteArtists)
{
if (localArtists == null || localArtists.Length == 0) return 0.0;
if (remoteArtists == null || remoteArtists.Length == 0) return 0.0;
// 将艺术家数组排序并连接,避免顺序不同导致的不匹配
var s1 = string.Join(" ", localArtists.OrderBy(a => a).Select(a => a.Trim().ToLowerInvariant()));
var s2 = string.Join(" ", remoteArtists.OrderBy(a => a).Select(a => a.Trim().ToLowerInvariant()));
return _algo.Similarity(s1, s2);
}
private static double GetDurationSimilarity(double localMs, double? remoteSeconds)
{
if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配
double localSeconds = localMs / 1000.0;
double diff = Math.Abs(localSeconds - remoteSeconds.Value);
// 差距 <= 3秒100% 相似
// 差距 >= 20秒0% 相似
// 中间线性插值
const double PerfectTolerance = 3.0;
const double MaxTolerance = 20.0;
if (diff <= PerfectTolerance) return 1.0;
if (diff >= MaxTolerance) return 0.0;
return 1.0 - ((diff - PerfectTolerance) / (MaxTolerance - PerfectTolerance));
}
private static string CreateSortedFingerprint(string input)
{
if (string.IsNullOrWhiteSpace(input)) return "";
input = input.ToLowerInvariant();
string cleaned = NonWordCharactersRegex().Replace(input, " ");
var tokens = cleaned.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.OrderBy(t => t); // 排序
return string.Join(" ", tokens);
}
[GeneratedRegex(@"[\p{P}\p{S}]")]
private static partial Regex NonWordCharactersRegex();
}
}

View File

@@ -1,18 +1,12 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.WinUI.Helpers;
using Windows.ApplicationModel;
namespace BetterLyrics.WinUI3.Helper
{
public static class MetadataHelper
{
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
public static string AppVersion => Package.Current.Id.Version.ToFormattedString();
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.IO;
using System.Reflection;
namespace BetterLyrics.WinUI3.Helper
{
public static class ObjectHelper
{
// Credit/Copyright to https://gist.github.com/tcartwright/dab50ebaff7c59f05013de0fb349cabd
public static bool IsDisposed(this IDisposable obj)
{
/*
TIM C: This hacky code is because MSFT does not provide a standard way to interrogate if an object is disposed or not.
I wrote this based upon streams, but it should work for many other types of MSFT objects (maybe).
*/
if (obj == null) { return true; }
var objType = obj.GetType();
//var foo = new System.IO.BufferedStream();
// the _disposed pattern should catch a lot of msft objects.... hopefully
var isDisposedField = objType.GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance) ??
objType.GetField("disposed", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return Convert.ToBoolean(isDisposedField.GetValue(obj)); }
isDisposedField = objType.GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return !Convert.ToBoolean(isDisposedField.GetValue(obj)); }
// Windows.Graphics.Imaging.SoftwareBitmap
isDisposedField = objType.GetField("_objRef_global__System_IDisposable", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return !Convert.ToBoolean(isDisposedField.GetValue(obj)); }
// System.IO.FileStream
var strategyField = objType.GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance);
if (strategyField != null)
{
var strategy = strategyField.GetValue(obj);
var isClosedField = strategy.GetType().GetProperty("IsClosed", BindingFlags.NonPublic | BindingFlags.Instance);
if (isClosedField != null) { return Convert.ToBoolean(isClosedField.GetValue(strategy)); }
}
// other streams that use this pattern to determine if they are disposed
if (obj is Stream stream) { return !stream.CanRead && !stream.CanWrite; }
return false;
}
}
}

View File

@@ -12,23 +12,23 @@ namespace BetterLyrics.WinUI3.Helper
/// <param name="value">要保存的值</param>
public static void Save(string resource, string key, string value)
{
var vault = new PasswordVault();
// 删除旧值(避免重复存储)
try
{
var vault = new PasswordVault();
var oldCredential = vault.Retrieve(resource, key);
if (oldCredential != null)
{
vault.Remove(oldCredential);
}
vault.Add(new PasswordCredential(resource, key, value));
}
catch
{
// 没有旧值就忽略
}
vault.Add(new PasswordCredential(resource, key, value));
}
/// <summary>
@@ -39,9 +39,10 @@ namespace BetterLyrics.WinUI3.Helper
/// <returns>存储的值,若不存在则返回 null</returns>
public static string? Get(string resource, string key)
{
var vault = new PasswordVault();
try
{
var vault = new PasswordVault();
var credential = vault.Retrieve(resource, key);
credential.RetrievePassword();
return credential.Password;
@@ -57,9 +58,10 @@ namespace BetterLyrics.WinUI3.Helper
/// </summary>
public static void Delete(string resource, string key)
{
var vault = new PasswordVault();
try
{
var vault = new PasswordVault();
var credential = vault.Retrieve(resource, key);
vault.Remove(credential);
}

View File

@@ -15,7 +15,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string LanguageProfilePath => Path.Combine(AssetsFolder, "Wiki82.profile.xml");
public static string LogoPath => Path.Combine(AssetsFolder, "Logo.ico");
public static string AlbumArtPlaceholderPath => "ms-appx:///Assets/AlbumArtPlaceholder.png";
public static string AlbumArtPlaceholderPath => Path.Combine(AssetsFolder, "AlbumArtPlaceholder.png");
public static string AIMPLogoPath => Path.Combine(AssetsFolder, "AIMP.png");
public static string Foobar2000LogoPath => Path.Combine(AssetsFolder, "foobar2000.png");
public static string MusicBeeLogoPath => Path.Combine(AssetsFolder, "MusicBee.png");
@@ -45,14 +45,9 @@ namespace BetterLyrics.WinUI3.Helper
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
public static string AppleMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "apple-music");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.json");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
public static string TranslationCacheDirectory => Path.Combine(CacheFolder, "translations");
public static string QQTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "qq");
public static string NeteaseTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "netease");
public static string KugouTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "kugou");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
@@ -71,11 +66,7 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(AppleMusicCacheDirectory);
Directory.CreateDirectory(QQTranslationCacheDirectory);
Directory.CreateDirectory(NeteaseTranslationCacheDirectory);
Directory.CreateDirectory(KugouTranslationCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}
}
}
}

View File

@@ -8,13 +8,13 @@ namespace BetterLyrics.WinUI3.Helper
{
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public const string PinyinCode = "zh-pinyin";
public const string JyutpingCode = "zh-jyutping";
public const string RomajiCode = "ja-romaji";
public const string PinyinCode = "zh-cmn-pinyin";
public const string JyutpingCode = "zh-yue-jyutping";
public const string RomanCode = "ja-latin";
public static bool IsPhoneticCode(string code)
{
return code == PinyinCode || code == JyutpingCode || code == RomajiCode;
return code == PinyinCode || code == JyutpingCode || code == RomanCode;
}
public static string GetDisplayName(string code)
@@ -25,7 +25,7 @@ namespace BetterLyrics.WinUI3.Helper
return _resourceService.GetLocalizedString("Pinyin");
case JyutpingCode:
return _resourceService.GetLocalizedString("Jyutping");
case RomajiCode:
case RomanCode:
return _resourceService.GetLocalizedString("Romaji");
default:
throw new ArgumentOutOfRangeException(nameof(code));

View File

@@ -1,4 +1,6 @@
using System;
using BetterLyrics.WinUI3.Hooks;
using DevWinUI;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Storage;
@@ -10,7 +12,7 @@ namespace BetterLyrics.WinUI3.Helper
{
public static async Task<StorageFolder?> PickSingleFolderAsync<T>()
{
var window = WindowHelper.GetWindowByWindowType<T>();
var window = WindowHook.GetWindow<T>();
if (window == null) return null;
var picker = new Windows.Storage.Pickers.FolderPicker();
@@ -26,7 +28,7 @@ namespace BetterLyrics.WinUI3.Helper
public static async Task<StorageFile?> PickSingleFileAsync<T>(string[] fileTypeFilter)
{
var window = WindowHelper.GetWindowByWindowType<T>();
var window = WindowHook.GetWindow<T>();
if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileOpenPicker();
@@ -42,7 +44,7 @@ namespace BetterLyrics.WinUI3.Helper
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices)
{
var window = WindowHelper.GetWindowByWindowType<T>();
var window = WindowHook.GetWindow<T>();
if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileSavePicker();

View File

@@ -0,0 +1,85 @@
using BetterLyrics.WinUI3.Constants;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class PlayerIDHelper
{
private static readonly List<string> neteaseFamilyRegex =
[
"cloudmusic.exe", //NetEaseCloudMusic
"^17588BrandonWong\\.LyricEase_", //LyricEase
"^48848aaaaaaccd\\.HyPlayer_" //HyPlayer
];
public static bool IsNeteaseFamily(string? id)
{
if (id is null) return false;
foreach (var regex in neteaseFamilyRegex)
{
var isMatch = Regex.IsMatch(id, regex);
if (isMatch) return true;
}
return false;
}
public static bool IsLXMusic(string? id) => id is PlayerID.LXMusic or PlayerID.LXMusicPortable;
public static bool IsAppleMusic(string? id) => id is PlayerID.AppleMusic or PlayerID.AppleMusicAlternative;
public static string? GetDisplayName(string? id) => id switch
{
PlayerID.Spotify => PlayerName.Spotify,
PlayerID.AppleMusic => PlayerName.AppleMusic,
PlayerID.iTunes => PlayerName.iTunes,
PlayerID.KugouMusic => PlayerName.KugouMusic,
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerID.QQMusic => PlayerName.QQMusic,
PlayerID.LXMusic => PlayerName.LXMusic,
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerID.AIMP => PlayerName.AIMP,
PlayerID.Foobar2000 => PlayerName.Foobar2000,
PlayerID.MusicBee => PlayerName.MusicBee,
PlayerID.PotPlayer => PlayerName.PotPlayer,
PlayerID.Chrome => PlayerName.Chrome,
PlayerID.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindows => PlayerName.SaltPlayerForWindows,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1,
_ => id,
};
public static string GetLogoPath(string? id) => id switch
{
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerID.QQMusic => PathHelper.QQMusicLogoPath,
PlayerID.LXMusic => PathHelper.LXMusicLogoPath,
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerID.AIMP => PathHelper.AIMPLogoPath,
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerID.Chrome => PathHelper.ChromeLogoPath,
PlayerID.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindows => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath,
_ => PathHelper.UnknownPlayerLogoPath,
};
}
}

View File

@@ -1,30 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class PlayerIdMatcher
{
private static readonly List<string> neteaseFamilyRegex =
[
"cloudmusic.exe", //NetEaseCloudMusic
"^17588BrandonWong\\.LyricEase_", //LyricEase
"^48848aaaaaaccd\\.HyPlayer_" //HyPlayer
];
public static bool IsNeteaseFamily(string id)
{
foreach (var regex in neteaseFamilyRegex)
{
var isMatch = Regex.IsMatch(id, regex);
if (isMatch) return true;
}
return false;
}
public static bool IsLXMusic(string? id)
{
return id == Constants.PlayerID.LXMusic || id == Constants.PlayerID.LXMusicPortable;
}
}
}

View File

@@ -1,13 +0,0 @@
using Windows.Foundation;
using Windows.Graphics;
namespace BetterLyrics.WinUI3.Helper
{
public static class PointHelper
{
public static PointInt32 ToPointInt32(this Point point)
{
return new PointInt32((int)point.X, (int)point.Y);
}
}
}

View File

@@ -1,57 +0,0 @@
using Windows.Graphics;
namespace BetterLyrics.WinUI3.Helper
{
public static class RectHelper
{
public static RectInt32 ToRectInt32(this Windows.Foundation.Rect rect)
{
return new RectInt32(
(int)rect.X,
(int)rect.Y,
(int)rect.Width,
(int)rect.Height
);
}
public static Windows.Foundation.Rect WithHeight(this Windows.Foundation.Rect rect, double height)
{
return new Windows.Foundation.Rect(
rect.X,
rect.Y,
rect.Width,
height
);
}
public static Windows.Foundation.Rect WithWidth(this Windows.Foundation.Rect rect, double width)
{
return new Windows.Foundation.Rect(
rect.X,
rect.Y,
width,
rect.Height
);
}
public static Windows.Foundation.Rect WithX(this Windows.Foundation.Rect rect, double x)
{
return new Windows.Foundation.Rect(
x,
rect.Y,
rect.Width,
rect.Height
);
}
public static Windows.Foundation.Rect WithY(this Windows.Foundation.Rect rect, double y)
{
return new Windows.Foundation.Rect(
rect.X,
y,
rect.Width,
rect.Height
);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class STATaskHelper
{
/// <summary>
/// 在一个专用的后台 STA 线程上运行一个函数。
/// </summary>
/// <typeparam name="TResult">返回类型</typeparam>
/// <param name="func">要执行的函数</param>
/// <returns>一个 Task其结果是函数的返回值</returns>
public static Task<TResult> RunAsSTATask<TResult>(Func<TResult> func)
{
var tcs = new TaskCompletionSource<TResult>();
var thread = new Thread(() =>
{
try
{
var result = func();
tcs.SetResult(result);
}
catch (Exception e)
{
tcs.SetException(e);
}
});
// (关键) 设置单元状态为 STA
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return tcs.Task;
}
/// <summary>
/// 在一个专用的后台 STA 线程上运行一个 Action。
/// </summary>
/// <param name="action">要执行的 Action</param>
/// <returns>一个 Task</returns>
public static Task RunAsSTATask(Action action)
{
return RunAsSTATask(() =>
{
action();
return true; // 返回一个虚拟结果
});
}
}
}

View File

@@ -11,5 +11,16 @@ namespace BetterLyrics.WinUI3.Helper
.ToArray())
.ToLowerInvariant();
public static string NewLine = "\n";
public static bool IsSwitchableNormalizedMatch(string source, string q1, string q2)
{
var normFileName = Normalize(source);
var normQ1 = Normalize(q1);
var normQ2 = Normalize(q2);
// 常见两种顺序
return normFileName == normQ1 + normQ2
|| normFileName == normQ2 + normQ1;
}
}
}

View File

@@ -0,0 +1,23 @@
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppNotifications;
using Microsoft.Windows.AppNotifications.Builder;
namespace BetterLyrics.WinUI3.Helper
{
public class ToastHelper
{
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public static void ShowToast(string localizedTitleKey, string? description, InfoBarSeverity severity)
{
AppNotification notification = new AppNotificationBuilder()
.AddText(_resourceService.GetLocalizedString(localizedTitleKey))
.AddText(description)
.BuildNotification();
AppNotificationManager.Default.Show(notification);
}
}
}

View File

@@ -1,26 +0,0 @@
using System.IO;
namespace BetterLyrics.WinUI3.Helper
{
public static class TrackHelper
{
public static string GetParentFolderName(this ATL.Track track)
{
return Directory.GetParent(track.Path)?.Name ?? "";
}
public static string GetParentFolderPath(this ATL.Track track)
{
return Directory.GetParent(track.Path)?.FullName ?? "";
}
public static string GetLyrics(this ATL.Track track)
{
if (track.Path is string path)
{
return TagLib.File.Create(path).Tag.Lyrics;
}
return "";
}
}
}

View File

@@ -1,17 +0,0 @@
using System.Numerics;
namespace BetterLyrics.WinUI3.Helper
{
public static class VectorHelper
{
public static Vector2 WithX(this Vector2 source, float x)
{
return new Vector2(x, source.Y);
}
public static Vector2 WithY(this Vector2 source, float y)
{
return new Vector2(source.X, y);
}
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Vanara.Windows.Shell;
using static Vanara.PInvoke.Shell32;
namespace BetterLyrics.WinUI3.Hooks
{
public class AppHook
{
public static HICON? GetIcon(ShellItem shellItem, int size = 32)
{
HICON hIconCopy = HICON.NULL;
try
{
using Bitmap baseIcon = shellItem.GetImage(new SIZE { Height = size, Width = size }, ShellItemGetImageOptions.ResizeToFit).ToBitmap();
hIconCopy = User32.CopyIcon(baseIcon.GetHicon());
}
catch (Exception) { }
if (hIconCopy.IsNull)
{
User32.DestroyIcon(hIconCopy);
return null;
}
else
{
return hIconCopy;
}
}
public static async Task<BitmapImage?> ToBitmapImageAsync(HICON hIcon)
{
if (hIcon.IsNull)
{
return null;
}
using Icon icon = Icon.FromHandle(hIcon.DangerousGetHandle());
using Bitmap bitmap = icon.ToBitmap();
using var memoryStream = new MemoryStream();
bitmap.Save(memoryStream, ImageFormat.Png);
memoryStream.Seek(0, SeekOrigin.Begin);
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(memoryStream.AsRandomAccessStream());
User32.DestroyIcon(hIcon);
return bitmapImage;
}
public static ShellItem? GetShellItem(string aumid)
{
string path = $"shell:AppsFolder\\{aumid}";
if (Path.Exists(path))
{
return new ShellItem(path);
}
else
{
var shellFolder = new ShellFolder(KNOWNFOLDERID.FOLDERID_AppsFolder);
var found = shellFolder.FirstOrDefault(x => x.ParsingName?.EndsWith(aumid) == true);
return found;
}
}
public static string? GetDisplayName(ShellItem shellItem) => shellItem.GetDisplayName(ShellItemDisplayString.NormalDisplay);
}
}

View File

@@ -3,9 +3,9 @@ using System;
using System.Collections.Generic;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3.Helper
namespace BetterLyrics.WinUI3.Hooks
{
public class ForegroundWindowWatcher
public class ForegroundWindowHook
{
private readonly User32.WinEventProc _winEventDelegate;
private readonly List<User32.HWINEVENTHOOK> _hooks = new();
@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.Helper
private readonly DispatcherTimer _timer;
public ForegroundWindowWatcher(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
public ForegroundWindowHook(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;

View File

@@ -5,9 +5,9 @@ using Vanara.PInvoke;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.Helper
namespace BetterLyrics.WinUI3.Hooks
{
public class GlobalHotKeyHelper
public class GlobalHotKeyHook
{
private static Dictionary<int, Action> _actions = [];
private static Dictionary<int, List<string>> _keys = [];
@@ -23,7 +23,7 @@ namespace BetterLyrics.WinUI3.Helper
{
if (keys.Count == 0) return;
var window = WindowHelper.GetWindowByWindowType<T>();
var window = WindowHook.GetWindow<T>();
if (window == null) return;
HWND hwnd = WindowNative.GetWindowHandle(window);
@@ -62,7 +62,7 @@ namespace BetterLyrics.WinUI3.Helper
private static void UnregisterHotKey<T>(ShortcutID id)
{
var window = WindowHelper.GetWindowByWindowType<T>();
var window = WindowHook.GetWindow<T>();
if (window == null) return;
HWND hwnd = WindowNative.GetWindowHandle(window);

View File

@@ -5,9 +5,9 @@ using System.Runtime.InteropServices;
using Vanara.PInvoke;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.Helper
namespace BetterLyrics.WinUI3.Hooks
{
public static class MonitorHelper
public static class MonitorHook
{
public static IEnumerable<string> GetAllMonitorDeviceNames()
{

View File

@@ -2,9 +2,9 @@
using NAudio.CoreAudioApi;
using System;
namespace BetterLyrics.WinUI3.Helper
namespace BetterLyrics.WinUI3.Hooks
{
public static class SystemVolumeHelper
public static class SystemVolumeHook
{
private static MMDeviceEnumerator? _deviceEnumerator;
private static MMDevice? _defaultDevice;
@@ -15,7 +15,7 @@ namespace BetterLyrics.WinUI3.Helper
/// </summary>
public static event EventHandler<int>? VolumeNotification;
static SystemVolumeHelper()
static SystemVolumeHook()
{
_deviceEnumerator = new MMDeviceEnumerator();
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);

View File

@@ -1,6 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Views;
@@ -18,9 +19,9 @@ using Windows.Foundation;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
namespace BetterLyrics.WinUI3.Hooks
{
public static class WindowHelper
public static class WindowHook
{
private static List<object> _activeWindows = [];
private static List<object> _workAreas = [];
@@ -54,7 +55,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static T? GetWindowByWindowType<T>()
public static T? GetWindow<T>()
{
foreach (var window in _activeWindows)
{
@@ -72,7 +73,19 @@ namespace BetterLyrics.WinUI3.Helper
{
return frameworkElement.XamlRoot.ContentIslandEnvironment.AppWindowId.GetWindowHandle();
}
return null;
else if (obj != null)
{
return WindowNative.GetWindowHandle(obj);
}
else
{
return null;
}
}
public static IntPtr? GetWindowHandle<T>()
{
return GetWindowHandle(GetWindow<T>());
}
public static void OpenOrShowWindow<T>()
@@ -123,6 +136,17 @@ namespace BetterLyrics.WinUI3.Helper
lyricsWindow.ViewModel.InitFgWindowWatcher();
_mediaSessionsService.InitPlaybackShortcuts();
//TaskbarList.ThumbBarAddButtons(hwnd,
// [
// new Shell32.THUMBBUTTON()
// {
// szTip = "Previous",
// dwFlags = Shell32.THUMBBUTTONFLAGS.THBF_ENABLED,
// dwMask = Shell32.THUMBBUTTONMASK.THB_TOOLTIP | Shell32.THUMBBUTTONMASK.THB_FLAGS,
// }
// ]
//);
}
}
else
@@ -188,7 +212,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void SetIsClickThrough<T>(bool enable)
{
Window? window = GetWindowByWindowType<T>() as Window;
Window? window = GetWindow<T>() as Window;
if (window == null) return;
IntPtr hwnd = WindowNative.GetWindowHandle(window);
@@ -205,7 +229,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void SetIsWorkArea<T>(bool enable)
{
Window? window = GetWindowByWindowType<T>() as Window;
Window? window = GetWindow<T>() as Window;
if (window == null) return;
IntPtr hwnd = WindowNative.GetWindowHandle(window);
@@ -222,7 +246,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void SetIsBorderless<T>(bool enable)
{
var window = GetWindowByWindowType<T>() as Window;
var window = GetWindow<T>() as Window;
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
@@ -239,7 +263,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void SetIsShowInSwitchers<T>(bool enable)
{
var window = GetWindowByWindowType<T>() as Window;
var window = GetWindow<T>() as Window;
if (window == null) return;
window.AppWindow.IsShownInSwitchers = enable;
@@ -247,7 +271,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void SetIsAlwaysOnTop<T>(bool enable)
{
var window = GetWindowByWindowType<T>() as Window;
var window = GetWindow<T>() as Window;
if (window == null) return;
if (window.AppWindow.Presenter is OverlappedPresenter presenter)
@@ -258,7 +282,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void MoveAndResize<T>(Rect rect)
{
var window = GetWindowByWindowType<T>() as Window;
var window = GetWindow<T>() as Window;
if (window == null) return;
window.AppWindow.Move(new Windows.Graphics.PointInt32((int)rect.X, (int)rect.Y));
@@ -269,7 +293,7 @@ namespace BetterLyrics.WinUI3.Helper
{
if (typeof(T) == typeof(LyricsWindow))
{
LyricsWindow? lyricsWindow = GetWindowByWindowType<LyricsWindow>();
LyricsWindow? lyricsWindow = GetWindow<LyricsWindow>();
lyricsWindow?.SetTitleBarArea(titleBarArea);
}
else
@@ -326,7 +350,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void UpdateWorkArea<T>()
{
var window = GetWindowByWindowType<T>() as Window;
var window = GetWindow<T>() as Window;
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
@@ -370,10 +394,10 @@ namespace BetterLyrics.WinUI3.Helper
_setLyricsWindowVisibilityByPlayingStatusTimer.Debounce(() =>
{
var window = GetWindowByWindowType<LyricsWindow>();
var window = GetWindow<LyricsWindow>();
if (window == null) return;
if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && !_mediaSessionsService.IsPlaying)
if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && !_mediaSessionsService.CurrentIsPlaying)
{
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
{
@@ -383,7 +407,7 @@ namespace BetterLyrics.WinUI3.Helper
}
HideWindow<LyricsWindow>();
}
else if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && _mediaSessionsService.IsPlaying)
else if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && _mediaSessionsService.CurrentIsPlaying)
{
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
{

View File

@@ -17,6 +17,7 @@ namespace BetterLyrics.WinUI3.Models
get => field ?? LanguageHelper.DetectLanguageCode(WrappedOriginalText);
set => field = value;
}
public bool AutoGenerated { get; set; } = false;
public string WrappedOriginalText => string.Join(StringHelper.NewLine, LyricsLines.Select(line => line.OriginalText));
public bool IsWordByWord => LyricsLines.Any(x => x.LyricsChars.Count != 0);

View File

@@ -1,6 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;

View File

@@ -1,16 +1,60 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using NTextCat.Commons;
using System;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsSearchResult
public partial class LyricsSearchResult : ObservableObject, ICloneable
{
public bool IsFound => !string.IsNullOrEmpty(Raw);
public LyricsSearchProvider? Provider { get; set; }
public LyricsSearchProvider Provider { get; set; }
public string? Raw { get; set; }
/// <summary>
/// 翻译也可能位于 <see cref="Raw"/>
/// </summary>
public string? Translation { get; set; }
/// <summary>
/// 音译也可能位于 <see cref="Raw"/>
/// </summary>
public string? Transliteration { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
public string[]? Artists { get; set; }
public string? Album { get; set; }
public double? Duration { get; set; }
[ObservableProperty] public partial int MatchPercentage { get; set; } = -1;
[ObservableProperty] public partial string Reference { get; set; } = "about:blank";
public bool IsFound => !string.IsNullOrEmpty(Raw);
public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
public string? DisplayArtists => Artists?.Join("; ");
public object Clone()
{
return new LyricsSearchResult()
{
Album = this.Album,
Duration = this.Duration,
Raw = this.Raw,
Translation = this.Translation,
Title = this.Title,
Artists = this.Artists,
MatchPercentage = this.MatchPercentage,
Provider = this.Provider,
Reference = this.Reference
};
}
public void CopyFromSongInfo(SongInfo songInfo)
{
Title = songInfo.Title;
Artists = songInfo.Artists;
Album = songInfo.Album;
}
}
}

View File

@@ -1,10 +1,8 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using System;
using Windows.Foundation;
@@ -104,10 +102,10 @@ namespace BetterLyrics.WinUI3.Models
public void UpdateMonitorNameAndBounds()
{
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
var lyricsWindow = WindowHook.GetWindow<LyricsWindow>();
if (lyricsWindow == null) return;
var mointor = MonitorHelper.GetMonitorInfoExFromWindow(lyricsWindow);
var mointor = MonitorHook.GetMonitorInfoExFromWindow(lyricsWindow);
MonitorDeviceName = mointor.szDevice;
MonitorBounds = new Rect(
mointor.rcMonitor.Left,
@@ -119,7 +117,7 @@ namespace BetterLyrics.WinUI3.Models
public void UpdateMonitorBounds()
{
var mointor = MonitorHelper.GetMonitorInfoExFromDeviceName(MonitorDeviceName);
var mointor = MonitorHook.GetMonitorInfoExFromDeviceName(MonitorDeviceName);
MonitorBounds = new Rect(
mointor.rcMonitor.Left,
mointor.rcMonitor.Top,
@@ -200,98 +198,4 @@ namespace BetterLyrics.WinUI3.Models
};
}
}
public static class LyricsWindowStatusExtensions
{
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public static LyricsWindowStatus DesktopMode()
{
return new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("DesktopMode"),
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
WindowBounds = new Rect(100, 100, 600, 250),
IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true,
IsBorderless = true,
IsClickThrough = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
EnvironmentSampleMode = WindowPixelSampleMode.WindowEdge,
LyricsStyleSettings = new()
{
LyricsAlignmentType = TextAlignmentType.Center,
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsFluidOverlayEnabled = false,
}
};
}
public static LyricsWindowStatus DockedMode()
{
var status = new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("DockedMode"),
IsWorkArea = true,
IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true,
IsBorderless = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
EnvironmentSampleMode = WindowPixelSampleMode.BelowWindow,
TitleBarArea = TitleBarArea.None,
LyricsStyleSettings = new LyricsStyleSettings
{
LyricsAlignmentType = TextAlignmentType.Center,
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsFluidOverlayEnabled = false,
IsPureColorOverlayEnabled = true,
}
};
status.WindowBounds = status.GetWindowBoundsWhenWorkArea();
return status;
}
public static LyricsWindowStatus FullscreenMode()
{
var status = new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("FullscreenMode"),
IsBorderless = true,
IsAlwaysOnTop = true,
TitleBarArea = TitleBarArea.None,
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
LyricsStyleSettings = new LyricsStyleSettings
{
LyricsAlignmentType = TextAlignmentType.Center,
},
};
status.WindowBounds = status.MonitorBounds;
return status;
}
public static LyricsWindowStatus StandardMode()
{
return new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("StandardMode"),
};
}
public static LyricsWindowStatus NarrowMode()
{
return new LyricsWindowStatus
{
Name = _resourceService.GetLocalizedString("NarrowMode"),
WindowBounds = new Rect(100, 100, 400, 800),
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
};
}
}
}

View File

@@ -1,7 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Collections;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
@@ -16,10 +16,14 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Provider { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLastFMTrackEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsDiscordPresenceEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTimelineSyncEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int TimelineSyncThreshold { get; set; }
/// <summary>
/// Unit: ms
/// </summary>
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PositionOffset { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ResetPositionOffsetOnSongChanged { get; set; } = false;
@@ -27,8 +31,13 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; } = [.. Enum.GetValues<LyricsSearchProvider>().Select(p => new LyricsSearchProviderInfo(p, true))];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; } = [.. Enum.GetValues<AlbumArtSearchProvider>().Select(p => new AlbumArtSearchProviderInfo(p, true))];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchType LyricsSearchType { get; set; } = LyricsSearchType.Sequential;
public bool IsLXMusic => PlayerIdMatcher.IsLXMusic(Provider);
public string LogoPath => PlayerIDHelper.GetLogoPath(Provider);
public string? DisplayName => PlayerIDHelper.GetDisplayName(Provider);
public bool IsLXMusic => PlayerIDHelper.IsLXMusic(Provider);
public MediaSourceProviderInfo()
{
@@ -98,6 +107,5 @@ namespace BetterLyrics.WinUI3.Models
{
OnPropertyChanged(nameof(LyricsSearchProvidersInfo));
}
}
}

View File

@@ -1,4 +1,4 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models.Settings

View File

@@ -12,6 +12,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ShowOrHideLyricsWindowShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "H" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnLyricsWindowClosed { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ListenOnNewPlaybackSource { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IgnoreCacheWhenSearching { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> BorderlessShortcut { get; set; } = new List<string>() { "Ctrl", "Alt", "B" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ClickThroughShortcut { get; set; } = new List<string>() { "Ctrl", "Alt", "C" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> LyricsWindowSwitchShortcut { get; set; } = new List<string>() { "Ctrl", "Alt", "S" };

View File

@@ -26,7 +26,10 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SpectrumPlacement SpectrumPlacement { get; set; } = SpectrumPlacement.Bottom;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsSnowFlakeOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SnowFlakeOverlayAmount { get; set; } = 50;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SnowFlakeOverlayAmount { get; set; } = 10;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SnowFlakeOverlaySpeed { get; set; } = 1;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFogOverlayEnabled { get; set; } = false;
public LyricsBackgroundSettings() { }

View File

@@ -1,22 +1,21 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using NTextCat.Commons;
using System;
namespace BetterLyrics.WinUI3.Models
{
public partial class SongInfo : ObservableObject
public partial class SongInfo : ObservableObject, ICloneable
{
[ObservableProperty]
public partial string Album { get; set; }
[ObservableProperty]
public partial string Artist { get; set; }
public partial string[] Artists { get; set; }
[ObservableProperty]
public partial int? Duration { get; set; }
[ObservableProperty]
public partial double? DurationMs { get; set; }
public partial double DurationMs { get; set; }
[ObservableProperty]
public partial string? PlayerId { get; set; } = null;
@@ -27,16 +26,43 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string? SongId { get; set; } = null;
public SongInfo() { }
}
public string? LinkedFileName { get; set; } = null;
public static class SongInfoExtensions
{
public static SongInfo Placeholder => new()
public double Duration => DurationMs / 1000;
public string DisplayArtists => Artists.Join(ATL.Settings.DisplayValueSeparator.ToString());
public SongInfo() { }
public object Clone()
{
Title = "N/A",
Album = "N/A",
Artist = "N/A",
};
return new SongInfo()
{
Title = this.Title,
Artists = this.Artists,
Album = this.Album,
DurationMs = this.DurationMs,
PlayerId = this.PlayerId,
SongId = this.SongId,
LinkedFileName = this.LinkedFileName,
};
}
public override string ToString()
{
return
$"Title: {Title}, " +
$"Artist: {DisplayArtists}, " +
$"Album: {Album}, " +
$"Duration: {Duration} sec, " +
$"Plauer ID: {PlayerId}, " +
$"Song ID: {SongId}, " +
$"Linked file name: {LinkedFileName}.";
}
public string ToFileName()
{
return $"{DisplayArtists} - {Title} - {Album} - {Duration}";
}
}
}

View File

@@ -0,0 +1,130 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
[GeneratedRegex(@"\[(\d*):(\d*)(\.|\:)(\d*)\]")]
private static partial Regex LrcRegex();
[GeneratedRegex(@"(\[|\<)(\d*):(\d*)\.(\d*)(\]|\>)([^\[\]\<\>]*)")]
private static partial Regex SyllableRegex();
private void ParseLrc(string raw)
{
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
new List<(int time, string text, List<(int time, string text)> syllables)>();
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
var syllableRegex = SyllableRegex();
foreach (var line in lines)
{
var matches = syllableRegex.Matches(line);
var syllables = new List<(int, string)>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
int min = int.Parse(m.Groups[2].Value);
int sec = int.Parse(m.Groups[3].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
int totalMs = min * 60_000 + sec * 1000 + ms;
string text = m.Groups[6].Value;
syllables.Add((totalMs, text));
}
if (syllables.Count > 1)
{
lrcLines.Add(
(
syllables[0].Item1,
string.Concat(syllables.Select(s => s.Item2)),
syllables
)
);
}
else
{
// 普通LRC行
Regex? bracketRegex = LrcRegex();
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 0)
{
var m = bracketMatches[0];
int min = int.Parse(m.Groups[1].Value);
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex!.Replace(line, "").Trim();
if (content == "//") content = "";
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
}
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = 0;
if (grouped != null && grouped.Count > 0)
{
// 计算最大语言数量
languageCount = grouped.Max(g => g.Count());
}
// 初始化每种语言的歌词列表
int langStartIndex = LyricsDataArr.Count;
for (int i = 0; i < languageCount; i++) LyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
if (grouped != null)
{
foreach (var group in grouped)
{
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
// 只添加有对应行的语言,否则跳过
if (langIdx < linesInGroup.Count)
{
var (start, text, syllables) = linesInGroup[langIdx];
var line = new LyricsLine
{
StartMs = start,
OriginalText = text,
LyricsChars = [],
};
if (syllables != null && syllables.Count > 0)
{
int currentIndex = 0;
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int startIndex = currentIndex;
line.LyricsChars.Add(
new LyricsChar
{
StartMs = charStart,
Text = charText ?? "",
StartIndex = startIndex,
}
);
currentIndex += charText?.Length ?? 0;
}
}
LyricsDataArr[langStartIndex + langIdx].LyricsLines.Add(line);
}
// 没有翻译行则不补原文,直接跳过
}
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
using BetterLyrics.WinUI3.Models;
using System.Collections.Generic;
using System.Linq;
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
private void ParseQrcKrc(List<Lyricify.Lyrics.Models.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 = lineRead.EndTime ?? 0,
OriginalText = lineRead.Text,
LyricsChars = [],
};
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;
if (syllables != null)
{
int startIndex = 0;
for (
int syllableIndex = 0;
syllableIndex < syllables.Count;
syllableIndex++
)
{
var syllable = syllables[syllableIndex];
var charTiming = new LyricsChar
{
StartMs = syllable.StartTime,
EndMs = syllable.EndTime,
Text = syllable.Text,
StartIndex = startIndex,
};
lineWrite.LyricsChars.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
lyricsLines.Add(lineWrite);
}
}
LyricsDataArr.Add(new LyricsData(lyricsLines));
}
}
}

View File

@@ -0,0 +1,208 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
private void ParseTtml(string raw)
{
try
{
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
List<LyricsLine> romanLines = [];
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 只获取一级span
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span")
.ToList();
var romanTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-roman")
.ToList();
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == null)
.ToList();
var translationTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
// 处理原文span后的空白
for (int i = 0; i < originalTextSpans.Count; i++)
{
var span = originalTextSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += textNode.Value;
}
}
// 拼接空白字符后的原文
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
var originalCharTimings = new List<LyricsChar>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
originalCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
{
originalText = p.Value;
}
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = originalText,
LyricsChars = originalCharTimings,
});
// 解析 x-role
ParseTtmlXRole(spans, translationLines, "x-translation", pStartMs, pEndMs);
ParseTtmlXRole(spans, romanLines, "x-roman", pStartMs, pEndMs);
}
LyricsDataArr.Add(new LyricsData(originalLines));
if (translationLines.Count > 0)
{
LyricsDataArr.Add(new LyricsData(translationLines));
}
if (romanLines.Count > 0)
{
LyricsDataArr.Add(new LyricsData(romanLines) { LanguageCode = PhoneticHelper.RomanCode });
}
}
catch
{
// 解析失败,忽略
}
}
private void ParseTtmlXRole(List<XElement> sourceSpans, List<LyricsLine> saveLyricsLines, string xRole, int pStartMs, int? pEndMs)
{
var textSpans = sourceSpans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == xRole)
.ToList();
string text = string.Concat(textSpans.Select(s => s.Value));
var charTimings = new List<LyricsChar>();
int startIndex = 0;
foreach (var span in textSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
charTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = startIndex,
Text = span.Value
});
startIndex += span.Value.Length;
}
if (textSpans.Count > 0)
{
saveLyricsLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = text,
LyricsChars = charTimings,
});
}
}
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
}
}

View File

@@ -0,0 +1,164 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Parsers;
using System.Collections.Generic;
using System.Linq;
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
public List<LyricsData> LyricsDataArr { get; private set; } = [];
public void Parse(SongInfo? songInfo, LyricsSearchResult? lyricsSearchResult)
{
LyricsDataArr = [];
if (lyricsSearchResult?.Raw == null)
{
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder((int)(songInfo?.DurationMs ?? 0)));
}
else
{
switch (lyricsSearchResult.Raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(lyricsSearchResult.Raw);
break;
case LyricsFormat.Qrc:
ParseQrcKrc(QrcParser.Parse(lyricsSearchResult.Raw).Lines);
break;
case LyricsFormat.Krc:
ParseQrcKrc(KrcParser.Parse(lyricsSearchResult.Raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(lyricsSearchResult.Raw);
break;
default:
break;
}
}
LoadTranslation(lyricsSearchResult);
LoadTransliteration(lyricsSearchResult);
GenerateTransliterationLyricsData();
}
private void LoadTranslation(LyricsSearchResult? lyricsSearchResult)
{
if (lyricsSearchResult?.Translation != null)
{
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.QQ:
case LyricsSearchProvider.Kugou:
case LyricsSearchProvider.Netease:
ParseLrc(lyricsSearchResult.Translation);
break;
default:
break;
}
}
}
private void LoadTransliteration(LyricsSearchResult? lyricsSearchResult)
{
if (lyricsSearchResult?.Transliteration != null)
{
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.Netease:
ParseLrc(lyricsSearchResult.Transliteration);
LyricsDataArr.LastOrDefault()?.LanguageCode = PhoneticHelper.RomanCode;
break;
default:
break;
}
}
}
/// <summary>
/// 在音译不存在的情况下生成音译歌词
/// </summary>
private void GenerateTransliterationLyricsData()
{
var main = LyricsDataArr.FirstOrDefault();
if (main != null)
{
string? languageCode = main.LanguageCode;
if (languageCode == LanguageHelper.ChineseCode)
{
if (!LyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.PinyinCode))
{
LyricsDataArr.Add(new LyricsData
{
LanguageCode = PhoneticHelper.PinyinCode,
AutoGenerated = true,
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = PhoneticHelper.ToPinyin(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToPinyin(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
if (!LyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.JyutpingCode))
{
LyricsDataArr.Add(new LyricsData
{
LanguageCode = PhoneticHelper.JyutpingCode,
AutoGenerated = true,
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = PhoneticHelper.ToJyutping(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToJyutping(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
}
else if (languageCode == LanguageHelper.JapaneseCode)
{
if (!LyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.RomanCode))
{
LyricsDataArr.Add(new LyricsData
{
LanguageCode = PhoneticHelper.RomanCode,
AutoGenerated = true,
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = PhoneticHelper.ToRomaji(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToRomaji(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
}
}
}
}
}

View File

@@ -1,11 +1,14 @@
using System;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
namespace BetterLyrics.WinUI3.Providers
{
public class AppleMusic
{
@@ -13,6 +16,7 @@ namespace BetterLyrics.WinUI3.Helper
private string _accessToken = "";
private string _storefront = "";
private string _language = "";
private bool _isInited = false;
public AppleMusic()
{
@@ -25,11 +29,18 @@ namespace BetterLyrics.WinUI3.Helper
public async Task<bool> InitAsync()
{
await GetAccessTokenAsync();
await SetMediaUserTokenAsync();
return
!string.IsNullOrEmpty(_accessToken) &&
!string.IsNullOrEmpty(PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey));
if (!_isInited)
{
var mediaUserToken = PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey);
if (!string.IsNullOrEmpty(mediaUserToken))
{
await GetAccessTokenAsync();
await SetMediaUserTokenAsync(mediaUserToken);
_isInited = !string.IsNullOrEmpty(_accessToken);
}
}
return _isInited;
}
private async Task GetAccessTokenAsync()
@@ -46,11 +57,10 @@ namespace BetterLyrics.WinUI3.Helper
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_accessToken}");
}
private async Task SetMediaUserTokenAsync()
private async Task SetMediaUserTokenAsync(string token)
{
_client.DefaultRequestHeaders.Remove("media-user-token");
_client.DefaultRequestHeaders.Add("media-user-token",
PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey));
_client.DefaultRequestHeaders.Add("media-user-token", token);
var resp = await _client.GetStringAsync("https://amp-api.music.apple.com/v1/me/storefront");
var json = JsonSerializer.Deserialize(resp, Serialization.SourceGenerationContext.Default.JsonElement);
_storefront = json.GetProperty("data")[0].GetProperty("id").ToString();
@@ -59,10 +69,8 @@ namespace BetterLyrics.WinUI3.Helper
_client.DefaultRequestHeaders.Add("Accept-Language", $"{_language},en;q=0.9");
}
public async Task<string?> GetLyricsAsync(string title, string artist)
private async Task<string?> GetLyricsAsync(string id)
{
string id = await SearchSongInfoAsync(artist, title);
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/songs/{id}";
var url = apiUrl + $"?include[songs]=lyrics,syllable-lyrics&l={_language}";
var resp = await _client.GetStringAsync(url);
@@ -103,9 +111,14 @@ namespace BetterLyrics.WinUI3.Helper
return null;
}
private async Task<string> SearchSongInfoAsync(string artist, string title)
public async Task<LyricsSearchResult> SearchSongInfoAsync(Models.SongInfo songInfo)
{
var query = $"{artist} {title}";
LyricsSearchResult lyricsSearchResult = new()
{
Provider = Enums.LyricsSearchProvider.AppleMusic
};
var query = $"{songInfo.DisplayArtists} {songInfo.Title}";
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/search";
var url = apiUrl + $"?term={WebUtility.UrlEncode(query)}&types=songs&limit=1&l={_language}";
var resp = await _client.GetStringAsync(url);
@@ -114,9 +127,26 @@ namespace BetterLyrics.WinUI3.Helper
if (results.TryGetProperty("songs", out var songs) && songs.GetProperty("data").GetArrayLength() > 0)
{
var song = songs.GetProperty("data")[0];
return song.GetProperty("id").ToString();
var id = song.GetProperty("id").ToString();
var attr = song.GetProperty("attributes");
lyricsSearchResult.Title = attr.GetProperty("name").ToString();
lyricsSearchResult.Artists = attr.GetProperty("artistName").ToString().SplitByCommonSplitter();
lyricsSearchResult.Album = attr.GetProperty("albumName").ToString();
lyricsSearchResult.Duration = attr.GetProperty("durationInMillis").GetInt32() / 1000.0;
lyricsSearchResult.Reference = $"https://music.apple.com/song/{id}";
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
if (id != null)
{
lyricsSearchResult.Raw = await GetLyricsAsync(id);
}
}
return string.Empty;
return lyricsSearchResult;
}
}
}

View File

@@ -10,6 +10,7 @@ namespace BetterLyrics.WinUI3.Serialization
[JsonSerializable(typeof(TranslateResponse))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(AppSettings))]
[JsonSerializable(typeof(LyricsSearchResult))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class SourceGenerationContext : JsonSerializerContext { }
}

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