Compare commits

...

20 Commits

Author SHA1 Message Date
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
143 changed files with 3713 additions and 2056 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

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

@@ -61,10 +61,13 @@
<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="DiscordRichPresence" Version="1.6.1.70" />
<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" />
@@ -87,8 +90,9 @@
<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="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 +110,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 +125,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 +218,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>

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

@@ -1,4 +1,8 @@
namespace BetterLyrics.WinUI3.Constants
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Constants
{
public class PlayerName
{

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}"
@@ -126,21 +137,67 @@
<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}}" />
<HyperlinkButton
Padding="0"
Content="{x:Bind Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}"
NavigateUri="{x:Bind Reference, FallbackValue=about:blank}" />
<!-- Title -->
<Grid ColumnSpacing="12" Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlTitle" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Artist -->
<Grid ColumnSpacing="12" Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlArtist" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Album -->
<Grid ColumnSpacing="12" Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlAlbum" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Duration -->
<StackPanel
Orientation="Horizontal"
Spacing="6"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock x:Uid="LyricsSearchControlDurauion" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Duration}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="s" />
</StackPanel>
<!-- Match percentage -->
<StackPanel
Orientation="Horizontal"
Spacing="6"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock x:Uid="LyricsPageMatchPercentage" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind MatchPercentage}" />
</StackPanel>
<!-- NOT FOUND -->
<TextBlock
x:Uid="LyricsSearchControlNotFound"
VerticalAlignment="Center"

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"
@@ -238,18 +257,95 @@
<!-- Provider 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 ContentAlignment="Left">
<StackPanel Spacing="6">
<!-- Playback source -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="SettingsPagePlaybackSource" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Playback source ID -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="SettingsPagePlaybackSourceID" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Song title -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlTitle" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Song artists -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlArtist" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Song album -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlAlbum" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Lyrics source -->
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock x:Uid="LyricsPageLyricsProviderPrefix" />
<HyperlinkButton
Padding="0"
Content="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}"
IsEnabled="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.IsFound, Mode=OneWay}"
NavigateUri="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}" />
</StackPanel>
<!-- Translation source -->
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock x:Uid="LyricsPageTranslationProviderPrefix" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.MediaSessionsService.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
</StackPanel>
<!-- Match percentage -->
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Uid="LyricsPageMatchPercentage" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -272,12 +368,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

@@ -1,4 +1,4 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Input;
@@ -89,7 +89,7 @@ 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"));

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

@@ -17,70 +17,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,12 @@
using System;
using System.Collections.Generic;
using System.Text;
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,37 @@
using BetterLyrics.WinUI3.Enums;
using System;
using System.Collections.Generic;
using System.Text;
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,62 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Text;
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,84 @@
using BetterLyrics.WinUI3.Enums;
using NTextCat.Commons;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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,10 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Serialization;
using System;
using System.IO;
using System.Text;
@@ -35,12 +39,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 +64,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

