Compare commits

...

15 Commits

Author SHA1 Message Date
Zhe Fang
a5b3671ce3 chores: bump version code to 147 2025-11-23 14:59:36 -05:00
Zhe Fang
181a06c932 add: support 0 duration for lyrics animation 2025-11-23 09:46:18 -05:00
Zhe Fang
93d567e21d Delete .github/workflows/jekyll-gh-pages.yml 2025-11-22 20:04:09 -05:00
Zhe Fang
9da8510de6 Remove remote_theme from Jekyll GitHub Pages workflow
Removed remote_theme configuration from the workflow.
2025-11-22 20:01:41 -05:00
Zhe Fang
3ed9e599be Add remote theme for Jekyll GitBook deployment 2025-11-22 19:59:24 -05:00
Zhe Fang
e277faea9e Update links in README.CN.md for better navigation 2025-11-22 17:15:38 -05:00
Zhe Fang
21dcd7de4b Update README.md 2025-11-22 17:12:31 -05:00
Zhe Fang
31540beaa0 chores: bump version code to 1.0.146.0 2025-11-22 16:36:27 -05:00
Zhe Fang
63ffe6b661 fix: lyrics matching rule 2025-11-22 14:46:17 -05:00
Zhe Fang
5cb880021c fix: add error parser placeholder 2025-11-22 09:38:49 -05:00
Zhe Fang
b00f2b5865 fix: parse lrc for ncm 2025-11-22 09:27:55 -05:00
Zhe Fang
90a75f1b96 add: globally set lyrics matching threshold 2025-11-21 20:58:28 -05:00
Zhe Fang
735f03542f add: support adjusting matching threshold 2025-11-21 19:53:18 -05:00
Zhe Fang
b7853ded26 fix: ncm transation wrongly tagged with roman 2025-11-21 12:13:10 -05:00
Zhe Fang
88fc0adbec fix: AM album title 2025-11-21 12:08:08 -05:00
42 changed files with 988 additions and 392 deletions

View File

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

View File

@@ -2,16 +2,17 @@
{
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 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";
public const string MicrosoftStore = "https://apps.microsoft.com/detail/9p1wcd1p597r";
public const string GitHub = "https://github.com/jayfunc/BetterLyrics";
public const string ShareHub = $"{GitHub}/blob/dev/ShareHub/index.md";
public const string TermsOfService = $"{GitHub}/blob/dev/TermsofService.md";
public const string PrivacyPolicy = $"{GitHub}/blob/dev/PrivacyPolicy.md";
public const string UserGuide = $"{GitHub}/wiki/User-Guide";
public const string AppleMusicCfg = $"{UserGuide}#lyrics-source-configuration";
public const string QQGroup = "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 Discord = "https://discord.gg/5yAQPnyCKv";
public const string Telegram = "https://t.me/+svhSLZ7awPsxNGY1";
}
}

View File

@@ -5,5 +5,6 @@ namespace BetterLyrics.WinUI3.Constants
public static class Time
{
public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(300);
public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(300);
}
}

View File

@@ -51,11 +51,10 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<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 Content="GitHub" NavigateUri="{x:Bind const:Link.GitHub}" />
<HyperlinkButton x:Uid="UserGuide" NavigateUri="{x:Bind const:Link.UserGuide}" />
<HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfServiceUrl}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfService}" />
</StackPanel>
</StackPanel>
@@ -65,9 +64,9 @@
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageFeedback" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton x:Uid="SettingsPageQQGroup" NavigateUri="{x:Bind const:Link.QQGroupUrl}" />
<HyperlinkButton x:Uid="SettingsPageDiscord" NavigateUri="{x:Bind const:Link.DiscordUrl}" />
<HyperlinkButton x:Uid="SettingsPageTelegram" NavigateUri="{x:Bind const:Link.TelegramUrl}" />
<HyperlinkButton x:Uid="SettingsPageQQGroup" NavigateUri="{x:Bind const:Link.QQGroup}" />
<HyperlinkButton x:Uid="SettingsPageDiscord" NavigateUri="{x:Bind const:Link.Discord}" />
<HyperlinkButton x:Uid="SettingsPageTelegram" NavigateUri="{x:Bind const:Link.Telegram}" />
</StackPanel>
</StackPanel>
</dev:SettingsCard>

View File

@@ -0,0 +1,174 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Controls
{
public static class InstantTip
{
public static readonly DependencyProperty ContentProperty =
DependencyProperty.RegisterAttached("Content", typeof(object), typeof(InstantTip), new PropertyMetadata(null, OnContentChanged));
public static void SetContent(DependencyObject element, object value) => element.SetValue(ContentProperty, value);
public static object GetContent(DependencyObject element) => element.GetValue(ContentProperty);
private static Dictionary<int, Popup> _activePopups = [];
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement element)
{
element.PointerEntered -= Element_PointerEntered;
element.PointerExited -= Element_PointerExited;
element.Unloaded -= Element_Unloaded;
if (e.NewValue != null)
{
element.PointerEntered += Element_PointerEntered;
element.PointerExited += Element_PointerExited;
element.Unloaded += Element_Unloaded;
}
}
}
private static void Element_PointerEntered(object sender, PointerRoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
int hashCode = element.GetHashCode();
if (_activePopups.ContainsKey(hashCode))
{
_activePopups.TryGetValue(hashCode, out var popup);
if (popup != null)
{
popup.IsOpen = true;
}
}
else
{
var rawContent = GetContent(element);
if (rawContent == null) return;
// 创建可视卡片容器 (Visual Card)
var visualCard = new Grid
{
Background = (Brush)Application.Current.Resources["AcrylicBackgroundFillColorDefaultBrush"],
CornerRadius = new CornerRadius(4),
Shadow = new ThemeShadow(),
Translation = new Vector3(0, 0, 32),
Padding = new Thickness(8, 4, 8, 4)
};
var popupContent = new Grid
{
IsHitTestVisible = false,
Opacity = 0,
Padding = new Thickness(16)
};
popupContent.Children.Add(visualCard);
var popup = new Popup
{
Child = popupContent,
IsHitTestVisible = false,
ShouldConstrainToRootBounds = false,
XamlRoot = element.XamlRoot,
};
object finalContent = rawContent;
if (rawContent is ToolTip toolTipWrapper)
{
finalContent = toolTipWrapper.Content;
}
if (finalContent is string text)
{
var textBlock = new TextBlock
{
Text = text,
FontSize = 12,
MaxWidth = 320,
TextWrapping = TextWrapping.Wrap,
Foreground = (Brush)Application.Current.Resources["TextFillColorPrimaryBrush"]
};
visualCard.Children.Add(textBlock);
}
else if (finalContent != null)
{
var textBlock = new TextBlock
{
Text = finalContent.ToString(),
FontSize = 12,
MaxWidth = 320,
TextWrapping = TextWrapping.Wrap,
Foreground = (Brush)Application.Current.Resources["TextFillColorPrimaryBrush"]
};
visualCard.Children.Add(textBlock);
}
var transform = element.TransformToVisual(null);
var point = transform.TransformPoint(new Point(0, element.ActualHeight));
popup.VerticalOffset = point.Y;
popup.HorizontalOffset = point.X - popupContent.Padding.Left;
popup.IsOpen = true;
App.Current.Resources.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
popupContent.OpacityTransition = new ScalarTransition();
popupContent.Opacity = 1;
});
_activePopups.Add(hashCode, popup);
}
}
}
private static void Element_PointerExited(object sender, PointerRoutedEventArgs e)
{
var element = sender as FrameworkElement;
if (element != null)
{
int hashCode = element.GetHashCode();
if (!_activePopups.ContainsKey(hashCode))
{
return;
}
_activePopups.TryGetValue(hashCode, out var popup);
if (popup != null)
{
if (popup.Child is Grid popupContent)
{
popupContent.Opacity = 0;
}
popup.IsOpen = false;
_activePopups.Remove(hashCode);
}
}
}
private static void Element_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
element.PointerEntered -= Element_PointerEntered;
element.PointerExited -= Element_PointerExited;
element.Unloaded -= Element_Unloaded;
}
}
}
}

