mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 19:24:55 +08:00
Compare commits
25 Commits
v1.0.120.0
...
v1.0.140.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0f2b4106a | ||
|
|
c704edde19 | ||
|
|
35de1ef7db | ||
|
|
1c8840e053 | ||
|
|
2d0839d777 | ||
|
|
7e6ce08c56 | ||
|
|
28f75fa751 | ||
|
|
051a7af21a | ||
|
|
c8b8853561 | ||
|
|
baab2bcc7a | ||
|
|
9a3e4adc0c | ||
|
|
8d909da139 | ||
|
|
d97f5fec37 | ||
|
|
9a12b06b7e | ||
|
|
3fca90ca72 | ||
|
|
220e940bbb | ||
|
|
dc627bd1ef | ||
|
|
3c852a9d2f | ||
|
|
ecff788c6c | ||
|
|
c471f128c1 | ||
|
|
4a389d5e33 | ||
|
|
ae1fa1c54d | ||
|
|
738a4a0bec | ||
|
|
37ae278a11 | ||
|
|
45094bde31 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="37412.BetterLyrics"
|
||||
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
|
||||
Version="1.0.121.0" />
|
||||
Version="1.0.140.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<None Remove="Controls\LyricsWindowSwitchControl.xaml" />
|
||||
<None Remove="Controls\MediaSettingsControl.xaml" />
|
||||
<None Remove="Controls\PlaybackSettingsControl.xaml" />
|
||||
<None Remove="Controls\PropertyRow.xaml" />
|
||||
<None Remove="Controls\ShortcutTextBox.xaml" />
|
||||
<None Remove="Controls\SystemTray.xaml" />
|
||||
<None Remove="Views\LyricsSearchWindow.xaml" />
|
||||
@@ -61,17 +62,19 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.250402" />
|
||||
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
|
||||
<PackageReference Include="csharp-kana" Version="1.0.2" />
|
||||
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
|
||||
<PackageReference Include="DevWinUI.Controls" Version="9.5.0" />
|
||||
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
|
||||
<PackageReference Include="F23.StringSimilarity" Version="7.0.0" />
|
||||
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.2" />
|
||||
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
|
||||
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
@@ -87,8 +90,10 @@
|
||||
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" />
|
||||
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
|
||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="7.7.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="7.8.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
|
||||
@@ -106,8 +111,13 @@
|
||||
</ItemGroup>
|
||||
<!--Disable Trimming for Specific Packages-->
|
||||
<ItemGroup>
|
||||
<TrimmerRootAssembly Include="TagLibSharp" />
|
||||
<TrimmerRootAssembly Include="NAudio.Wasapi" />
|
||||
<TrimmerRootAssembly Include="TagLibSharp" />
|
||||
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
|
||||
<TrimmerRootAssembly Include="Vanara.PInvoke.Gdi32" />
|
||||
<TrimmerRootAssembly Include="Vanara.PInvoke.Shell32" />
|
||||
<TrimmerRootAssembly Include="Vanara.PInvoke.User32" />
|
||||
<TrimmerRootAssembly Include="Vanara.Windows.Shell" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\AIMP.png">
|
||||
@@ -116,6 +126,9 @@
|
||||
<Content Update="Assets\AlbumArtPlaceholder.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\Alipay.jpg">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\AMLLPlayer.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
@@ -206,6 +219,9 @@
|
||||
<Content Update="Assets\Spotify.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\WeChatReward.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\Wiki82.profile.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
@@ -318,6 +334,11 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="TemplateSelector\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\PropertyRow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\AboutControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BetterLyrics.WinUI3.Constants
|
||||
{
|
||||
class Discord
|
||||
{
|
||||
public const string AppID = "Your Discord app ID here";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BetterLyrics.WinUI3.Constants
|
||||
{
|
||||
public class ExtendedGenreFiled
|
||||
{
|
||||
public const string NetEaseCloudMusicTrackID = "NCM-";
|
||||
public const string FileName = "FILENAME-";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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=}">
|
||||
<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=}">
|
||||
<local:ExtendedSlider
|
||||
Default="12"
|
||||
|
||||
Maximum="64"
|
||||
Minimum="0"
|
||||
Value="{x:Bind AlbumArtLayoutSettings.CoverImageShadowAmount, Mode=TwoWay}" />
|
||||
|
||||
@@ -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=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreCacheWhenSearching, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageShowHideHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.GeneralSettings.ShowOrHideLyricsWindowShortcut, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
@@ -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=}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsFogOverlayEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsExpander
|
||||
x:Uid="SettingsPageSpectrumLayer"
|
||||
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
Glyph=}"
|
||||
Glyph=}"
|
||||
IsExpanded="{x:Bind LyricsBackgroundSettings.IsSpectrumOverlayEnabled, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsSpectrumOverlayEnabled, Mode=TwoWay}" />
|
||||
<dev:SettingsExpander.Items>
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
|
||||
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
|
||||
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}" TextWrapping="Wrap" />
|
||||
<RichTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap">
|
||||
<Paragraph>
|
||||
<Run x:Uid="ArtistsSplitHint" />
|
||||
<Run Text=";" />
|
||||
<Run Text="," />
|
||||
<Run Text="/" />
|
||||
<Run Text=";" />
|
||||
<Run Text="、" />
|
||||
<Run Text="," />
|
||||
</Paragraph>
|
||||
</RichTextBlock>
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
Command="{x:Bind ViewModel.ResetMappedArtistCommand}"
|
||||
@@ -125,22 +136,27 @@
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LyricsSearchResult">
|
||||
<ListViewItem IsEnabled="{x:Bind IsFound}">
|
||||
<StackPanel Padding="3,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
|
||||
<TextBlock Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" Text="{x:Bind Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
|
||||
<TextBlock
|
||||
Text="{x:Bind Title}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Artist}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Album}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
|
||||
<local:PropertyRow
|
||||
Margin="-12,0,0,0"
|
||||
Link="{x:Bind Reference, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind Reference, TargetNullValue=N/A, Mode=OneWay}"
|
||||
Value="{x:Bind Provider, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
|
||||
<!-- Lyrics search result -->
|
||||
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsSearchControlDurauion"
|
||||
Unit="s"
|
||||
Value="{x:Bind Duration, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageMatchPercentage"
|
||||
Unit="%"
|
||||
Value="{x:Bind MatchPercentage, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<!-- NOT FOUND -->
|
||||
<TextBlock
|
||||
x:Uid="LyricsSearchControlNotFound"
|
||||
VerticalAlignment="Center"
|
||||
@@ -209,23 +225,30 @@
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
<Pivot.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{Binding LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
|
||||
<DataTemplate x:DataType="models:LyricsData">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
|
||||
<InfoBadge
|
||||
x:Uid="LyricsSearchControlAutoGenerated"
|
||||
Margin="6,0,0,0"
|
||||
Style="{StaticResource StringInfoBadgeStyle}"
|
||||
Visibility="{x:Bind AutoGenerated, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</Pivot.HeaderTemplate>
|
||||
<Pivot.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ListView ItemsSource="{Binding LyricsLines, Mode=OneWay}" SelectionChanged="ListView_SelectionChanged">
|
||||
<DataTemplate x:DataType="models:LyricsData">
|
||||
<ListView ItemsSource="{x:Bind LyricsLines, Mode=OneWay}" SelectionChanged="ListView_SelectionChanged">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DataTemplate x:DataType="models:LyricsLine">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<TextBlock
|
||||
Margin="1,0"
|
||||
Foreground="{ThemeResource SystemFillColorNeutralBrush}"
|
||||
Text="-" />
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding EndMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<TextBlock Margin="6,0" Text="{Binding OriginalText}" />
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind EndMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<TextBlock Margin="6,0" Text="{x:Bind OriginalText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
|
||||
@@ -2,7 +2,6 @@ using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Serialization;
|
||||
using BetterLyrics.WinUI3.Services.LiveStatesService;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
@@ -27,7 +26,6 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
|
||||
public LyricsWindowSettingsControl()
|
||||
{
|
||||
@@ -87,7 +85,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
clonedData.IsDefault = false;
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(clonedData, SourceGenerationContext.Default.LyricsWindowStatus);
|
||||
File.WriteAllText(file.Path, json);
|
||||
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("ExportSettingsSuccess"));
|
||||
ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +145,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
if (data != null)
|
||||
{
|
||||
ViewModel.AppSettings.WindowBoundsRecords.Add(data);
|
||||
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("ImportSettingsSuccess"));
|
||||
ToastHelper.ShowToast("ImportSettingsSuccess", null, InfoBarSeverity.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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="" />
|
||||
<Button
|
||||
Margin="12"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Click="Button_Click"
|
||||
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
Glyph=}"
|
||||
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=}"
|
||||
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="" />
|
||||
<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>
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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="⠿" />
|
||||
<ImageIcon Height="16" Source="{Binding Provider, Converter={StaticResource MediaSourceProviderToLogoUriConverter}, Mode=OneWay}" />
|
||||
<Grid
|
||||
Width="16"
|
||||
Height="16"
|
||||
CornerRadius="4">
|
||||
<ImageIcon Source="{Binding LogoPath}" />
|
||||
</Grid>
|
||||
<TextBlock
|
||||
MaxWidth="200"
|
||||
Text="{Binding Provider, Converter={StaticResource MediaSourceProviderToDisplayedNameConverter}, Mode=OneWay}"
|
||||
Text="{Binding DisplayName, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
@@ -90,6 +98,10 @@
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SelectedMediaSourceProvider.IsLastFMTrackEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageDiscordPresence">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SelectedMediaSourceProvider.IsDiscordPresenceEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!-- LX music server -->
|
||||
<dev:SettingsCard x:Uid="SettingsPageLXMusicServer" Visibility="{x:Bind ViewModel.SelectedMediaSourceProvider.IsLXMusic, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid ColumnSpacing="6">
|
||||
@@ -170,6 +182,13 @@
|
||||
|
||||
<!-- 歌词源配置 -->
|
||||
<TextBlock x:Uid="SettingsPageLyricsSearchProvidersConfig" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
<dev:SettingsCard x:Uid="SettingsPageLyricsSearchType">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.SelectedMediaSourceProvider.LyricsSearchType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
||||
<ComboBoxItem x:Uid="SettingsPageLyricsSearchSequential" />
|
||||
<ComboBoxItem x:Uid="SettingsPageLyricsSearchBestMatch" />
|
||||
</ComboBox>
|
||||
</dev:SettingsCard>
|
||||
|
||||
<ListView
|
||||
x:Name="LyricsSearchProvidersListView"
|
||||
AllowDrop="True"
|
||||
@@ -236,20 +255,75 @@
|
||||
<Grid Style="{StaticResource SettingsGridStyle}">
|
||||
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<!-- Provider info -->
|
||||
<!-- Realtime info -->
|
||||
<TextBlock x:Uid="SettingsPageRealtimeStatus" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
<dev:SettingsCard x:Uid="LyricsPageLyricsProviderPrefix">
|
||||
<HyperlinkButton
|
||||
Content="{x:Bind ViewModel.LyricsSearchProvider, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}"
|
||||
IsEnabled="False"
|
||||
NavigateUri="{x:Bind ViewModel.OriginalLyricsRef, Mode=OneWay}" />
|
||||
</dev:SettingsCard>
|
||||
<dev:SettingsCard x:Uid="LyricsPageTranslationProviderPrefix">
|
||||
<HyperlinkButton
|
||||
Content="{x:Bind ViewModel.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}"
|
||||
IsEnabled="False"
|
||||
NavigateUri="{x:Bind ViewModel.TranslatedLyricsRef, Mode=OneWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!-- Playback source info -->
|
||||
<Expander
|
||||
x:Uid="SettingsPagePlaybackStatus"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left">
|
||||
<StackPanel Spacing="6">
|
||||
<!-- Playback source -->
|
||||
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
|
||||
<!-- Playback source ID -->
|
||||
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
<!-- Song info -->
|
||||
<Expander
|
||||
x:Uid="SettingsPageSongStatus"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left">
|
||||
<StackPanel Spacing="6">
|
||||
<!-- Song title -->
|
||||
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Song artists -->
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Song album -->
|
||||
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Song duration -->
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsSearchControlDurauion"
|
||||
Unit="s"
|
||||
Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Duration, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
<!-- Search result info -->
|
||||
<Expander
|
||||
x:Uid="SettingsPageSearchResultStatus"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left">
|
||||
<StackPanel Spacing="6">
|
||||
<!-- Searched title -->
|
||||
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Searched artists -->
|
||||
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Searched album -->
|
||||
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Searched duration -->
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsSearchControlDurauion"
|
||||
Unit="s"
|
||||
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<!-- Lyrics source -->
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageLyricsProviderPrefix"
|
||||
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay}"
|
||||
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
|
||||
<!-- Translation source -->
|
||||
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
|
||||
<!-- Match percentage -->
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageMatchPercentage"
|
||||
Unit="%"
|
||||
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
@@ -272,12 +346,7 @@
|
||||
</dev:SettingsCard>
|
||||
<dev:SettingsCard x:Uid="SettingsPageTranslationConfig" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=OneWay}">
|
||||
<dev:SettingsCard.Description>
|
||||
<HyperlinkButton Margin="0,6,0,0" NavigateUri="https://github.com/LibreTranslate/LibreTranslate">
|
||||
<TextBlock
|
||||
x:Uid="SettingsPageTranslationInfoLink"
|
||||
FontSize="14"
|
||||
TextWrapping="Wrap" />
|
||||
</HyperlinkButton>
|
||||
<HyperlinkButton Content="https://github.com/LibreTranslate/LibreTranslate" NavigateUri="https://github.com/LibreTranslate/LibreTranslate" />
|
||||
</dev:SettingsCard.Description>
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="BetterLyrics.WinUI3.Controls.PropertyRow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid HorizontalAlignment="Left" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Top"
|
||||
Text="{x:Bind Header, Mode=OneWay}" />
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Top"
|
||||
PointerEntered="OnPointerEntered"
|
||||
PointerExited="OnPointerExited">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<RichTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Visibility="{x:Bind TextVisibility, Mode=OneWay}">
|
||||
<Paragraph>
|
||||
<Run Text="{x:Bind Value, Mode=OneWay}" />
|
||||
<Run Text="{x:Bind Unit, Mode=OneWay}" />
|
||||
</Paragraph>
|
||||
</RichTextBlock>
|
||||
|
||||
<HyperlinkButton
|
||||
Margin="0,-2,0,0"
|
||||
Padding="0"
|
||||
HorizontalContentAlignment="Left"
|
||||
Click="OnLinkClicked"
|
||||
Content="{x:Bind Value, Mode=OneWay}"
|
||||
Visibility="{x:Bind LinkVisibility, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<Button
|
||||
x:Name="CopyButton"
|
||||
Grid.Column="1"
|
||||
Margin="8,-2,0,0"
|
||||
Padding="4"
|
||||
VerticalAlignment="Top"
|
||||
Click="OnCopyClicked"
|
||||
Opacity="0">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="Copy" />
|
||||
</ToolTipService.ToolTip>
|
||||
<Button.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</Button.OpacityTransition>
|
||||
<Grid>
|
||||
<FontIcon
|
||||
x:Name="CopyIcon"
|
||||
FontFamily="{StaticResource IconFontFamily}"
|
||||
FontSize="16"
|
||||
Glyph=""
|
||||
Opacity="1">
|
||||
<FontIcon.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</FontIcon.OpacityTransition>
|
||||
</FontIcon>
|
||||
<FontIcon
|
||||
x:Name="CheckIcon"
|
||||
FontFamily="{StaticResource IconFontFamily}"
|
||||
FontSize="16"
|
||||
Glyph=""
|
||||
Opacity="0">
|
||||
<FontIcon.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</FontIcon.OpacityTransition>
|
||||
</FontIcon>
|
||||
</Grid>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,115 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
public sealed partial class PropertyRow : UserControl
|
||||
{
|
||||
public PropertyRow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
public string Header
|
||||
{
|
||||
get => (string)GetValue(HeaderProperty);
|
||||
set => SetValue(HeaderProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty HeaderProperty =
|
||||
DependencyProperty.Register(nameof(Header), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => (string)GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty ValueProperty =
|
||||
DependencyProperty.Register(nameof(Value), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Link
|
||||
{
|
||||
get => (string)GetValue(LinkProperty);
|
||||
set => SetValue(LinkProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty LinkProperty =
|
||||
DependencyProperty.Register(nameof(Link), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
|
||||
|
||||
public string Unit
|
||||
{
|
||||
get => (string)GetValue(UnitProperty);
|
||||
set => SetValue(UnitProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty UnitProperty =
|
||||
DependencyProperty.Register(nameof(Unit), typeof(string), typeof(PropertyRow), new PropertyMetadata(string.Empty));
|
||||
|
||||
private Visibility TextVisibility => Link == string.Empty ? Visibility.Visible : Visibility.Collapsed;
|
||||
private Visibility LinkVisibility => Link != string.Empty ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Value))
|
||||
{
|
||||
CopyButton.Opacity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
CopyButton.Opacity = 0;
|
||||
}
|
||||
|
||||
private void OnCopyClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var targetValue = string.IsNullOrEmpty(Link) ? Value : Link;
|
||||
|
||||
if (string.IsNullOrEmpty(targetValue)) return;
|
||||
|
||||
try
|
||||
{
|
||||
DataPackage dataPackage = new DataPackage();
|
||||
dataPackage.SetText(targetValue);
|
||||
Clipboard.SetContent(dataPackage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Copy failed: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
CheckIcon.Opacity = 1;
|
||||
CopyIcon.Opacity = 0;
|
||||
|
||||
this.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
|
||||
CheckIcon.Opacity = 0;
|
||||
CopyIcon.Opacity = 1;
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnLinkClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Uri.TryCreate(Link, UriKind.Absolute, out var uri);
|
||||
if (uri != null)
|
||||
{
|
||||
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
|
||||
{
|
||||
await Windows.System.Launcher.LaunchUriAsync(uri);
|
||||
}
|
||||
else if (uri.Scheme == Uri.UriSchemeFile)
|
||||
{
|
||||
await LauncherHelper.SelectAndShowFile(uri.LocalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Input;
|
||||
@@ -89,14 +90,14 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private void CheckButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool registered = GlobalHotKeyHelper.IsHotKeyRegistered(Shortcut);
|
||||
bool registered = GlobalHotKeyHook.IsHotKeyRegistered(Shortcut);
|
||||
if (registered)
|
||||
{
|
||||
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("SettingsPageShortcutRegSuccessInfo"));
|
||||
ToastHelper.ShowToast("SettingsPageShortcutRegSuccessInfo", null, InfoBarSeverity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
DevWinUI.Growl.Error(_resourceService.GetLocalizedString("SettingsPageShortcutRegFailInfo"));
|
||||
ToastHelper.ShowToast("SettingsPageShortcutRegFailInfo", null, InfoBarSeverity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
{
|
||||
public enum LyricsSearchProvider
|
||||
@@ -17,70 +15,4 @@ namespace BetterLyrics.WinUI3.Enums
|
||||
LocalTtmlFile,
|
||||
AppleMusic,
|
||||
}
|
||||
|
||||
public static class LyricsSearchProviderExtensions
|
||||
{
|
||||
public static string GetCacheDirectory(this LyricsSearchProvider provider)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
|
||||
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
|
||||
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
|
||||
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
|
||||
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
|
||||
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
|
||||
_ => throw new System.ArgumentOutOfRangeException(nameof(provider)),
|
||||
};
|
||||
}
|
||||
|
||||
public static LyricsFormat GetLyricsFormat(this LyricsSearchProvider provider)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
|
||||
LyricsSearchProvider.QQ => LyricsFormat.Qrc,
|
||||
LyricsSearchProvider.Kugou => LyricsFormat.Krc,
|
||||
LyricsSearchProvider.Netease => LyricsFormat.Lrc,
|
||||
LyricsSearchProvider.AmllTtmlDb => LyricsFormat.Ttml,
|
||||
LyricsSearchProvider.AppleMusic => LyricsFormat.Ttml,
|
||||
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
|
||||
LyricsSearchProvider.LocalEslrcFile => LyricsFormat.Eslrc,
|
||||
LyricsSearchProvider.LocalTtmlFile => LyricsFormat.Ttml,
|
||||
_ => LyricsFormat.NotSpecified,
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsLocal(this LyricsSearchProvider provider)
|
||||
{
|
||||
return provider
|
||||
is LyricsSearchProvider.LocalMusicFile
|
||||
or LyricsSearchProvider.LocalLrcFile
|
||||
or LyricsSearchProvider.LocalEslrcFile
|
||||
or LyricsSearchProvider.LocalTtmlFile;
|
||||
}
|
||||
|
||||
public static bool IsRemote(this LyricsSearchProvider provider)
|
||||
{
|
||||
return !provider.IsLocal();
|
||||
}
|
||||
|
||||
public static TranslationSearchProvider? ToTranslationSearchProvider(this LyricsSearchProvider? provider)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => TranslationSearchProvider.LrcLib,
|
||||
LyricsSearchProvider.QQ => TranslationSearchProvider.QQ,
|
||||
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
|
||||
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
|
||||
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
|
||||
LyricsSearchProvider.AppleMusic => TranslationSearchProvider.AppleMusic,
|
||||
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
|
||||
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
|
||||
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,
|
||||
LyricsSearchProvider.LocalTtmlFile => TranslationSearchProvider.LocalTtmlFile,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
{
|
||||
public enum LyricsSearchType
|
||||
{
|
||||
Sequential,
|
||||
BestMatch
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
{
|
||||
public enum SpectrumPlacement
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class LyricsFormatExtensions
|
||||
{
|
||||
extension(LyricsFormat format)
|
||||
{
|
||||
public string ToFileExtension()
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
LyricsFormat.Lrc => ".lrc",
|
||||
LyricsFormat.Qrc => ".qrc",
|
||||
LyricsFormat.Krc => ".krc",
|
||||
LyricsFormat.Eslrc => ".eslrc",
|
||||
LyricsFormat.Ttml => ".ttml",
|
||||
_ => ".*",
|
||||
};
|
||||
}
|
||||
|
||||
public LyricsSearchProvider? ToLyricsSearchProvider()
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
LyricsFormat.Lrc => LyricsSearchProvider.LocalLrcFile,
|
||||
LyricsFormat.Eslrc => LyricsSearchProvider.LocalEslrcFile,
|
||||
LyricsFormat.Ttml => LyricsSearchProvider.LocalTtmlFile,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class LyricsSearchProviderExtensions
|
||||
{
|
||||
extension(LyricsSearchProvider provider)
|
||||
{
|
||||
public string GetCacheDirectory() => provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
|
||||
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
|
||||
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
|
||||
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
|
||||
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
|
||||
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
|
||||
};
|
||||
|
||||
public LyricsFormat GetLyricsFormat() => provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
|
||||
LyricsSearchProvider.QQ => LyricsFormat.Qrc,
|
||||
LyricsSearchProvider.Kugou => LyricsFormat.Krc,
|
||||
LyricsSearchProvider.Netease => LyricsFormat.Lrc,
|
||||
LyricsSearchProvider.AmllTtmlDb => LyricsFormat.Ttml,
|
||||
LyricsSearchProvider.AppleMusic => LyricsFormat.Ttml,
|
||||
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
|
||||
LyricsSearchProvider.LocalEslrcFile => LyricsFormat.Eslrc,
|
||||
LyricsSearchProvider.LocalTtmlFile => LyricsFormat.Ttml,
|
||||
_ => LyricsFormat.NotSpecified,
|
||||
};
|
||||
|
||||
public bool IsLocal() => provider
|
||||
is LyricsSearchProvider.LocalMusicFile
|
||||
or LyricsSearchProvider.LocalLrcFile
|
||||
or LyricsSearchProvider.LocalEslrcFile
|
||||
or LyricsSearchProvider.LocalTtmlFile;
|
||||
|
||||
public bool IsRemote() => !provider.IsLocal();
|
||||
|
||||
public TranslationSearchProvider? ToTranslationSearchProvider() => provider switch
|
||||
{
|
||||
LyricsSearchProvider.LrcLib => TranslationSearchProvider.LrcLib,
|
||||
LyricsSearchProvider.QQ => TranslationSearchProvider.QQ,
|
||||
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
|
||||
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
|
||||
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
|
||||
LyricsSearchProvider.AppleMusic => TranslationSearchProvider.AppleMusic,
|
||||
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
|
||||
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
|
||||
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,
|
||||
LyricsSearchProvider.LocalTtmlFile => TranslationSearchProvider.LocalTtmlFile,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
private static readonly string[] _splitter =
|
||||
[
|
||||
";"
|
||||
,
|
||||
","
|
||||
,
|
||||
"/"
|
||||
,
|
||||
";"
|
||||
,
|
||||
"、"
|
||||
,
|
||||
","
|
||||
];
|
||||
|
||||
extension(string str)
|
||||
{
|
||||
public string[] SplitByCommonSplitter()
|
||||
{
|
||||
var splitter = _splitter.FirstOrDefault(str.Contains);
|
||||
if (splitter != null)
|
||||
{
|
||||
return str.Split(splitter);
|
||||
}
|
||||
else
|
||||
{
|
||||
return [str];
|
||||
}
|
||||
}
|
||||
|
||||
public LyricsFormat? DetectFormat()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
return null;
|
||||
|
||||
// TTML: 检查 <tt ... xmlns="http://www.w3.org/ns/ttml"
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(
|
||||
str,
|
||||
@"<tt\b[^>]*\bxmlns\s*=\s*[""']http://www\.w3\.org/ns/ttml[""']",
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
return LyricsFormat.Ttml;
|
||||
}
|
||||
// KRC: 检测主内容格式 [start,duration]<offset,duration,0>字...
|
||||
else if (System.Text.RegularExpressions.Regex.IsMatch(
|
||||
str,
|
||||
@"^\[\d+,\d+\](<\d+,\d+,0>.+)+",
|
||||
System.Text.RegularExpressions.RegexOptions.Multiline))
|
||||
{
|
||||
return LyricsFormat.Krc;
|
||||
}
|
||||
// QRC: 检测主内容格式 [start,duration]字(offset,duration)
|
||||
else if (System.Text.RegularExpressions.Regex.IsMatch(
|
||||
str,
|
||||
@"^\[\d+,\d+\].*?\(\d+,\d+\)",
|
||||
System.Text.RegularExpressions.RegexOptions.Multiline))
|
||||
{
|
||||
return LyricsFormat.Qrc;
|
||||
}
|
||||
// 标准LRC和增强型LRC
|
||||
else if (System.Text.RegularExpressions.Regex.IsMatch(str, @"\[\d{1,2}:\d{2}") ||
|
||||
System.Text.RegularExpressions.Regex.IsMatch(str, @"<\d{1,2}:\d{2}\.\d{2,3}>"))
|
||||
{
|
||||
return LyricsFormat.Lrc;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Serialization;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
@@ -35,12 +38,17 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string? ReadLyricsCache(string title, string artist, string album, LyricsFormat format, string cacheFolderPath)
|
||||
public static LyricsSearchResult? ReadLyricsCache(SongInfo songInfo, LyricsSearchProvider lyricsSearchProvider)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title} - {album}{format.ToFileExtension()}"));
|
||||
var cacheFilePath = Path.Combine(
|
||||
lyricsSearchProvider.GetCacheDirectory(),
|
||||
SanitizeFileName($"{songInfo.ToFileName()}.json"));
|
||||
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
return File.ReadAllText(cacheFilePath);
|
||||
var json = File.ReadAllText(cacheFilePath);
|
||||
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -55,29 +63,21 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void WriteLyricsCache(string title, string artist, string album, string lyrics, LyricsFormat format, string cacheFolderPath)
|
||||
public static void WriteLyricsCache(SongInfo songInfo, LyricsSearchResult lyricsSearchResult)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title} - {album}{format.ToFileExtension()}"));
|
||||
File.WriteAllText(cacheFilePath, lyrics);
|
||||
var cacheFilePath = Path.Combine(
|
||||
lyricsSearchResult.Provider.GetCacheDirectory(),
|
||||
SanitizeFileName($"{songInfo.ToFileName()}.json"));
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
|
||||
File.WriteAllText(cacheFilePath, json);
|
||||
}
|
||||
|
||||
public static void WriteAlbumArtCache(string album, string artist, byte[] img, string format, string cacheFolderPath)
|
||||
public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath)
|
||||
{
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
|
||||
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.DisplayArtists} - {songInfo.Album}{format}"));
|
||||
File.WriteAllBytes(cacheFilePath, img);
|
||||
}
|
||||
|
||||
public static bool IsSwitchableNormalizedMatch(string fileName, string q1, string q2)
|
||||
{
|
||||
var normFileName = StringHelper.Normalize(fileName);
|
||||
var normQ1 = StringHelper.Normalize(q1);
|
||||
var normQ2 = StringHelper.Normalize(q2);
|
||||
|
||||
// 常见两种顺序
|
||||
return normFileName == normQ1 + normQ2
|
||||
|| normFileName == normQ2 + normQ1;
|
||||
}
|
||||
|
||||
public static readonly string[] MusicExtensions = {
|
||||
".mp3", ".aac", ".m4a", ".ogg", ".opus", ".wma", ".amr",
|
||||
".flac", ".alac", ".ape", ".wv", ".tak",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
private static readonly RankedLanguageIdentifier _identifier;
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
|
||||
public const string ChineseCode = "zh";
|
||||
public const string JapaneseCode = "ja";
|
||||
|
||||
public static List<ExtendedLanguage> SupportedTranslationTargetLanguages { get; set; } =
|
||||
[
|
||||
new ExtendedLanguage("ar"),
|
||||
@@ -105,7 +108,6 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
public static string? DetectLanguageCode(string? text)
|
||||
{
|
||||
if (text == null) return null;
|
||||
|
||||
var guessList = _identifier.Identify(text);
|
||||
string? code = guessList?.FirstOrDefault()?.Item1.Iso639_2T;
|
||||
code = code switch
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using Lyricify.Lyrics.Parsers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public partial class LyricsParser
|
||||
{
|
||||
public List<LyricsData> LyricsDataArr { get; private set; } = [];
|
||||
|
||||
public void Parse(List<MappedSongSearchQuery> mappedSongSearchQueries, string title, string artist, string album, string? raw, int? durationMs, LyricsSearchProvider? lyricsSearchProvider)
|
||||
{
|
||||
var overridenTitle = title;
|
||||
var overridenArtist = artist;
|
||||
var overridenAlbum = album;
|
||||
|
||||
var found = mappedSongSearchQueries
|
||||
.Where(x => x.OriginalTitle == overridenTitle && x.OriginalArtist == overridenArtist && x.OriginalAlbum == overridenAlbum)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (found != null)
|
||||
{
|
||||
overridenTitle = found.MappedTitle;
|
||||
overridenArtist = found.MappedArtist;
|
||||
overridenAlbum = found.MappedAlbum;
|
||||
}
|
||||
|
||||
LyricsDataArr = [];
|
||||
durationMs ??= (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
|
||||
if (raw == null)
|
||||
{
|
||||
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder(durationMs.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (raw.DetectFormat())
|
||||
{
|
||||
case LyricsFormat.Lrc:
|
||||
case LyricsFormat.Eslrc:
|
||||
ParseLrc(raw);
|
||||
break;
|
||||
case LyricsFormat.Qrc:
|
||||
ParseQrcKrc(QrcParser.Parse(raw).Lines);
|
||||
break;
|
||||
case LyricsFormat.Krc:
|
||||
ParseQrcKrc(KrcParser.Parse(raw).Lines);
|
||||
break;
|
||||
case LyricsFormat.Ttml:
|
||||
ParseTtml(raw);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
FillRomanizationLyricsData();
|
||||
FillTranslationFromCache(overridenTitle, overridenArtist, overridenAlbum, lyricsSearchProvider);
|
||||
}
|
||||
|
||||
private void FillTranslationFromCache(string title, string artist, string album, LyricsSearchProvider? provider)
|
||||
{
|
||||
string? translationRaw = null;
|
||||
switch (provider)
|
||||
{
|
||||
case LyricsSearchProvider.QQ:
|
||||
translationRaw = FileHelper.ReadLyricsCache(title, artist, album, LyricsFormat.Lrc, PathHelper.QQTranslationCacheDirectory);
|
||||
break;
|
||||
case LyricsSearchProvider.Kugou:
|
||||
translationRaw = FileHelper.ReadLyricsCache(title, artist, album, LyricsFormat.Lrc, PathHelper.KugouTranslationCacheDirectory);
|
||||
break;
|
||||
case LyricsSearchProvider.Netease:
|
||||
translationRaw = FileHelper.ReadLyricsCache(title, artist, album, LyricsFormat.Lrc, PathHelper.NeteaseTranslationCacheDirectory);
|
||||
break;
|
||||
case LyricsSearchProvider.LrcLib:
|
||||
break;
|
||||
case LyricsSearchProvider.AmllTtmlDb:
|
||||
break;
|
||||
case LyricsSearchProvider.LocalMusicFile:
|
||||
break;
|
||||
case LyricsSearchProvider.LocalLrcFile:
|
||||
break;
|
||||
case LyricsSearchProvider.LocalEslrcFile:
|
||||
break;
|
||||
case LyricsSearchProvider.LocalTtmlFile:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (translationRaw != null)
|
||||
{
|
||||
switch (provider)
|
||||
{
|
||||
case LyricsSearchProvider.QQ:
|
||||
case LyricsSearchProvider.Kugou:
|
||||
case LyricsSearchProvider.Netease:
|
||||
ParseLrc(translationRaw);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FillRomanizationLyricsData()
|
||||
{
|
||||
var chinese = LyricsDataArr.Where(x => x.LanguageCode == "zh").FirstOrDefault();
|
||||
if (chinese != null)
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.PinyinCode,
|
||||
LyricsLines = chinese.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToPinyin(line.OriginalText),
|
||||
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToPinyin(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
LyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.JyutpingCode,
|
||||
LyricsLines = chinese.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToJyutping(line.OriginalText),
|
||||
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToJyutping(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
var japanese = LyricsDataArr.Where(x => x.LanguageCode == "ja").FirstOrDefault();
|
||||
if (japanese != null)
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.RomajiCode,
|
||||
LyricsLines = japanese.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToRomaji(line.OriginalText),
|
||||
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToRomaji(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseLrc(string raw)
|
||||
{
|
||||
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
|
||||
var lrcLines =
|
||||
new List<(int time, string text, List<(int time, string text)> syllables)>();
|
||||
|
||||
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
|
||||
var syllableRegex = SyllableRegex();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var matches = syllableRegex.Matches(line);
|
||||
var syllables = new List<(int, string)>();
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
var m = matches[i];
|
||||
int min = int.Parse(m.Groups[2].Value);
|
||||
int sec = int.Parse(m.Groups[3].Value);
|
||||
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
|
||||
int totalMs = min * 60_000 + sec * 1000 + ms;
|
||||
string text = m.Groups[6].Value;
|
||||
|
||||
syllables.Add((totalMs, text));
|
||||
}
|
||||
if (syllables.Count > 1)
|
||||
{
|
||||
lrcLines.Add(
|
||||
(
|
||||
syllables[0].Item1,
|
||||
string.Concat(syllables.Select(s => s.Item2)),
|
||||
syllables
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通LRC行
|
||||
Regex? bracketRegex = LrcRegex();
|
||||
var bracketMatches = bracketRegex.Matches(line);
|
||||
|
||||
string content = line;
|
||||
int? lineStartTime = null;
|
||||
if (bracketMatches.Count > 0)
|
||||
{
|
||||
var m = bracketMatches![0];
|
||||
int min = int.Parse(m.Groups[1].Value);
|
||||
int sec = int.Parse(m.Groups[2].Value);
|
||||
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
|
||||
lineStartTime = min * 60_000 + sec * 1000 + ms;
|
||||
content = bracketRegex!.Replace(line, "");
|
||||
if (content == "//") content = "";
|
||||
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间分组
|
||||
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
|
||||
int languageCount = 0;
|
||||
if (grouped != null && grouped.Count > 0)
|
||||
{
|
||||
// 计算最大语言数量
|
||||
languageCount = grouped.Max(g => g.Count());
|
||||
}
|
||||
|
||||
// 初始化每种语言的歌词列表
|
||||
//LyricsDataArr.Clear();
|
||||
int langStartIndex = LyricsDataArr.Count;
|
||||
for (int i = 0; i < languageCount; i++) LyricsDataArr.Add(new LyricsData());
|
||||
|
||||
// 遍历每个时间分组
|
||||
if (grouped != null)
|
||||
{
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
var linesInGroup = group.ToList();
|
||||
for (int langIdx = 0; langIdx < languageCount; langIdx++)
|
||||
{
|
||||
// 只添加有对应行的语言,否则跳过
|
||||
if (langIdx < linesInGroup.Count)
|
||||
{
|
||||
var (start, text, syllables) = linesInGroup[langIdx];
|
||||
var line = new LyricsLine
|
||||
{
|
||||
StartMs = start,
|
||||
OriginalText = text,
|
||||
LyricsChars = [],
|
||||
};
|
||||
if (syllables != null && syllables.Count > 0)
|
||||
{
|
||||
int currentIndex = 0;
|
||||
for (int j = 0; j < syllables.Count; j++)
|
||||
{
|
||||
var (charStart, charText) = syllables[j];
|
||||
int startIndex = currentIndex;
|
||||
line.LyricsChars.Add(
|
||||
new LyricsChar
|
||||
{
|
||||
StartMs = charStart,
|
||||
Text = charText ?? "",
|
||||
StartIndex = startIndex,
|
||||
}
|
||||
);
|
||||
currentIndex += charText?.Length ?? 0;
|
||||
}
|
||||
}
|
||||
LyricsDataArr[langStartIndex + langIdx].LyricsLines.Add(line);
|
||||
}
|
||||
// 没有翻译行则不补原文,直接跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseTtml(string raw)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<LyricsLine> originalLines = [];
|
||||
List<LyricsLine> translationLines = [];
|
||||
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
|
||||
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
|
||||
if (body == null) return;
|
||||
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
|
||||
foreach (var p in ps)
|
||||
{
|
||||
// 句级时间
|
||||
string? pBegin = p.Attribute("begin")?.Value;
|
||||
string? pEnd = p.Attribute("end")?.Value;
|
||||
int pStartMs = ParseTtmlTime(pBegin);
|
||||
int pEndMs = ParseTtmlTime(pEnd);
|
||||
|
||||
// 只获取一级span,且排除 ttm:role="x-bg" 的 span 和 ttm:role="x-roman"
|
||||
var spans = p.Elements()
|
||||
.Where(s => s.Name.LocalName == "span" &&
|
||||
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-bg" &&
|
||||
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-roman")
|
||||
.ToList();
|
||||
|
||||
// 原文和翻译分离
|
||||
var originalTextSpans = spans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-translation")
|
||||
.ToList();
|
||||
var translationTextSpans = spans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
|
||||
.ToList();
|
||||
|
||||
// 处理原文span后的空白
|
||||
for (int i = 0; i < originalTextSpans.Count; i++)
|
||||
{
|
||||
var span = originalTextSpans[i];
|
||||
var nextNode = span.NodesAfterSelf().FirstOrDefault();
|
||||
if (nextNode is XText textNode)
|
||||
{
|
||||
span.Value += textNode.Value;
|
||||
}
|
||||
}
|
||||
// 拼接空白字符后的原文
|
||||
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
|
||||
|
||||
var originalCharTimings = new List<LyricsChar>();
|
||||
int originalStartIndex = 0;
|
||||
foreach (var span in originalTextSpans)
|
||||
{
|
||||
string? sBegin = span.Attribute("begin")?.Value;
|
||||
string? sEnd = span.Attribute("end")?.Value;
|
||||
int sStartMs = ParseTtmlTime(sBegin);
|
||||
int sEndMs = ParseTtmlTime(sEnd);
|
||||
originalCharTimings.Add(new LyricsChar
|
||||
{
|
||||
StartMs = sStartMs,
|
||||
EndMs = sEndMs,
|
||||
StartIndex = originalStartIndex,
|
||||
Text = span.Value
|
||||
});
|
||||
originalStartIndex += span.Value.Length;
|
||||
}
|
||||
if (originalTextSpans.Count == 0)
|
||||
originalText = p.Value;
|
||||
|
||||
originalLines.Add(new LyricsLine
|
||||
{
|
||||
StartMs = pStartMs,
|
||||
EndMs = pEndMs,
|
||||
OriginalText = originalText,
|
||||
LyricsChars = originalCharTimings,
|
||||
});
|
||||
|
||||
// 翻译
|
||||
string translationText = string.Concat(translationTextSpans.Select(s => s.Value));
|
||||
var translationCharTimings = new List<LyricsChar>();
|
||||
int translationStartIndex = 0;
|
||||
foreach (var span in translationTextSpans)
|
||||
{
|
||||
string? sBegin = span.Attribute("begin")?.Value;
|
||||
string? sEnd = span.Attribute("end")?.Value;
|
||||
int sStartMs = ParseTtmlTime(sBegin);
|
||||
int sEndMs = ParseTtmlTime(sEnd);
|
||||
translationCharTimings.Add(new LyricsChar
|
||||
{
|
||||
StartMs = sStartMs,
|
||||
EndMs = sEndMs,
|
||||
StartIndex = translationStartIndex,
|
||||
Text = span.Value
|
||||
});
|
||||
translationStartIndex += span.Value.Length;
|
||||
}
|
||||
if (translationTextSpans.Count > 0)
|
||||
{
|
||||
translationLines.Add(new LyricsLine
|
||||
{
|
||||
StartMs = pStartMs,
|
||||
EndMs = pEndMs,
|
||||
OriginalText = translationText,
|
||||
LyricsChars = translationCharTimings,
|
||||
});
|
||||
}
|
||||
}
|
||||
LyricsDataArr.Add(new LyricsData(originalLines));
|
||||
if (translationLines.Count > 0)
|
||||
LyricsDataArr.Add(new LyricsData(translationLines));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
private static int ParseTtmlTime(string? t)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(t))
|
||||
return 0;
|
||||
|
||||
t = t.Trim();
|
||||
|
||||
// 支持 "1.000s"
|
||||
if (t.EndsWith("s"))
|
||||
{
|
||||
if (
|
||||
double.TryParse(
|
||||
t.TrimEnd('s'),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out double seconds
|
||||
)
|
||||
)
|
||||
return (int)(seconds * 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = t.Split(':');
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
// hh:mm:ss.xxx
|
||||
int h = int.Parse(parts[0]);
|
||||
int m = int.Parse(parts[1]);
|
||||
double s = double.Parse(
|
||||
parts[2],
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
return (int)((h * 3600 + m * 60 + s) * 1000);
|
||||
}
|
||||
else if (parts.Length == 2)
|
||||
{
|
||||
// mm:ss.xxx
|
||||
int m = int.Parse(parts[0]);
|
||||
double s = double.Parse(
|
||||
parts[1],
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
return (int)((m * 60 + s) * 1000);
|
||||
}
|
||||
else if (parts.Length == 1)
|
||||
{
|
||||
// ss.xxx
|
||||
if (
|
||||
double.TryParse(
|
||||
parts[0],
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out double s
|
||||
)
|
||||
)
|
||||
return (int)(s * 1000);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ParseQrcKrc(List<Lyricify.Lyrics.Models.ILineInfo>? lines)
|
||||
{
|
||||
lines = lines?.Where(x => x.Text != string.Empty).ToList();
|
||||
List<LyricsLine> lyricsLines = [];
|
||||
|
||||
if (lines != null && lines.Count > 0)
|
||||
{
|
||||
lyricsLines = [];
|
||||
for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
|
||||
{
|
||||
var lineRead = lines[lineIndex];
|
||||
var lineWrite = new LyricsLine
|
||||
{
|
||||
StartMs = lineRead.StartTime ?? 0,
|
||||
EndMs = lineRead.EndTime ?? 0,
|
||||
OriginalText = lineRead.Text,
|
||||
LyricsChars = [],
|
||||
};
|
||||
|
||||
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;
|
||||
if (syllables != null)
|
||||
{
|
||||
int startIndex = 0;
|
||||
for (
|
||||
int syllableIndex = 0;
|
||||
syllableIndex < syllables.Count;
|
||||
syllableIndex++
|
||||
)
|
||||
{
|
||||
var syllable = syllables[syllableIndex];
|
||||
var charTiming = new LyricsChar
|
||||
{
|
||||
StartMs = syllable.StartTime,
|
||||
EndMs = syllable.EndTime,
|
||||
Text = syllable.Text,
|
||||
StartIndex = startIndex,
|
||||
};
|
||||
lineWrite.LyricsChars.Add(charTiming);
|
||||
startIndex += syllable.Text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
lyricsLines.Add(lineWrite);
|
||||
}
|
||||
}
|
||||
|
||||
LyricsDataArr.Add(new LyricsData(lyricsLines));
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\[(\d*):(\d*)(\.|\:)(\d*)\]")]
|
||||
private static partial Regex LrcRegex();
|
||||
[GeneratedRegex(@"(\[|\<)(\d*):(\d*)\.(\d*)(\]|\>)([^\[\]\<\>]*)")]
|
||||
private static partial Regex SyllableRegex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using F23.StringSimilarity;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public static partial class MetadataComparer
|
||||
{
|
||||
private const double WeightTitle = 0.40;
|
||||
private const double WeightArtist = 0.40;
|
||||
private const double WeightAlbum = 0.10;
|
||||
private const double WeightDuration = 0.10;
|
||||
|
||||
// JaroWinkler 适合短字符串匹配
|
||||
private static readonly JaroWinkler _algo = new JaroWinkler();
|
||||
|
||||
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
|
||||
{
|
||||
if (local == null || remote == null) return 0;
|
||||
|
||||
double titleScore = GetStringSimilarity(local.Title, remote.Title);
|
||||
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
|
||||
double albumScore = GetStringSimilarity(local.Album, remote.Album);
|
||||
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration);
|
||||
|
||||
double totalScore = (titleScore * WeightTitle) +
|
||||
(artistScore * WeightArtist) +
|
||||
(albumScore * WeightAlbum) +
|
||||
(durationScore * WeightDuration);
|
||||
|
||||
return (int)Math.Round(totalScore * 100);
|
||||
}
|
||||
|
||||
public static int CalculateScore(SongInfo songInfo, string filePathOrName)
|
||||
{
|
||||
if (songInfo == null || string.IsNullOrWhiteSpace(filePathOrName)) return 0;
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(filePathOrName);
|
||||
string fileFingerprint = CreateSortedFingerprint(fileName);
|
||||
|
||||
var infoParts = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(songInfo.Title))
|
||||
infoParts.Add(songInfo.Title);
|
||||
|
||||
if (songInfo.Artists != null)
|
||||
infoParts.AddRange(songInfo.Artists);
|
||||
|
||||
string infoRaw = string.Join(" ", infoParts);
|
||||
string infoFingerprint = CreateSortedFingerprint(infoRaw);
|
||||
|
||||
double score = _algo.Similarity(infoFingerprint, fileFingerprint);
|
||||
|
||||
return (int)Math.Round(score * 100);
|
||||
}
|
||||
|
||||
private static double GetStringSimilarity(string? s1, string? s2)
|
||||
{
|
||||
s1 = s1?.Trim().ToLowerInvariant() ?? "";
|
||||
s2 = s2?.Trim().ToLowerInvariant() ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 1.0; // 都是空,视为匹配
|
||||
if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2)) return 0.0; // 其中一个为空
|
||||
|
||||
return _algo.Similarity(s1, s2);
|
||||
}
|
||||
|
||||
private static double GetArtistSimilarity(string[]? localArtists, string[]? remoteArtists)
|
||||
{
|
||||
if (localArtists == null || localArtists.Length == 0) return 0.0;
|
||||
if (remoteArtists == null || remoteArtists.Length == 0) return 0.0;
|
||||
|
||||
// 将艺术家数组排序并连接,避免顺序不同导致的不匹配
|
||||
var s1 = string.Join(" ", localArtists.OrderBy(a => a).Select(a => a.Trim().ToLowerInvariant()));
|
||||
var s2 = string.Join(" ", remoteArtists.OrderBy(a => a).Select(a => a.Trim().ToLowerInvariant()));
|
||||
|
||||
return _algo.Similarity(s1, s2);
|
||||
}
|
||||
|
||||
private static double GetDurationSimilarity(double localMs, double? remoteSeconds)
|
||||
{
|
||||
if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配
|
||||
|
||||
double localSeconds = localMs / 1000.0;
|
||||
double diff = Math.Abs(localSeconds - remoteSeconds.Value);
|
||||
|
||||
// 差距 <= 3秒:100% 相似
|
||||
// 差距 >= 20秒:0% 相似
|
||||
// 中间线性插值
|
||||
|
||||
const double PerfectTolerance = 3.0;
|
||||
const double MaxTolerance = 20.0;
|
||||
|
||||
if (diff <= PerfectTolerance) return 1.0;
|
||||
if (diff >= MaxTolerance) return 0.0;
|
||||
|
||||
return 1.0 - ((diff - PerfectTolerance) / (MaxTolerance - PerfectTolerance));
|
||||
}
|
||||
|
||||
private static string CreateSortedFingerprint(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input)) return "";
|
||||
|
||||
input = input.ToLowerInvariant();
|
||||
|
||||
string cleaned = NonWordCharactersRegex().Replace(input, " ");
|
||||
|
||||
var tokens = cleaned.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.OrderBy(t => t); // 排序
|
||||
|
||||
return string.Join(" ", tokens);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[\p{P}\p{S}]")]
|
||||
private static partial Regex NonWordCharactersRegex();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,23 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
/// <param name="value">要保存的值</param>
|
||||
public static void Save(string resource, string key, string value)
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
|
||||
// 删除旧值(避免重复存储)
|
||||
try
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
|
||||
var oldCredential = vault.Retrieve(resource, key);
|
||||
if (oldCredential != null)
|
||||
{
|
||||
vault.Remove(oldCredential);
|
||||
}
|
||||
|
||||
vault.Add(new PasswordCredential(resource, key, value));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 没有旧值就忽略
|
||||
}
|
||||
|
||||
vault.Add(new PasswordCredential(resource, key, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,9 +39,10 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
/// <returns>存储的值,若不存在则返回 null</returns>
|
||||
public static string? Get(string resource, string key)
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
try
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
|
||||
var credential = vault.Retrieve(resource, key);
|
||||
credential.RetrievePassword();
|
||||
return credential.Password;
|
||||
@@ -57,9 +58,10 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
/// </summary>
|
||||
public static void Delete(string resource, string key)
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
try
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
|
||||
var credential = vault.Retrieve(resource, key);
|
||||
vault.Remove(credential);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
|
||||
public const string PinyinCode = "zh-pinyin";
|
||||
public const string JyutpingCode = "zh-jyutping";
|
||||
public const string RomajiCode = "ja-romaji";
|
||||
public const string PinyinCode = "zh-cmn-pinyin";
|
||||
public const string JyutpingCode = "zh-yue-jyutping";
|
||||
public const string RomanCode = "ja-latin";
|
||||
|
||||
public static bool IsPhoneticCode(string code)
|
||||
{
|
||||
return code == PinyinCode || code == JyutpingCode || code == RomajiCode;
|
||||
return code == PinyinCode || code == JyutpingCode || code == RomanCode;
|
||||
}
|
||||
|
||||
public static string GetDisplayName(string code)
|
||||
@@ -25,7 +25,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
return _resourceService.GetLocalizedString("Pinyin");
|
||||
case JyutpingCode:
|
||||
return _resourceService.GetLocalizedString("Jyutping");
|
||||
case RomajiCode:
|
||||
case RomanCode:
|
||||
return _resourceService.GetLocalizedString("Romaji");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(code));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using BetterLyrics.WinUI3.Constants;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public static class PlayerIDHelper
|
||||
{
|
||||
private static readonly List<string> neteaseFamilyRegex =
|
||||
[
|
||||
"cloudmusic.exe", //NetEaseCloudMusic
|
||||
"^17588BrandonWong\\.LyricEase_", //LyricEase
|
||||
"^48848aaaaaaccd\\.HyPlayer_" //HyPlayer
|
||||
];
|
||||
|
||||
public static bool IsNeteaseFamily(string? id)
|
||||
{
|
||||
if (id is null) return false;
|
||||
|
||||
foreach (var regex in neteaseFamilyRegex)
|
||||
{
|
||||
var isMatch = Regex.IsMatch(id, regex);
|
||||
if (isMatch) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsLXMusic(string? id) => id is PlayerID.LXMusic or PlayerID.LXMusicPortable;
|
||||
|
||||
public static bool IsAppleMusic(string? id) => id is PlayerID.AppleMusic or PlayerID.AppleMusicAlternative;
|
||||
|
||||
public static string? GetDisplayName(string? id) => id switch
|
||||
{
|
||||
PlayerID.Spotify => PlayerName.Spotify,
|
||||
PlayerID.AppleMusic => PlayerName.AppleMusic,
|
||||
PlayerID.iTunes => PlayerName.iTunes,
|
||||
PlayerID.KugouMusic => PlayerName.KugouMusic,
|
||||
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
|
||||
PlayerID.QQMusic => PlayerName.QQMusic,
|
||||
PlayerID.LXMusic => PlayerName.LXMusic,
|
||||
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable,
|
||||
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
|
||||
PlayerID.AIMP => PlayerName.AIMP,
|
||||
PlayerID.Foobar2000 => PlayerName.Foobar2000,
|
||||
PlayerID.MusicBee => PlayerName.MusicBee,
|
||||
PlayerID.PotPlayer => PlayerName.PotPlayer,
|
||||
PlayerID.Chrome => PlayerName.Chrome,
|
||||
PlayerID.Edge => PlayerName.Edge,
|
||||
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
|
||||
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
|
||||
PlayerID.SaltPlayerForWindows => PlayerName.SaltPlayerForWindows,
|
||||
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
|
||||
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
|
||||
PlayerID.Listen1 => PlayerName.Listen1,
|
||||
_ => id,
|
||||
};
|
||||
|
||||
public static string GetLogoPath(string? id) => id switch
|
||||
{
|
||||
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
|
||||
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
|
||||
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
|
||||
PlayerID.iTunes => PathHelper.iTunesLogoPath,
|
||||
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
|
||||
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
|
||||
PlayerID.QQMusic => PathHelper.QQMusicLogoPath,
|
||||
PlayerID.LXMusic => PathHelper.LXMusicLogoPath,
|
||||
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath,
|
||||
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
|
||||
PlayerID.AIMP => PathHelper.AIMPLogoPath,
|
||||
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath,
|
||||
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath,
|
||||
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath,
|
||||
PlayerID.Chrome => PathHelper.ChromeLogoPath,
|
||||
PlayerID.Edge => PathHelper.EdgeLogoPath,
|
||||
PlayerID.BetterLyrics => PathHelper.LogoPath,
|
||||
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
|
||||
PlayerID.SaltPlayerForWindows => PathHelper.SaltPlayerForWindowsLogoPath,
|
||||
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
|
||||
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
|
||||
PlayerID.Listen1 => PathHelper.Listen1LogoPath,
|
||||
_ => PathHelper.UnknownPlayerLogoPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public static class STATaskHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 在一个专用的后台 STA 线程上运行一个函数。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">返回类型</typeparam>
|
||||
/// <param name="func">要执行的函数</param>
|
||||
/// <returns>一个 Task,其结果是函数的返回值</returns>
|
||||
public static Task<TResult> RunAsSTATask<TResult>(Func<TResult> func)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<TResult>();
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = func();
|
||||
tcs.SetResult(result);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
tcs.SetException(e);
|
||||
}
|
||||
});
|
||||
|
||||
// (关键) 设置单元状态为 STA
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在一个专用的后台 STA 线程上运行一个 Action。
|
||||
/// </summary>
|
||||
/// <param name="action">要执行的 Action</param>
|
||||
/// <returns>一个 Task</returns>
|
||||
public static Task RunAsSTATask(Action action)
|
||||
{
|
||||
return RunAsSTATask(() =>
|
||||
{
|
||||
action();
|
||||
return true; // 返回一个虚拟结果
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Microsoft.Windows.AppNotifications.Builder;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public class ToastHelper
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
|
||||
public static void ShowToast(string localizedTitleKey, string? description, InfoBarSeverity severity)
|
||||
{
|
||||
AppNotification notification = new AppNotificationBuilder()
|
||||
.AddText(_resourceService.GetLocalizedString(localizedTitleKey))
|
||||
.AddText(description)
|
||||
.BuildNotification();
|
||||
|
||||
AppNotificationManager.Default.Show(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Hooks/AppHook.cs
Normal file
77
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Hooks/AppHook.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Vanara.PInvoke;
|
||||
using Vanara.Windows.Shell;
|
||||
using static Vanara.PInvoke.Shell32;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Hooks
|
||||
{
|
||||
public class AppHook
|
||||
{
|
||||
public static HICON? GetIcon(ShellItem shellItem, int size = 32)
|
||||
{
|
||||
HICON hIconCopy = HICON.NULL;
|
||||
|
||||
try
|
||||
{
|
||||
using Bitmap baseIcon = shellItem.GetImage(new SIZE { Height = size, Width = size }, ShellItemGetImageOptions.ResizeToFit).ToBitmap();
|
||||
hIconCopy = User32.CopyIcon(baseIcon.GetHicon());
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
if (hIconCopy.IsNull)
|
||||
{
|
||||
User32.DestroyIcon(hIconCopy);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return hIconCopy;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<BitmapImage?> ToBitmapImageAsync(HICON hIcon)
|
||||
{
|
||||
if (hIcon.IsNull)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using Icon icon = Icon.FromHandle(hIcon.DangerousGetHandle());
|
||||
using Bitmap bitmap = icon.ToBitmap();
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
bitmap.Save(memoryStream, ImageFormat.Png);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
await bitmapImage.SetSourceAsync(memoryStream.AsRandomAccessStream());
|
||||
|
||||
User32.DestroyIcon(hIcon);
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
|
||||
public static ShellItem? GetShellItem(string aumid)
|
||||
{
|
||||
string path = $"shell:AppsFolder\\{aumid}";
|
||||
if (Path.Exists(path))
|
||||
{
|
||||
return new ShellItem(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
var shellFolder = new ShellFolder(KNOWNFOLDERID.FOLDERID_AppsFolder);
|
||||
var found = shellFolder.FirstOrDefault(x => x.ParsingName?.EndsWith(aumid) == true);
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GetDisplayName(ShellItem shellItem) => shellItem.GetDisplayName(ShellItemDisplayString.NormalDisplay);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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);
|
||||
@@ -1,6 +1,7 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Services.LiveStatesService;
|
||||
using BetterLyrics.WinUI3.Services.MediaSessionsService;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
@@ -18,9 +19,9 @@ using Windows.Foundation;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
namespace BetterLyrics.WinUI3.Hooks
|
||||
{
|
||||
public static class WindowHelper
|
||||
public static class WindowHook
|
||||
{
|
||||
private static List<object> _activeWindows = [];
|
||||
private static List<object> _workAreas = [];
|
||||
@@ -54,7 +55,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
}
|
||||
}
|
||||
|
||||
public static T? GetWindowByWindowType<T>()
|
||||
public static T? GetWindow<T>()
|
||||
{
|
||||
foreach (var window in _activeWindows)
|
||||
{
|
||||
@@ -72,7 +73,19 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
return frameworkElement.XamlRoot.ContentIslandEnvironment.AppWindowId.GetWindowHandle();
|
||||
}
|
||||
return null;
|
||||
else if (obj != null)
|
||||
{
|
||||
return WindowNative.GetWindowHandle(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static IntPtr? GetWindowHandle<T>()
|
||||
{
|
||||
return GetWindowHandle(GetWindow<T>());
|
||||
}
|
||||
|
||||
public static void OpenOrShowWindow<T>()
|
||||
@@ -123,6 +136,17 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
lyricsWindow.ViewModel.InitFgWindowWatcher();
|
||||
|
||||
_mediaSessionsService.InitPlaybackShortcuts();
|
||||
|
||||
//TaskbarList.ThumbBarAddButtons(hwnd,
|
||||
// [
|
||||
// new Shell32.THUMBBUTTON()
|
||||
// {
|
||||
// szTip = "Previous",
|
||||
// dwFlags = Shell32.THUMBBUTTONFLAGS.THBF_ENABLED,
|
||||
// dwMask = Shell32.THUMBBUTTONMASK.THB_TOOLTIP | Shell32.THUMBBUTTONMASK.THB_FLAGS,
|
||||
// }
|
||||
// ]
|
||||
//);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -188,7 +212,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void SetIsClickThrough<T>(bool enable)
|
||||
{
|
||||
Window? window = GetWindowByWindowType<T>() as Window;
|
||||
Window? window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
IntPtr hwnd = WindowNative.GetWindowHandle(window);
|
||||
@@ -205,7 +229,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void SetIsWorkArea<T>(bool enable)
|
||||
{
|
||||
Window? window = GetWindowByWindowType<T>() as Window;
|
||||
Window? window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
IntPtr hwnd = WindowNative.GetWindowHandle(window);
|
||||
@@ -222,7 +246,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void SetIsBorderless<T>(bool enable)
|
||||
{
|
||||
var window = GetWindowByWindowType<T>() as Window;
|
||||
var window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
var hwnd = WindowNative.GetWindowHandle(window);
|
||||
@@ -239,7 +263,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void SetIsShowInSwitchers<T>(bool enable)
|
||||
{
|
||||
var window = GetWindowByWindowType<T>() as Window;
|
||||
var window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
window.AppWindow.IsShownInSwitchers = enable;
|
||||
@@ -247,7 +271,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void SetIsAlwaysOnTop<T>(bool enable)
|
||||
{
|
||||
var window = GetWindowByWindowType<T>() as Window;
|
||||
var window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
if (window.AppWindow.Presenter is OverlappedPresenter presenter)
|
||||
@@ -258,7 +282,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void MoveAndResize<T>(Rect rect)
|
||||
{
|
||||
var window = GetWindowByWindowType<T>() as Window;
|
||||
var window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
window.AppWindow.Move(new Windows.Graphics.PointInt32((int)rect.X, (int)rect.Y));
|
||||
@@ -269,7 +293,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
if (typeof(T) == typeof(LyricsWindow))
|
||||
{
|
||||
LyricsWindow? lyricsWindow = GetWindowByWindowType<LyricsWindow>();
|
||||
LyricsWindow? lyricsWindow = GetWindow<LyricsWindow>();
|
||||
lyricsWindow?.SetTitleBarArea(titleBarArea);
|
||||
}
|
||||
else
|
||||
@@ -326,7 +350,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static void UpdateWorkArea<T>()
|
||||
{
|
||||
var window = GetWindowByWindowType<T>() as Window;
|
||||
var window = GetWindow<T>() as Window;
|
||||
if (window == null) return;
|
||||
|
||||
var hwnd = WindowNative.GetWindowHandle(window);
|
||||
@@ -370,10 +394,10 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
_setLyricsWindowVisibilityByPlayingStatusTimer.Debounce(() =>
|
||||
{
|
||||
var window = GetWindowByWindowType<LyricsWindow>();
|
||||
var window = GetWindow<LyricsWindow>();
|
||||
if (window == null) return;
|
||||
|
||||
if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && !_mediaSessionsService.IsPlaying)
|
||||
if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && !_mediaSessionsService.CurrentIsPlaying)
|
||||
{
|
||||
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
|
||||
{
|
||||
@@ -383,7 +407,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
}
|
||||
HideWindow<LyricsWindow>();
|
||||
}
|
||||
else if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && _mediaSessionsService.IsPlaying)
|
||||
else if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && _mediaSessionsService.CurrentIsPlaying)
|
||||
{
|
||||
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
|
||||
{
|
||||
@@ -17,6 +17,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
get => field ?? LanguageHelper.DetectLanguageCode(WrappedOriginalText);
|
||||
set => field = value;
|
||||
}
|
||||
public bool AutoGenerated { get; set; } = false;
|
||||
public string WrappedOriginalText => string.Join(StringHelper.NewLine, LyricsLines.Select(line => line.OriginalText));
|
||||
public bool IsWordByWord => LyricsLines.Any(x => x.LyricsChars.Count != 0);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,16 +1,60 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using NTextCat.Commons;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class LyricsSearchResult
|
||||
public partial class LyricsSearchResult : ObservableObject, ICloneable
|
||||
{
|
||||
public bool IsFound => !string.IsNullOrEmpty(Raw);
|
||||
public LyricsSearchProvider? Provider { get; set; }
|
||||
public LyricsSearchProvider Provider { get; set; }
|
||||
|
||||
public string? Raw { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻译也可能位于 <see cref="Raw"/>
|
||||
/// </summary>
|
||||
public string? Translation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 音译也可能位于 <see cref="Raw"/>
|
||||
/// </summary>
|
||||
public string? Transliteration { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
public string? Artist { get; set; }
|
||||
public string[]? Artists { get; set; }
|
||||
public string? Album { get; set; }
|
||||
public double? Duration { get; set; }
|
||||
[ObservableProperty] public partial int MatchPercentage { get; set; } = -1;
|
||||
[ObservableProperty] public partial string Reference { get; set; } = "about:blank";
|
||||
|
||||
public bool IsFound => !string.IsNullOrEmpty(Raw);
|
||||
|
||||
public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
|
||||
|
||||
public string? DisplayArtists => Artists?.Join("; ");
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
return new LyricsSearchResult()
|
||||
{
|
||||
Album = this.Album,
|
||||
Duration = this.Duration,
|
||||
Raw = this.Raw,
|
||||
Translation = this.Translation,
|
||||
Title = this.Title,
|
||||
Artists = this.Artists,
|
||||
MatchPercentage = this.MatchPercentage,
|
||||
Provider = this.Provider,
|
||||
Reference = this.Reference
|
||||
};
|
||||
}
|
||||
|
||||
public void CopyFromSongInfo(SongInfo songInfo)
|
||||
{
|
||||
Title = songInfo.Title;
|
||||
Artists = songInfo.Artists;
|
||||
Album = songInfo.Album;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Collections;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
@@ -16,10 +16,14 @@ namespace BetterLyrics.WinUI3.Models
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Provider { get; set; }
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLastFMTrackEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsDiscordPresenceEnabled { get; set; } = false;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTimelineSyncEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int TimelineSyncThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unit: ms
|
||||
/// </summary>
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PositionOffset { get; set; }
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ResetPositionOffsetOnSongChanged { get; set; } = false;
|
||||
@@ -27,8 +31,13 @@ namespace BetterLyrics.WinUI3.Models
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; } = [.. Enum.GetValues<LyricsSearchProvider>().Select(p => new LyricsSearchProviderInfo(p, true))];
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; } = [.. Enum.GetValues<AlbumArtSearchProvider>().Select(p => new AlbumArtSearchProviderInfo(p, true))];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchType LyricsSearchType { get; set; } = LyricsSearchType.Sequential;
|
||||
|
||||
public bool IsLXMusic => PlayerIdMatcher.IsLXMusic(Provider);
|
||||
public string LogoPath => PlayerIDHelper.GetLogoPath(Provider);
|
||||
|
||||
public string? DisplayName => PlayerIDHelper.GetDisplayName(Provider);
|
||||
|
||||
public bool IsLXMusic => PlayerIDHelper.IsLXMusic(Provider);
|
||||
|
||||
public MediaSourceProviderInfo()
|
||||
{
|
||||
@@ -98,6 +107,5 @@ namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
OnPropertyChanged(nameof(LyricsSearchProvidersInfo));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Collections;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.Settings
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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() { }
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
{
|
||||
public partial class LyricsParser
|
||||
{
|
||||
[GeneratedRegex(@"\[(\d*):(\d*)(\.|\:)(\d*)\]")]
|
||||
private static partial Regex LrcRegex();
|
||||
[GeneratedRegex(@"(\[|\<)(\d*):(\d*)\.(\d*)(\]|\>)([^\[\]\<\>]*)")]
|
||||
private static partial Regex SyllableRegex();
|
||||
|
||||
private void ParseLrc(string raw)
|
||||
{
|
||||
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
|
||||
var lrcLines =
|
||||
new List<(int time, string text, List<(int time, string text)> syllables)>();
|
||||
|
||||
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
|
||||
var syllableRegex = SyllableRegex();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var matches = syllableRegex.Matches(line);
|
||||
var syllables = new List<(int, string)>();
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
var m = matches[i];
|
||||
int min = int.Parse(m.Groups[2].Value);
|
||||
int sec = int.Parse(m.Groups[3].Value);
|
||||
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
|
||||
int totalMs = min * 60_000 + sec * 1000 + ms;
|
||||
string text = m.Groups[6].Value;
|
||||
|
||||
syllables.Add((totalMs, text));
|
||||
}
|
||||
if (syllables.Count > 1)
|
||||
{
|
||||
lrcLines.Add(
|
||||
(
|
||||
syllables[0].Item1,
|
||||
string.Concat(syllables.Select(s => s.Item2)),
|
||||
syllables
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通LRC行
|
||||
Regex? bracketRegex = LrcRegex();
|
||||
var bracketMatches = bracketRegex.Matches(line);
|
||||
|
||||
string content = line;
|
||||
int? lineStartTime = null;
|
||||
if (bracketMatches.Count > 0)
|
||||
{
|
||||
var m = bracketMatches[0];
|
||||
int min = int.Parse(m.Groups[1].Value);
|
||||
int sec = int.Parse(m.Groups[2].Value);
|
||||
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
|
||||
lineStartTime = min * 60_000 + sec * 1000 + ms;
|
||||
content = bracketRegex!.Replace(line, "").Trim();
|
||||
if (content == "//") content = "";
|
||||
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间分组
|
||||
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
|
||||
int languageCount = 0;
|
||||
if (grouped != null && grouped.Count > 0)
|
||||
{
|
||||
// 计算最大语言数量
|
||||
languageCount = grouped.Max(g => g.Count());
|
||||
}
|
||||
|
||||
// 初始化每种语言的歌词列表
|
||||
int langStartIndex = LyricsDataArr.Count;
|
||||
for (int i = 0; i < languageCount; i++) LyricsDataArr.Add(new LyricsData());
|
||||
|
||||
// 遍历每个时间分组
|
||||
if (grouped != null)
|
||||
{
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
var linesInGroup = group.ToList();
|
||||
for (int langIdx = 0; langIdx < languageCount; langIdx++)
|
||||
{
|
||||
// 只添加有对应行的语言,否则跳过
|
||||
if (langIdx < linesInGroup.Count)
|
||||
{
|
||||
var (start, text, syllables) = linesInGroup[langIdx];
|
||||
var line = new LyricsLine
|
||||
{
|
||||
StartMs = start,
|
||||
OriginalText = text,
|
||||
LyricsChars = [],
|
||||
};
|
||||
if (syllables != null && syllables.Count > 0)
|
||||
{
|
||||
int currentIndex = 0;
|
||||
for (int j = 0; j < syllables.Count; j++)
|
||||
{
|
||||
var (charStart, charText) = syllables[j];
|
||||
int startIndex = currentIndex;
|
||||
line.LyricsChars.Add(
|
||||
new LyricsChar
|
||||
{
|
||||
StartMs = charStart,
|
||||
Text = charText ?? "",
|
||||
StartIndex = startIndex,
|
||||
}
|
||||
);
|
||||
currentIndex += charText?.Length ?? 0;
|
||||
}
|
||||
}
|
||||
LyricsDataArr[langStartIndex + langIdx].LyricsLines.Add(line);
|
||||
}
|
||||
// 没有翻译行则不补原文,直接跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
{
|
||||
public partial class LyricsParser
|
||||
{
|
||||
private void ParseQrcKrc(List<Lyricify.Lyrics.Models.ILineInfo>? lines)
|
||||
{
|
||||
lines = lines?.Where(x => x.Text != string.Empty).ToList();
|
||||
List<LyricsLine> lyricsLines = [];
|
||||
|
||||
if (lines != null && lines.Count > 0)
|
||||
{
|
||||
lyricsLines = [];
|
||||
for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
|
||||
{
|
||||
var lineRead = lines[lineIndex];
|
||||
var lineWrite = new LyricsLine
|
||||
{
|
||||
StartMs = lineRead.StartTime ?? 0,
|
||||
EndMs = lineRead.EndTime ?? 0,
|
||||
OriginalText = lineRead.Text,
|
||||
LyricsChars = [],
|
||||
};
|
||||
|
||||
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;
|
||||
if (syllables != null)
|
||||
{
|
||||
int startIndex = 0;
|
||||
for (
|
||||
int syllableIndex = 0;
|
||||
syllableIndex < syllables.Count;
|
||||
syllableIndex++
|
||||
)
|
||||
{
|
||||
var syllable = syllables[syllableIndex];
|
||||
var charTiming = new LyricsChar
|
||||
{
|
||||
StartMs = syllable.StartTime,
|
||||
EndMs = syllable.EndTime,
|
||||
Text = syllable.Text,
|
||||
StartIndex = startIndex,
|
||||
};
|
||||
lineWrite.LyricsChars.Add(charTiming);
|
||||
startIndex += syllable.Text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
lyricsLines.Add(lineWrite);
|
||||
}
|
||||
}
|
||||
|
||||
LyricsDataArr.Add(new LyricsData(lyricsLines));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
{
|
||||
public partial class LyricsParser
|
||||
{
|
||||
private void ParseTtml(string raw)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<LyricsLine> originalLines = [];
|
||||
List<LyricsLine> translationLines = [];
|
||||
List<LyricsLine> romanLines = [];
|
||||
|
||||
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
|
||||
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
|
||||
if (body == null) return;
|
||||
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
|
||||
foreach (var p in ps)
|
||||
{
|
||||
// 句级时间
|
||||
string? pBegin = p.Attribute("begin")?.Value;
|
||||
string? pEnd = p.Attribute("end")?.Value;
|
||||
int pStartMs = ParseTtmlTime(pBegin);
|
||||
int pEndMs = ParseTtmlTime(pEnd);
|
||||
|
||||
// 只获取一级span
|
||||
var spans = p.Elements()
|
||||
.Where(s => s.Name.LocalName == "span")
|
||||
.ToList();
|
||||
|
||||
var romanTextSpans = spans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-roman")
|
||||
.ToList();
|
||||
var originalTextSpans = spans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == null)
|
||||
.ToList();
|
||||
var translationTextSpans = spans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
|
||||
.ToList();
|
||||
|
||||
// 处理原文span后的空白
|
||||
for (int i = 0; i < originalTextSpans.Count; i++)
|
||||
{
|
||||
var span = originalTextSpans[i];
|
||||
var nextNode = span.NodesAfterSelf().FirstOrDefault();
|
||||
if (nextNode is XText textNode)
|
||||
{
|
||||
span.Value += textNode.Value;
|
||||
}
|
||||
}
|
||||
// 拼接空白字符后的原文
|
||||
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
|
||||
|
||||
var originalCharTimings = new List<LyricsChar>();
|
||||
int originalStartIndex = 0;
|
||||
foreach (var span in originalTextSpans)
|
||||
{
|
||||
string? sBegin = span.Attribute("begin")?.Value;
|
||||
string? sEnd = span.Attribute("end")?.Value;
|
||||
int sStartMs = ParseTtmlTime(sBegin);
|
||||
int sEndMs = ParseTtmlTime(sEnd);
|
||||
originalCharTimings.Add(new LyricsChar
|
||||
{
|
||||
StartMs = sStartMs,
|
||||
EndMs = sEndMs,
|
||||
StartIndex = originalStartIndex,
|
||||
Text = span.Value
|
||||
});
|
||||
originalStartIndex += span.Value.Length;
|
||||
}
|
||||
if (originalTextSpans.Count == 0)
|
||||
{
|
||||
originalText = p.Value;
|
||||
}
|
||||
|
||||
originalLines.Add(new LyricsLine
|
||||
{
|
||||
StartMs = pStartMs,
|
||||
EndMs = pEndMs,
|
||||
OriginalText = originalText,
|
||||
LyricsChars = originalCharTimings,
|
||||
});
|
||||
|
||||
// 解析 x-role
|
||||
ParseTtmlXRole(spans, translationLines, "x-translation", pStartMs, pEndMs);
|
||||
ParseTtmlXRole(spans, romanLines, "x-roman", pStartMs, pEndMs);
|
||||
}
|
||||
|
||||
LyricsDataArr.Add(new LyricsData(originalLines));
|
||||
if (translationLines.Count > 0)
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData(translationLines));
|
||||
}
|
||||
if (romanLines.Count > 0)
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData(romanLines) { LanguageCode = PhoneticHelper.RomanCode });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseTtmlXRole(List<XElement> sourceSpans, List<LyricsLine> saveLyricsLines, string xRole, int pStartMs, int? pEndMs)
|
||||
{
|
||||
var textSpans = sourceSpans
|
||||
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == xRole)
|
||||
.ToList();
|
||||
|
||||
string text = string.Concat(textSpans.Select(s => s.Value));
|
||||
var charTimings = new List<LyricsChar>();
|
||||
int startIndex = 0;
|
||||
foreach (var span in textSpans)
|
||||
{
|
||||
string? sBegin = span.Attribute("begin")?.Value;
|
||||
string? sEnd = span.Attribute("end")?.Value;
|
||||
int sStartMs = ParseTtmlTime(sBegin);
|
||||
int sEndMs = ParseTtmlTime(sEnd);
|
||||
charTimings.Add(new LyricsChar
|
||||
{
|
||||
StartMs = sStartMs,
|
||||
EndMs = sEndMs,
|
||||
StartIndex = startIndex,
|
||||
Text = span.Value
|
||||
});
|
||||
startIndex += span.Value.Length;
|
||||
}
|
||||
if (textSpans.Count > 0)
|
||||
{
|
||||
saveLyricsLines.Add(new LyricsLine
|
||||
{
|
||||
StartMs = pStartMs,
|
||||
EndMs = pEndMs,
|
||||
OriginalText = text,
|
||||
LyricsChars = charTimings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static int ParseTtmlTime(string? t)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(t))
|
||||
return 0;
|
||||
|
||||
t = t.Trim();
|
||||
|
||||
// 支持 "1.000s"
|
||||
if (t.EndsWith("s"))
|
||||
{
|
||||
if (
|
||||
double.TryParse(
|
||||
t.TrimEnd('s'),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out double seconds
|
||||
)
|
||||
)
|
||||
return (int)(seconds * 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = t.Split(':');
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
// hh:mm:ss.xxx
|
||||
int h = int.Parse(parts[0]);
|
||||
int m = int.Parse(parts[1]);
|
||||
double s = double.Parse(
|
||||
parts[2],
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
return (int)((h * 3600 + m * 60 + s) * 1000);
|
||||
}
|
||||
else if (parts.Length == 2)
|
||||
{
|
||||
// mm:ss.xxx
|
||||
int m = int.Parse(parts[0]);
|
||||
double s = double.Parse(
|
||||
parts[1],
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
return (int)((m * 60 + s) * 1000);
|
||||
}
|
||||
else if (parts.Length == 1)
|
||||
{
|
||||
// ss.xxx
|
||||
if (
|
||||
double.TryParse(
|
||||
parts[0],
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out double s
|
||||
)
|
||||
)
|
||||
return (int)(s * 1000);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using Lyricify.Lyrics.Parsers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
{
|
||||
public partial class LyricsParser
|
||||
{
|
||||
public List<LyricsData> LyricsDataArr { get; private set; } = [];
|
||||
|
||||
public void Parse(SongInfo? songInfo, LyricsSearchResult? lyricsSearchResult)
|
||||
{
|
||||
LyricsDataArr = [];
|
||||
if (lyricsSearchResult?.Raw == null)
|
||||
{
|
||||
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder((int)(songInfo?.DurationMs ?? 0)));
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (lyricsSearchResult.Raw.DetectFormat())
|
||||
{
|
||||
case LyricsFormat.Lrc:
|
||||
case LyricsFormat.Eslrc:
|
||||
ParseLrc(lyricsSearchResult.Raw);
|
||||
break;
|
||||
case LyricsFormat.Qrc:
|
||||
ParseQrcKrc(QrcParser.Parse(lyricsSearchResult.Raw).Lines);
|
||||
break;
|
||||
case LyricsFormat.Krc:
|
||||
ParseQrcKrc(KrcParser.Parse(lyricsSearchResult.Raw).Lines);
|
||||
break;
|
||||
case LyricsFormat.Ttml:
|
||||
ParseTtml(lyricsSearchResult.Raw);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
LoadTranslation(lyricsSearchResult);
|
||||
LoadTransliteration(lyricsSearchResult);
|
||||
GenerateTransliterationLyricsData();
|
||||
}
|
||||
|
||||
private void LoadTranslation(LyricsSearchResult? lyricsSearchResult)
|
||||
{
|
||||
if (lyricsSearchResult?.Translation != null)
|
||||
{
|
||||
switch (lyricsSearchResult.Provider)
|
||||
{
|
||||
case LyricsSearchProvider.QQ:
|
||||
case LyricsSearchProvider.Kugou:
|
||||
case LyricsSearchProvider.Netease:
|
||||
ParseLrc(lyricsSearchResult.Translation);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadTransliteration(LyricsSearchResult? lyricsSearchResult)
|
||||
{
|
||||
if (lyricsSearchResult?.Transliteration != null)
|
||||
{
|
||||
switch (lyricsSearchResult.Provider)
|
||||
{
|
||||
case LyricsSearchProvider.Netease:
|
||||
ParseLrc(lyricsSearchResult.Transliteration);
|
||||
LyricsDataArr.LastOrDefault()?.LanguageCode = PhoneticHelper.RomanCode;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在音译不存在的情况下生成音译歌词
|
||||
/// </summary>
|
||||
private void GenerateTransliterationLyricsData()
|
||||
{
|
||||
var main = LyricsDataArr.FirstOrDefault();
|
||||
if (main != null)
|
||||
{
|
||||
string? languageCode = main.LanguageCode;
|
||||
if (languageCode == LanguageHelper.ChineseCode)
|
||||
{
|
||||
if (!LyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.PinyinCode))
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.PinyinCode,
|
||||
AutoGenerated = true,
|
||||
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToPinyin(line.OriginalText),
|
||||
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToPinyin(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
if (!LyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.JyutpingCode))
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.JyutpingCode,
|
||||
AutoGenerated = true,
|
||||
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToJyutping(line.OriginalText),
|
||||
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToJyutping(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (languageCode == LanguageHelper.JapaneseCode)
|
||||
{
|
||||
if (!LyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.RomanCode))
|
||||
{
|
||||
LyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.RomanCode,
|
||||
AutoGenerated = true,
|
||||
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToRomaji(line.OriginalText),
|
||||
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToRomaji(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
using System;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
namespace BetterLyrics.WinUI3.Providers
|
||||
{
|
||||
public class AppleMusic
|
||||
{
|
||||
@@ -13,6 +16,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
private string _accessToken = "";
|
||||
private string _storefront = "";
|
||||
private string _language = "";
|
||||
private bool _isInited = false;
|
||||
|
||||
public AppleMusic()
|
||||
{
|
||||
@@ -25,11 +29,18 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public async Task<bool> InitAsync()
|
||||
{
|
||||
await GetAccessTokenAsync();
|
||||
await SetMediaUserTokenAsync();
|
||||
return
|
||||
!string.IsNullOrEmpty(_accessToken) &&
|
||||
!string.IsNullOrEmpty(PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey));
|
||||
if (!_isInited)
|
||||
{
|
||||
var mediaUserToken = PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey);
|
||||
if (!string.IsNullOrEmpty(mediaUserToken))
|
||||
{
|
||||
await GetAccessTokenAsync();
|
||||
await SetMediaUserTokenAsync(mediaUserToken);
|
||||
_isInited = !string.IsNullOrEmpty(_accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
return _isInited;
|
||||
}
|
||||
|
||||
private async Task GetAccessTokenAsync()
|
||||
@@ -46,11 +57,10 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_accessToken}");
|
||||
}
|
||||
|
||||
private async Task SetMediaUserTokenAsync()
|
||||
private async Task SetMediaUserTokenAsync(string token)
|
||||
{
|
||||
_client.DefaultRequestHeaders.Remove("media-user-token");
|
||||
_client.DefaultRequestHeaders.Add("media-user-token",
|
||||
PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey));
|
||||
_client.DefaultRequestHeaders.Add("media-user-token", token);
|
||||
var resp = await _client.GetStringAsync("https://amp-api.music.apple.com/v1/me/storefront");
|
||||
var json = JsonSerializer.Deserialize(resp, Serialization.SourceGenerationContext.Default.JsonElement);
|
||||
_storefront = json.GetProperty("data")[0].GetProperty("id").ToString();
|
||||
@@ -59,10 +69,8 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
_client.DefaultRequestHeaders.Add("Accept-Language", $"{_language},en;q=0.9");
|
||||
}
|
||||
|
||||
public async Task<string?> GetLyricsAsync(string title, string artist)
|
||||
private async Task<string?> GetLyricsAsync(string id)
|
||||
{
|
||||
string id = await SearchSongInfoAsync(artist, title);
|
||||
|
||||
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/songs/{id}";
|
||||
var url = apiUrl + $"?include[songs]=lyrics,syllable-lyrics&l={_language}";
|
||||
var resp = await _client.GetStringAsync(url);
|
||||
@@ -103,9 +111,14 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string> SearchSongInfoAsync(string artist, string title)
|
||||
public async Task<LyricsSearchResult> SearchSongInfoAsync(Models.SongInfo songInfo)
|
||||
{
|
||||
var query = $"{artist} {title}";
|
||||
LyricsSearchResult lyricsSearchResult = new()
|
||||
{
|
||||
Provider = Enums.LyricsSearchProvider.AppleMusic
|
||||
};
|
||||
|
||||
var query = $"{songInfo.DisplayArtists} {songInfo.Title}";
|
||||
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/search";
|
||||
var url = apiUrl + $"?term={WebUtility.UrlEncode(query)}&types=songs&limit=1&l={_language}";
|
||||
var resp = await _client.GetStringAsync(url);
|
||||
@@ -114,9 +127,26 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
if (results.TryGetProperty("songs", out var songs) && songs.GetProperty("data").GetArrayLength() > 0)
|
||||
{
|
||||
var song = songs.GetProperty("data")[0];
|
||||
return song.GetProperty("id").ToString();
|
||||
|
||||
var id = song.GetProperty("id").ToString();
|
||||
|
||||
var attr = song.GetProperty("attributes");
|
||||
|
||||
lyricsSearchResult.Title = attr.GetProperty("name").ToString();
|
||||
lyricsSearchResult.Artists = attr.GetProperty("artistName").ToString().SplitByCommonSplitter();
|
||||
lyricsSearchResult.Album = attr.GetProperty("albumName").ToString();
|
||||
lyricsSearchResult.Duration = attr.GetProperty("durationInMillis").GetInt32() / 1000.0;
|
||||
|
||||
lyricsSearchResult.Reference = $"https://music.apple.com/song/{id}";
|
||||
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
|
||||
|
||||
if (id != null)
|
||||
{
|
||||
lyricsSearchResult.Raw = await GetLyricsAsync(id);
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace BetterLyrics.WinUI3.Serialization
|
||||
[JsonSerializable(typeof(TranslateResponse))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(AppSettings))]
|
||||
[JsonSerializable(typeof(LyricsSearchResult))]
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
internal partial class SourceGenerationContext : JsonSerializerContext { }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user