@@ -1,8 +1,10 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Parsers;
using NTextCat.Commons;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,92 +17,48 @@ namespace BetterLyrics.WinUI3.Helper
{
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)
public void Parse(SongInfo? songInfo, LyricsSearchResult? lyricsSearchResult)
{
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)
if (lyricsSearchResult?.Raw == null)
{
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder(durationMs.Value));
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder((int)(songInfo?.DurationMs ?? 0)));
}
else
{
switch (raw.DetectFormat())
switch (lyricsSearchResult.Raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw);
ParseLrc(lyricsSearchResult.Raw);
break;
case LyricsFormat.Qrc:
ParseQrcKrc(QrcParser.Parse(raw).Lines);
ParseQrcKrc(QrcParser.Parse(lyricsSearchResult.Raw).Lines);
break;
case LyricsFormat.Krc:
ParseQrcKrc(KrcParser.Parse(raw).Lines);
ParseQrcKrc(KrcParser.Parse(lyricsSearchResult.Raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(raw);
ParseTtml(lyricsSearchResult.Raw);
break;
default:
break;
}
}
FillRomanizationLyricsData();
FillTranslationFromCache(overridenTitle, overridenArtist, overridenAlbum, lyricsSearchProvider);
FillTranslationFromCache(lyricsSearchResult);
}
private void FillTranslationFromCache(string title, string artist, string album, LyricsSearchProvider? provider)
private void FillTranslationFromCache(LyricsSearchResult? lyricsSearchResult)
{
string? translationRaw = null;
switch (provider)
if (lyricsSearchResult?.Translation != null)
{
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)
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.QQ:
case LyricsSearchProvider.Kugou:
case LyricsSearchProvider.Netease:
ParseLrc(translationRaw);
ParseLrc(lyricsSearchResult.Translation);
break;
default:
break;
@@ -110,7 +68,7 @@ namespace BetterLyrics.WinUI3.Helper
private void FillRomanizationLyricsData()
{
var chinese = LyricsDataArr.Where(x => x.LanguageCode == "zh").FirstOrDefault();
var chinese = LyricsDataArr.FirstOrDefault(x => x.LanguageCode == "zh");
if (chinese != null)
{
LyricsDataArr.Add(new LyricsData
@@ -148,7 +106,7 @@ namespace BetterLyrics.WinUI3.Helper
}).ToList()
});
}
var japanese = LyricsDataArr.Where(x => x.LanguageCode == "ja").FirstOrDefault();
var japanese = LyricsDataArr.FirstOrDefault(x => x.LanguageCode == "ja");
if (japanese != null)
{
LyricsDataArr.Add(new LyricsData
@@ -215,12 +173,12 @@ namespace BetterLyrics.WinUI3.Helper
int? lineStartTime = null;
if (bracketMatches.Count > 0)
{
var m = bracketMatches![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, "");
content = bracketRegex!.Replace(line, "").Trim();
if (content == "//") content = "";
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}

View File

@@ -0,0 +1,96 @@
using BetterLyrics.WinUI3.Models;
using F23.StringSimilarity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BetterLyrics.WinUI3.Helper
{
public static class MetadataComparer
{
// 权重配置 (总和 1.0)
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();
/// <summary>
/// 计算 SongInfo 和 LyricsSearchResult 的相似度 (0-100)
/// </summary>
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
{
if (local == null || remote == null) return 0;
// 1. 标题相似度
double titleScore = GetStringSimilarity(local.Title, remote.Title);
// 2. 艺术家相似度 (需要处理数组顺序)
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
// 3. 专辑相似度
double albumScore = GetStringSimilarity(local.Album, remote.Album);
// 4. 时长相似度 (基于毫秒 vs 秒的转换和容差)
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration);
// 5. 加权汇总
double totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
(albumScore * WeightAlbum) +
(durationScore * WeightDuration);
return (int)Math.Round(totalScore * 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;
// 技巧:将艺术家数组排序并连接,避免顺序不同导致的不匹配
// 例如: ["Jay-Z", "Linkin Park"] 和 ["Linkin Park", "Jay-Z"] 应该是一样的
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));
}
}
}

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

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

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using System;
using System.Linq;
namespace BetterLyrics.WinUI3.Helper
{

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,86 @@
using BetterLyrics.WinUI3.Constants;
using Lyricify.Lyrics.Providers;
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,54 @@
using System;
using System.Collections.Generic;
using System.Text;
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

@@ -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,82 @@
using DevWinUI;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
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;
@@ -13,14 +14,15 @@ using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Vanara.PInvoke;
using Vanara.Windows.Shell;
using Windows.ApplicationModel.Core;
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 +56,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static T? GetWindowByWindowType<T>()
public static T? GetWindow<T>()
{
foreach (var window in _activeWindows)
{
@@ -72,7 +74,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 +137,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 +213,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 +230,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 +247,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 +264,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 +272,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 +283,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 +294,7 @@ namespace BetterLyrics.WinUI3.Helper
{
if (typeof(T) == typeof(LyricsWindow))
{
LyricsWindow? lyricsWindow = GetWindowByWindowType<LyricsWindow>();
LyricsWindow? lyricsWindow = GetWindow<LyricsWindow>();
lyricsWindow?.SetTitleBarArea(titleBarArea);
}
else
@@ -326,7 +351,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 +395,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 +408,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

@@ -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,51 @@
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; }
public string? Translation { 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,11 +1,17 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Collections;
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
@@ -16,10 +22,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 +37,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 +113,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

@@ -1,11 +1,12 @@
using System;
using BetterLyrics.WinUI3.Helper;
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 +14,7 @@ namespace BetterLyrics.WinUI3.Helper
private string _accessToken = "";
private string _storefront = "";
private string _language = "";
private bool _isInited = false;
public AppleMusic()
{
@@ -25,11 +27,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 +55,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 +67,8 @@ namespace BetterLyrics.WinUI3.Helper
_client.DefaultRequestHeaders.Add("Accept-Language", $"{_language},en;q=0.9");
}
public async Task<string?> GetLyricsAsync(string title, string artist)
public 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,7 +109,7 @@ namespace BetterLyrics.WinUI3.Helper
return null;
}
private async Task<string> SearchSongInfoAsync(string artist, string title)
public async Task<string> SearchSongInfoAsync(string artist, string title)
{
var query = $"{artist} {title}";
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/search";

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 { }
}

View File

@@ -2,8 +2,8 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -26,20 +26,20 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public AlbumArtSearchService(ISettingsService settingsService)
public AlbumArtSearchService(ISettingsService settingsService, ILogger<AlbumArtSearchService> logger)
{
_settingsService = settingsService;
_logger = Ioc.Default.GetRequiredService<ILogger<AlbumArtSearchService>>();
_logger = logger;
_iTunesHttpClinet = new();
}
public async Task<IBuffer?> SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token)
public async Task<IBuffer?> SearchAsync(SongInfo songInfo, IBuffer? bufferFromSMTC, CancellationToken token)
{
IBuffer? result = null;
try
{
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == mediaSessionId).FirstOrDefault()?.AlbumArtSearchProvidersInfo ?? [])
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == songInfo.PlayerId)?.AlbumArtSearchProvidersInfo ?? [])
{
if (!provider.IsEnabled)
{
@@ -49,7 +49,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchFile(artist, title)?.AsBuffer();
result = SearchFile(songInfo)?.AsBuffer();
break;
case AlbumArtSearchProvider.SMTC:
result = bufferFromSMTC;
@@ -57,7 +57,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
case AlbumArtSearchProvider.iTunes:
foreach (string countryCode in new List<string>() { "us", "cn", "jp", "kr" })
{
var byteArray = await SearchiTunesAsync(artist, album, title, countryCode);
var byteArray = await SearchiTunesAsync(songInfo, countryCode);
result = byteArray?.AsBuffer();
if (token.IsCancellationRequested) return result;
if (result != null) break;
@@ -77,7 +77,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
return null;
}
private byte[]? SearchFile(string artist, string title)
private byte[]? SearchFile(SongInfo songInfo)
{
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
@@ -88,7 +88,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
{
Track track = new(file);
if ((track.Title == title && track.Artist == artist) || FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), artist, title))
if ((track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists) || StringHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), songInfo.DisplayArtists, songInfo.Title))
{
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
@@ -103,13 +103,13 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
return null;
}
private async Task<byte[]?> SearchiTunesAsync(string artist, string album, string title, string countryCode)
private async Task<byte[]?> SearchiTunesAsync(SongInfo songInfo, string countryCode)
{
// Source: https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce
try
{
string format = ".jpg";
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(artist, album, format, PathHelper.iTunesAlbumArtCacheDirectory);
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(songInfo.DisplayArtists, songInfo.Album, format, PathHelper.iTunesAlbumArtCacheDirectory);
if (cachedAlbumArt != null)
{
@@ -117,7 +117,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
}
// Build the iTunes API URL
string url = $"{Constants.iTunes.QueryPrefix}term=" + WebUtility.UrlEncode($"{artist} {album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
string url = $"{Constants.iTunes.QueryPrefix}term=" + WebUtility.UrlEncode($"{songInfo.Artists} {songInfo.Album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
// Make a request to the API
using HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);
@@ -139,7 +139,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
if (fetched != null && fetched.Length > 0)
{
// Write to cache
FileHelper.WriteAlbumArtCache(artist, album, fetched, format, PathHelper.iTunesAlbumArtCacheDirectory);
FileHelper.WriteAlbumArtCache(songInfo, fetched, format, PathHelper.iTunesAlbumArtCacheDirectory);
return fetched;
}
}
@@ -147,7 +147,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching iTunes album art for {Artist} - {Album}", artist, album);
_logger.LogError(ex, "SearchiTunesAsync");
}
return null;
}