View File

@@ -224,7 +224,7 @@
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<!-- 滚动动画 -->
<!-- 歌词动画 -->
<dev:SettingsExpander x:Uid="SettingsPageScrollEasing" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xECE7;}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingTypeLinear" />
@@ -246,7 +246,7 @@
Default="500"
Frequency="50"
Maximum="1000"
Minimum="50"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollTopDuration, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -255,7 +255,7 @@
Default="500"
Frequency="50"
Maximum="1000"
Minimum="50"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollDuration, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -264,7 +264,7 @@
Default="500"
Frequency="50"
Maximum="1000"
Minimum="50"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollBottomDuration, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -27,6 +27,7 @@
<Grid Grid.Column="0">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="LyricsSearchControlSongInfoMapping" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Grid
@@ -34,22 +35,32 @@
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="4">
<StackPanel Spacing="6">
<TextBlock x:Uid="LyricsSearchControlTitle" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Text="{x:Bind ViewModel.MappedSongSearchQuery.OriginalTitle, Mode=OneWay}"
TextWrapping="Wrap" />
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedTitle, Mode=TwoWay}" TextWrapping="Wrap" />
<Button
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedTitleCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
<local:PropertyRow x:Uid="LyricsSearchControlTitle" Value="{x:Bind ViewModel.MappedSongSearchQuery.OriginalTitle, Mode=OneWay}" />
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsSearchControlMappedAs"
Grid.Column="0"
VerticalAlignment="Center" />
<TextBox
Grid.Column="1"
Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedTitle, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
Grid.Column="2"
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedTitleCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
</Grid>
</StackPanel>
</Grid>
@@ -60,33 +71,52 @@
CornerRadius="4">
<StackPanel Spacing="6">
<TextBlock x:Uid="LyricsSearchControlArtist" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Text="{x:Bind ViewModel.MappedSongSearchQuery.OriginalArtist, Mode=OneWay}"
TextWrapping="Wrap" />
<local:PropertyRow x:Uid="LyricsSearchControlArtist" Value="{x:Bind ViewModel.MappedSongSearchQuery.OriginalArtist, Mode=OneWay}" />
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}" TextWrapping="Wrap" />
<RichTextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap">
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsSearchControlMappedAs"
Grid.Column="0"
VerticalAlignment="Center" />
<TextBox
Grid.Column="1"
Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
Grid.Column="2"
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedArtistCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
</Grid>
<RichTextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
<Paragraph>
<Run Text="*" />
<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}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
<RichTextBlock
FontSize="12"
FontWeight="Bold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Loaded="ArtistsSplitHintRichTextBlock_Loaded"
TextWrapping="Wrap">
<Paragraph>
<Run Text="; , / " />
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
@@ -97,22 +127,32 @@
CornerRadius="4">
<StackPanel Spacing="6">
<TextBlock x:Uid="LyricsSearchControlAlbum" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
Text="{x:Bind ViewModel.MappedSongSearchQuery.OriginalAlbum, Mode=OneWay}"
TextWrapping="Wrap" />
<local:PropertyRow x:Uid="LyricsSearchControlAlbum" Value="{x:Bind ViewModel.MappedSongSearchQuery.OriginalAlbum, Mode=OneWay}" />
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedAlbum, Mode=TwoWay}" TextWrapping="Wrap" />
<Button
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedAlbumCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsSearchControlMappedAs"
Grid.Column="0"
VerticalAlignment="Center" />
<TextBox
Grid.Column="1"
Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedAlbum, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
Grid.Column="2"
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedAlbumCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
</Grid>
</StackPanel>
</Grid>
@@ -138,7 +178,7 @@
<ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow
Margin="-12,0,0,0"
Margin="-8,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}}" />
@@ -155,6 +195,10 @@
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
<!-- NOT FOUND -->
<TextBlock

View File

@@ -1,7 +1,10 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -22,5 +25,26 @@ namespace BetterLyrics.WinUI3.Controls
{
ViewModel.SelectedLyricsLine = e.OriginalSource as LyricsLine;
}
private void ArtistsSplitHintRichTextBlock_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (sender is RichTextBlock richTextBlock)
{
TextHighlighter highlighter = new()
{
Background = App.Current.Resources["AccentTextFillColorPrimaryBrush"] as SolidColorBrush,
Ranges =
{
new() { StartIndex = 0, Length = 1 },
new() { StartIndex = 5, Length = 1 },
new() { StartIndex = 10, Length = 1 },
new() { StartIndex = 15, Length = 1 },
new() { StartIndex = 20, Length = 1 },
new() { StartIndex = 25, Length = 1 },
}
};
richTextBlock.TextHighlighters.Add(highlighter);
}
}
}
}

View File

@@ -60,7 +60,7 @@
</Button>
<!-- Sharing hub -->
<HyperlinkButton x:Uid="SettingsPageShareHub" NavigateUri="{x:Bind constants:Link.ShareHubUrl}" />
<HyperlinkButton x:Uid="SettingsPageShareHub" NavigateUri="{x:Bind constants:Link.ShareHub}" />
</StackPanel>

View File

@@ -188,6 +188,14 @@
<ComboBoxItem x:Uid="SettingsPageLyricsSearchBestMatch" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageMatchingThreshold">
<local:ExtendedSlider
Default="0"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind ViewModel.SelectedMediaSourceProvider.MatchingThreshold, Mode=TwoWay}" />
</dev:SettingsCard>
<ListView
x:Name="LyricsSearchProvidersListView"
@@ -209,12 +217,48 @@
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchProviderInfo">
<dev:SettingsCard Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<dev:SettingsCard.HeaderIcon>
<FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" />
</dev:SettingsCard.HeaderIcon>
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<Grid>
<dev:SettingsExpander Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}" IsExpanded="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}">
<dev:SettingsExpander.HeaderIcon>
<FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" />
</dev:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageOverwriteMatchingThreshold">
<ToggleSwitch IsOn="{Binding IsMatchingThresholdOverwritten, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageMatchingThreshold" IsEnabled="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}">
<local:ExtendedSlider
Default="0"
Maximum="100"
Minimum="0"
Unit="%"
Value="{Binding MatchingThreshold, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<Grid
Width="48"
HorizontalAlignment="Left"
Background="{ThemeResource ControlStrokeColorDefaultBrush}"
CornerRadius="4,0,0,4"
Opacity="0">
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<ToolTipService.ToolTip>
<ToolTip x:Uid="SettingsPageHoldDragSort" />
</ToolTipService.ToolTip>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
@@ -264,9 +308,7 @@
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>
@@ -277,13 +319,9 @@
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"
@@ -297,30 +335,27 @@
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}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -464,7 +499,7 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE897;}"
NavigateUri="{x:Bind constants:Link.AppleMusicCfgUrl}" />
NavigateUri="{x:Bind constants:Link.AppleMusicCfg}" />
<Button
Grid.Column="2"
Command="{x:Bind ViewModel.SaveAppleMusicMediaUserTokenCommand}"

View File

@@ -57,7 +57,7 @@
Click="OnCopyClicked"
Opacity="0">
<ToolTipService.ToolTip>
<TextBlock x:Uid="Copy" />
<ToolTip x:Uid="Copy" />
</ToolTipService.ToolTip>
<Button.OpacityTransition>
<ScalarTransition />

View File

@@ -16,6 +16,10 @@ namespace BetterLyrics.WinUI3.Extensions
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
LyricsSearchProvider.LocalMusicFile => PathHelper.LocalMusicCacheDirectory,
LyricsSearchProvider.LocalLrcFile => PathHelper.LocalLrcCacheDirectory,
LyricsSearchProvider.LocalEslrcFile => PathHelper.LocalEslrcCacheDirectory,
LyricsSearchProvider.LocalTtmlFile => PathHelper.LocalTtmlCacheDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
};

View File

@@ -5,6 +5,7 @@ using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
@@ -55,11 +56,11 @@ namespace BetterLyrics.WinUI3.Helper
Source = new GaussianBlurEffect
{
Source = backgroundFontEffect,
BlurAmount = (float)lyricsLine.BlurAmountTransition.Value,
BlurAmount = (float)Math.Max(lyricsLine.BlurAmountTransition.Value, 0),
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Speed,
},
Opacity = (float)(lyricsLine.OpacityTransition.Value * lyricsLayerOpacity),
Opacity = (float)Math.Max(lyricsLine.OpacityTransition.Value * lyricsLayerOpacity, 0),
};
}
}

View File

@@ -48,6 +48,7 @@ namespace BetterLyrics.WinUI3.Helper
{
var json = File.ReadAllText(cacheFilePath);
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
data?.SelfPath = cacheFilePath;
return data;
}
return null;
@@ -68,6 +69,7 @@ namespace BetterLyrics.WinUI3.Helper
var cacheFilePath = Path.Combine(
lyricsSearchResult.Provider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
lyricsSearchResult.SelfPath = cacheFilePath;
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
File.WriteAllText(cacheFilePath, json);
}

View File

@@ -16,52 +16,51 @@ namespace BetterLyrics.WinUI3.Helper
private const double WeightDuration = 0.10;
// JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new JaroWinkler();
private static readonly JaroWinkler _algo = new();
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 = 0;
double totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
(albumScore * WeightAlbum) +
(durationScore * WeightDuration);
bool localHasMetadata = !string.IsNullOrWhiteSpace(local.Title);
bool remoteHasMetadata = !string.IsNullOrWhiteSpace(remote.Title);
if (localHasMetadata && remoteHasMetadata)
{
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);
totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
(albumScore * WeightAlbum) +
(durationScore * WeightDuration);
}
else
{
string? localQuery = localHasMetadata
? $"{local.Title} {string.Join(" ", local.Artists ?? [])}"
: Path.GetFileNameWithoutExtension(local.LinkedFileName);
string remoteQuery = remoteHasMetadata
? $"{remote.Title} {string.Join(" ", remote.Artists ?? [])}"
: Path.GetFileNameWithoutExtension(remote.Reference);
string fp1 = CreateSortedFingerprint(localQuery);
string fp2 = CreateSortedFingerprint(remoteQuery);
if (string.IsNullOrWhiteSpace(fp1) || string.IsNullOrWhiteSpace(fp2))
totalScore = 0;
else
totalScore = _algo.Similarity(fp1, fp2);
}
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);
if (string.IsNullOrEmpty(infoRaw) && songInfo.LinkedFileName is string linkedFileName)
infoRaw = linkedFileName;
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() ?? "";
@@ -105,7 +104,7 @@ namespace BetterLyrics.WinUI3.Helper
return 1.0 - ((diff - PerfectTolerance) / (MaxTolerance - PerfectTolerance));
}
private static string CreateSortedFingerprint(string input)
private static string CreateSortedFingerprint(string? input)
{
if (string.IsNullOrWhiteSpace(input)) return "";

View File

@@ -45,6 +45,10 @@ 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 LocalMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-music");
public static string LocalLrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-lrc");
public static string LocalEslrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-eslrc");
public static string LocalTtmlCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-ttml");
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");
@@ -65,6 +69,10 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(AppleMusicCacheDirectory);
Directory.CreateDirectory(LocalMusicCacheDirectory);
Directory.CreateDirectory(LocalLrcCacheDirectory);
Directory.CreateDirectory(LocalEslrcCacheDirectory);
Directory.CreateDirectory(LocalTtmlCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}

View File

@@ -57,7 +57,7 @@ namespace BetterLyrics.WinUI3.Helper
public void SetDuration(double seconds)
{
if (seconds <= 0)
if (seconds < 0)
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive.");
_durationSeconds = seconds;
}
@@ -145,7 +145,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
private Func<T, T, double, T> GetInterpolatorByEasingType(EasingType type)
private Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type)
{
if (typeof(T) == typeof(double))
{
@@ -193,6 +193,7 @@ namespace BetterLyrics.WinUI3.Helper
t = EasingHelper.Linear(t);
break;
default:
t = EasingHelper.EaseInOutQuad(t);
break;
}
return (T)(object)(s + (e - s) * t);
@@ -201,7 +202,7 @@ namespace BetterLyrics.WinUI3.Helper
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
}
public void SetEasingType(EasingType easingType)
public void SetEasingType(EasingType? easingType)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);

View File

@@ -118,6 +118,7 @@ namespace BetterLyrics.WinUI3.Hooks
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
TrackWindow(window);
var castedWindow = (Window)window;
castedWindow.Restore();

View File

@@ -131,14 +131,23 @@ namespace BetterLyrics.WinUI3.Models
return result;
}
public static LyricsData GetNotfoundPlaceholder(int durationMs)
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = durationMs,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = _resourceService.GetLocalizedString("LyricsNotFound"),
LyricsChars = [],
}]);
}
public static LyricsData GetParseErrorPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = _resourceService.GetLocalizedString("LyricsParseError"),
}]);
}
@@ -149,10 +158,7 @@ namespace BetterLyrics.WinUI3.Models
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PhoneticText = "",
OriginalText = "● ● ●",
TranslatedText = "",
LyricsChars = [],
},
]);
}

View File