View File

@@ -1,4 +1,5 @@
using System.Threading;
using BetterLyrics.WinUI3.Models;
using System.Threading;
using System.Threading.Tasks;
using Windows.Storage.Streams;
@@ -6,6 +7,6 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
{
public interface IAlbumArtSearchService
{
Task<IBuffer?> SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token);
Task<IBuffer?> SearchAsync(SongInfo songInfo, IBuffer? bufferFromSMTC, CancellationToken token);
}
}

View File

@@ -0,0 +1,49 @@
using BetterLyrics.WinUI3.Models;
using DiscordRPC;
namespace BetterLyrics.WinUI3.Services.DiscordService
{
public class DiscordService : IDiscordService
{
private DiscordRpcClient? _client;
public DiscordService()
{
}
public void Enable()
{
if (_client == null)
{
_client = new DiscordRpcClient(Constants.Discord.AppID);
_client.Initialize();
}
}
public void UpdateRichPresence(SongInfo songInfo)
{
_client?.SetPresence(new RichPresence
{
StatusDisplay = StatusDisplayType.Details,
Type = ActivityType.Listening,
Buttons = new Button[] { new() { Label = "Get this status", Url = Constants.Link.MicrosoftStoreUrl } },
Assets = new Assets
{
LargeImageKey = "banner",
SmallImageKey = "logo"
},
Details = songInfo.Title,
State = songInfo.DisplayArtists,
Timestamps = Timestamps.FromTimeSpan(songInfo.Duration)
});
}
public void Disable()
{
_client?.ClearPresence();
_client?.Dispose();
_client = null;
}
}
}

View File

@@ -0,0 +1,11 @@
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Services.DiscordService
{
public interface IDiscordService
{
void Enable();
void Disable();
void UpdateRichPresence(SongInfo songInfo);
}
}

View File