@@ -3,6 +3,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
@@ -13,37 +14,13 @@ namespace BetterLyrics.WinUI3.Models
{
public class LyricsLine
{
private const double _animationDuration = 0.3;
public ValueTransition<double> AngleTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> BlurAmountTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> HighlightOpacityTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> OpacityTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> ScaleTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> YOffsetTransition { get; set; } = new(
initialValue: 0,
durationSeconds: 0.5,
easingType: EasingType.EaseInOutQuad
);
public double AnimationDuration { get; set; } = 0.3;
public ValueTransition<double> AngleTransition { get; set; }
public ValueTransition<double> BlurAmountTransition { get; set; }
public ValueTransition<double> HighlightOpacityTransition { get; set; }
public ValueTransition<double> OpacityTransition { get; set; }
public ValueTransition<double> ScaleTransition { get; set; }
public ValueTransition<double> YOffsetTransition { get; set; }
public CanvasTextLayout? OriginalCanvasTextLayout { get; private set; }
public CanvasTextLayout? TranslatedCanvasTextLayout { get; private set; }
@@ -86,6 +63,40 @@ namespace BetterLyrics.WinUI3.Models
public CanvasGeometry? TranslatedCanvasGeometry { get; private set; }
public CanvasGeometry? PhoneticCanvasGeometry { get; private set; }
public LyricsLine()
{
AngleTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutQuad
);
BlurAmountTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutQuad
);
HighlightOpacityTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutQuad
);
OpacityTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutQuad
);
ScaleTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutQuad
);
YOffsetTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutQuad
);
}
public void UpdateCenterPosition(double maxWidth, TextAlignmentType type)
{
if (OriginalCanvasTextLayout == null)

View File

@@ -8,8 +8,9 @@ namespace BetterLyrics.WinUI3.Models
public partial class LyricsSearchProviderInfo : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchProvider Provider { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsMatchingThresholdOverwritten { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int MatchingThreshold { get; set; } = 0;
public LyricsSearchProviderInfo() { }

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using NTextCat.Commons;
using System;
@@ -28,6 +29,8 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty] public partial int MatchPercentage { get; set; } = -1;
[ObservableProperty] public partial string Reference { get; set; } = "about:blank";
public string? SelfPath { get; set; }
public bool IsFound => !string.IsNullOrEmpty(Raw);
public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;

View File

@@ -32,6 +32,7 @@ namespace BetterLyrics.WinUI3.Models
[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;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int MatchingThreshold { get; set; } = 0;
public string LogoPath => PlayerIDHelper.GetLogoPath(Provider);

View File

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

View File

@@ -4,7 +4,9 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Parsers;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@@ -12,14 +14,17 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
private static readonly ILogger<LyricsParser> _logger = Ioc.Default.GetRequiredService<ILogger<LyricsParser>>();
public List<LyricsData> LyricsDataArr { get; private set; } = [];
public void Parse(SongInfo? songInfo, LyricsSearchResult? lyricsSearchResult)
{
_logger.LogInformation("LyricsParser.Parse");
LyricsDataArr = [];
if (lyricsSearchResult?.Raw == null)
if (string.IsNullOrWhiteSpace(lyricsSearchResult?.Raw))
{
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder((int)(songInfo?.DurationMs ?? 0)));
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder());
}
else
{
@@ -27,7 +32,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(lyricsSearchResult.Raw);
ParseLrc(lyricsSearchResult.Raw, lyricsSearchResult.Provider.IsRemote());
break;
case LyricsFormat.Qrc:
ParseQrcKrc(QrcParser.Parse(lyricsSearchResult.Raw).Lines);
@@ -41,6 +46,11 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
default:
break;
}
if (LyricsDataArr.Count == 0)
{
LyricsDataArr.Add(LyricsData.GetParseErrorPlaceholder());
}
}
LoadTranslation(lyricsSearchResult);
LoadTransliteration(lyricsSearchResult);
@@ -49,14 +59,14 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
private void LoadTranslation(LyricsSearchResult? lyricsSearchResult)
{
if (lyricsSearchResult?.Translation != null)
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Translation))
{
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.QQ:
case LyricsSearchProvider.Kugou:
case LyricsSearchProvider.Netease:
ParseLrc(lyricsSearchResult.Translation);
ParseLrc(lyricsSearchResult.Translation, true);
break;
default:
break;
@@ -66,12 +76,12 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
private void LoadTransliteration(LyricsSearchResult? lyricsSearchResult)
{
if (lyricsSearchResult?.Transliteration != null)
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Transliteration))
{
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.Netease:
ParseLrc(lyricsSearchResult.Transliteration);
ParseLrc(lyricsSearchResult.Transliteration, true);
LyricsDataArr.LastOrDefault()?.LanguageCode = PhoneticHelper.RomanCode;
break;
default:

View File

@@ -26,7 +26,7 @@ namespace BetterLyrics.WinUI3.Services.DiscordService
{
StatusDisplay = StatusDisplayType.Details,
Type = ActivityType.Listening,
Buttons = new Button[] { new() { Label = "Get this status", Url = Constants.Link.MicrosoftStoreUrl } },
Buttons = new Button[] { new() { Label = "Get this status", Url = Constants.Link.MicrosoftStore } },
Assets = new Assets
{
LargeImageKey = "banner",

View File

@@ -40,7 +40,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
_lrcLibHttpClient = new();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{Constants.App.AppName} {MetadataHelper.AppVersion} ({Constants.Link.GitHubUrl})"
$"{Constants.App.AppName} {MetadataHelper.AppVersion} ({Constants.Link.GitHub})"
);
_amllTtmlDbHttpClient = new();
_appleMusic = new AppleMusic();
@@ -144,32 +144,42 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
List<LyricsSearchResult> lyricsSearchResults = [];
// 曲目没有被映射
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == songInfo.PlayerId)?.LyricsSearchProvidersInfo ?? [])
var mediaSourceProviderInfo = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == songInfo.PlayerId);
if (mediaSourceProviderInfo != null)
{
if (!provider.IsEnabled)
// 曲目没有被映射
foreach (var provider in mediaSourceProviderInfo.LyricsSearchProvidersInfo)
{
continue;
}
lyricsSearchResult = await SearchSingleAsync(
((SongInfo)songInfo.Clone())
.WithTitle(overridenTitle)
.WithArtist(overridenArtists)
.WithAlbum(overridenAlbum),
provider.Provider, checkCache, token);
if (lyricsSearchResult.IsFound)
{
switch (lyricsSearchType)
if (!provider.IsEnabled)
{
case LyricsSearchType.Sequential:
return lyricsSearchResult;
case LyricsSearchType.BestMatch:
lyricsSearchResults.Add((LyricsSearchResult)lyricsSearchResult.Clone());
break;
default:
break;
continue;
}
lyricsSearchResult = await SearchSingleAsync(
((SongInfo)songInfo.Clone())
.WithTitle(overridenTitle)
.WithArtist(overridenArtists)
.WithAlbum(overridenAlbum),
provider.Provider, checkCache, token);
int matchingThreshold = mediaSourceProviderInfo.MatchingThreshold;
if (provider.IsMatchingThresholdOverwritten)
{
matchingThreshold = provider.MatchingThreshold;
}
if (lyricsSearchResult.IsFound && lyricsSearchResult.MatchPercentage >= matchingThreshold)
{
switch (lyricsSearchType)
{
case LyricsSearchType.Sequential:
return lyricsSearchResult;
case LyricsSearchType.BestMatch:
lyricsSearchResults.Add((LyricsSearchResult)lyricsSearchResult.Clone());
break;
default:
break;
}
}
}
}
@@ -214,42 +224,36 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
if (provider.IsLocal())
switch (provider)
{
if (provider == LyricsSearchProvider.LocalMusicFile)
{
case LyricsSearchProvider.QQ:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.Netease);
break;
case LyricsSearchProvider.LrcLib:
lyricsSearchResult = await SearchLrcLibAsync(songInfo);
break;
case LyricsSearchProvider.AmllTtmlDb:
lyricsSearchResult = await SearchAmllTtmlDbAsync(songInfo);
break;
case LyricsSearchProvider.LocalMusicFile:
lyricsSearchResult = SearchEmbedded(songInfo);
}
else
{
break;
case LyricsSearchProvider.LocalLrcFile:
case LyricsSearchProvider.LocalEslrcFile:
case LyricsSearchProvider.LocalTtmlFile:
lyricsSearchResult = await SearchFile(songInfo, provider.GetLyricsFormat());
}
}
else
{
switch (provider)
{
case LyricsSearchProvider.LrcLib:
lyricsSearchResult = await SearchLrcLibAsync(songInfo);
break;
case LyricsSearchProvider.QQ:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(songInfo, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
lyricsSearchResult = await SearchAmllTtmlDbAsync(songInfo);
break;
case LyricsSearchProvider.AppleMusic:
lyricsSearchResult = await SearchAppleMusicAsync(songInfo);
break;
default:
break;
}
break;
case LyricsSearchProvider.AppleMusic:
lyricsSearchResult = await SearchAppleMusicAsync(songInfo);
break;
default:
break;
}
if (token.IsCancellationRequested)
@@ -262,12 +266,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
}
if (lyricsSearchResult.IsFound)
if (provider.IsRemote())
{
if (provider.IsRemote())
{
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
}
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
}
return lyricsSearchResult;
@@ -293,7 +294,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
{
int score = MetadataComparer.CalculateScore(songInfo, file);
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = file });
if (score > maxScore)
{
bestFile = file;
@@ -324,8 +325,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private LyricsSearchResult SearchEmbedded(SongInfo songInfo)
{
int maxScore = 0;
int bestScore = 0;
string? bestFile = null;
string? bestRaw = null;
var lyricsSearchResult = new LyricsSearchResult
{
@@ -341,18 +343,25 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
{
var track = new Track(file);
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
{
Title = track.Title,
Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator),
Album = track.Album,
Duration = track.Duration
});
var raw = track.GetRawLyrics();
if (score > maxScore)
if (!string.IsNullOrEmpty(raw))
{
maxScore = score;
bestFile = file;
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
{
Title = track.Title,
Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator),
Album = track.Album,
Duration = track.Duration,
Reference = file,
});
if (score > bestScore)
{
bestScore = score;
bestFile = file;
bestRaw = raw;
}
}
}
}
@@ -368,10 +377,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult.Album = track.Album;
lyricsSearchResult.Duration = track.Duration;
lyricsSearchResult.Raw = track.GetRawLyrics();
lyricsSearchResult.Raw = bestRaw;
lyricsSearchResult.Reference = bestFile;
lyricsSearchResult.MatchPercentage = maxScore;
lyricsSearchResult.MatchPercentage = bestScore;
}
return lyricsSearchResult;

View File

@@ -183,6 +183,9 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
case nameof(MediaSourceProviderInfo.LyricsSearchType):
UpdateLyrics();
break;
case nameof(MediaSourceProviderInfo.MatchingThreshold):
UpdateLyrics();
break;
default:
break;
}
@@ -318,14 +321,16 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
string fixedArtist = mediaProperties?.Artist ?? "N/A";
string fixedAlbum = mediaProperties?.AlbumTitle ?? "N/A";
string? fixedArtist = mediaProperties?.Artist;
string? fixedAlbum = mediaProperties?.AlbumTitle;
string? songId = null;
if (PlayerIDHelper.IsAppleMusic(sessionId))
{
fixedArtist = mediaProperties?.Artist.Split(" — ").FirstOrDefault() ?? (mediaProperties?.Artist ?? "N/A");
fixedAlbum = mediaProperties?.Artist.Split(" — ").LastOrDefault() ?? (mediaProperties?.AlbumTitle ?? "N/A");
fixedArtist = mediaProperties?.Artist.Split(" — ").FirstOrDefault();
fixedAlbum = mediaProperties?.Artist.Split(" — ").LastOrDefault();
fixedAlbum = fixedAlbum?.Replace(" - Single", "");
fixedAlbum = fixedAlbum?.Replace(" - EP", "");
}
else if (PlayerIDHelper.IsNeteaseFamily(sessionId))
{
@@ -340,18 +345,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentSongInfo = new SongInfo
{
Title = mediaProperties?.Title ?? "N/A",
Artists = fixedArtist.SplitByCommonSplitter(),
Album = fixedAlbum,
Title = mediaProperties?.Title ?? "",
Artists = fixedArtist?.SplitByCommonSplitter() ?? [],
Album = fixedAlbum ?? "",
DurationMs = mediaSession?.ControlSession?.GetTimelineProperties().EndTime.TotalMilliseconds ?? 0,
PlayerId = sessionId,
SongId = songId,
LinkedFileName = linkedFileName
};
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties?.Title, mediaProperties?.Artist, mediaProperties?.AlbumTitle);
if (PlayerIDHelper.IsLXMusic(sessionId))
{
StartSSE();

View File

@@ -144,7 +144,7 @@
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Copy.Text" xml:space="preserve">
<data name="Copy.Content" xml:space="preserve">
<value>Copy</value>
</data>
<data name="CreatePlaylistSuccessfully" xml:space="preserve">
@@ -183,7 +183,7 @@
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>Music gallery</value>
</data>
<data name="HostWindowSettingsButtonToolTip.Text" xml:space="preserve">
<data name="HostWindowSettingsButtonToolTip.Content" xml:space="preserve">
<value>Settings</value>
</data>
<data name="ImportPlaylistSuccessfully" xml:space="preserve">
@@ -231,6 +231,12 @@
<data name="LyricsNotFound" xml:space="preserve">
<value>Lyrics not found</value>
</data>
<data name="LyricsPageCachePath.Header" xml:space="preserve">
<value>Cache path</value>
</data>
<data name="LyricsPageCachePath.Value" xml:space="preserve">
<value>Cache path</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>Lyrics provider</value>
</data>
@@ -270,10 +276,13 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Translation provider</value>
</data>
<data name="LyricsSearchControlAlbum.Text" xml:space="preserve">
<data name="LyricsParseError" xml:space="preserve">
<value>Lyrics parsing failed</value>
</data>
<data name="LyricsSearchControlAlbum.Header" xml:space="preserve">
<value>Album</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>Artist</value>
</data>
<data name="LyricsSearchControlAutoGenerated.Tag" xml:space="preserve">
@@ -312,7 +321,7 @@
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Target lyrics search provider</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Title</value>
</data>
<data name="LyricsSearchPageTitle" xml:space="preserve">
@@ -859,6 +868,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>Automatically hide/show windows</value>
</data>
<data name="SettingsPageHoldDragSort.Content" xml:space="preserve">
<value>Hold here and drag to sort</value>
</data>
<data name="SettingsPageImport.Content" xml:space="preserve">
<value>Import</value>
</data>
@@ -1120,6 +1132,15 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Lyrics window status switcher shortcut</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>Lyrics window</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Adjusting this value will affect sequential search and best match search results, but will not affect search results in the manual lyrics search interface</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>Minimum matching threshold</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>Media library</value>
</data>
@@ -1168,6 +1189,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageOriginalText.Header" xml:space="preserve">
<value>Original text</value>
</data>
<data name="SettingsPageOverwriteMatchingThreshold.Header" xml:space="preserve">
<value>Set the minimum match threshold individually</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>Color picker style</value>
</data>
@@ -1268,7 +1292,7 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<value>Current line duration</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>Lyrics scrolling animation type</value>
<value>Lyric animation type</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>First line delay</value>
@@ -1483,4 +1507,7 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics is already running</value>
</data>
<data name="UserGuide.Content" xml:space="preserve">
<value>User Guide</value>
</data>
</root>

View File

@@ -144,7 +144,7 @@
<data name="Cancel" xml:space="preserve">
<value>キャンセル</value>
</data>
<data name="Copy.Text" xml:space="preserve">
<data name="Copy.Content" xml:space="preserve">
<value>コピー</value>
</data>
<data name="CreatePlaylistSuccessfully" xml:space="preserve">
@@ -183,7 +183,7 @@
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>ミュージックギャラリー</value>
</data>
<data name="HostWindowSettingsButtonToolTip.Text" xml:space="preserve">
<data name="HostWindowSettingsButtonToolTip.Content" xml:space="preserve">
<value>設定</value>
</data>
<data name="ImportPlaylistSuccessfully" xml:space="preserve">
@@ -231,6 +231,12 @@
<data name="LyricsNotFound" xml:space="preserve">
<value>歌詞が見つかりません</value>
</data>
<data name="LyricsPageCachePath.Header" xml:space="preserve">
<value>キャッシュパス</value>
</data>
<data name="LyricsPageCachePath.Value" xml:space="preserve">
<value>キャッシュパス</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞プロバイダー</value>
</data>
@@ -270,10 +276,13 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻訳プロバイダー</value>
</data>
<data name="LyricsSearchControlAlbum.Text" xml:space="preserve">
<data name="LyricsParseError" xml:space="preserve">
<value>歌詞の解析に失敗しました</value>
</data>
<data name="LyricsSearchControlAlbum.Header" xml:space="preserve">
<value>アルバム</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>アーティスト</value>
</data>
<data name="LyricsSearchControlAutoGenerated.Tag" xml:space="preserve">
@@ -312,7 +321,7 @@
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>ターゲット歌詞検索プロバイダー</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>タイトル</value>
</data>
<data name="LyricsSearchPageTitle" xml:space="preserve">
@@ -859,6 +868,9 @@
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>Windowsを自動的に非表示/表示します</value>
</data>
<data name="SettingsPageHoldDragSort.Content" xml:space="preserve">
<value>ここを長押ししてドラッグして並べ替えましょう</value>
</data>
<data name="SettingsPageImport.Content" xml:space="preserve">
<value>インポート</value>
</data>
@@ -1120,6 +1132,15 @@
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウステータススイッチャーショートカット</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>歌詞ウィンドウ</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>この値を調整すると、シーケンシャル検索と最適な検索結果に影響しますが、手動歌詞検索インターフェースの検索結果には影響しません</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>最小マッチングしきい値</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
</data>
@@ -1168,6 +1189,9 @@
<data name="SettingsPageOriginalText.Header" xml:space="preserve">
<value>原文</value>
</data>
<data name="SettingsPageOverwriteMatchingThreshold.Header" xml:space="preserve">
<value>最低マッチしきい値を個別に設定してください</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>カラーピックスタイル</value>
</data>
@@ -1268,7 +1292,7 @@
<value>現在のライン期間</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞スクロールアニメーションタイプ</value>
<value>リリックアニメーションタイプ</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>最初の行の遅延</value>
@@ -1483,4 +1507,7 @@
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics はすでに実行されています</value>
</data>
<data name="UserGuide.Content" xml:space="preserve">
<value>利用案内</value>
</data>
</root>

View File

@@ -144,7 +144,7 @@
<data name="Cancel" xml:space="preserve">
<value>취소</value>
</data>
<data name="Copy.Text" xml:space="preserve">
<data name="Copy.Content" xml:space="preserve">
<value>접수</value>
</data>
<data name="CreatePlaylistSuccessfully" xml:space="preserve">
@@ -183,7 +183,7 @@
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>음악 갤러리</value>
</data>
<data name="HostWindowSettingsButtonToolTip.Text" xml:space="preserve">
<data name="HostWindowSettingsButtonToolTip.Content" xml:space="preserve">
<value>설정</value>
</data>
<data name="ImportPlaylistSuccessfully" xml:space="preserve">
@@ -231,6 +231,12 @@
<data name="LyricsNotFound" xml:space="preserve">
<value>가사를 찾을 수 없습니다</value>
</data>
<data name="LyricsPageCachePath.Header" xml:space="preserve">
<value>캐시</value>
</data>
<data name="LyricsPageCachePath.Value" xml:space="preserve">
<value>캐시</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>가사 제공자</value>
</data>
@@ -270,10 +276,13 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>번역 제공자</value>
</data>
<data name="LyricsSearchControlAlbum.Text" xml:space="preserve">
<data name="LyricsParseError" xml:space="preserve">
<value>가사 구문 분석에 실패했습니다</value>
</data>
<data name="LyricsSearchControlAlbum.Header" xml:space="preserve">
<value>앨범</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>아티스트</value>
</data>
<data name="LyricsSearchControlAutoGenerated.Tag" xml:space="preserve">
@@ -312,7 +321,7 @@
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>가사 검색 공급자를 타겟팅하십시오</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>제목</value>
</data>
<data name="LyricsSearchPageTitle" xml:space="preserve">
@@ -859,6 +868,9 @@
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>Windows를 자동으로 숨기고 표시합니다</value>
</data>
<data name="SettingsPageHoldDragSort.Content" xml:space="preserve">
<value>여기를 누른 상태에서 드래그하여 정렬하세요</value>
</data>
<data name="SettingsPageImport.Content" xml:space="preserve">
<value>가져오기</value>
</data>
@@ -1120,6 +1132,15 @@
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>가사 창 상태 전환기 바로 가기</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>가사 창</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>이 값을 조정하면 순차 검색 및 검색 결과와 가장 일치하는 검색 결과에 영향을 미치지만 수동 가사 검색 인터페이스의 검색 결과에는 영향을 미치지 않습니다</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>최소 일치 임계값</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>미디어 라이브러리</value>
</data>
@@ -1168,6 +1189,9 @@
<data name="SettingsPageOriginalText.Header" xml:space="preserve">
<value>원본</value>
</data>
<data name="SettingsPageOverwriteMatchingThreshold.Header" xml:space="preserve">
<value>최소 일치 임계값을 개별적으로 설정하십시오</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>색상 선택 스타일</value>
</data>
@@ -1268,7 +1292,7 @@
<value>현재 라인 기간</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>가사 스크롤링 애니메이션 유형</value>
<value>가사 애니메이션 유형</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>첫 번째 줄 지연</value>
@@ -1483,4 +1507,7 @@
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 가 이미 실행 중입니다</value>
</data>
<data name="UserGuide.Content" xml:space="preserve">
<value>사용 안내서</value>
</data>
</root>

View File

@@ -144,7 +144,7 @@
<data name="Cancel" xml:space="preserve">
<value>取消</value>
</data>
<data name="Copy.Text" xml:space="preserve">
<data name="Copy.Content" xml:space="preserve">
<value>拷贝</value>
</data>
<data name="CreatePlaylistSuccessfully" xml:space="preserve">
@@ -183,7 +183,7 @@
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>音乐库</value>
</data>
<data name="HostWindowSettingsButtonToolTip.Text" xml:space="preserve">
<data name="HostWindowSettingsButtonToolTip.Content" xml:space="preserve">
<value>设置</value>
</data>
<data name="ImportPlaylistSuccessfully" xml:space="preserve">
@@ -231,6 +231,12 @@
<data name="LyricsNotFound" xml:space="preserve">
<value>未找到歌词</value>
</data>
<data name="LyricsPageCachePath.Header" xml:space="preserve">
<value>缓存路径</value>
</data>
<data name="LyricsPageCachePath.Value" xml:space="preserve">
<value>缓存路径</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌词来源</value>
</data>
@@ -270,10 +276,13 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻译来源</value>
</data>
<data name="LyricsSearchControlAlbum.Text" xml:space="preserve">
<data name="LyricsParseError" xml:space="preserve">
<value>歌词解析失败</value>
</data>
<data name="LyricsSearchControlAlbum.Header" xml:space="preserve">
<value>专辑</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>艺术家</value>
</data>
<data name="LyricsSearchControlAutoGenerated.Tag" xml:space="preserve">
@@ -312,7 +321,7 @@
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>目标歌词搜索提供商</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>标题</value>
</data>
<data name="LyricsSearchPageTitle" xml:space="preserve">
@@ -859,6 +868,9 @@
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自动隐藏/显示窗口</value>
</data>
<data name="SettingsPageHoldDragSort.Content" xml:space="preserve">
<value>按住此处并拖动以排序</value>
</data>
<data name="SettingsPageImport.Content" xml:space="preserve">
<value>导入</value>
</data>
@@ -1120,6 +1132,15 @@
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌词窗口状态切换器快捷方式</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>歌词窗口</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>调整此值将影响顺序搜索和最佳匹配搜索结果,但不会影响手动歌词搜索界面中的搜索结果</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>最小匹配阈值</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒体库</value>
</data>
@@ -1168,6 +1189,9 @@
<data name="SettingsPageOriginalText.Header" xml:space="preserve">
<value>原文</value>
</data>
<data name="SettingsPageOverwriteMatchingThreshold.Header" xml:space="preserve">
<value>单独设置最小匹配阈值</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>取色风格</value>
</data>
@@ -1268,7 +1292,7 @@
<value>当前行持续时间</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌词滚动动画类型</value>
<value>歌词动画类型</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>首行延时</value>
@@ -1483,4 +1507,7 @@
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 已经在运行</value>
</data>
<data name="UserGuide.Content" xml:space="preserve">
<value>使用指南</value>
</data>
</root>

View File

@@ -144,7 +144,7 @@
<data name="Cancel" xml:space="preserve">
<value>取消</value>
</data>
<data name="Copy.Text" xml:space="preserve">
<data name="Copy.Content" xml:space="preserve">
<value>複製</value>
</data>
<data name="CreatePlaylistSuccessfully" xml:space="preserve">
@@ -183,7 +183,7 @@
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>音樂庫</value>
</data>
<data name="HostWindowSettingsButtonToolTip.Text" xml:space="preserve">
<data name="HostWindowSettingsButtonToolTip.Content" xml:space="preserve">
<value>設定</value>
</data>
<data name="ImportPlaylistSuccessfully" xml:space="preserve">
@@ -231,6 +231,12 @@
<data name="LyricsNotFound" xml:space="preserve">
<value>找不到歌詞</value>
</data>
<data name="LyricsPageCachePath.Header" xml:space="preserve">
<value>檔案路徑緩存</value>
</data>
<data name="LyricsPageCachePath.Value" xml:space="preserve">
<value>檔案路徑緩存</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞來源</value>
</data>
@@ -270,10 +276,13 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻譯來源</value>
</data>
<data name="LyricsSearchControlAlbum.Text" xml:space="preserve">
<data name="LyricsParseError" xml:space="preserve">
<value>歌詞解析失敗</value>
</data>
<data name="LyricsSearchControlAlbum.Header" xml:space="preserve">
<value>專輯</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>藝術家</value>
</data>
<data name="LyricsSearchControlAutoGenerated.Tag" xml:space="preserve">
@@ -312,7 +321,7 @@
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>目標歌詞搜尋提供者</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>標題</value>
</data>
<data name="LyricsSearchPageTitle" xml:space="preserve">
@@ -859,6 +868,9 @@
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自動隱藏/顯示窗口</value>
</data>
<data name="SettingsPageHoldDragSort.Content" xml:space="preserve">
<value>按住這裡並拖曳以進行排序</value>
</data>
<data name="SettingsPageImport.Content" xml:space="preserve">
<value>匯入</value>
</data>
@@ -1120,6 +1132,15 @@
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌詞視窗狀態切換器捷徑</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>歌詞視窗</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>調整此值將影響順序搜索和最佳匹配搜索結果,但不會影響手動歌詞搜索界面中的搜索結果</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>最低匹配閾值</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒體庫</value>
</data>
@@ -1168,6 +1189,9 @@
<data name="SettingsPageOriginalText.Header" xml:space="preserve">
<value>原文</value>
</data>
<data name="SettingsPageOverwriteMatchingThreshold.Header" xml:space="preserve">
<value>單獨設置最低匹配閾值</value>
</data>
<data name="SettingsPagePaletteGeneratorType.Header" xml:space="preserve">
<value>取色風格</value>
</data>
@@ -1268,7 +1292,7 @@
<value>當前行持續時間</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞滾動動畫類型</value>
<value>歌詞動畫類型</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>首行延時</value>
@@ -1483,4 +1507,7 @@
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 已經在運作</value>
</data>
<data name="UserGuide.Content" xml:space="preserve">
<value>使用手冊</value>
</data>
</root>

View File

@@ -36,7 +36,7 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Windows.System.Launcher.LaunchUriAsync(new Uri(Constants.Link.GitHubUrl));
await Windows.System.Launcher.LaunchUriAsync(new Uri(Constants.Link.GitHub));
}
[RelayCommand]