@@ -1,5 +1,6 @@
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.SettingsService;
@@ -59,7 +60,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
public async Task AuthAsync()
{
var dialogXamlRoot = WindowHelper.GetWindowByWindowType<SettingsWindow>()?.Content.XamlRoot;
var dialogXamlRoot = WindowHook.GetWindow<SettingsWindow>()?.Content.XamlRoot;
if (dialogXamlRoot == null)
{
return;
@@ -86,7 +87,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
public async Task UnAuthAsync()
{
var dialogXamlRoot = WindowHelper.GetWindowByWindowType<SettingsWindow>()?.Content.XamlRoot;
var dialogXamlRoot = WindowHook.GetWindow<SettingsWindow>()?.Content.XamlRoot;
if (dialogXamlRoot == null)
{
return;
@@ -132,7 +133,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
await _client.Track.ScrobbleAsync(new Hqub.Lastfm.Entities.Scrobble
{
Track = songInfo.Title,
Artist = songInfo.Artist,
Artist = songInfo.DisplayArtists,
Date = DateTime.Now,
});
}

View File

@@ -24,7 +24,7 @@ namespace BetterLyrics.WinUI3.Services.LibWatcherService
UpdateWatchers();
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, Extensions.ItemPropertyChangedEventArgs e)
private void LocalMediaFolders_ItemPropertyChanged(object? sender, Collections.ItemPropertyChangedEventArgs e)
{
UpdateWatchers();
}

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
@@ -44,21 +45,21 @@ namespace BetterLyrics.WinUI3.Services.LiveStatesService
LiveStates.LyricsWindowStatus.UpdateMonitorBounds();
WindowHelper.SetIsWorkArea<LyricsWindow>(LiveStates.LyricsWindowStatus.IsWorkArea);
WindowHook.SetIsWorkArea<LyricsWindow>(LiveStates.LyricsWindowStatus.IsWorkArea);
if (LiveStates.LyricsWindowStatus.IsWorkArea)
{
WindowHelper.UpdateWorkArea<LyricsWindow>();
WindowHook.UpdateWorkArea<LyricsWindow>();
}
await Task.Delay(300);
WindowHelper.SetIsShowInSwitchers<LyricsWindow>(LiveStates.LyricsWindowStatus.IsShownInSwitchers);
WindowHelper.SetIsAlwaysOnTop<LyricsWindow>(LiveStates.LyricsWindowStatus.IsAlwaysOnTop);
WindowHook.SetIsShowInSwitchers<LyricsWindow>(LiveStates.LyricsWindowStatus.IsShownInSwitchers);
WindowHook.SetIsAlwaysOnTop<LyricsWindow>(LiveStates.LyricsWindowStatus.IsAlwaysOnTop);
WindowHelper.SetIsClickThrough<LyricsWindow>(LiveStates.LyricsWindowStatus.IsClickThrough);
WindowHelper.SetIsBorderless<LyricsWindow>(LiveStates.LyricsWindowStatus.IsBorderless);
WindowHook.SetIsClickThrough<LyricsWindow>(LiveStates.LyricsWindowStatus.IsClickThrough);
WindowHook.SetIsBorderless<LyricsWindow>(LiveStates.LyricsWindowStatus.IsBorderless);
WindowHelper.SetLyricsWindowVisibilityByPlayingStatus(_dispatcherQueue);
WindowHelper.SetTitleBarArea<LyricsWindow>(LiveStates.LyricsWindowStatus.TitleBarArea);
WindowHook.SetLyricsWindowVisibilityByPlayingStatus(_dispatcherQueue);
WindowHook.SetTitleBarArea<LyricsWindow>(LiveStates.LyricsWindowStatus.TitleBarArea);
// 下述代码可以删除,但是为了避免给用户造成操作上的疑虑,暂时保留
if (LiveStates.LyricsWindowStatus.IsWorkArea)
@@ -66,7 +67,7 @@ namespace BetterLyrics.WinUI3.Services.LiveStatesService
LiveStates.LyricsWindowStatus.WindowBounds = LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea();
}
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds);
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds);
LiveStates.LyricsWindowStatus.WindowX = LiveStates.LyricsWindowStatus.WindowBounds.X;
LiveStates.LyricsWindowStatus.WindowY = LiveStates.LyricsWindowStatus.WindowBounds.Y;
LiveStates.LyricsWindowStatus.WindowWidth = LiveStates.LyricsWindowStatus.WindowBounds.Width;
@@ -83,11 +84,11 @@ namespace BetterLyrics.WinUI3.Services.LiveStatesService
{
case nameof(LyricsWindowStatus.IsWorkArea):
LiveStates.IsLyricsWindowStatusRefreshing = true;
WindowHelper.SetIsWorkArea<LyricsWindow>(LiveStates.LyricsWindowStatus.IsWorkArea);
WindowHook.SetIsWorkArea<LyricsWindow>(LiveStates.LyricsWindowStatus.IsWorkArea);
LiveStates.IsLyricsWindowStatusRefreshing = false;
if (LiveStates.LyricsWindowStatus.IsWorkArea)
{
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea());
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea());
}
break;
case nameof(LyricsWindowStatus.DockHeight):
@@ -97,40 +98,40 @@ namespace BetterLyrics.WinUI3.Services.LiveStatesService
if (LiveStates.LyricsWindowStatus.IsWorkArea)
{
LiveStates.IsLyricsWindowStatusRefreshing = true;
WindowHelper.UpdateWorkArea<LyricsWindow>();
WindowHook.UpdateWorkArea<LyricsWindow>();
LiveStates.IsLyricsWindowStatusRefreshing = false;
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea());
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea());
}
break;
case nameof(LyricsWindowStatus.IsShownInSwitchers):
WindowHelper.SetIsShowInSwitchers<LyricsWindow>(LiveStates.LyricsWindowStatus.IsShownInSwitchers);
WindowHook.SetIsShowInSwitchers<LyricsWindow>(LiveStates.LyricsWindowStatus.IsShownInSwitchers);
break;
case nameof(LyricsWindowStatus.IsAlwaysOnTop):
WindowHelper.SetIsAlwaysOnTop<LyricsWindow>(LiveStates.LyricsWindowStatus.IsAlwaysOnTop);
WindowHook.SetIsAlwaysOnTop<LyricsWindow>(LiveStates.LyricsWindowStatus.IsAlwaysOnTop);
break;
case nameof(LyricsWindowStatus.IsClickThrough):
WindowHelper.SetIsClickThrough<LyricsWindow>(LiveStates.LyricsWindowStatus.IsClickThrough);
WindowHook.SetIsClickThrough<LyricsWindow>(LiveStates.LyricsWindowStatus.IsClickThrough);
break;
case nameof(LyricsWindowStatus.IsBorderless):
WindowHelper.SetIsBorderless<LyricsWindow>(LiveStates.LyricsWindowStatus.IsBorderless);
WindowHook.SetIsBorderless<LyricsWindow>(LiveStates.LyricsWindowStatus.IsBorderless);
break;
case nameof(LyricsWindowStatus.WindowX):
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithX(LiveStates.LyricsWindowStatus.WindowX));
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithX(LiveStates.LyricsWindowStatus.WindowX));
break;
case nameof(LyricsWindowStatus.WindowY):
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithY(LiveStates.LyricsWindowStatus.WindowY));
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithY(LiveStates.LyricsWindowStatus.WindowY));
break;
case nameof(LyricsWindowStatus.WindowWidth):
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithWidth(LiveStates.LyricsWindowStatus.WindowWidth));
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithWidth(LiveStates.LyricsWindowStatus.WindowWidth));
break;
case nameof(LyricsWindowStatus.WindowHeight):
WindowHelper.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithHeight(LiveStates.LyricsWindowStatus.WindowHeight));
WindowHook.MoveAndResize<LyricsWindow>(LiveStates.LyricsWindowStatus.WindowBounds.WithHeight(LiveStates.LyricsWindowStatus.WindowHeight));
break;
case nameof(LyricsWindowStatus.TitleBarArea):
WindowHelper.SetTitleBarArea<LyricsWindow>(LiveStates.LyricsWindowStatus.TitleBarArea);
WindowHook.SetTitleBarArea<LyricsWindow>(LiveStates.LyricsWindowStatus.TitleBarArea);
break;
case nameof(LyricsWindowStatus.AutoShowOrHideWindow):
WindowHelper.SetLyricsWindowVisibilityByPlayingStatus(_dispatcherQueue);
WindowHook.SetLyricsWindowVisibilityByPlayingStatus(_dispatcherQueue);
break;
default:
break;

View File