View File

@@ -26,6 +26,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
{
DrawBlurredLyrics(control, blurredLyricsDs);
//DrawBlurredLyrics2(control, blurredLyricsDs);
}
using var combined = new CanvasCommandList(control);
@@ -297,17 +298,18 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
//// 组合变换:缩放 -> 旋转 -> 平移
ds.Transform =
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition)
* Matrix3x2.CreateRotation((float)line.AngleTransition.Value,
currentPlayingLine.OriginalPosition.WithX(_liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings.FanLyricsAngle < 0 ? (float)_maxLyricsWidth : 0))
* Matrix3x2.CreateTranslation((float)_lyricsX, (float)yOffset);
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition) *
Matrix3x2.CreateRotation(
(float)line.AngleTransition.Value,
currentPlayingLine.OriginalPosition.WithX(_liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings.FanLyricsAngle < 0 ? (float)_maxLyricsWidth : 0)) *
Matrix3x2.CreateTranslation((float)_lyricsX, (float)yOffset);
using var combined = new CanvasCommandList(control);
using var combinedDs = combined.CreateDrawingSession();
// 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层(背景歌词层次)
using var backgroundFontEffect = CanvasHelper.CreateFontEffect(line, control, _strokeFontColor,
_liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontStrokeWidth, _bgFontColor);
_liveStatesService.LiveStates.LyricsWindowStatus.LyricsStyleSettings.LyricsFontStrokeWidth, _bgFontColor);
using var backgroundEffect = CanvasHelper.CreateBackgroundEffect(line, backgroundFontEffect, _lyricsOpacityTransition.Value);
combinedDs.DrawImage(backgroundEffect);
@@ -368,7 +370,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
combinedDs.DrawImage(new OpacityEffect
{
Source = effectLayer,
Opacity = (float)(line.HighlightOpacityTransition.Value * _lyricsOpacityTransition.Value),
Opacity = (float)Math.Max(line.HighlightOpacityTransition.Value * _lyricsOpacityTransition.Value, 0),
});
if (i == _playingLineIndex)
@@ -404,6 +406,125 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
}
}
public void DrawBlurredLyrics2(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null) return;
var settings = _liveStatesService.LiveStates.LyricsWindowStatus;
var effectSettings = settings.LyricsEffectSettings;
bool isKaraokeEnabled = effectSettings.IsLyricsLineFadeEnabled;
var styleSettings = settings.LyricsStyleSettings; // 获取样式设置(描边宽等)
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null || line.OriginalCanvasTextLayout == null) continue;
var textLayout = line.OriginalCanvasTextLayout;
// === 1. 恢复您原始的矩阵变换逻辑 ===
// 不要减去 CenterPosition保持和您原始代码一致
double yOffset = line.YOffsetTransition.Value + _canvasHeight / 2 + _lyricsYTransition.Value;
float fanAngleX = effectSettings.FanLyricsAngle < 0 ? (float)_maxLyricsWidth : 0;
ds.Transform =
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition)
* Matrix3x2.CreateRotation((float)line.AngleTransition.Value,
currentPlayingLine.OriginalPosition.WithX(fanAngleX))
* Matrix3x2.CreateTranslation((float)_lyricsX, (float)yOffset);
// === 2. 坐标关键:必须使用 line.OriginalPosition ===
// CanvasHelper 里是画在 OriginalPosition 上的,我们这里必须一致
var drawPos = line.OriginalPosition;
// A. 绘制阴影 (优化:直接绘制,替代 ShadowEffect)
if (effectSettings.IsLyricsShadowEnabled)
{
var shadowColor = _albumArtAccentColor1Transition.Value.WithAlpha((byte)(effectSettings.LyricsShadowAmount * 255));
// 偏移 2px 绘制阴影
ds.DrawTextLayout(textLayout, drawPos.X + 2f, drawPos.Y + 2f, shadowColor);
}
// B. 绘制描边 (如果设置里有)
// 对应 CanvasHelper.CreateFontEffect 里的 stroke 逻辑
if (styleSettings.LyricsFontStrokeWidth > 0)
{
// 注意DrawTextLayout 不支持直接描边,需要用 DrawGeometry
// 如果这一步很卡,可以考虑去掉描边,或者只对当前行描边
if (line.OriginalCanvasGeometry != null)
{
ds.DrawGeometry(line.OriginalCanvasGeometry, drawPos, _strokeFontColor, styleSettings.LyricsFontStrokeWidth);
}
}
// C. 绘制底层文本 (底色)
// 对应 CanvasHelper 里的 DrawTextLayout
var baseColor = _strokeFontColor.WithAlpha((byte)(_strokeFontColor.A * _lyricsOpacityTransition.Value));
ds.DrawTextLayout(textLayout, drawPos, baseColor);
// D. 绘制高亮/卡拉OK效果 (核心优化点)
if (line.HighlightOpacityTransition.Value > 0)
{
GetLinePlayingProgress(i, out int charStartIndex, out int charLength, out double charProgress);
// 整行高亮或非逐字模式
if (charStartIndex >= line.OriginalText.Length || !isKaraokeEnabled)
{
var hlColor = _fgFontColor.WithAlpha((byte)(_fgFontColor.A * line.HighlightOpacityTransition.Value));
ds.DrawTextLayout(textLayout, drawPos, hlColor);
}
else
{
// === 计算裁剪区域 (照搬 CreateCharMask 的逻辑) ===
var regions = textLayout.GetCharacterRegions(charStartIndex, 1);
// 默认裁剪宽度覆盖到当前字之前
double validWidth = 0;
if (regions.Length > 0)
{
var region = regions[0];
// CanvasHelper 里的逻辑region.LayoutBounds.X 是相对于 Layout 左上角的
// highlightWidth = TotalWidth * Progress
double highlightWidth = region.LayoutBounds.Width * charProgress;
// 当前高亮的总右边界 = 当前字左边 + 当前字播放过的宽度
validWidth = region.LayoutBounds.X + highlightWidth;
}
else if (charStartIndex > 0)
{
// 容错:如果取不到当前字区域(例如空格),取上一个字的右边缘
var prevRegions = textLayout.GetCharacterRegions(charStartIndex - 1, 1);
if (prevRegions.Length > 0) validWidth = prevRegions[0].LayoutBounds.Right;
}
if (validWidth > 0)
{
// === 创建 Layer 进行裁剪 ===
// 裁剪矩形的 X/Y 必须加上 drawPos (OriginalPosition)
// 因为 CreateLayer 是基于当前 Transform 的全局坐标
var clipRect = new Rect(
drawPos.X, // 从文字绘制起点的 X 开始
drawPos.Y, // 从文字绘制起点的 Y 开始
validWidth, // 宽度
textLayout.LayoutBounds.Height // 高度
);
using (ds.CreateLayer(1.0f, clipRect))
{
var hlColor = _fgFontColor.WithAlpha((byte)(_fgFontColor.A * line.HighlightOpacityTransition.Value));
ds.DrawTextLayout(textLayout, drawPos, hlColor);
}
}
}
}
// 重置变换,准备画下一行
ds.Transform = Matrix3x2.Identity;
}
}
private void DrawSnowEffect(CanvasDrawingSession ds)
{
if (_snowEffect != null && _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.IsSnowFlakeOverlayEnabled)

View File

@@ -892,7 +892,27 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
yScrollDelay = distanceFactor * _liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings.LyricsScrollBottomDelay / 1000.0;
}
line.YOffsetTransition.SetEasingType(_canvasYScrollTransition.EasingType ?? EasingType.Linear);
line.AngleTransition.SetEasingType(_canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.SetEasingType(_canvasYScrollTransition.EasingType);
line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.SetEasingType(_canvasYScrollTransition.EasingType);
line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay);
line.OpacityTransition.SetEasingType(_canvasYScrollTransition.EasingType);
line.OpacityTransition.SetDuration(yScrollDuration);
line.OpacityTransition.SetDelay(yScrollDelay);
line.HighlightOpacityTransition.SetEasingType(_canvasYScrollTransition.EasingType);
line.HighlightOpacityTransition.SetDuration(yScrollDuration);
line.HighlightOpacityTransition.SetDelay(yScrollDelay);
line.YOffsetTransition.SetEasingType(_canvasYScrollTransition.EasingType);
line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay);
line.YOffsetTransition.StartTransition(_canvasTargetYScrollOffset, _isLayoutChanged);