@@ -1,5 +1,6 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using System.Collections.Generic;
using System.Threading;
@@ -9,8 +10,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
public interface ILyricsSearchService
{
Task<LyricsSearchResult> SearchSmartlyAsync(string mediaSessionId, string title, string artist, string album, double durationMs, string? songId, CancellationToken token);
Task<LyricsSearchResult?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token);
Task<List<LyricsSearchResult>> SearchAllAsync(string title, string artist, string album, double durationMs, CancellationToken token);
Task<List<LyricsSearchResult>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token);
}
}

View File

@@ -2,11 +2,12 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Providers;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Helpers;
using Lyricify.Lyrics.Searchers;
using Microsoft.Extensions.Logging;
@@ -17,8 +18,10 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Documents;
namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
@@ -31,10 +34,10 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public LyricsSearchService(ISettingsService settingsService)
public LyricsSearchService(ISettingsService settingsService, ILogger<LyricsSearchService> logger)
{
_settingsService = settingsService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsSearchService>>();
_logger = logger;
_lrcLibHttpClient = new();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
@@ -89,24 +92,32 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
public async Task<LyricsSearchResult> SearchSmartlyAsync(string mediaSessionId, string title, string artist, string album, double durationMs, string? songId, CancellationToken token)
public async Task<LyricsSearchResult?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token)
{
if (lyricsSearchType == null)
{
return null;
}
var lyricsSearchResult = new LyricsSearchResult();
string overridenTitle = title;
string overridenArtist = artist;
string overridenAlbum = album;
string overridenTitle = songInfo.Title;
string[] overridenArtists = songInfo.Artists;
string overridenAlbum = songInfo.Album;
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
_logger.LogInformation("SearchSmartlyAsync {SongInfo}", songInfo);
// 先检查该曲目是否已被用户映射
var found = _settingsService.AppSettings.MappedSongSearchQueries
.Where(x => x.OriginalTitle == overridenTitle && x.OriginalArtist == overridenArtist && x.OriginalAlbum == overridenAlbum)
.FirstOrDefault();
.FirstOrDefault(x =>
x.OriginalTitle == overridenTitle &&
x.OriginalArtist == overridenArtists.Join(ATL.Settings.DisplayValueSeparator.ToString()) &&
x.OriginalAlbum == overridenAlbum);
if (found != null)
{
overridenTitle = found.MappedTitle;
overridenArtist = found.MappedArtist;
overridenArtists = found.MappedArtist.Split(ATL.Settings.DisplayValueSeparator);
overridenAlbum = found.MappedAlbum;
_logger.LogInformation("Found mapped song search query: {MappedSongSearchQuery}", found);
@@ -115,7 +126,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (pureMusic)
{
lyricsSearchResult.Title = overridenTitle;
lyricsSearchResult.Artist = overridenArtist;
lyricsSearchResult.Artists = overridenArtists;
lyricsSearchResult.Album = overridenAlbum;
lyricsSearchResult.Raw = "[99:00.000]🎶🎶🎶";
return lyricsSearchResult;
@@ -124,41 +135,68 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var targetProvider = found.LyricsSearchProvider;
if (targetProvider != null)
{
return await SearchSingleAsync(targetProvider.Value, overridenTitle, overridenArtist, overridenAlbum, durationMs, songId, token);
return await SearchSingleAsync(
((SongInfo)songInfo.Clone())
.WithTitle(overridenTitle)
.WithArtist(overridenArtists)
.WithAlbum(overridenAlbum),
targetProvider.Value, checkCache, token);
}
}
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == mediaSessionId).FirstOrDefault()?.LyricsSearchProvidersInfo ?? [])
List<LyricsSearchResult> lyricsSearchResults = [];
// 曲目没有被映射
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == songInfo.PlayerId)?.LyricsSearchProvidersInfo ?? [])
{
if (!provider.IsEnabled)
{
continue;
}
lyricsSearchResult = await SearchSingleAsync(provider.Provider, overridenTitle, overridenArtist, overridenAlbum, durationMs, null, token);
lyricsSearchResult = await SearchSingleAsync(
((SongInfo)songInfo.Clone())
.WithTitle(overridenTitle)
.WithArtist(overridenArtists)
.WithAlbum(overridenAlbum),
provider.Provider, checkCache, token);
if (lyricsSearchResult.IsFound)
{
return lyricsSearchResult;
switch (lyricsSearchType)
{
case LyricsSearchType.Sequential:
return lyricsSearchResult;
case LyricsSearchType.BestMatch:
lyricsSearchResults.Add((LyricsSearchResult)lyricsSearchResult.Clone());
break;
default:
break;
}
}
}
return lyricsSearchResult;
return lyricsSearchType switch
{
LyricsSearchType.Sequential => lyricsSearchResult,
LyricsSearchType.BestMatch => lyricsSearchResults.OrderByDescending(x => x.MatchPercentage).FirstOrDefault(),
_ => null,
};
}
public async Task<List<LyricsSearchResult>> SearchAllAsync(string title, string artist, string album, double durationMs, CancellationToken token)
public async Task<List<LyricsSearchResult>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token)
{
_logger.LogInformation("Searching all lyrics for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
_logger.LogInformation("SearchAllAsync {SongInfo}", songInfo);
var results = new List<LyricsSearchResult>();
foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
{
var searchResult = await SearchSingleAsync(provider, title, artist, album, durationMs, null, token);
var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token);
results.Add(searchResult);
}
return results;
}
private async Task<LyricsSearchResult> SearchSingleAsync(LyricsSearchProvider provider, string title, string artist, string album, double durationMs, string? songId, CancellationToken token)
private async Task<LyricsSearchResult> SearchSingleAsync(SongInfo songInfo, LyricsSearchProvider provider, bool checkCache, CancellationToken token)
{
var lyricsSearchResult = new LyricsSearchResult
{
@@ -169,16 +207,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
LyricsFormat lyricsFormat = provider.GetLyricsFormat();
// Check cache first
if (provider.IsRemote())
// Check cache first if allowed
if (checkCache && provider.IsRemote())
{
var cachedLyrics = FileHelper.ReadLyricsCache(title, artist, album, lyricsFormat, provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
var cached = FileHelper.ReadLyricsCache(songInfo, provider);
if (cached != null)
{
lyricsSearchResult.Raw = cachedLyrics;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
lyricsSearchResult.Album = album;
lyricsSearchResult = cached;
return lyricsSearchResult;
}
}
@@ -187,11 +222,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
if (provider == LyricsSearchProvider.LocalMusicFile)
{
lyricsSearchResult = SearchEmbedded(title, artist, album);
lyricsSearchResult = SearchEmbedded(songInfo);
}
else
{
lyricsSearchResult = await SearchFile(title, artist, album, lyricsFormat);
lyricsSearchResult = await SearchFile(songInfo, lyricsFormat);
}
}
else
@@ -199,22 +234,22 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
switch (provider)
{
case LyricsSearchProvider.LrcLib:
lyricsSearchResult = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
lyricsSearchResult = await SearchLrcLibAsync(songInfo);
break;
case LyricsSearchProvider.QQ:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, songId, Searchers.QQMusic);
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, songId, Searchers.Kugou);
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, songId, Searchers.Netease);
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
lyricsSearchResult = await SearchAmllTtmlDbAsync(title, artist, album);
lyricsSearchResult = await SearchAmllTtmlDbAsync(songInfo);
break;
case LyricsSearchProvider.AppleMusic:
lyricsSearchResult = await SearchAppleMusicAsync(title, artist, album, (int)durationMs);
lyricsSearchResult = await SearchAppleMusicAsync(songInfo);
break;
default:
break;
@@ -223,30 +258,36 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (token.IsCancellationRequested)
{
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
return lyricsSearchResult;
}
if (lyricsSearchResult.IsFound)
{
if (provider.IsRemote())
{
FileHelper.WriteLyricsCache(title, artist, album, lyricsSearchResult.Raw!, lyricsFormat, provider.GetCacheDirectory());
}
}
}
catch (Exception)
{
}
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
if (lyricsSearchResult.IsFound)
{
if (provider.IsRemote())
{
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
}
}
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchFile(string title, string artist, string album, LyricsFormat format)
private async Task<LyricsSearchResult> SearchFile(SongInfo songInfo, LyricsFormat format)
{
var lyricsSearchResult = new LyricsSearchResult
var lyricsSearchResult = new LyricsSearchResult();
if (format.ToLyricsSearchProvider() is LyricsSearchProvider lyricsSearchProvider)
{
Provider = format.ToLyricsSearchProvider(),
};
lyricsSearchResult.Provider = lyricsSearchProvider;
}
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
@@ -256,15 +297,15 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
{
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
var fileName = Path.GetFileNameWithoutExtension(file);
if (StringHelper.IsSwitchableNormalizedMatch(fileName, songInfo.Title, songInfo.DisplayArtists) || songInfo.LinkedFileName == fileName)
{
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
{
lyricsSearchResult.Raw = raw;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
lyricsSearchResult.Album = album;
lyricsSearchResult.CopyFromSongInfo(songInfo);
lyricsSearchResult.Reference = file;
return lyricsSearchResult;
}
@@ -279,7 +320,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private LyricsSearchResult SearchEmbedded(string title, string artist, string album)
private LyricsSearchResult SearchEmbedded(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
{
@@ -295,17 +336,16 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
{
var track = new Track(file);
if ((album != "" && track.Title == title && track.Artist == artist && track.Album == album)
|| (album == "" && track.Title == title && track.Artist == artist)
|| (album == "" && FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist)))
if ((songInfo.Album != "" && track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists && track.Album == songInfo.Album)
|| (songInfo.Album == "" && track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists)
|| (songInfo.Album == "" && StringHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), songInfo.Title, songInfo.DisplayArtists)))
{
var plain = track.GetLyrics();
var plain = track.GetRawLyrics();
if (!plain.IsNullOrEmpty())
{
lyricsSearchResult.Raw = plain;
lyricsSearchResult.Title = track.Title;
lyricsSearchResult.Artist = track.Artist;
lyricsSearchResult.Album = track.Album;
lyricsSearchResult.CopyFromSongInfo(songInfo);
lyricsSearchResult.Reference = file;
return lyricsSearchResult;
}
@@ -317,7 +357,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchAmllTtmlDbAsync(string title, string artist, string album)
private async Task<LyricsSearchResult> SearchAmllTtmlDbAsync(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
{
@@ -333,6 +373,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
int bestScore = 0;
string? rawLyricFile = null;
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
{
@@ -344,8 +385,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var root = doc.RootElement;
if (!root.TryGetProperty("metadata", out var metadataArr))
continue;
string? musicName = null;
string? artists = null;
string? title = null;
string[]? artists = null;
string? album = null;
foreach (var meta in metadataArr.EnumerateArray())
{
if (meta.GetArrayLength() != 2)
@@ -353,19 +397,28 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var key = meta[0].GetString();
var valueArr = meta[1];
if (key == "musicName" && valueArr.GetArrayLength() > 0)
musicName = valueArr[0].GetString();
title = valueArr[0].GetString();
if (key == "artists" && valueArr.GetArrayLength() > 0)
artists = valueArr[0].GetString();
artists = valueArr.EnumerateArray().Select(x => x.GetString() ?? "").ToArray();
if (key == "album" && valueArr.GetArrayLength() > 0)
album = valueArr[0].GetString();
}
if (musicName == null || artists == null)
continue;
if (FileHelper.IsSwitchableNormalizedMatch($"{artists} - {musicName}", title, artist))
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
{
Title = title,
Artists = artists,
Album = album,
});
if (score > bestScore)
{
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
{
rawLyricFile = rawLyricFileProp.GetString();
break;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artists = artists;
lyricsSearchResult.Album = album;
bestScore = score;
}
}
}
@@ -379,6 +432,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
// 下载歌词内容
var url = $"{_settingsService.AppSettings.GeneralSettings.AmllTtmlDbBaseUrl}/{Constants.AmllTTmlDB.QueryPrefix}/{rawLyricFile}";
lyricsSearchResult.Reference = url;
try
{
using var response = await _amllTtmlDbHttpClient.GetAsync(url);
@@ -389,9 +443,6 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
string lyrics = await response.Content.ReadAsStringAsync();
lyricsSearchResult.Raw = lyrics;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
lyricsSearchResult.Album = album;
}
catch
{
@@ -400,7 +451,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchLrcLibAsync(string title, string artist, string album, int duration)
private async Task<LyricsSearchResult> SearchLrcLibAsync(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
{
@@ -410,10 +461,10 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
// Build API query URL
var url =
$"https://lrclib.net/api/search?" +
$"track_name={Uri.EscapeDataString(title)}&" +
$"artist_name={Uri.EscapeDataString(artist)}&" +
$"&album_name={Uri.EscapeDataString(album)}" +
$"&durationMs={Uri.EscapeDataString(duration.ToString())}";
$"track_name={Uri.EscapeDataString(songInfo.Title)}&" +
$"artist_name={Uri.EscapeDataString(songInfo.DisplayArtists)}&" +
$"&album_name={Uri.EscapeDataString(songInfo.Album)}" +
$"&durationMs={Uri.EscapeDataString(songInfo.DurationMs.ToString())}";
using var response = await _lrcLibHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
@@ -432,6 +483,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
string? searchedTitle = null;
string? searchedArtist = null;
string? searchedAlbum = null;
double? searchedDuration = null;
if (jArr.ValueKind == JsonValueKind.Array && jArr.GetArrayLength() > 0)
{
@@ -440,17 +492,21 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
searchedTitle = first.GetProperty("trackName").GetString();
searchedArtist = first.GetProperty("artistName").GetString();
searchedAlbum = first.GetProperty("albumName").GetString();
searchedDuration = first.GetProperty("duration").GetDouble();
}
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = searchedTitle;
lyricsSearchResult.Artist = searchedArtist;
lyricsSearchResult.Artists = searchedArtist?.SplitByCommonSplitter();
lyricsSearchResult.Album = searchedAlbum;
lyricsSearchResult.Duration = searchedDuration;
lyricsSearchResult.Reference = url;
return lyricsSearchResult;
}
private static async Task<LyricsSearchResult> SearchQQNeteaseKugouAsync(string title, string artist, string album, int durationMs, string? songId, Searchers searcher)
private static async Task<LyricsSearchResult> SearchQQNeteaseKugouAsync(SongInfo songInfo, Searchers searcher)
{
var lyricsSearchResult = new LyricsSearchResult();
@@ -472,20 +528,20 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
ISearchResult? result;
if (searcher == Searchers.Netease && songId != null)
if (searcher == Searchers.Netease && songInfo.SongId != null)
{
result = new NeteaseSearchResult(title, [artist], album, null, durationMs, songId);
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, songInfo.Artists, (int)songInfo.DurationMs, songInfo.SongId);
}
else
{
result = await SearchHelper.Search(new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs = durationMs,
Album = album,
AlbumArtists = [artist],
Artists = [artist],
Title = title,
}, searcher);
DurationMs = (int)songInfo.DurationMs,
Album = songInfo.Album,
AlbumArtists = songInfo.Artists.ToList(),
Artists = songInfo.Artists.ToList(),
Title = songInfo.Title,
}, searcher, Lyricify.Lyrics.Searchers.Helpers.CompareHelper.MatchType.NoMatch);
}
if (result != null)
@@ -493,45 +549,24 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (result is QQMusicSearchResult qqResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.QQMusicApi.GetLyricsAsync(qqResult.Id);
var original = response?.Lyrics;
var translated = response?.Trans;
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(
title,
artist,
album,
translated,
LyricsFormat.Lrc,
PathHelper.QQTranslationCacheDirectory
);
}
lyricsSearchResult.Raw = original;
lyricsSearchResult.Raw = response?.Lyrics;
lyricsSearchResult.Translation = response?.Trans;
lyricsSearchResult.Reference = $"https://y.qq.com/n/ryqq/songDetail/{qqResult.Mid}";
}
else if (result is NeteaseSearchResult neteaseResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(neteaseResult.Id);
var original = response?.Lrc?.Lyric;
var translated = response?.Tlyric?.Lyric;
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(
title,
artist,
album,
translated,
LyricsFormat.Lrc,
PathHelper.NeteaseTranslationCacheDirectory
);
}
lyricsSearchResult.Raw = original;
lyricsSearchResult.Raw = response?.Lrc?.Lyric;
lyricsSearchResult.Translation = response?.Tlyric?.Lyric;
lyricsSearchResult.Reference = $"https://music.163.com/song?id={neteaseResult.Id}";
}
else if (result is KugouSearchResult kugouResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(hash: kugouResult.Hash);
string? original = null;
string? translated = null;
var candidate = response?.Candidates.FirstOrDefault();
if (candidate != null)
{
@@ -541,7 +576,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var parsedList = Lyricify.Lyrics.Parsers.KrcParser.ParseLyrics(original);
if (parsedList != null)
{
string translated = "";
translated = "";
foreach (var item in parsedList)
{
if (item is Lyricify.Lyrics.Models.FullSyllableLineInfo fullSyllableLineInfo)
@@ -552,33 +587,25 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
translated += $"[{startTimeStr}]{chTranslation}\n";
}
}
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(
title,
artist,
album,
translated,
LyricsFormat.Lrc,
PathHelper.KugouTranslationCacheDirectory
);
}
}
}
lyricsSearchResult.Reference = $"https://www.kugou.com/";
}
lyricsSearchResult.Raw = original;
lyricsSearchResult.Translation = translated;
}
}
lyricsSearchResult.Title = result?.Title;
lyricsSearchResult.Artist = result?.Artists.Join(" | ");
lyricsSearchResult.Artists = result?.Artists;
lyricsSearchResult.Album = result?.Album;
lyricsSearchResult.Duration = result?.DurationMs / 1000;
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchAppleMusicAsync(string title, string artist, string album, int durationMs)
private async Task<LyricsSearchResult> SearchAppleMusicAsync(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
{
@@ -587,12 +614,14 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (await _appleMusic.InitAsync())
{
var raw = await _appleMusic.GetLyricsAsync(title, artist);
_logger.LogInformation("Apple Music lyrics search result for {Title} - {Artist}: {Raw}", title, artist, raw ?? "null");
string id = await _appleMusic.SearchSongInfoAsync(songInfo.DisplayArtists, songInfo.Title);
string? raw = await _appleMusic.GetLyricsAsync(id);
_logger.LogInformation("SearchAppleMusicAsync");
lyricsSearchResult.Raw = raw;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
lyricsSearchResult.Title = songInfo.Title;
lyricsSearchResult.Artists = songInfo.Artists;
lyricsSearchResult.Album = "";
lyricsSearchResult.Reference = $"https://music.apple.com/song/{id}";
}
return lyricsSearchResult;

View File

@@ -4,18 +4,17 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
public interface IMediaSessionsService
public interface IMediaSessionsService : INotifyPropertyChanged
{
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<TimelineChangedEventArgs>? TimelineChanged;
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChanged;
event EventHandler<LyricsChangedEventArgs>? LyricsChanged;
event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
Task PlayAsync();
Task PauseAsync();
@@ -23,19 +22,23 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
Task NextAsync();
Task ChangePosition(double seconds);
MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo();
void UpdateLyrics();
void UpdateTranslations();
void InitPlaybackShortcuts();
bool IsPlaying { get; }
SongInfo? SongInfo { get; }
TimeSpan Position { get; }
MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; }
bool CurrentIsPlaying { get; }
SongInfo? CurrentSongInfo { get; }
TimeSpan CurrentPosition { get; }
LyricsData? CurrentLyricsData { get; }
LyricsSearchProvider? LyricsSearchProvider { get; }
SoftwareBitmap? SoftwareBitmap { get; }
List<Color> LightAccentColors { get; }
List<Color> DarkAccentColors { get; }
TranslationSearchProvider? TranslationSearchProvider { get; }
LyricsSearchResult? CurrentLyricsSearchResult { get; }
}
}

View File

@@ -1,12 +1,15 @@
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.Logging;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
@@ -14,30 +17,28 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();
public event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChanged;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SoftwareBitmap? SoftwareBitmap { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<Color> LightAccentColors { get; set; } = Enumerable.Repeat(Colors.Black, 4).ToList();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<Color> DarkAccentColors { get; set; } = Enumerable.Repeat(Colors.Black, 4).ToList();
private void UpdateAlbumArt()
{
_albumArtRefreshRunner.RunAsync(RefreshArtAlbum);
_ = _albumArtRefreshRunner.RunAsync(RefreshArtAlbum);
}
private async Task RefreshArtAlbum(CancellationToken token)
{
if (_cachedSongInfo == null)
_logger.LogInformation("RefreshArtAlbum");
if (CurrentSongInfo == null)
{
_logger.LogWarning("Cached song info is null, cannot update album art.");
_logger.LogWarning("CurrentSongInfo == null");
return;
}
IBuffer? buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(
SongInfo?.PlayerId ?? "",
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo.Album,
_SMTCAlbumArtBuffer,
token
), token);
IBuffer? buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(CurrentSongInfo, _SMTCAlbumArtBuffer, token), token);
if (token.IsCancellationRequested) return;
BitmapDecoder? decoder = null;
if (buffer == null)
@@ -45,22 +46,27 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
using var placeHolderStream = await ImageHelper.GetAlbumArtPlaceholderAsync();
var tempBuffer = new Windows.Storage.Streams.Buffer((uint)placeHolderStream.Size);
await placeHolderStream.ReadAsync(tempBuffer, (uint)placeHolderStream.Size, InputStreamOptions.None);
if (token.IsCancellationRequested) return;
buffer = tempBuffer;
token.ThrowIfCancellationRequested();
}
decoder = await ImageHelper.MakeSquareWithThemeColor(buffer, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType);
token.ThrowIfCancellationRequested();
if (token.IsCancellationRequested) return;
var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
if (token.IsCancellationRequested) return;
albumArtSwBitmap.DpiX = 96;
albumArtSwBitmap.DpiY = 96;
token.ThrowIfCancellationRequested();
var albumArtLightAccentColors = await ImageHelper.GetAccentColorsAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, false);
var lightColorBytes = albumArtLightAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList();
var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, true);
var darkColorBytes = albumArtDarkAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList();
AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, lightColorBytes, darkColorBytes));
var lightPalette = await ImageHelper.GetAccentColorsAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, false);
var darkPalette = await ImageHelper.GetAccentColorsAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, true);
if (token.IsCancellationRequested) return;
SoftwareBitmap = albumArtSwBitmap;
LightAccentColors = lightPalette.Palette.Select(Helper.ColorHelper.FromVector3).ToList();
DarkAccentColors = darkPalette.Palette.Select(Helper.ColorHelper.FromVector3).ToList();
}
}
}

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