View File

@@ -65,7 +65,7 @@
Glyph=&#xE713;}"
Style="{StaticResource TitleBarButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="HostWindowSettingsButtonToolTip" />
<ToolTip x:Uid="HostWindowSettingsButtonToolTip" />
</ToolTipService.ToolTip>
</Button>

View File

@@ -564,13 +564,11 @@
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip>
<ToolTip.Content>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip.Content>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>

View File

@@ -6,6 +6,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -22,7 +23,7 @@
Glyph=&#xE90B;}"
Style="{StaticResource TitleBarButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SystemTrayLyrics" />
<ToolTip x:Uid="SettingsPageLyricsWindowToolTip" />
</ToolTipService.ToolTip>
</Button>
<Button
@@ -33,7 +34,7 @@
Glyph=&#xE713;}"
Style="{StaticResource TitleBarButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="HostWindowSettingsButtonToolTip" />
<ToolTip x:Uid="HostWindowSettingsButtonToolTip" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>

View File

@@ -6,6 +6,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -21,7 +22,7 @@
Glyph=&#xE90B;}"
Style="{StaticResource TitleBarButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SystemTrayLyrics" />
<ToolTip x:Uid="SettingsPageLyricsWindowToolTip" />
</ToolTipService.ToolTip>
</Button>
<Button

View File

@@ -14,7 +14,7 @@ BetterLyrics
<div align="center">
[操作指南](https://jayfunc.blog/work/betterlyrics-cn) | [隐私政策](PrivacyPolicy.CN.md) | [服务协议](TermsofService.CN.md)
[使用指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [隐私政策](PrivacyPolicy.CN.md) | [服务协议](TermsofService.CN.md)
</div>
@@ -75,7 +75,7 @@ BetterLyrics
- 🎶 **支持众多音乐播放器**
- 点击 [此处](https://jayfunc.blog/work/betterlyrics-cn#yi-zhi-zhi-chi-de-yin-le-bo-fang-qi-pei-zhi-zhi-nan) 查看详细信息
- 点击 [此处](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 查看详细信息
- 🪟 **多种显示模式**
- **标准模式**

View File

@@ -14,7 +14,7 @@ BetterLyrics
<div align="center">
[Wiki](https://jayfunc.blog/work/betterlyrics) | [Privacy Policy](PrivacyPolicy.md) | [Terms of Service](TermsofService.md)
[User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [Privacy Policy](PrivacyPolicy.md) | [Terms of Service](TermsofService.md)
</div>
@@ -81,7 +81,7 @@ Check out the article: [BetterLyrics An immersive and smooth lyrics display
- 🎶 **Multiple Music Players Supported**
- Check it out [here](https://jayfunc.blog/work/betterlyrics#known-supported-music-players-configuration-guidance)) for detailed info
- Check it out [here](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide) for detailed info
- 🪟 **Multiple Display Modes**
- **Standard Mode**