Compare commits

..

73 Commits

Author SHA1 Message Date
Zhe Fang
6f02a1a46c fix: font selector 2025-12-02 19:19:15 -05:00
Zhe Fang
4f74e48cfb chores: bump version code to 1.1.164.0 2025-12-02 17:33:23 -05:00
Zhe Fang
06f08558cc fix: i18n 2025-12-02 17:24:57 -05:00
Zhe Fang
a2b21ed3d5 chores: improve font family selection ux 2025-12-02 16:59:36 -05:00
Zhe Fang
4cb1ca0bb3 chores: improve layout 2025-12-02 15:10:18 -05:00
Zhe Fang
81ed341e47 fix: phonetic opacity 2025-12-02 14:04:53 -05:00
Zhe Fang
a9f92f4cb7 fix: local file lyrics read algo;
feat: support lyrics scrolling and clicking
2025-12-02 13:47:21 -05:00
Zhe Fang
62719ed513 chores: bump version code to 1.1.161.0 2025-12-01 15:18:21 -05:00
Zhe Fang
c9bd7725d0 fix: lyrics style not refreshed when switching lyrics window status 2025-12-01 14:54:32 -05:00
Zhe Fang
b60952916c chores: improve layout 2025-12-01 13:11:12 -05:00
Zhe Fang
ec0917b6c8 chores: delete unused code 2025-12-01 10:27:23 -05:00
Zhe Fang
583fa106ce chores: code cleanup 2025-12-01 09:22:30 -05:00
Zhe Fang
88488e4813 chores: improve layout 2025-12-01 08:16:22 -05:00
Zhe Fang
6e65310b6d chores: add minimize button and bump version code to 1.1.158.0 2025-11-30 21:00:42 -05:00
Zhe Fang
22bd7c2252 chores: add comments 2025-11-30 20:15:43 -05:00
Zhe Fang
5c50bd569a chores: fix floating animation; improve layout 2025-11-30 20:14:47 -05:00
Zhe Fang
401c33003c fix: stroke not drawn on current playing lyrics line 2025-11-30 14:42:00 -05:00
Zhe Fang
664451c530 chores: add translation for spectrum 2025-11-30 13:19:44 -05:00
Zhe Fang
657c81add5 chores: improve layout 2025-11-30 13:13:51 -05:00
Zhe Fang
1dd63ab9ba fix: overlay on album art 2025-11-30 12:21:43 -05:00
Zhe Fang
0a9b9bf484 fix: theme type listener 2025-11-30 12:19:19 -05:00
Zhe Fang
794079f20b fix: album art corner radius load delay 2025-11-30 11:00:05 -05:00
Zhe Fang
be9a67f57d chores: improve layout 2025-11-30 10:23:31 -05:00
Zhe Fang
5b5d62d688 fix: syllable glow effect 2025-11-30 09:06:50 -05:00
Zhe Fang
dfe428645e chores: increase IsLongDuration threshold 2025-11-29 21:57:15 -05:00
Zhe Fang
8e2c977a44 refactor 2025-11-29 21:46:24 -05:00
Zhe Fang
47806c924d refactor: improve layout custom ssytem 2025-11-29 11:11:19 -05:00
Zhe Fang
bbda1cd797 refactor 2025-11-28 18:25:20 -05:00
Zhe Fang
2099332f02 refactor 2025-11-27 14:36:10 -05:00
Zhe Fang
016d9a626f Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-11-26 12:02:23 -05:00
Zhe Fang
cff6f202d6 refactor: lyrics render, album art render; delete album art background; change some features 2025-11-26 12:02:19 -05:00
Zhe Fang
f643f86567 更新 README.CN.md 2025-11-24 22:11:53 -05:00
Zhe Fang
e9b50c84ac 更新 README.md 2025-11-24 22:09:12 -05:00
Zhe Fang
4c590bcf6f fix: improve floating animation and add word-by-word scale animation 2025-11-24 21:19:22 -05:00
Zhe Fang
68a1c6a465 chores: delete surplus args 2025-11-24 16:19:56 -05:00
Zhe Fang
f46364a491 chores: remove unused code 2025-11-24 16:19:13 -05:00
Zhe Fang
75f047e389 Update releases-to-discord.yml 2025-11-24 10:43:43 -05:00
Zhe Fang
1cb21b1373 Update release event types for Telegram notification 2025-11-24 10:43:35 -05:00
Zhe Fang
4592be10e8 chores: bump version code to 149 2025-11-24 10:43:07 -05:00
Zhe Fang
c0217150c1 Add Chinese translations to index.md
Updated the index.md file to include Chinese translations for section titles and descriptions.
2025-11-24 09:47:14 -05:00
Zhe Fang
0705bde0e2 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-11-24 09:30:25 -05:00
Zhe Fang
3f5122c93f chores: replace parse error placeholde with no lyrics found 2025-11-24 09:30:24 -05:00
Zhe Fang
97f061d887 Update links and headings in index.md 2025-11-24 09:24:05 -05:00
Zhe Fang
77525a62ff chores: ui 2025-11-24 09:23:54 -05:00
Zhe Fang
ad473bbd1d fix: ncm return exception when roman is null 2025-11-24 08:42:57 -05:00
Zhe Fang
b1126026c2 Update Discord notification workflow for releases 2025-11-23 17:25:13 -05:00
Zhe Fang
2f84155e6e Update release event types for Telegram notification 2025-11-23 17:24:38 -05:00
Zhe Fang
1769167811 Add workflow_dispatch trigger to Telegram release workflow 2025-11-23 17:14:53 -05:00
Zhe Fang
551da8c0b0 Add workflow_dispatch trigger to releases workflow 2025-11-23 17:14:36 -05:00
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
Zhe Fang
a3366422a2 chores: code cleanup 2025-11-20 17:35:36 -05:00
Zhe Fang
8f6e106282 chores: bump version code 2025-11-20 13:02:33 -05:00
Zhe Fang
08d4f4ce90 fix: clear all media props when switching tracks (local player) 2025-11-20 12:30:06 -05:00
Zhe Fang
212041a509 fix: can not find local .lrc lyrics file when metadata is missing in the music file 2025-11-20 10:09:31 -05:00
Zhe Fang
c0f2b4106a fix: change discord presence dep 2025-11-19 18:38:18 -05:00
Zhe Fang
c704edde19 chores: code cleanup 2025-11-19 17:50:59 -05:00
Zhe Fang
35de1ef7db add: roman parse for ncm and amll-ttml-db lyrics source 2025-11-19 17:49:24 -05:00
Zhe Fang
1c8840e053 fix: search control title missing 2025-11-19 11:40:58 -05:00
Zhe Fang
2d0839d777 fix: apple music metadata fetch missing 2025-11-19 11:32:05 -05:00
143 changed files with 7020 additions and 5891 deletions

View File

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

View File

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

View File

@@ -69,6 +69,11 @@
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
<converter:MillisecondsToSecondsConverter x:Key="MillisecondsToSecondsConverter" />
<converter:PictureInfosToImageSourceConverter x:Key="PictureInfosToImageSourceConverter" />
<converter:LyricsFontWeightToFontWeightConverter x:Key="LyricsFontWeightToFontWeightConverter" />
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />

View File

@@ -13,7 +13,6 @@ using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
@@ -69,7 +68,7 @@ namespace BetterLyrics.WinUI3
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHook.OpenOrShowWindow<LyricsWindow>();
WindowHook.OpenOrShowWindow<NowPlayingWindow>();
if (Ioc.Default.GetRequiredService<ISettingsService>().AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
@@ -110,13 +109,12 @@ namespace BetterLyrics.WinUI3
.AddSingleton<LyricsWindowSettingsControlViewModel>()
.AddSingleton<LyricsWindowSwitchControlViewModel>()
.AddSingleton<LyricsWindowSwitchWindowViewModel>()
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<NowPlayingWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsPageViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<NowPlayingPageViewModel>()
.AddSingleton<MusicGalleryViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<AboutControlViewModel>()
.BuildServiceProvider()
);

View File

@@ -34,6 +34,7 @@
<None Remove="Controls\LyricsWindowSwitchControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
@@ -64,8 +65,7 @@
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
<PackageReference Include="csharp-kana" Version="1.0.2" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="DevWinUI.Controls" Version="9.5.0" />
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="DevWinUI.Controls" Version="9.6.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" />
@@ -74,13 +74,13 @@
<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" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="NTextCat" Version="0.3.65" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.3-dev-02320" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.0" />
@@ -91,8 +91,9 @@
<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.8.0" />
<PackageReference Include="z440.atl.core" Version="7.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
@@ -333,6 +334,11 @@
<ItemGroup>
<Folder Include="TemplateSelector\" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -3,6 +3,7 @@
public class ExtendedGenreFiled
{
public const string NetEaseCloudMusicTrackID = "NCM-";
public const string QQMusicTrackID = "QQ-";
public const string FileName = "FILENAME-";
}
}

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

@@ -20,7 +20,8 @@
public const string Edge = "MSEdge";
public const string BetterLyrics = "37412.BetterLyrics_rd1g0rsrrtxw8!App";
public const string BetterLyricsDebug = "37412.BetterLyrics_c8mj3v9sysxb4!App";
public const string SaltPlayerForWindows = "Sakawish.SaltPlayerforWindows_q65q631pyh094!SaltPlayerforWindows";
public const string SaltPlayerForWindowsMS = "Sakawish.SaltPlayerforWindows_q65q631pyh094!SaltPlayerforWindows";
public const string SaltPlayerForWindowsSteam = "Salt Player for Windows.exe";
public const string MoeKoeMusic = "cn.MoeKoe.Music";
public const string MoeKoeMusicAlternative = "electron.app.MoeKoe Music";
public const string Listen1 = "com.listen1.listen1";

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Constants
namespace BetterLyrics.WinUI3.Constants
{
public class PlayerName
{
@@ -24,7 +20,8 @@ namespace BetterLyrics.WinUI3.Constants
public const string Edge = "Microsoft Edge";
public const string BetterLyrics = "BetterLyrics";
public const string BetterLyricsDebug = "BetterLyrics (Debug)";
public const string SaltPlayerForWindows = "Salt Player for Windows";
public const string SaltPlayerForWindowsMS = "Salt Player for Windows (Microsoft Store)";
public const string SaltPlayerForWindowsSteam = "Salt Player for Windows (Steam)";
public const string MoeKoeMusic = "MoeKoe Music";
public const string Listen1 = "Listen 1";
}

View File

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

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

@@ -16,28 +16,17 @@
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="SettingsPageAlbumArt" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsExpander
x:Uid="SettingsPageAlbumArtSize"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE744;}"
IsExpanded="True">
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageAutoAdjust">
<ToggleSwitch IsOn="{x:Bind AlbumArtLayoutSettings.AutoAlbumArtSize, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard IsEnabled="{x:Bind AlbumArtLayoutSettings.AutoAlbumArtSize, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<local:ExtendedSlider
Frequency="2"
Maximum="800"
Minimum="10"
ResetButtonVisibility="Collapsed"
Unit="px"
Value="{x:Bind AlbumArtLayoutSettings.AlbumArtSize, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageAlignment" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind AlbumArtLayoutSettings.SongInfoAlignmentType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLeft" />
<ComboBoxItem x:Uid="SettingsPageCenter" />
<ComboBoxItem x:Uid="SettingsPageRight" />
</ComboBox>
</dev:SettingsCard>
<TextBlock x:Uid="SettingsPageAlbumArt" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEA3A;}">
<local:ExtendedSlider
@@ -58,14 +47,6 @@
<TextBlock x:Uid="SettingsPageSongInfo" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageSongInfoAlignment" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind AlbumArtLayoutSettings.SongInfoAlignmentType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageSongInfoLeft" />
<ComboBoxItem x:Uid="SettingsPageSongInfoCenter" />
<ComboBoxItem x:Uid="SettingsPageSongInfoRight" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsExpander
x:Uid="SettingsPageLyricsFontSize"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -87,21 +68,15 @@
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPageShowTitle"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xEF3D;}"
IsExpanded="True">
<dev:SettingsCard x:Uid="SettingsPageShowTitle" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEF3D;}">
<ToggleSwitch IsOn="{x:Bind AlbumArtLayoutSettings.ShowTitle, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageShowArtists">
<ToggleSwitch IsEnabled="{x:Bind AlbumArtLayoutSettings.ShowTitle, Mode=OneWay}" IsOn="{x:Bind AlbumArtLayoutSettings.ShowArtists, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageShowAlbum">
<ToggleSwitch IsEnabled="{x:Bind AlbumArtLayoutSettings.ShowTitle, Mode=OneWay}" IsOn="{x:Bind AlbumArtLayoutSettings.ShowAlbum, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageShowArtists" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEF3D;}">
<ToggleSwitch IsOn="{x:Bind AlbumArtLayoutSettings.ShowArtists, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageShowAlbum" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEF3D;}">
<ToggleSwitch IsOn="{x:Bind AlbumArtLayoutSettings.ShowAlbum, Mode=TwoWay}" />
</dev:SettingsCard>
</StackPanel>
</Grid>

View File

@@ -6,25 +6,54 @@
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:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock
x:Name="SelectedLocalizedFontFamilyTextBlock"
VerticalAlignment="Center"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True" />
<AutoSuggestBox
x:Name="AutoSuggestBox"
Width="150"
MinWidth="180"
GotFocus="AutoSuggestBox_GotFocus"
LostFocus="AutoSuggestBox_LostFocus"
SuggestionChosen="AutoSuggestBox_SuggestionChosen"
Text="{x:Bind SelectedFontFamily, Mode=OneWay}"
TextChanged="AutoSuggestBox_TextChanged" />
TextChanged="AutoSuggestBox_TextChanged">
<!--<AutoSuggestBox.ItemTemplate>
<DataTemplate x:DataType="models:ExtendedFontFamily">
<StackPanel>
<TextBlock Text="{x:Bind LocalizedFontFamily}" TextWrapping="Wrap" />
<TextBlock
FontSize="12"
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Text="{x:Bind FontFamily}"
TextWrapping="Wrap" />
</StackPanel>
</DataTemplate>
</AutoSuggestBox.ItemTemplate>-->
</AutoSuggestBox>
<Button
Click="Button_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE72C;}"
Style="{StaticResource GhostButtonStyle}" />
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Uid="SettingsPageRefreshDropdown" />
</ToolTipService.ToolTip>
</Button>
<Button Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, FontSize=12, Glyph=&#xE74A;}" Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Uid="SettingsPageCollapseDropdown" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,8 +1,15 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -11,11 +18,15 @@ namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class FontFamilyAutoSuggestBox : UserControl
{
private List<string> SystemFontNames { get; set; } = [.. FontHelper.SystemFontFamilies];
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
//private List<ExtendedFontFamily> FontFamilies { get; set; } = [];
private List<string> FontFamilies { get; set; } = [];
public FontFamilyAutoSuggestBox()
{
InitializeComponent();
RefreshFontFamilies();
}
public static readonly DependencyProperty SelectedFontFamilyProperty =
@@ -27,20 +38,47 @@ namespace BetterLyrics.WinUI3.Controls
set => SetValue(SelectedFontFamilyProperty, value);
}
private void RefreshFontFamilies()
{
//Task.Run(() =>
//{
// var fontFamilies = FontHelper.SystemFontFamilies.Select(x => new ExtendedFontFamily()
// {
// FontFamily = x,
// LocalizedFontFamily = FontHelper.GetLocalizedFontFamilyName(x, _settingsService.AppSettings.GeneralSettings.LanguageCode)
// }).OrderBy(x => x.LocalizedFontFamily).ToList();
// DispatcherQueue.TryEnqueue(() =>
// {
// FontFamilies = fontFamilies;
// });
//});
FontFamilies = FontHelper.SystemFontFamilies.OrderBy(x => x).ToList();
}
private void AutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
{
SelectedFontFamily = args.SelectedItem.ToString() ?? "";
if (args.SelectedItem is ExtendedFontFamily extendedFontFamily)
{
SelectedFontFamily = extendedFontFamily.FontFamily;
}
else
{
SelectedFontFamily = args.SelectedItem.ToString() ?? "";
}
}
private void UpdateAutoSuggestBoxItemsSource()
private void UpdateAutoSuggestBoxItemsSource(string? query = null)
{
query ??= AutoSuggestBox.Text;
//var suitableItems = new List<ExtendedFontFamily>();
var suitableItems = new List<string>();
var splitText = AutoSuggestBox.Text.ToLower().Split(" ");
foreach (var fontFamily in SystemFontNames)
var splitText = query.ToLower().Split(" ");
foreach (var fontFamily in FontFamilies)
{
var found = splitText.All((key) =>
bool found = splitText.All((key) =>
{
//return fontFamily.FontFamily.ToLower().Contains(key) || fontFamily.LocalizedFontFamily.ToLower().Contains(key);
return fontFamily.ToLower().Contains(key);
});
if (found)
@@ -50,9 +88,15 @@ namespace BetterLyrics.WinUI3.Controls
}
if (suitableItems.Count == 0)
{
//suitableItems.Add(new ExtendedFontFamily()
//{
// FontFamily = "",
// LocalizedFontFamily = "N/A"
//});
suitableItems.Add("N/A");
}
AutoSuggestBox.ItemsSource = suitableItems.Order();
//AutoSuggestBox.ItemsSource = suitableItems.OrderBy(x => x.LocalizedFontFamily);
AutoSuggestBox.ItemsSource = suitableItems.OrderBy(x => x);
}
private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
@@ -63,16 +107,18 @@ namespace BetterLyrics.WinUI3.Controls
{
UpdateAutoSuggestBoxItemsSource();
}
SelectedLocalizedFontFamilyTextBlock.Text = FontHelper.GetLocalizedFontFamilyName(SelectedFontFamily, _settingsService.AppSettings.GeneralSettings.LanguageCode);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
SystemFontNames = [.. FontHelper.SystemFontFamilies];
RefreshFontFamilies();
}
private void AutoSuggestBox_GotFocus(object sender, RoutedEventArgs e)
{
UpdateAutoSuggestBoxItemsSource();
UpdateAutoSuggestBoxItemsSource("");
}
private void AutoSuggestBox_LostFocus(object sender, RoutedEventArgs e)

View File

@@ -47,51 +47,6 @@
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPageAlbumArtLayer"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE93C;}"
IsExpanded="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageOpacity" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="100"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayOpacity, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageSpeed" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="50"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsBackgroundSettings.CoverOverlaySpeed, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageBlurAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="100"
Maximum="100"
Minimum="0"
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayBlurAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageBackgroundAcrylicEffectAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="0"
Maximum="10"
Minimum="0"
Value="{x:Bind LyricsBackgroundSettings.CoverAcrylicEffectAmount, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPageFluidLayer"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -162,6 +117,21 @@
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageSpectrumLayerStyle" IsEnabled="{x:Bind LyricsBackgroundSettings.IsSpectrumOverlayEnabled, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind LyricsBackgroundSettings.SpectrumStyle, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageSpectrumStyleCurve" />
<ComboBoxItem x:Uid="SettingsPageSpectrumStyleBar" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsSpectrumOverlayEnabled, Mode=OneWay}">
<uc:ExtendedSlider
Default="128"
Maximum="1024"
Minimum="1"
Value="{x:Bind LyricsBackgroundSettings.SpectrumCount, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>

View File

@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Renderer.LyricsRenderer"
x:Class="BetterLyrics.WinUI3.Controls.LyricsCanvas"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Renderer"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Unloaded="LyricsCanvas_Unloaded"
Unloaded="Canvas_Unloaded"
mc:Ignorable="d">
<Grid>
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
CreateResources="LyricsCanvas_CreateResources"
Draw="LyricsCanvas_Draw"
Update="LyricsCanvas_Update" />
x:Name="Canvas"
CreateResources="Canvas_CreateResources"
Draw="Canvas_Draw"
Update="Canvas_Update" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,837 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Logic;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class LyricsCanvas : UserControl,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<AlbumArtThemeColors>>,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsData?>>,
IRecipient<PropertyChangedMessage<LyricsWindowStatus>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<TextAlignmentType>>,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<string>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
private readonly PureColorBackgroundRenderer _pureColorRenderer = new();
private readonly SnowRenderer _snowRenderer = new();
private readonly FogRenderer _fogRenderer = new();
private readonly SpectrumRenderer _spectrumRenderer = new();
private readonly LyricsSynchronizer _synchronizer = new();
private readonly LyricsLayoutManager _layoutManager = new();
private readonly LyricsAnimator _animator = new();
private readonly SpectrumAnalyzer _spectrumAnalyzer = new();
private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f,
durationSeconds: 0.3f
);
private readonly ValueTransition<Color> _accentColor1Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor2Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor3Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor4Transition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutSine
);
private readonly ValueTransition<double> _mouseYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutSine
);
private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1>
private TimeSpan _totalPlayedTime; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD>ŵ<EFBFBD>ʱ<EFBFBD>
private bool _isLastFMTracked = false;
private double _renderLyricsStartX = 0;
private double _renderLyricsStartY = 0;
private double _renderLyricsWidth = 0;
private double _renderLyricsHeight = 0;
private double _renderLyricsOpacity = 0;
private Point _mousePosition = new(0, 0);
private int _mouseHoverLineIndex = -1;
private bool _isMouseInLyricsArea = false;
private bool _isMousePressing = false;
private bool _isMouseScrolling = false;
private LyricsData? _lyricsData;
private bool _isLayoutChanged = true;
private bool _isMouseScrollingChanged = false;
private int _playingLineIndex;
private (int Start, int End) _visibleRange;
private double _canvasTargetScrollOffset;
public TimeSpan SongPosition => _songPosition;
public double CurrentCanvasYScroll => _canvasYScrollTransition.Value;
public double ActualLyricsHeight => _layoutManager.CalculateActualHeight(_lyricsData?.LyricsLines);
public int CurrentHoveringLineIndex => _mouseHoverLineIndex;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><CABC> X <20><><EFBFBD><EFBFBD>
public double LyricsStartX
{
get { return (double)GetValue(LyricsStartXProperty); }
set { SetValue(LyricsStartXProperty, value); }
}
public static readonly DependencyProperty LyricsStartXProperty =
DependencyProperty.Register(nameof(LyricsStartX), typeof(double), typeof(LyricsCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʼ Y <20><><EFBFBD><EFBFBD>
public double LyricsStartY
{
get { return (double)GetValue(LyricsStartYProperty); }
set { SetValue(LyricsStartYProperty, value); }
}
public static readonly DependencyProperty LyricsStartYProperty =
DependencyProperty.Register(nameof(LyricsStartY), typeof(double), typeof(LyricsCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
public double LyricsWidth
{
get { return (double)GetValue(LyricsWidthProperty); }
set { SetValue(LyricsWidthProperty, value); }
}
public static readonly DependencyProperty LyricsWidthProperty =
DependencyProperty.Register(nameof(LyricsWidth), typeof(double), typeof(LyricsCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߶<EFBFBD>
public double LyricsHeight
{
get { return (double)GetValue(LyricsHeightProperty); }
set { SetValue(LyricsHeightProperty, value); }
}
public static readonly DependencyProperty LyricsHeightProperty =
DependencyProperty.Register(nameof(LyricsHeight), typeof(double), typeof(LyricsCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD><CDB8><EFBFBD><EFBFBD>
public double LyricsOpacity
{
get { return (double)GetValue(LyricsOpacityProperty); }
set { SetValue(LyricsOpacityProperty, value); }
}
public static readonly DependencyProperty LyricsOpacityProperty =
DependencyProperty.Register(nameof(LyricsOpacity), typeof(double), typeof(LyricsCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
/// <summary>
/// <20>û<EFBFBD><C3BB>ٿ<EFBFBD><D9BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><D1B9><EFBFBD><EFBFBD>ľ<EFBFBD><C4BE><EFBFBD><EBA3A8> 0 <20><>ʼ<EFBFBD>
/// </summary>
public double MouseScrollOffset
{
get { return (double)GetValue(MouseScrollOffsetProperty); }
set { SetValue(MouseScrollOffsetProperty, value); }
}
public static readonly DependencyProperty MouseScrollOffsetProperty =
DependencyProperty.Register(nameof(MouseScrollOffset), typeof(double), typeof(LyricsCanvas), new PropertyMetadata(0.0, OnLayoutPropChanged));
/// <summary>
/// <20>û<EFBFBD><C3BB><EFBFBD><EFBFBD>굱ǰ<EAB5B1><C7B0>λ<EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڸ<EFBFBD><DAB8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͻǣ<CFBD>
/// </summary>
public Point MousePosition
{
get { return (Point)GetValue(MousePositionProperty); }
set { SetValue(MousePositionProperty, value); }
}
public static readonly DependencyProperty MousePositionProperty =
DependencyProperty.Register(nameof(MousePosition), typeof(Point), typeof(LyricsCanvas), new PropertyMetadata(new Point(0, 0), OnLayoutPropChanged));
public bool IsMouseInLyricsArea
{
get { return (bool)GetValue(IsMouseInLyricsAreaProperty); }
set { SetValue(IsMouseInLyricsAreaProperty, value); }
}
public static readonly DependencyProperty IsMouseInLyricsAreaProperty =
DependencyProperty.Register(nameof(IsMouseInLyricsArea), typeof(bool), typeof(LyricsCanvas), new PropertyMetadata(false, OnLayoutPropChanged));
public bool IsMousePressing
{
get { return (bool)GetValue(IsMousePressingProperty); }
set { SetValue(IsMousePressingProperty, value); }
}
public static readonly DependencyProperty IsMousePressingProperty =
DependencyProperty.Register(nameof(IsMousePressing), typeof(bool), typeof(LyricsCanvas), new PropertyMetadata(false, OnLayoutPropChanged));
public bool IsMouseScrolling
{
get { return (bool)GetValue(IsMouseScrollingProperty); }
set { SetValue(IsMouseScrollingProperty, value); }
}
public static readonly DependencyProperty IsMouseScrollingProperty =
DependencyProperty.Register(nameof(IsMouseScrolling), typeof(bool), typeof(LyricsCanvas), new PropertyMetadata(false, OnLayoutPropChanged));
public LyricsCanvas()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<int>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<AlbumArtThemeColors>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<TimeSpan>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<LyricsData?>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<LyricsWindowStatus>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<double>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<bool>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<TextAlignmentType>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<SongInfo?>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<LyricsFontWeight>>(this);
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<string>>(this);
}
private static void OnLayoutPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is LyricsCanvas canvas)
{
if (e.Property == LyricsStartXProperty)
{
canvas._renderLyricsStartX = Convert.ToDouble(e.NewValue);
canvas._isLayoutChanged = true;
}
else if (e.Property == LyricsStartYProperty)
{
canvas._renderLyricsStartY = Convert.ToDouble(e.NewValue);
canvas._isLayoutChanged = true;
}
else if (e.Property == LyricsWidthProperty)
{
canvas._renderLyricsWidth = Convert.ToDouble(e.NewValue);
canvas._isLayoutChanged = true;
}
else if (e.Property == LyricsHeightProperty)
{
canvas._renderLyricsHeight = Convert.ToDouble(e.NewValue);
canvas._isLayoutChanged = true;
}
else if (e.Property == LyricsOpacityProperty)
{
canvas._renderLyricsOpacity = Convert.ToDouble(e.NewValue);
canvas._isLayoutChanged = true;
}
else if (e.Property == MouseScrollOffsetProperty)
{
canvas._mouseYScrollTransition.StartTransition(Convert.ToDouble(e.NewValue));
}
else if (e.Property == MousePositionProperty)
{
canvas._mousePosition = (Point)e.NewValue;
}
else if (e.Property == IsMouseInLyricsAreaProperty)
{
canvas._isMouseInLyricsArea = (bool)e.NewValue;
}
else if (e.Property == IsMousePressingProperty)
{
canvas._isMousePressing = (bool)e.NewValue;
}
else if (e.Property == IsMouseScrollingProperty)
{
var value = (bool)e.NewValue;
if (canvas._isMouseScrolling != value)
{
canvas._isMouseScrollingChanged = true;
}
canvas._isMouseScrolling = value;
}
}
}
// ====
private void Canvas_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
var bounds = new Rect(0, 0, sender.Size.Width, sender.Size.Height);
var status = _liveStatesService.LiveStates.LyricsWindowStatus;
var albumArtLayout = status.AlbumArtLayoutSettings;
var lyricsBg = status.LyricsBackgroundSettings;
var lyricsStyle = status.LyricsStyleSettings;
var lyricsEffect = status.LyricsEffectSettings;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
double fixedSongPositionMs = _songPosition.TotalMilliseconds + (_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
var lyricsThemeColors = _mediaSessionsService.AlbumArtThemeColors;
Color overlayColor;
double finalOpacity;
if (status.IsAdaptToEnvironment)
{
// <20><><EFBFBD><EFBFBD>Ӧɫ
overlayColor = _immersiveBgColorTransition.Value;
finalOpacity = _immersiveBgOpacityTransition.Value * lyricsBg.PureColorOverlayOpacity / 100.0;
}
else
{
// ר<><D7A8>ɫ
overlayColor = _accentColor1Transition.Value;
finalOpacity = lyricsBg.PureColorOverlayOpacity / 100.0;
}
_pureColorRenderer.Draw(
args.DrawingSession,
bounds,
overlayColor,
finalOpacity,
lyricsBg.IsPureColorOverlayEnabled
);
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity;
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
_fluidRenderer.Draw(sender, args.DrawingSession);
_snowRenderer.Draw(sender, args.DrawingSession);
_fogRenderer.Draw(sender, args.DrawingSession);
_lyricsRenderer.Draw(
control: sender,
ds: args.DrawingSession,
lyricsData: _lyricsData,
playingLineIndex: _playingLineIndex,
mouseHoverLineIndex: _mouseHoverLineIndex,
isMousePressing: _isMousePressing,
startVisibleIndex: _visibleRange.Start,
endVisibleIndex: _visibleRange.End,
lyricsX: _renderLyricsStartX,
lyricsY: _renderLyricsStartY,
lyricsWidth: _renderLyricsWidth,
lyricsHeight: _renderLyricsHeight,
userScrollOffset: _mouseYScrollTransition.Value,
lyricsOpacity: _renderLyricsOpacity,
windowStatus: status,
strokeColor: lyricsThemeColors.StrokeFontColor,
bgColor: lyricsThemeColors.BgFontColor,
fgColor: lyricsThemeColors.FgFontColor,
getPlaybackState: (lineIndex) =>
{
if (_lyricsData == null) return new LinePlaybackState();
var line = _lyricsData.LyricsLines.ElementAtOrDefault(lineIndex);
if (line == null) return new LinePlaybackState();
var nextLine = _lyricsData.LyricsLines.ElementAtOrDefault(lineIndex + 1);
return _synchronizer.GetLinePlayingProgress(
fixedSongPositionMs,
line,
nextLine,
songDuration,
isForceWordByWord
);
}
);
if (_spectrumAnalyzer.IsCapturing)
{
_spectrumRenderer.Draw(
resourceCreator: sender,
ds: args.DrawingSession,
spectrumData: _spectrumAnalyzer?.SmoothSpectrum,
barCount: _spectrumAnalyzer?.BarCount ?? 1,
isEnabled: lyricsBg.IsSpectrumOverlayEnabled,
placement: lyricsBg.SpectrumPlacement,
style: lyricsBg.SpectrumStyle,
canvasWidth: sender.Size.Width,
canvasHeight: sender.Size.Height,
fillColor: lyricsThemeColors.BgFontColor
);
}
#if DEBUG
args.DrawingSession.DrawText(
$"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
$"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
$"Lyrics actual height: {_layoutManager.CalculateActualHeight(_lyricsData?.LyricsLines)}\n" +
$"Playing line (idx): {_playingLineIndex}\n" +
$"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
$"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
$"Total line count: {_layoutManager.CalculateMaxRange(_lyricsData?.LyricsLines).End + 1}\n" +
$"Played: {TimeSpan.FromMilliseconds(fixedSongPositionMs)} / {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" +
$"Y offset: {_canvasYScrollTransition.Value}\n" +
$"User scroll offset: {_mouseYScrollTransition.Value}",
new Vector2(0, 0), Colors.Red);
#endif
}
private void Canvas_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args)
{
var lyricsBg = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings;
var lyricsEffect = _liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings;
var albumArtThemeColors = _mediaSessionsService.AlbumArtThemeColors;
TimeSpan elapsedTime = args.Timing.ElapsedTime;
_accentColor1Transition.Update(elapsedTime);
_accentColor2Transition.Update(elapsedTime);
_accentColor3Transition.Update(elapsedTime);
_accentColor4Transition.Update(elapsedTime);
_immersiveBgOpacityTransition.Update(elapsedTime);
_immersiveBgColorTransition.Update(elapsedTime);
UpdatePlaybackState(elapsedTime);
TriggerRelayout();
#region UpdatePlayingLineIndex
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPosition.TotalMilliseconds, _lyricsData);
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex;
_playingLineIndex = newPlayingIndex;
#endregion
#region UpdateTargetScrollOffset
if (isPlayingLineChanged || _isLayoutChanged)
{
var targetScroll = _layoutManager.CalculateTargetScrollOffset(_lyricsData, _playingLineIndex);
if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value;
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType);
_canvasYScrollTransition.SetDuration(lyricsEffect.LyricsScrollDuration / 1000.0);
_canvasYScrollTransition.StartTransition(_canvasTargetScrollOffset, _isLayoutChanged);
}
_canvasYScrollTransition.Update(elapsedTime);
#endregion
_mouseYScrollTransition.Update(elapsedTime);
_mouseHoverLineIndex = _layoutManager.FindMouseHoverLineIndex(
_lyricsData?.LyricsLines,
_isMouseInLyricsArea,
_mousePosition,
_canvasYScrollTransition.Value + _mouseYScrollTransition.Value,
_renderLyricsStartY,
_renderLyricsHeight
);
_visibleRange = _layoutManager.CalculateVisibleRange(
_lyricsData?.LyricsLines,
_canvasYScrollTransition.Value + _mouseYScrollTransition.Value, // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>λ<EFBFBD><CEBB>
_renderLyricsStartY,
_renderLyricsHeight,
sender.Size.Height
);
var maxRange = _layoutManager.CalculateMaxRange(_lyricsData?.LyricsLines);
_animator.UpdateLines(
_lyricsData,
_isMouseScrolling ? maxRange.Start : _visibleRange.Start,
_isMouseScrolling ? maxRange.End : _visibleRange.End,
_playingLineIndex,
sender.Size.Height,
_canvasTargetScrollOffset,
_liveStatesService.LiveStates.LyricsWindowStatus.LyricsEffectSettings,
_canvasYScrollTransition,
albumArtThemeColors.BgFontColor,
albumArtThemeColors.FgFontColor,
elapsedTime,
_isMouseScrolling,
_isLayoutChanged,
isPlayingLineChanged,
_isMouseScrollingChanged
);
_isMouseScrollingChanged = false;
_lyricsRenderer.CalculateLyrics3DMatrix(
lyricsEffect: lyricsEffect,
lyricsX: _renderLyricsStartX,
lyricsY: _renderLyricsStartY,
lyricsWidth: _renderLyricsWidth,
canvasHeight: sender.Size.Height
);
_isLayoutChanged = false;
if (_fluidRenderer.IsEnabled)
{
_fluidRenderer.UpdateColors(
_accentColor1Transition.Value,
_accentColor2Transition.Value,
_accentColor3Transition.Value,
_accentColor4Transition.Value
);
_fluidRenderer.Update(elapsedTime);
}
_snowRenderer.IsEnabled = lyricsBg.IsSnowFlakeOverlayEnabled;
_snowRenderer.Amount = lyricsBg.SnowFlakeOverlayAmount / 100f;
_snowRenderer.Speed = lyricsBg.SnowFlakeOverlaySpeed;
_snowRenderer.Update(elapsedTime.TotalSeconds);
_fogRenderer.IsEnabled = lyricsBg.IsFogOverlayEnabled;
_fogRenderer.Update(elapsedTime.TotalSeconds);
if (lyricsBg.IsSpectrumOverlayEnabled && !_spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.BarCount = lyricsBg.SpectrumCount;
_spectrumAnalyzer.StartCapture();
}
else if (!lyricsBg.IsSpectrumOverlayEnabled && _spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.StopCapture();
}
if (_spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.UpdateSmoothSpectrum();
}
}
private void Canvas_Unloaded(object sender, RoutedEventArgs e)
{
Canvas.RemoveFromVisualTree();
Canvas = null;
_fluidRenderer.Dispose();
_snowRenderer.Dispose();
_fogRenderer.Dispose();
_spectrumRenderer.Dispose();
DisposeAnalyzer();
}
private async void Canvas_CreateResources(CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
{
args.TrackAsyncAction(_fluidRenderer.LoadResourcesAsync().AsAsyncAction());
_snowRenderer.LoadResources();
_fogRenderer.LoadResources();
_isLayoutChanged = true;
TriggerRelayout();
}
// ====
private void DisposeAnalyzer()
{
if (_spectrumAnalyzer.IsCapturing)
{
_spectrumAnalyzer.StopCapture();
}
_spectrumAnalyzer.Dispose();
}
private void TriggerRelayout()
{
if (_layoutManager == null || _lyricsData == null || !_isLayoutChanged) return;
_layoutManager.MeasureAndArrange(
resourceCreator: Canvas,
lyricsData: _lyricsData,
status: _liveStatesService.LiveStates.LyricsWindowStatus,
appSettings: _settingsService.AppSettings,
canvasWidth: Canvas.Size.Width,
canvasHeight: Canvas.Size.Height,
lyricsWidth: _renderLyricsWidth,
lyricsHeight: _renderLyricsHeight
);
}
private void UpdatePlaybackState(TimeSpan elapsedTime)
{
if (_mediaSessionsService.CurrentIsPlaying)
{
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
}
}
private void ResetPlaybackState()
{
_totalPlayedTime = TimeSpan.Zero;
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
public void Receive(PropertyChangedMessage<AlbumArtThemeColors> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtThemeColors))
{
var lyricsThemeColors = message.NewValue;
_immersiveBgColorTransition.StartTransition(lyricsThemeColors.EnvColor);
_accentColor1Transition.StartTransition(lyricsThemeColors.AccentColor1);
_accentColor2Transition.StartTransition(lyricsThemeColors.AccentColor2);
_accentColor3Transition.StartTransition(lyricsThemeColors.AccentColor3);
_accentColor4Transition.StartTransition(lyricsThemeColors.AccentColor4);
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
var realPosition = message.NewValue;
var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds);
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
// ƫ<><C6AB> or seek
if (diff >= timelineSyncThreshold)
{
_songPosition = realPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˿<EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD> LastFM ͳ<><CDB3>״̬
if (_songPosition.TotalSeconds <= 1)
{
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>
if (diff >= timelineSyncThreshold + 5000)
{
_isLayoutChanged = true;
}
}
}
}
public void Receive(PropertyChangedMessage<LyricsData?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentLyricsData))
{
_lyricsData = message.NewValue;
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<LyricsWindowStatus> message)
{
if (message.Sender is LiveStates)
{
if (message.PropertyName == nameof(LiveStates.LyricsWindowStatus))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.PhoneticLyricsFontSize))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.OriginalLyricsFontSize))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.TranslatedLyricsFontSize))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsFontStrokeWidth))
{
_isLayoutChanged = true;
}
}
else if (message.Sender is LyricsEffectSettings)
{
if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollDuration))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollTopDuration))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollBottomDuration))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollTopDelay))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollBottomDelay))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.FanLyricsAngle))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<double> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsLineSpacingFactor))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is LyricsEffectSettings)
{
if (message.PropertyName == nameof(LyricsEffectSettings.IsFanLyricsEnabled))
{
_isLayoutChanged = true;
}
}
else if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.IsDynamicLyricsFontSize))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<TextAlignmentType> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsAlignmentType))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
ResetPlaybackState();
}
}
}
public void Receive(PropertyChangedMessage<LyricsFontWeight> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsFontWeight))
{
_isLayoutChanged = true;
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsCJKFontFamily))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsWesternFontFamily))
{
_isLayoutChanged = true;
}
}
}
}
}

View File

@@ -18,147 +18,25 @@
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Effect -->
<TextBlock
x:Uid="SettingsPageLyricsEffect"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Effect" />
<dev:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB42;}">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsVerticalEdgeOpacitySlider"
Default="0"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsEffectSettings.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsBlurAmount" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsBlurAmountExtendedSlider"
Default="5"
Maximum="10"
Minimum="0"
Value="{x:Bind LyricsEffectSettings.LyricsBlurAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsLineFade" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED3A;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsLineFadeEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- 高亮 -->
<dev:SettingsExpander x:Uid="SettingsPageLyricsHighlight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7E6;}">
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPagePhoneticText">
<local:ExtendedSlider
Default="60"
Frequency="5"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsEffectSettings.PhoneticLyricsHighlightAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsHighlightScope">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.OriginalLyricsHighlightScope, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeLineStartToCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentLine" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageOriginalText">
<local:ExtendedSlider
Default="60"
Frequency="5"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsEffectSettings.OriginalLyricsHighlightAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageTranslatedText">
<local:ExtendedSlider
Default="60"
Frequency="5"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsEffectSettings.TranslatedLyricsHighlightAmount, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<!-- 阴影 -->
<dev:SettingsExpander
x:Uid="SettingsPageLyricsShadow"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF5EF;}"
IsExpanded="{x:Bind LyricsEffectSettings.IsLyricsShadowEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsShadowEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageScope" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsShadowEnabled, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsShadowScope, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeLineStartToCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentLine" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsShadowEnabled, Mode=OneWay}">
<local:ExtendedSlider
Default="8"
Maximum="20"
Minimum="1"
Value="{x:Bind LyricsEffectSettings.LyricsShadowAmount, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<!-- 辉光效果 -->
<dev:SettingsExpander
x:Uid="SettingsPageLyricsGlowEffect"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE9A9;}"
IsExpanded="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<dev:SettingsCard x:Uid="SettingsPageLyricsGlowEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE9A9;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageScope" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsGlowEffectScope, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeLineStartToCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentLine" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<local:ExtendedSlider
Default="8"
Maximum="20"
Minimum="1"
Value="{x:Bind LyricsEffectSettings.LyricsGlowEffectAmount, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
</dev:SettingsCard>
<!-- 缩放效果 -->
<dev:SettingsCard x:Uid="SettingsPageLyricsScaleEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8A3;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsScaleEffectEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- 浮动动画 -->
<dev:SettingsExpander
x:Uid="SettingsPageLyricsFloatAnimation"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8C5;}"
IsExpanded="True">
<dev:SettingsCard x:Uid="SettingsPageLyricsFloatAnimation" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8C5;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsFloatAnimationEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<local:ExtendedSlider
Default="1"
Maximum="4"
Minimum="1"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAmount, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
</dev:SettingsCard>
<!-- 扇形歌词 -->
<dev:SettingsExpander
@@ -224,7 +102,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 +124,7 @@
Default="500"
Frequency="50"
Maximum="1000"
Minimum="50"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollTopDuration, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -255,7 +133,7 @@
Default="500"
Frequency="50"
Maximum="1000"
Minimum="50"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollDuration, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -264,7 +142,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>
@@ -136,66 +176,29 @@
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchResult">
<ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="3,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<HyperlinkButton
Padding="0"
Content="{x:Bind Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}"
NavigateUri="{x:Bind Reference, FallbackValue=about:blank}" />
<!-- Title -->
<Grid ColumnSpacing="12" Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlTitle" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Artist -->
<Grid ColumnSpacing="12" Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlArtist" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Album -->
<Grid ColumnSpacing="12" Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlAlbum" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Duration -->
<StackPanel
Orientation="Horizontal"
Spacing="6"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock x:Uid="LyricsSearchControlDurauion" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Duration}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="s" />
</StackPanel>
<!-- Match percentage -->
<StackPanel
Orientation="Horizontal"
Spacing="6"
Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock x:Uid="LyricsPageMatchPercentage" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind MatchPercentage}" />
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow
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}}" />
<!-- 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}" />
<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
@@ -266,23 +269,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>
@@ -313,31 +323,29 @@
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="1">
<RelativePanel>
<TextBlock
x:Uid="LyricsSearchControlHelp"
Margin="0,0,24,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
RelativePanel.AlignVerticalCenterWithPanel="True"
RelativePanel.LeftOf="Reset"
TextWrapping="Wrap" />
<Button
x:Name="Reset"
x:Uid="LyricsSearchControlReset"
Margin="0,0,6,0"
Command="{x:Bind ViewModel.ResetCommand}"
RelativePanel.AlignVerticalCenterWithPanel="True"
RelativePanel.LeftOf="SaveChanges" />
<Button
x:Name="SaveChanges"
x:Uid="LyricsSearchControlSaveChanges"
Command="{x:Bind ViewModel.SaveCommand}"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource AccentButtonStyle}" />
</RelativePanel>
<Grid Grid.Row="1" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsSearchControlHelp"
Grid.Column="1"
VerticalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
<Button
x:Uid="LyricsSearchControlReset"
Grid.Column="2"
Command="{x:Bind ViewModel.ResetCommand}" />
<Button
x:Uid="LyricsSearchControlSaveChanges"
Grid.Column="3"
Command="{x:Bind ViewModel.SaveCommand}"
Style="{StaticResource AccentButtonStyle}" />
</Grid>
</Grid>
</UserControl>

View File

@@ -2,6 +2,8 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
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 +24,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

@@ -23,9 +23,9 @@
<dev:SettingsCard x:Uid="SettingsPageLyricsAlignment" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind LyricsStyleSettings.LyricsAlignmentType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsLeft" />
<ComboBoxItem x:Uid="SettingsPageLyricsCenter" />
<ComboBoxItem x:Uid="SettingsPageLyricsRight" />
<ComboBoxItem x:Uid="SettingsPageLeft" />
<ComboBoxItem x:Uid="SettingsPageCenter" />
<ComboBoxItem x:Uid="SettingsPageRight" />
</ComboBox>
</dev:SettingsCard>
@@ -56,15 +56,6 @@
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsBgFontOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB42;}">
<local:ExtendedSlider
Default="30"
Maximum="100"
Minimum="0"
Unit="%"
Value="{x:Bind LyricsStyleSettings.LyricsBgFontOpacity, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsFontStrokeWidth" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEC12;}">
<local:ExtendedSlider
Default="0"
@@ -227,13 +218,6 @@
Value="{x:Bind LyricsStyleSettings.LyricsLineSpacingFactor, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLyricsTranslationSeparator" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF464;}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBox AcceptsReturn="True" Text="{x:Bind LyricsStyleSettings.LyricsTranslationSeparator, Mode=TwoWay}" />
<Button Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, FontSize=12, Glyph=&#xE8FB;}" Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</dev:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -13,21 +13,32 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid>
<Grid x:Name="DisplayGrid" SizeChanged="DisplayGrid_SizeChanged">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock
x:Uid="SettingsPageRecordedWindowStatus"
RelativePanel.AlignLeftWithPanel="True"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="SettingsPageRecordedWindowStatus"
Grid.Column="0"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Button
Grid.Column="2"
Margin="0,30,0,0"
Command="{x:Bind ViewModel.OpenConfigPanelCommand}"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Uid="LyricsWindowSettingsControlCurrentLyricsWindowConfig" />
</Button>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="3">
@@ -60,7 +71,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>
@@ -114,12 +125,40 @@
</Grid>
</ScrollViewer>
</Grid>
<Grid Grid.Column="1">
<Grid
x:Name="ConfigGrid"
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
Opacity="{x:Bind ViewModel.IsConfigPanelOpened, Mode=OneWay, Converter={StaticResource BoolToOpacityConverter}}"
Translation="{x:Bind ViewModel.ConfigPanelTranslation, Mode=OneWay}">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
<Grid Padding="36,0" Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="LyricsWindowSettingsControlCurrentLyricsWindowConfig" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsWindowSettingsControlCurrentLyricsWindowConfig"
Grid.Column="0"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Button
Grid.Column="2"
Margin="0,30,0,0"
Command="{x:Bind ViewModel.CloseConfigPanelCommand}"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE711;}"
Style="{StaticResource AccentButtonStyle}" />
</Grid>
<Pivot SelectionChanged="Pivot_SelectionChanged">
@@ -132,6 +171,15 @@
</PivotItem.Header>
</PivotItem>
<PivotItem Tag="Layout">
<PivotItem.Header>
<TextBlock
x:Uid="SettingsPageLayout"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}" />
</PivotItem.Header>
</PivotItem>
<PivotItem Tag="AlbumArtStyle">
<PivotItem.Header>
<TextBlock
@@ -168,15 +216,6 @@
</PivotItem.Header>
</PivotItem>
<PivotItem Tag="Advanced">
<PivotItem.Header>
<TextBlock
x:Uid="SettingsPageAdvanced"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}" />
</PivotItem.Header>
</PivotItem>
</Pivot>
</StackPanel>
@@ -189,7 +228,7 @@
</TransitionCollection>
</controls:SwitchPresenter.ContentTransitions>
<!-- General -->
<!-- Window -->
<controls:Case Value="General">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
@@ -207,26 +246,6 @@
</StackPanel>
</dev:SettingsCard>
<dev:SettingsExpander
x:Uid="SettingsPageDisplayTypeSwitcher"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF246;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsDisplayType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MainPageAlbumArtOnly" />
<ComboBoxItem x:Uid="MainPageLyriscOnly" />
<ComboBoxItem x:Uid="MainPageSplitView" />
</ComboBox>
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageLayoutOrientation">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLayoutOrientationHorizontal" />
<ComboBoxItem x:Uid="SettingsPageLayoutOrientationVertical" />
</ComboBox>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPageWorkArea"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -336,37 +355,6 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow, Mode=TwoWay}" />
</dev:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</controls:Case>
<!-- Album art area style -->
<controls:Case Value="AlbumArtStyle">
<uc:AlbumArtLayoutSettingsControl AlbumArtLayoutSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings, Mode=OneWay}" />
</controls:Case>
<!-- Lyrics style -->
<controls:Case Value="LyricsStyle">
<uc:LyricsStyleSettingsControl LyricsStyleSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings, Mode=OneWay}" />
</controls:Case>
<!-- Lyrics effect -->
<controls:Case Value="LyricsEffect">
<uc:LyricsEffectSettingsControl LyricsEffectSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsEffectSettings, Mode=OneWay}" />
</controls:Case>
<!-- Lyrics background -->
<controls:Case Value="LyricsBackground">
<uc:LyricsBackgroundSettingsControl LyricsBackgroundSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings, Mode=OneWay}" />
</controls:Case>
<!-- Advanced -->
<controls:Case Value="Advanced">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<dev:SettingsCard x:Uid="SettingsPageShowInSwitchers" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7C4;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.IsShownInSwitchers, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -392,8 +380,58 @@
</ScrollViewer>
</controls:Case>
<!-- Layout -->
<controls:Case Value="Layout">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsExpander x:Uid="SettingsPageDisplayTypeSwitcher" IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsDisplayType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MainPageAlbumArtOnly" />
<ComboBoxItem x:Uid="MainPageLyriscOnly" />
<ComboBoxItem x:Uid="MainPageSplitView" />
</ComboBox>
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageLayoutOrientation">
<ComboBox SelectedIndex="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsLayoutOrientation, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLayoutOrientationHorizontal" />
<ComboBoxItem x:Uid="SettingsPageLayoutOrientationVertical" />
</ComboBox>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
</StackPanel>
</Grid>
</ScrollViewer>
</controls:Case>
<!-- Album art area style -->
<controls:Case Value="AlbumArtStyle">
<uc:AlbumArtLayoutSettingsControl AlbumArtLayoutSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.AlbumArtLayoutSettings, Mode=OneWay}" />
</controls:Case>
<!-- Lyrics style -->
<controls:Case Value="LyricsStyle">
<uc:LyricsStyleSettingsControl LyricsStyleSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsStyleSettings, Mode=OneWay}" />
</controls:Case>
<!-- Lyrics effect -->
<controls:Case Value="LyricsEffect">
<uc:LyricsEffectSettingsControl LyricsEffectSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsEffectSettings, Mode=OneWay}" />
</controls:Case>
<!-- Lyrics background -->
<controls:Case Value="LyricsBackground">
<uc:LyricsBackgroundSettingsControl LyricsBackgroundSettings="{x:Bind ViewModel.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings, Mode=OneWay}" />
</controls:Case>
</controls:SwitchPresenter>
</Grid>
</Grid>
</UserControl>

View File

@@ -2,7 +2,6 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
@@ -27,7 +26,6 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly ILiveStatesService _liveStatesService = Ioc.Default.GetRequiredService<ILiveStatesService>();
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public LyricsWindowSettingsControl()
{
@@ -75,7 +73,7 @@ namespace BetterLyrics.WinUI3.Controls
StorageFile? file;
if (this.Parent is FlyoutPresenter)
{
file = await PickerHelper.PickSaveFileAsync<LyricsWindow>(fileTypeChoices);
file = await PickerHelper.PickSaveFileAsync<NowPlayingWindow>(fileTypeChoices);
}
else
{
@@ -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);
}
}
}
@@ -134,7 +132,7 @@ namespace BetterLyrics.WinUI3.Controls
StorageFile? file;
if (this.Parent is FlyoutPresenter)
{
file = await PickerHelper.PickSingleFileAsync<LyricsWindow>(fileTypeFilter);
file = await PickerHelper.PickSingleFileAsync<NowPlayingWindow>(fileTypeFilter);
}
else
{
@@ -147,9 +145,14 @@ namespace BetterLyrics.WinUI3.Controls
if (data != null)
{
ViewModel.AppSettings.WindowBoundsRecords.Add(data);
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("ImportSettingsSuccess"));
ToastHelper.ShowToast("ImportSettingsSuccess", null, InfoBarSeverity.Success);
}
}
}
private void DisplayGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.DisplayPanelHeight = e.NewSize.Height;
}
}
}

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>
@@ -255,96 +299,65 @@
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Provider info -->
<!-- Realtime info -->
<TextBlock x:Uid="SettingsPageRealtimeStatus" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard ContentAlignment="Left">
<!-- Playback source info -->
<Expander
x:Uid="SettingsPagePlaybackStatus"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<!-- Playback source -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="SettingsPagePlaybackSource" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Playback source ID -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="SettingsPagePlaybackSourceID" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Song title -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlTitle" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Song artists -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlArtist" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Song album -->
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock x:Uid="LyricsSearchControlAlbum" Grid.Column="0" />
<RichTextBlock Grid.Column="1" Foreground="{ThemeResource TextFillColorSecondaryBrush}">
<Paragraph>
<Run Text="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</Grid>
<!-- Lyrics source -->
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock x:Uid="LyricsPageLyricsProviderPrefix" />
<HyperlinkButton
Padding="0"
Content="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}"
IsEnabled="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.IsFound, Mode=OneWay}"
NavigateUri="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}" />
</StackPanel>
<!-- Translation source -->
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock x:Uid="LyricsPageTranslationProviderPrefix" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.MediaSessionsService.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
</StackPanel>
<!-- Match percentage -->
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Uid="LyricsPageMatchPercentage" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
</StackPanel>
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</dev:SettingsCard>
</Expander>
<!-- Song info -->
<Expander
x:Uid="SettingsPageSongStatus"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<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">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, TargetNullValue=N/A, Mode=OneWay}" />
<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}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.TranslationSearchProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
<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>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
@@ -486,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

@@ -0,0 +1,91 @@
<?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="6">
<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"
Visibility="{x:Bind LinkVisibility, Mode=OneWay}">
<TextBlock Text="{x:Bind Value, Mode=OneWay}" TextWrapping="Wrap" />
</HyperlinkButton>
</Grid>
<Button
x:Name="CopyButton"
Grid.Column="1"
Margin="8,-2,0,0"
Padding="4"
VerticalAlignment="Top"
Click="OnCopyClicked"
Opacity="0">
<ToolTipService.ToolTip>
<ToolTip x:Uid="Copy" />
</ToolTipService.ToolTip>
<Button.OpacityTransition>
<ScalarTransition />
</Button.OpacityTransition>
<Grid>
<FontIcon
x:Name="CopyIcon"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8C8;"
Opacity="1">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<FontIcon
x:Name="CheckIcon"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE73E;"
Opacity="0">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button>
</Grid>
</Grid>
</UserControl>

View File

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

View File

@@ -23,7 +23,11 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE894;}"
Style="{StaticResource GhostButtonStyle}" />
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Uid="SettingsPageClear" />
</ToolTipService.ToolTip>
</Button>
<Button
Margin="3,0,0,0"
HorizontalAlignment="Right"
@@ -31,7 +35,11 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE721;}"
Style="{StaticResource GhostButtonStyle}" />
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Uid="SettingsPageCheckShortcut" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,3 +1,4 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
@@ -92,11 +93,11 @@ namespace BetterLyrics.WinUI3.Controls
bool registered = GlobalHotKeyHook.IsHotKeyRegistered(Shortcut);
if (registered)
{
DevWinUI.Growl.Success(_resourceService.GetLocalizedString("SettingsPageShortcutRegSuccessInfo"));
ToastHelper.ShowToast("SettingsPageShortcutRegSuccessInfo", null, InfoBarSeverity.Success);
}
else
{
DevWinUI.Growl.Error(_resourceService.GetLocalizedString("SettingsPageShortcutRegFailInfo"));
ToastHelper.ShowToast("SettingsPageShortcutRegFailInfo", null, InfoBarSeverity.Error);
}
}
}

View File

@@ -48,7 +48,7 @@
x:Uid="SystemTrayMusicGallery"
Command="{x:Bind ViewModel.OpenMusicGalleryCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xEA69;}" />
Glyph=&#xE8F1;}" />
<MenuFlyoutItem
x:Uid="SystemTraySettings"
Command="{x:Bind ViewModel.OpenSettingsCommand}"

View File

@@ -0,0 +1,24 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class LyricsFontWeightToFontWeightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsFontWeight weight)
{
return weight.ToFontWeight();
}
return FontWeights.Normal;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class LyricsLayoutOrientationNegationToOrientationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsLayoutOrientation orientation)
{
return orientation.ToOrientationInverse();
}
return Orientation.Horizontal;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class LyricsLayoutOrientationToOrientationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsLayoutOrientation orientation)
{
return orientation.ToOrientation();
}
return Orientation.Horizontal;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,32 @@
using ATL;
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.Linq;
namespace BetterLyrics.WinUI3.Converter
{
public partial class PictureInfosToImageSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
BitmapImage bitmapImage = new();
if (value is IList<PictureInfo> list && list.FirstOrDefault()?.PictureData is byte[] pictureData)
{
bitmapImage.SetSource(ImageHelper.ToIRandomAccessStream(pictureData));
}
else
{
bitmapImage.UriSource = new Uri(PathHelper.AlbumArtPlaceholderPath);
}
return bitmapImage;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,24 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class TextAlignmentTypeToHorizontalAlignmentConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TextAlignmentType type)
{
return type.ToHorizontalAlignment();
}
return HorizontalAlignment.Left;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,7 +1,5 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchProvider

View File

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

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum SpectrumStyle
{
Curve,
Bar
}
}

View File

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

View File

@@ -1,7 +1,4 @@
using BetterLyrics.WinUI3.Enums;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions
{

View File

@@ -0,0 +1,26 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Controls;
using System;
namespace BetterLyrics.WinUI3.Extensions
{
public static class LyricsLayoutOrientationExtensions
{
extension(LyricsLayoutOrientation orientation)
{
public Orientation ToOrientation() => orientation switch
{
LyricsLayoutOrientation.Horizontal => Orientation.Horizontal,
LyricsLayoutOrientation.Vertical => Orientation.Vertical,
_ => throw new ArgumentOutOfRangeException(nameof(orientation)),
};
public Orientation ToOrientationInverse() => orientation switch
{
LyricsLayoutOrientation.Horizontal => Orientation.Vertical,
LyricsLayoutOrientation.Vertical => Orientation.Horizontal,
_ => throw new ArgumentOutOfRangeException(nameof(orientation)),
};
}
}
}

View File

@@ -1,8 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions
{
@@ -18,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

@@ -70,7 +70,7 @@ namespace BetterLyrics.WinUI3.Extensions
{
Name = _resourceService.GetLocalizedString("FullscreenMode"),
IsBorderless = true,
IsAlwaysOnTop = false,
IsAlwaysOnTop = true,
TitleBarArea = TitleBarArea.None,
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
LyricsStyleSettings = new LyricsStyleSettings

View File

@@ -8,6 +8,9 @@ namespace BetterLyrics.WinUI3.Extensions
extension(Point point)
{
public PointInt32 ToPointInt32() => new((int)point.X, (int)point.Y);
public Point AddX(double deltaX) => new(point.X + deltaX, point.Y);
public Point AddY(double deltaY) => new(point.X, point.Y + deltaY);
}
}
}

View File

@@ -41,6 +41,41 @@ namespace BetterLyrics.WinUI3.Extensions
rect.Width,
rect.Height
);
public Rect AddY(double y) => new(
rect.X,
rect.Y + y,
rect.Width,
rect.Height
);
public Rect Extend(double left, double top, double right, double bottom) => new(
rect.X - left,
rect.Y - top,
rect.Width + left + right,
rect.Height + top + bottom
);
public Rect Extend(double padding) => Extend(rect, padding, padding, padding, padding);
public Rect Scale(double scale)
{
double originalWidth = rect.Width;
double originalHeight = rect.Height;
double scaledWidth = originalWidth * scale;
double scaledHeight = originalHeight * scale;
double scaleOffsetX = (scaledWidth - originalWidth) / 2;
double scaleOffsetY = (scaledHeight - originalHeight) / 2;
return new Rect(
rect.X - scaleOffsetX,
rect.Y - scaleOffsetY,
scaledWidth,
scaledHeight
);
}
}
}
}

View File

@@ -1,9 +1,5 @@
using BetterLyrics.WinUI3.Enums;
using NTextCat.Commons;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions
{

View File

@@ -15,7 +15,14 @@ namespace BetterLyrics.WinUI3.Extensions
{
if (track.Path is string path)
{
return TagLib.File.Create(path).Tag.Lyrics;
try
{
return TagLib.File.Create(path).Tag.Lyrics;
}
catch (System.Exception)
{
return "";
}
}
return "";
}

View File

@@ -1,14 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using System;
using Windows.Graphics.Effects;
using Windows.UI;
@@ -16,351 +8,6 @@ namespace BetterLyrics.WinUI3.Helper
{
public class CanvasHelper
{
public static CanvasLinearGradientBrush CreateHorizontalFillBrush(
ICanvasAnimatedControl control,
List<(double position, double opacity)> stops,
double startX,
double width
)
{
return new CanvasLinearGradientBrush(control, stops.Select(stops => new CanvasGradientStop
{
Position = (float)stops.position,
Color = Color.FromArgb((byte)(stops.opacity * 255), 128, 128, 128),
}).ToArray())
{
StartPoint = new Vector2((float)startX, 0),
EndPoint = new Vector2((float)(startX + width), 0),
};
}
/// <summary>
/// 背景层
/// </summary>
/// <param name="lyricsLayerOpacity">_lyricsOpacityTransition.Value</param>
public static OpacityEffect CreateBackgroundEffect(LyricsLine lyricsLine, CanvasCommandList backgroundFontEffect, double lyricsLayerOpacity)
{
if (lyricsLine.BlurAmountTransition.Value == 0)
{
return new OpacityEffect
{
Source = backgroundFontEffect,
Opacity = (float)(lyricsLine.OpacityTransition.Value * lyricsLayerOpacity),
};
}
else
{
return new OpacityEffect
{
Source = new GaussianBlurEffect
{
Source = backgroundFontEffect,
BlurAmount = (float)lyricsLine.BlurAmountTransition.Value,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Speed,
},
Opacity = (float)(lyricsLine.OpacityTransition.Value * lyricsLayerOpacity),
};
}
}
public static CanvasCommandList CreateFontEffect(LyricsLine lyricsLine, ICanvasAnimatedControl control, Color strokeColor, int strokeWidth, Color fontColor)
{
CanvasCommandList list = new(control);
using var ds = list.CreateDrawingSession();
// 描边
if (strokeWidth > 0)
{
if (lyricsLine.PhoneticCanvasGeometry != null)
{
ds.DrawGeometry(lyricsLine.PhoneticCanvasGeometry, lyricsLine.PhoneticPosition, strokeColor, strokeWidth);
}
if (lyricsLine.OriginalCanvasGeometry != null)
{
ds.DrawGeometry(lyricsLine.OriginalCanvasGeometry, lyricsLine.OriginalPosition, strokeColor, strokeWidth);
}
if (lyricsLine.TranslatedCanvasGeometry != null)
{
ds.DrawGeometry(lyricsLine.TranslatedCanvasGeometry, lyricsLine.TranslatedPosition, strokeColor, strokeWidth);
}
}
// 绘制文本(填充)
if (lyricsLine.PhoneticCanvasTextLayout != null)
{
ds.DrawTextLayout(lyricsLine.PhoneticCanvasTextLayout, lyricsLine.PhoneticPosition, fontColor);
}
if (lyricsLine.OriginalCanvasTextLayout != null)
{
ds.DrawTextLayout(lyricsLine.OriginalCanvasTextLayout, lyricsLine.OriginalPosition, fontColor);
}
if (lyricsLine.TranslatedCanvasTextLayout != null)
{
ds.DrawTextLayout(lyricsLine.TranslatedCanvasTextLayout, lyricsLine.TranslatedPosition, fontColor);
}
return list;
}
/// <summary>
/// 创建辉光效果层
/// 仅需在布局重构 (Relayout) 时调用
/// </summary>
/// <param name="lineRenderingType">_lyricsGlowEffectScope</param>
/// <param name="glowEffectAmount">_lyricsGlowEffectAmount</param>
public static GaussianBlurEffect CreateForegroundBlurEffect(CanvasCommandList foregroundFontEffect, IGraphicsEffectSource mask, double glowEffectAmount)
{
return new GaussianBlurEffect
{
Source = new AlphaMaskEffect
{
Source = foregroundFontEffect,
AlphaMask = mask,
},
BlurAmount = (float)glowEffectAmount,
Optimization = EffectOptimization.Speed,
};
}
public static CanvasCommandList CreateCharMask(ICanvasAnimatedControl control, LyricsLine lyricsLine, int charStartIndex, int charLength, double charProgress)
{
var mask = new CanvasCommandList(control);
using var ds = mask.CreateDrawingSession();
if (lyricsLine.OriginalCanvasTextLayout == null)
{
return mask;
}
var highlightRegion = lyricsLine.OriginalCanvasTextLayout.GetCharacterRegions(charStartIndex, charLength).FirstOrDefault();
double highlightTotalWidth = (double)highlightRegion.LayoutBounds.Width;
// Draw the highlight for the current character
double highlightWidth = highlightTotalWidth * charProgress;
double fadingWidth = (double)highlightRegion.LayoutBounds.Height / 2;
// Rects
var highlightRect = new Rect(
highlightRegion.LayoutBounds.X,
highlightRegion.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
var fadeOutRect = new Rect(
highlightRect.Right,
highlightRegion.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
// Brushes
using var fadeInBrush = CanvasHelper.CreateHorizontalFillBrush(
control,
[(0f, 0f), (1f, 1f)],
(double)highlightRect.Right - fadingWidth,
fadingWidth
);
using var fadeOutBrush = CanvasHelper.CreateHorizontalFillBrush(
control,
[(0f, 1f), (1f, 0f)],
(double)highlightRect.Right,
fadingWidth
);
ds.FillRectangle(fadeInRect, fadeInBrush);
ds.FillRectangle(fadeOutRect, fadeOutBrush);
return mask;
}
public static CanvasCommandList CreateLineStartToCharMask(ICanvasAnimatedControl control, LyricsLine lyricsLine, int charStartIndex, int charLength, double charProgress, bool fade)
{
var mask = new CanvasCommandList(control);
if (lyricsLine.OriginalCanvasTextLayout == null)
{
return mask;
}
using var ds = mask.CreateDrawingSession();
var regions = lyricsLine.OriginalCanvasTextLayout.GetCharacterRegions(0, charStartIndex);
var highlightRegion = lyricsLine.OriginalCanvasTextLayout
.GetCharacterRegions(charStartIndex, charLength)
.FirstOrDefault();
if (regions.Length > 0)
{
// Draw the mask for the current line
for (int j = 0; j < regions.Length; j++)
{
var region = regions[j];
var rect = new Rect(
region.LayoutBounds.X,
region.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
ds.FillRectangle(rect, Color.FromArgb(255, 128, 128, 128));
}
}
double highlightTotalWidth = (double)highlightRegion.LayoutBounds.Width;
// Draw the highlight for the current character
double highlightWidth = highlightTotalWidth * charProgress;
double fadingWidth = (double)highlightRegion.LayoutBounds.Height / 2;
// Rects
var highlightRect = new Rect(
highlightRegion.LayoutBounds.X,
highlightRegion.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
ds.FillRectangle(highlightRect, Color.FromArgb(255, 128, 128, 128));
if (fade)
{
var fadeOutRect = new Rect(
highlightRect.Right,
highlightRegion.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
using var fadeOutBrush = CreateHorizontalFillBrush(
control,
[(0f, 1f), (1f, 0f)],
(double)highlightRect.Right,
fadingWidth
);
ds.FillRectangle(fadeOutRect, fadeOutBrush);
}
return mask;
}
public static CanvasCommandList CreateLineMask(ICanvasAnimatedControl control, LyricsLine lyricsLine)
{
var mask = new CanvasCommandList(control);
using var ds = mask.CreateDrawingSession();
if (lyricsLine.OriginalCanvasTextLayout == null)
{
return mask;
}
var regions = lyricsLine.OriginalCanvasTextLayout.GetCharacterRegions(0, lyricsLine.OriginalText.Length);
if (regions.Length > 0)
{
for (int j = 0; j < regions.Length; j++)
{
var region = regions[j];
var rect = new Rect(
region.LayoutBounds.X,
region.LayoutBounds.Y + lyricsLine.OriginalPosition.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
ds.FillRectangle(rect, Colors.White);
}
}
return mask;
}
public static CanvasCommandList CreatePhoneticHighlightMask(ICanvasAnimatedControl control, LyricsLine lyricsLine)
{
var mask = new CanvasCommandList(control);
using var ds = mask.CreateDrawingSession();
if (lyricsLine.PhoneticCanvasTextLayout == null)
{
return mask;
}
var regions = lyricsLine.PhoneticCanvasTextLayout.GetCharacterRegions(0, lyricsLine.PhoneticText.Length);
if (regions.Length > 0)
{
for (int j = 0; j < regions.Length; j++)
{
var region = regions[j];
var rect = new Rect(
region.LayoutBounds.X,
region.LayoutBounds.Y + lyricsLine.PhoneticPosition.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
ds.FillRectangle(rect, Colors.White);
}
}
return mask;
}
public static CanvasCommandList CreateTranslatedHighlightMask(ICanvasAnimatedControl control, LyricsLine lyricsLine)
{
var mask = new CanvasCommandList(control);
using var ds = mask.CreateDrawingSession();
if (lyricsLine.TranslatedCanvasTextLayout == null)
{
return mask;
}
var regions = lyricsLine.TranslatedCanvasTextLayout.GetCharacterRegions(0, lyricsLine.TranslatedText.Length);
if (regions.Length > 0)
{
for (int j = 0; j < regions.Length; j++)
{
var region = regions[j];
var rect = new Rect(
region.LayoutBounds.X,
region.LayoutBounds.Y + lyricsLine.TranslatedPosition.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
ds.FillRectangle(rect, Colors.White);
}
}
return mask;
}
/// <summary>
/// 创建高亮效果层
/// </summary>
/// <param name="control"></param>
/// <param name="lineRenderingType"></param>
public static OpacityEffect CreateForegroundHighlightEffect(CanvasCommandList foregroundFontEffect, IGraphicsEffectSource mask, double opacity)
{
return new OpacityEffect
{
Source = new AlphaMaskEffect
{
Source = foregroundFontEffect,
AlphaMask = mask,
},
Opacity = (float)opacity,
};
}
public static ShadowEffect CreateForegroundShadowEffect(CanvasCommandList foregroundFontEffect, IGraphicsEffectSource mask, Color shadowColor, double shadowAmount)
{
return new ShadowEffect
@@ -371,34 +18,10 @@ namespace BetterLyrics.WinUI3.Helper
AlphaMask = mask,
},
ShadowColor = shadowColor,
BlurAmount = (float)shadowAmount,
BlurAmount = (float)Math.Clamp(shadowAmount, 0, 100),
Optimization = EffectOptimization.Speed,
};
}
public static OpacityEffect CreateForegroundTranslationEffect(CanvasCommandList foregroundFontEffect, IGraphicsEffectSource mask, double opacity)
{
return new OpacityEffect
{
Source = new AlphaMaskEffect
{
Source = foregroundFontEffect,
AlphaMask = mask,
},
Opacity = (float)opacity,
};
}
public static IGraphicsEffectSource GetAlphaMask(ICanvasAnimatedControl control, IGraphicsEffectSource charMask, IGraphicsEffectSource lineStartToCharMask, IGraphicsEffectSource lineMask, LineRenderingType lineRenderingType)
{
var result = lineRenderingType switch
{
LineRenderingType.CurrentChar => charMask,
LineRenderingType.LineStartToCurrentChar => lineStartToCharMask,
LineRenderingType.CurrentLine => lineMask,
_ => new CanvasCommandList(control),
};
return result;
}
}
}

View File

@@ -3,7 +3,6 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Serialization;
using System;
using System.IO;
@@ -49,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;
@@ -69,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

@@ -1,10 +1,39 @@
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI.Xaml;
using System;
using System.Globalization;
using System.Linq;
using System.Windows.Markup;
using System.Windows.Media;
namespace BetterLyrics.WinUI3.Helper
{
public static class FontHelper
{
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies().Order().ToArray();
public static string GetLocalizedFontFamilyName(string sourceName, string langCode)
{
if (langCode == "")
{
langCode = CultureInfo.CurrentCulture.Name;
}
foreach (var font in Fonts.SystemFontFamilies)
{
if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("en-us"), out string englishFamilyName) && englishFamilyName == sourceName)
{
if (font.FamilyNames.ContainsKey(XmlLanguage.GetLanguage(langCode)))
{
if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage(langCode), out string localizedFamilyName))
{
return localizedFamilyName;
}
}
}
}
return sourceName;
}
}
}

View File

@@ -137,7 +137,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static async Task<byte[]?> GetImageBytesFromUrlAsync(string url)
public static async Task<byte[]?> GetImageByteArrayFromUrlAsync(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
@@ -185,5 +185,21 @@ namespace BetterLyrics.WinUI3.Helper
{
return buffer.AsStream().AsRandomAccessStream();
}
public static byte[] ToByteArray(IBuffer buffer)
{
using (var dataReader = DataReader.FromBuffer(buffer))
{
byte[] byteArray = new byte[buffer.Length];
dataReader.ReadBytes(byteArray);
return byteArray;
}
}
public static IRandomAccessStream ToIRandomAccessStream(byte[] arr)
{
MemoryStream stream = new MemoryStream(arr);
return stream.AsRandomAccessStream();
}
}
}

View File

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

View File

@@ -0,0 +1,75 @@
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Helper
{
public static class LyricsLayoutHelper
{
// 硬性限制
private const float BaseMinFontSize = 14f;
private const float BaseMaxFontSize = 80f;
private const float TargetMinVisibleLines = 5f;
private const float WidthPaddingRatio = 0.85f;
// 比例配置
private const float RatioSongTitle = 1f;
private const float RatioArtist = 0.85f;
private const float RatioAlbum = 0.75f;
private const float RatioTranslation = 0.7f;
private const float RatioTransliteration = 0.55f;
private const float AbsoluteMinReadableSize = 10f;
public static LyricsLayoutMetrics CalculateLayout(double width, double height)
{
float baseSize = CalculateBaseFontSize(width, height);
return new LyricsLayoutMetrics
{
MainLyricsSize = baseSize,
TranslationSize = ApplyRatio(baseSize, RatioTranslation),
TransliterationSize = ApplyRatio(baseSize, RatioTransliteration),
SongTitleSize = ApplyRatio(baseSize, RatioSongTitle),
ArtistNameSize = ApplyRatio(baseSize, RatioArtist),
AlbumNameSize = ApplyRatio(baseSize, RatioAlbum)
};
}
private static float CalculateBaseFontSize(double width, double height)
{
float usableWidth = (float)width * WidthPaddingRatio;
// 宽度 300~500px 时,除以 14 (字大)
// 宽度 >1000px 时,除以 30 (字适中,展示更多内容)
float targetCharsPerLine;
if (width < 500)
{
targetCharsPerLine = 14f;
}
else if (width > 1000)
{
targetCharsPerLine = 30f;
}
else
{
// 平滑过渡
float t = (float)(width - 500) / 500f;
targetCharsPerLine = 14f + 16f * t;
}
float sizeByWidth = usableWidth / targetCharsPerLine;
float sizeByHeight = (float)height / TargetMinVisibleLines;
float targetSize = Math.Min(sizeByWidth, sizeByHeight);
// 窄屏时底线设高一点 (16px),宽屏如果高度不够可能允许更小
float currentMinLimit = (width < 400) ? 16f : BaseMinFontSize;
return Math.Clamp(targetSize, currentMinLimit, BaseMaxFontSize);
}
private static float ApplyRatio(float baseSize, float ratio)
{
return Math.Max(baseSize * ratio, AbsoluteMinReadableSize);
}
}
}

View File

@@ -1,475 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using Lyricify.Lyrics.Parsers;
using NTextCat.Commons;
using System;
using System.Collections.Generic;
using System.Linq;
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(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;
}
}
FillRomanizationLyricsData();
FillTranslationFromCache(lyricsSearchResult);
}
private void FillTranslationFromCache(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 FillRomanizationLyricsData()
{
var chinese = LyricsDataArr.FirstOrDefault(x => x.LanguageCode == "zh");
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.FirstOrDefault(x => x.LanguageCode == "ja");
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, "").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());
}
// 初始化每种语言的歌词列表
//LyricsDataArr.Clear();
int langStartIndex = LyricsDataArr.Count;
for (int i = 0; i < languageCount; i++) LyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
if (grouped != null)
{
foreach (var group in grouped)
{
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
// 只添加有对应行的语言,否则跳过
if (langIdx < linesInGroup.Count)
{
var (start, text, syllables) = linesInGroup[langIdx];
var line = new LyricsLine
{
StartMs = start,
OriginalText = text,
LyricsChars = [],
};
if (syllables != null && syllables.Count > 0)
{
int currentIndex = 0;
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int startIndex = currentIndex;
line.LyricsChars.Add(
new LyricsChar
{
StartMs = charStart,
Text = charText ?? "",
StartIndex = startIndex,
}
);
currentIndex += charText?.Length ?? 0;
}
}
LyricsDataArr[langStartIndex + langIdx].LyricsLines.Add(line);
}
// 没有翻译行则不补原文,直接跳过
}
}
}
}
private void ParseTtml(string raw)
{
try
{
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 只获取一级span且排除 ttm:role="x-bg" 的 span 和 ttm:role="x-roman"
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-bg" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-roman")
.ToList();
// 原文和翻译分离
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-translation")
.ToList();
var translationTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
// 处理原文span后的空白
for (int i = 0; i < originalTextSpans.Count; i++)
{
var span = originalTextSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += textNode.Value;
}
}
// 拼接空白字符后的原文
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
var originalCharTimings = new List<LyricsChar>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
originalCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
originalText = p.Value;
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = originalText,
LyricsChars = originalCharTimings,
});
// 翻译
string translationText = string.Concat(translationTextSpans.Select(s => s.Value));
var translationCharTimings = new List<LyricsChar>();
int translationStartIndex = 0;
foreach (var span in translationTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
translationCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = translationStartIndex,
Text = span.Value
});
translationStartIndex += span.Value.Length;
}
if (translationTextSpans.Count > 0)
{
translationLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = translationText,
LyricsChars = translationCharTimings,
});
}
}
LyricsDataArr.Add(new LyricsData(originalLines));
if (translationLines.Count > 0)
LyricsDataArr.Add(new LyricsData(translationLines));
}
catch
{
// 解析失败,忽略
}
}
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
private void ParseQrcKrc(List<Lyricify.Lyrics.Models.ILineInfo>? lines)
{
lines = lines?.Where(x => x.Text != string.Empty).ToList();
List<LyricsLine> lyricsLines = [];
if (lines != null && lines.Count > 0)
{
lyricsLines = [];
for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
{
var lineRead = lines[lineIndex];
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
EndMs = lineRead.EndTime ?? 0,
OriginalText = lineRead.Text,
LyricsChars = [],
};
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;
if (syllables != null)
{
int startIndex = 0;
for (
int syllableIndex = 0;
syllableIndex < syllables.Count;
syllableIndex++
)
{
var syllable = syllables[syllableIndex];
var charTiming = new LyricsChar
{
StartMs = syllable.StartTime,
EndMs = syllable.EndTime,
Text = syllable.Text,
StartIndex = startIndex,
};
lineWrite.LyricsChars.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
lyricsLines.Add(lineWrite);
}
}
LyricsDataArr.Add(new LyricsData(lyricsLines));
}
[GeneratedRegex(@"\[(\d*):(\d*)(\.|\:)(\d*)\]")]
private static partial Regex LrcRegex();
[GeneratedRegex(@"(\[|\<)(\d*):(\d*)\.(\d*)(\]|\>)([^\[\]\<\>]*)")]
private static partial Regex SyllableRegex();
}
}

View File

@@ -1,54 +1,67 @@
using BetterLyrics.WinUI3.Models;
using F23.StringSimilarity;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class MetadataComparer
public static partial class MetadataComparer
{
// 权重配置 (总和 1.0)
private const double WeightTitle = 0.40;
private const double WeightArtist = 0.40;
private const double WeightAlbum = 0.10;
private const double WeightDuration = 0.10;
// 实例化算法 (JaroWinkler 适合短字符串匹配)
private static readonly JaroWinkler _algo = new JaroWinkler();
// JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new();
/// <summary>
/// 计算 SongInfo 和 LyricsSearchResult 的相似度 (0-100)
/// </summary>
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
{
if (local == null || remote == null) return 0;
// 1. 标题相似度
double titleScore = GetStringSimilarity(local.Title, remote.Title);
double totalScore = 0;
// 2. 艺术家相似度 (需要处理数组顺序)
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
bool localHasMetadata = !string.IsNullOrWhiteSpace(local.Title);
bool remoteHasMetadata = !string.IsNullOrWhiteSpace(remote.Title);
// 3. 专辑相似度
double albumScore = GetStringSimilarity(local.Album, remote.Album);
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);
// 4. 时长相似度 (基于毫秒 vs 秒的转换和容差)
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);
// 5. 加权汇总
double totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
(albumScore * WeightAlbum) +
(durationScore * WeightDuration);
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);
}
private static double GetStringSimilarity(string? s1, string? s2)
{
// 归一化:转小写,去空白
s1 = s1?.Trim().ToLowerInvariant() ?? "";
s2 = s2?.Trim().ToLowerInvariant() ?? "";
@@ -63,8 +76,7 @@ namespace BetterLyrics.WinUI3.Helper
if (localArtists == null || localArtists.Length == 0) return 0.0;
if (remoteArtists == null || remoteArtists.Length == 0) return 0.0;
// 技巧:将艺术家数组排序并连接,避免顺序不同导致的不匹配
// 例如: ["Jay-Z", "Linkin Park"] 和 ["Linkin Park", "Jay-Z"] 应该是一样的
// 将艺术家数组排序并连接,避免顺序不同导致的不匹配
var s1 = string.Join(" ", localArtists.OrderBy(a => a).Select(a => a.Trim().ToLowerInvariant()));
var s2 = string.Join(" ", remoteArtists.OrderBy(a => a).Select(a => a.Trim().ToLowerInvariant()));
@@ -78,7 +90,6 @@ namespace BetterLyrics.WinUI3.Helper
double localSeconds = localMs / 1000.0;
double diff = Math.Abs(localSeconds - remoteSeconds.Value);
// 容差逻辑:
// 差距 <= 3秒100% 相似
// 差距 >= 20秒0% 相似
// 中间线性插值
@@ -89,8 +100,24 @@ namespace BetterLyrics.WinUI3.Helper
if (diff <= PerfectTolerance) return 1.0;
if (diff >= MaxTolerance) return 0.0;
// 线性递减公式
return 1.0 - ((diff - PerfectTolerance) / (MaxTolerance - PerfectTolerance));
}
private static string CreateSortedFingerprint(string? input)
{
if (string.IsNullOrWhiteSpace(input)) return "";
input = input.ToLowerInvariant();
string cleaned = NonWordCharactersRegex().Replace(input, " ");
var tokens = cleaned.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.OrderBy(t => t); // 排序
return string.Join(" ", tokens);
}
[GeneratedRegex(@"[\p{P}\p{S}]")]
private static partial Regex NonWordCharactersRegex();
}
}

View File

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

View File

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

@@ -1,7 +1,6 @@
using BetterLyrics.WinUI3.Services.ResourceService;
using CommunityToolkit.Mvvm.DependencyInjection;
using System;
using System.Linq;
namespace BetterLyrics.WinUI3.Helper
{
@@ -9,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)
@@ -26,7 +25,7 @@ namespace BetterLyrics.WinUI3.Helper
return _resourceService.GetLocalizedString("Pinyin");
case JyutpingCode:
return _resourceService.GetLocalizedString("Jyutping");
case RomajiCode:
case RomanCode:
return _resourceService.GetLocalizedString("Romaji");
default:
throw new ArgumentOutOfRangeException(nameof(code));

View File

@@ -1,5 +1,4 @@
using BetterLyrics.WinUI3.Constants;
using Lyricify.Lyrics.Providers;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@@ -49,7 +48,8 @@ namespace BetterLyrics.WinUI3.Helper
PlayerID.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindows => PlayerName.SaltPlayerForWindows,
PlayerID.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerID.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1,
@@ -76,7 +76,8 @@ namespace BetterLyrics.WinUI3.Helper
PlayerID.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindows => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath,

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

View File

@@ -6,6 +6,7 @@ namespace BetterLyrics.WinUI3.Helper
{
public partial class SpectrumAnalyzer : IDisposable
{
private readonly object _lock = new();
private WasapiLoopbackCapture? _capture;
private int _sampleRate = 48000;
@@ -28,7 +29,7 @@ namespace BetterLyrics.WinUI3.Helper
private float[]? _currentSpectrum;
public float[]? SmoothSpectrum { get; private set; }
public int BarCount { get; set; } = 16;
public int BarCount { get; set; } = 64;
public int Sensitivity { get; set; } = 100;
public float SmoothingFactor { get; set; } = 0.95f;
public bool IsCapturing { get; private set; } = false;
@@ -121,12 +122,15 @@ namespace BetterLyrics.WinUI3.Helper
Array.Copy(_spectrumRightData, 0, _spectrumData, _spectrumLeftData.Length, _spectrumRightData.Length);
}
for (int i = 0; i < BarCount; i++)
lock (_lock)
{
int index = (int)((float)i / BarCount * _spectrumData.Length);
if (index < _spectrumData.Length)
for (int i = 0; i < BarCount; i++)
{
_currentSpectrum[i] = _spectrumData[index] * 250f * Sensitivity;
int index = (int)((float)i / BarCount * _spectrumData.Length);
if (index < _spectrumData.Length)
{
_currentSpectrum[i] = _spectrumData[index] * 250f * Sensitivity;
}
}
}
@@ -139,10 +143,13 @@ namespace BetterLyrics.WinUI3.Helper
return;
}
for (int i = 0; i < BarCount; i++)
lock (_lock)
{
SmoothSpectrum[i] = SmoothSpectrum[i] * SmoothingFactor +
_currentSpectrum[i] * (1 - SmoothingFactor);
for (int i = 0; i < BarCount; i++)
{
SmoothSpectrum[i] = SmoothSpectrum[i] * SmoothingFactor +
_currentSpectrum[i] * (1 - SmoothingFactor);
}
}
}

View File

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

View File

@@ -24,8 +24,10 @@ namespace BetterLyrics.WinUI3.Helper
public bool IsTransitioning => _isTransitioning;
public T Value => _currentValue;
public T StartValue => _startValue;
public T TargetValue => _targetValue;
public EasingType? EasingType => _easingType;
public double Progress => _progress;
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null, double delaySeconds = 0)
{
@@ -57,7 +59,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 +147,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 +195,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 +204,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

@@ -1,14 +1,9 @@
using DevWinUI;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Vanara.Windows.Shell;

View File

@@ -14,7 +14,6 @@ using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Vanara.PInvoke;
using Vanara.Windows.Shell;
using Windows.ApplicationModel.Core;
using Windows.Foundation;
using WinRT.Interop;
@@ -44,7 +43,7 @@ namespace BetterLyrics.WinUI3.Hooks
public static void CloseWindow<T>()
{
if (typeof(T) == typeof(LyricsWindow))
if (typeof(T) == typeof(NowPlayingWindow))
{
EnsureDockModeReleased();
}
@@ -56,6 +55,15 @@ namespace BetterLyrics.WinUI3.Hooks
}
}
public static void MinimizeWindow<T>()
{
var window = _activeWindows.Find(w => w is T);
if (window is Window w)
{
w.Minimize();
}
}
public static T? GetWindow<T>()
{
foreach (var window in _activeWindows)
@@ -94,10 +102,10 @@ namespace BetterLyrics.WinUI3.Hooks
var window = _activeWindows.Find(w => w is T);
if (window == null)
{
if (typeof(T) == typeof(LyricsWindow))
if (typeof(T) == typeof(NowPlayingWindow))
{
window = new LyricsWindow();
((LyricsWindow)window).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
window = new NowPlayingWindow();
((NowPlayingWindow)window).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
}
else if (typeof(T) == typeof(SettingsWindow))
{
@@ -119,12 +127,13 @@ namespace BetterLyrics.WinUI3.Hooks
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
TrackWindow(window);
var castedWindow = (Window)window;
castedWindow.Restore();
castedWindow.Activate();
if (typeof(T) == typeof(LyricsWindow))
if (typeof(T) == typeof(NowPlayingWindow))
{
_liveStatesService.InitLyricsWindowStatus();
@@ -132,7 +141,7 @@ namespace BetterLyrics.WinUI3.Hooks
_defaultWindowStyle.Add(hwnd, castedWindow.GetWindowStyle());
_defaultExtendedWindowStyle.Add(hwnd, castedWindow.GetExtendedWindowStyle());
var lyricsWindow = (LyricsWindow)window;
var lyricsWindow = (NowPlayingWindow)window;
lyricsWindow.ViewModel.InitShortcuts();
lyricsWindow.ViewModel.InitFgWindowWatcher();
@@ -186,7 +195,7 @@ namespace BetterLyrics.WinUI3.Hooks
private static void EnsureDockModeReleased()
{
SetIsWorkArea<LyricsWindow>(false);
SetIsWorkArea<NowPlayingWindow>(false);
}
private static void TrackWindow(object window)
@@ -292,9 +301,9 @@ namespace BetterLyrics.WinUI3.Hooks
public static void SetTitleBarArea<T>(TitleBarArea titleBarArea)
{
if (typeof(T) == typeof(LyricsWindow))
if (typeof(T) == typeof(NowPlayingWindow))
{
LyricsWindow? lyricsWindow = GetWindow<LyricsWindow>();
NowPlayingWindow? lyricsWindow = GetWindow<NowPlayingWindow>();
lyricsWindow?.SetTitleBarArea(titleBarArea);
}
else
@@ -395,7 +404,7 @@ namespace BetterLyrics.WinUI3.Hooks
_setLyricsWindowVisibilityByPlayingStatusTimer.Debounce(() =>
{
var window = GetWindow<LyricsWindow>();
var window = GetWindow<NowPlayingWindow>();
if (window == null) return;
if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && !_mediaSessionsService.CurrentIsPlaying)
@@ -403,23 +412,23 @@ namespace BetterLyrics.WinUI3.Hooks
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
{
_liveStatesService.LiveStates.IsLyricsWindowStatusRefreshing = true;
SetIsWorkArea<LyricsWindow>(false);
SetIsWorkArea<NowPlayingWindow>(false);
_liveStatesService.LiveStates.IsLyricsWindowStatusRefreshing = false;
}
HideWindow<LyricsWindow>();
HideWindow<NowPlayingWindow>();
}
else if (_liveStatesService.LiveStates.LyricsWindowStatus.AutoShowOrHideWindow && _mediaSessionsService.CurrentIsPlaying)
{
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
{
_liveStatesService.LiveStates.IsLyricsWindowStatusRefreshing = true;
SetIsWorkArea<LyricsWindow>(true);
SetIsWorkArea<NowPlayingWindow>(true);
_liveStatesService.LiveStates.IsLyricsWindowStatusRefreshing = false;
}
OpenOrShowWindow<LyricsWindow>();
OpenOrShowWindow<NowPlayingWindow>();
if (_liveStatesService.LiveStates.LyricsWindowStatus.IsWorkArea)
{
MoveAndResize<LyricsWindow>(_liveStatesService.LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea());
MoveAndResize<NowPlayingWindow>(_liveStatesService.LiveStates.LyricsWindowStatus.GetWindowBoundsWhenWorkArea());
}
}
}, Constants.Time.DebounceTimeout);

View File

@@ -0,0 +1,127 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using System;
using System.Linq;
using Windows.UI;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsAnimator
{
private readonly double _defaultScale = 0.75f;
private readonly double _highlightedScale = 1.0f;
public void UpdateLines(
LyricsData? lyricsData,
int startIndex,
int endIndex,
int playingLineIndex,
double canvasHeight,
double targetYScrollOffset,
LyricsEffectSettings lyricsEffect,
ValueTransition<double> canvasYScrollTransition,
Color bgColor,
Color fgColor,
TimeSpan elapsedTime,
bool isMouseScrolling,
bool isLayoutChanged,
bool isPlayingLineChanged,
bool isMouseScrollingChanged
)
{
if (lyricsData == null) return;
var currentPlayingLine = lyricsData.LyricsLines.ElementAtOrDefault(playingLineIndex);
if (currentPlayingLine == null) return;
for (int i = startIndex; i <= endIndex + 1; i++)
{
var line = lyricsData.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
if (isLayoutChanged || isPlayingLineChanged || isMouseScrollingChanged)
{
int lineCountDelta = i - playingLineIndex;
int absLineCountDelta = Math.Abs(lineCountDelta);
double distanceFromPlayingLine = Math.Abs(line.OriginalPosition.Y - currentPlayingLine.OriginalPosition.Y);
double distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight / 2), 0, 1);
double yScrollDuration;
double yScrollDelay;
if (lineCountDelta < 0)
{
yScrollDuration =
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollTopDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollTopDelay / 1000.0;
}
else if (lineCountDelta == 0)
{
yScrollDuration = canvasYScrollTransition.DurationSeconds;
yScrollDelay = 0;
}
else
{
yScrollDuration =
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollBottomDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollBottomDelay / 1000.0;
}
line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.StartTransition(isMouseScrolling ? 0 : (5 * distanceFactor));
line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale));
line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
line.PhoneticOpacityTransition.StartTransition(absLineCountDelta == 0 ? 0.6 : (isMouseScrolling ? 0.3 : (1 - distanceFactor) * 0.3));
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.PlayedOriginalOpacityTransition.StartTransition(absLineCountDelta == 0 ? 1 : (isMouseScrolling ? 0.3 : (1 - distanceFactor) * 0.3));
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.UnplayedOriginalOpacityTransition.StartTransition(absLineCountDelta == 0 ? 0.3 : (isMouseScrolling ? 0.3 : (1 - distanceFactor) * 0.3));
line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
line.TranslatedOpacityTransition.StartTransition(absLineCountDelta == 0 ? 0.6 : (isMouseScrolling ? 0.3 : (1 - distanceFactor) * 0.3));
line.ColorTransition.SetDuration(yScrollDuration);
line.ColorTransition.SetDelay(yScrollDelay);
line.ColorTransition.StartTransition(absLineCountDelta == 0 ? fgColor : bgColor);
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.StartTransition(lyricsEffect.IsFanLyricsEnabled ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) : 0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay);
// 设计之初是当 isLayoutChanged 为真时 jumpTo
// 但考虑到动画视觉,强制使用动画
line.YOffsetTransition.StartTransition(targetYScrollOffset);
}
line.AngleTransition.Update(elapsedTime);
line.ScaleTransition.Update(elapsedTime);
line.BlurAmountTransition.Update(elapsedTime);
line.PhoneticOpacityTransition.Update(elapsedTime);
line.PlayedOriginalOpacityTransition.Update(elapsedTime);
line.UnplayedOriginalOpacityTransition.Update(elapsedTime);
line.TranslatedOpacityTransition.Update(elapsedTime);
line.YOffsetTransition.Update(elapsedTime);
line.ColorTransition.Update(elapsedTime);
}
}
}
}

View File

@@ -0,0 +1,254 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsLayoutManager
{
/// <summary>
/// 重排歌词Y 轴从 0 刻度开始算
/// </summary>
/// <param name="resourceCreator"></param>
/// <param name="lyricsData"></param>
/// <param name="status"></param>
/// <param name="appSettings"></param>
/// <param name="canvasWidth"></param>
/// <param name="canvasHeight"></param>
/// <param name="lyricsWidth"></param>
/// <param name="lyricsHeight"></param>
public void MeasureAndArrange(
ICanvasAnimatedControl resourceCreator,
LyricsData? lyricsData,
LyricsWindowStatus status,
AppSettings appSettings,
double canvasWidth,
double canvasHeight,
double lyricsWidth,
double lyricsHeight)
{
if (lyricsData == null || resourceCreator == null) return;
// 计算字体大小
int originalFontSize, phoneticFontSize, translatedFontSize;
var style = status.LyricsStyleSettings;
if (style.IsDynamicLyricsFontSize)
{
var lyricsLayoutMetrics = LyricsLayoutHelper.CalculateLayout(canvasWidth, canvasHeight);
phoneticFontSize = (int)lyricsLayoutMetrics.TransliterationSize;
originalFontSize = (int)lyricsLayoutMetrics.MainLyricsSize;
translatedFontSize = (int)lyricsLayoutMetrics.TranslationSize;
}
else
{
phoneticFontSize = style.PhoneticLyricsFontSize;
originalFontSize = style.OriginalLyricsFontSize;
translatedFontSize = style.TranslatedLyricsFontSize;
}
var fontWeight = style.LyricsFontWeight;
// 排版
double currentY = 0;
double actualWidth = 0;
foreach (var line in lyricsData.LyricsLines)
{
if (line == null) continue;
line.RecreateTextLayout(
resourceCreator,
appSettings.TranslationSettings.IsChineseRomanizationEnabled || appSettings.TranslationSettings.IsJapaneseRomanizationEnabled,
appSettings.TranslationSettings.IsTranslationEnabled,
phoneticFontSize, originalFontSize, translatedFontSize,
fontWeight,
style.LyricsCJKFontFamily, style.LyricsWesternFontFamily,
lyricsWidth, lyricsHeight, style.LyricsAlignmentType
);
line.RecreateTextGeometry();
// 左上角坐标
line.TopLeftPosition = new Vector2(0, (float)currentY);
// 注音层
line.PhoneticPosition = line.TopLeftPosition;
if (line.PhoneticCanvasTextLayout != null)
{
currentY += line.PhoneticCanvasTextLayout.LayoutBounds.Height;
// 间距
currentY += (line.PhoneticCanvasTextLayout.LayoutBounds.Height / line.PhoneticCanvasTextLayout.LineCount) * 0.1;
actualWidth = Math.Max(actualWidth, line.PhoneticCanvasTextLayout.LayoutBounds.Width);
}
// 原文层
line.OriginalPosition = new Vector2(0, (float)currentY);
if (line.OriginalCanvasTextLayout != null)
{
currentY += line.OriginalCanvasTextLayout.LayoutBounds.Height;
actualWidth = Math.Max(actualWidth, line.OriginalCanvasTextLayout.LayoutBounds.Width);
}
// 翻译层
if (line.TranslatedCanvasTextLayout != null)
{
// 间距
currentY += (line.TranslatedCanvasTextLayout.LayoutBounds.Height / line.TranslatedCanvasTextLayout.LineCount) * 0.1;
}
line.TranslatedPosition = new Vector2(0, (float)currentY);
if (line.TranslatedCanvasTextLayout != null)
{
currentY += line.TranslatedCanvasTextLayout.LayoutBounds.Height;
actualWidth = Math.Max(actualWidth, line.TranslatedCanvasTextLayout.LayoutBounds.Width);
}
// 右下角坐标
line.BottomRightPosition = new Vector2(0 + (float)actualWidth, (float)currentY);
// 行间距
if (line.OriginalCanvasTextLayout != null)
{
currentY += (line.OriginalCanvasTextLayout.LayoutBounds.Height / line.OriginalCanvasTextLayout.LineCount) * style.LyricsLineSpacingFactor;
}
// 更新中心点
line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType);
}
}
/// <summary>
/// 计算为了让当前歌词行的竖直几何中心点对齐到 0原点画布应该移动的距离从画布最初始状态计算的值
/// </summary>
public double? CalculateTargetScrollOffset(
LyricsData? lyricsData,
int playingLineIndex)
{
var lines = lyricsData?.LyricsLines;
if (lines == null || lines.Count == 0) return null;
var currentLine = lines.ElementAtOrDefault(playingLineIndex);
var firstLine = lines.FirstOrDefault();
if (currentLine?.OriginalCanvasTextLayout == null || firstLine == null) return null;
return -currentLine.OriginalPosition.Y
+ firstLine.OriginalPosition.Y
- (currentLine.TranslatedPosition.Y
+ (currentLine.TranslatedCanvasTextLayout?.LayoutBounds.Height ?? 0)
- currentLine.PhoneticPosition.Y) / 2.0;
}
/// <summary>
/// 计算当前屏幕可见的行范围
/// 返回值: (StartVisibleIndex, EndVisibleIndex)
/// </summary>
public (int Start, int End) CalculateVisibleRange(
IList<LyricsLine>? lines,
double currentScrollOffset,
double lyricsY,
double lyricsHeight,
double canvasHeight)
{
if (lines == null || lines.Count == 0) return (-1, -1);
double offset = currentScrollOffset + lyricsY + lyricsHeight / 2;
int start = FindFirstVisibleLine(lines, offset, lyricsY);
int end = FindLastVisibleLine(lines, offset, lyricsY, lyricsHeight, canvasHeight);
// 修正边界情况
if (start != -1 && end == -1)
{
end = lines.Count - 1;
}
return (start, end);
}
public (int Start, int End) CalculateMaxRange(IList<LyricsLine>? lines)
{
if (lines == null || lines.Count == 0) return (-1, -1);
return (0, lines.Count - 1);
}
public double CalculateActualHeight(IList<LyricsLine>? lines)
{
if (lines == null || lines.Count == 0) return 0;
return lines.Last().BottomRightPosition.Y;
}
public int FindMouseHoverLineIndex(
IList<LyricsLine>? lines,
bool isMouseInLyricsArea,
Point mousePosition,
double currentScrollOffset,
double lyricsY,
double lyricsHeight
)
{
if (!isMouseInLyricsArea) return -1;
if (lines == null || lines.Count == 0) return -1;
double offset = currentScrollOffset + lyricsY + lyricsHeight / 2;
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y;
if (value >= mousePosition.Y) { result = mid; right = mid - 1; }
else { left = mid + 1; }
}
return result;
}
private int FindFirstVisibleLine(IList<LyricsLine> lines, double offset, double lyricsY)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y;
// 理论上说应该使用下面这一行来精确计算视野内的首个可见行,但是考虑到动画视觉效果,还是注释掉了
//if (value >= lyricsY) { result = mid; right = mid - 1; }
if (value >= 0) { result = mid; right = mid - 1; }
else { left = mid + 1; }
}
return result;
}
private int FindLastVisibleLine(IList<LyricsLine> lines, double offset, double lyricsY, double lyricsHeight, double canvasHeight)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y;
// 同理
//if (value >= lyricsY + lyricsHeight) { result = mid; right = mid - 1; }
if (value >= canvasHeight) { result = mid; right = mid - 1; }
else { left = mid + 1; }
}
return result;
}
}
}

View File

@@ -0,0 +1,153 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Logic
{
public class LyricsSynchronizer
{
private int _lastFoundIndex = 0;
public void Reset()
{
_lastFoundIndex = 0;
}
public int GetCurrentLineIndex(double currentTimeMs, LyricsData? lyricsData)
{
if (lyricsData == null || lyricsData.LyricsLines.Count == 0) return 0;
var lines = lyricsData.LyricsLines;
// Cache hit
if (IsTimeInLine(currentTimeMs, lines, _lastFoundIndex)) return _lastFoundIndex;
if (_lastFoundIndex + 1 < lines.Count && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex + 1))
{
_lastFoundIndex++;
return _lastFoundIndex;
}
// Cache miss
for (int i = 0; i < lines.Count; i++)
{
if (IsTimeInLine(currentTimeMs, lines, i))
{
_lastFoundIndex = i;
return i;
}
}
// Default
return Math.Min(_lastFoundIndex, lines.Count - 1);
}
public LinePlaybackState GetLinePlayingProgress(
double currentTimeMs,
LyricsLine line,
LyricsLine? nextLine,
double songDurationMs,
bool isForceWordByWord)
{
var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 };
if (line == null) return state;
double lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = songDurationMs;
// 还没到
if (currentTimeMs < line.StartMs) return state;
// 过了
if (currentTimeMs > lineEndMs)
{
state.SyllableProgress = 1f;
state.SyllableStartIndex = Math.Max(0, line.OriginalText.Length - 1);
state.SyllableLength = 1;
return state;
}
// 逐字
if (line.LyricsSyllables != null && line.LyricsSyllables.Count > 1)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
// 强制逐字
if (isForceWordByWord && line.OriginalText.Length > 0)
{
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs);
}
else
{
// 普通行
state.SyllableStartIndex = line.OriginalText.Length;
state.SyllableProgress = 1f;
return state;
}
}
private LinePlaybackState CalculateSyllableProgress(double time, LyricsLine line, double lineEndMs)
{
var state = new LinePlaybackState();
int count = line.LyricsSyllables.Count;
for (int i = 0; i < count; i++)
{
var timing = line.LyricsSyllables[i];
var nextTiming = (i + 1 < count) ? line.LyricsSyllables[i + 1] : null;
double timingEndMs = timing.EndMs ?? nextTiming?.StartMs ?? lineEndMs;
// 在当前字范围内
if (time >= timing.StartMs && time <= timingEndMs)
{
state.SyllableStartIndex = timing.StartIndex;
state.SyllableLength = timing.Text.Length;
state.SyllableProgress = (timingEndMs > timing.StartMs)
? (time - timing.StartMs) / (timingEndMs - timing.StartMs)
: 0;
return state;
}
// 在空隙中 (已过当前字,未到下个字)
else if (time > timingEndMs && (nextTiming == null || time < nextTiming.StartMs))
{
state.SyllableProgress = 1f; // 保持上个字满进度
state.SyllableStartIndex = timing.StartIndex;
state.SyllableLength = timing.Text.Length;
return state;
}
}
return state;
}
private LinePlaybackState CalculateSimulatedProgress(double time, LyricsLine line, double lineEndMs)
{
var state = new LinePlaybackState();
int textLength = line.OriginalText.Length;
double progress = (time - line.StartMs) / (lineEndMs - line.StartMs);
progress = Math.Clamp(progress, 0, 1);
double charFloatIndex = progress * textLength;
int charIndex = (int)charFloatIndex;
state.SyllableStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
state.SyllableLength = 1;
state.SyllableProgress = charFloatIndex - charIndex;
return state;
}
private bool IsTimeInLine(double time, IList<LyricsLine> lines, int index)
{
if (index < 0 || index >= lines.Count) return false;
var line = lines[index];
var nextLine = (index + 1 < lines.Count) ? lines[index + 1] : null;
if (time < line.StartMs) return false;
if (nextLine != null && time >= nextLine.StartMs) return false;
return true;
}
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.UI.Xaml;
using Windows.UI;
namespace BetterLyrics.WinUI3.Models
{
public struct AlbumArtThemeColors
{
public Color BgFontColor;
public Color FgFontColor;
public Color StrokeFontColor;
public Color EnvColor;
public Color AccentColor1;
public Color AccentColor2;
public Color AccentColor3;
public Color AccentColor4;
public ElementTheme ThemeType;
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Models
{
public class ExtendedFontFamily
{
public string FontFamily { get; set; } = "";
public string LocalizedFontFamily { get; set; } = "";
}
}

View File

@@ -0,0 +1,9 @@
namespace BetterLyrics.WinUI3.Models
{
public struct LinePlaybackState
{
public int SyllableStartIndex;
public int SyllableLength;
public double SyllableProgress;
}
}

View File

@@ -17,8 +17,9 @@ 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);
public bool IsWordByWord => LyricsLines.Any(x => x.LyricsSyllables.Count != 0);
public LyricsData()
{
@@ -46,7 +47,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
public void SetTranslatedText(LyricsData translationData, string separator, int toleranceMs = 0)
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 0)
{
foreach (var line in LyricsLines)
{
@@ -67,7 +68,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
public void SetPhoneticText(LyricsData phoneticData, string separator, int toleranceMs = 0)
public void SetPhoneticText(LyricsData phoneticData, int toleranceMs = 0)
{
foreach (var line in LyricsLines)
{
@@ -88,7 +89,7 @@ namespace BetterLyrics.WinUI3.Models
}
}
public void SetTranslation(string translation, string separator)
public void SetTranslation(string translation)
{
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
@@ -130,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"),
}]);
}
@@ -148,10 +158,7 @@ namespace BetterLyrics.WinUI3.Models
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PhoneticText = "",
OriginalText = "● ● ●",
TranslatedText = "",
LyricsChars = [],
},
]);
}

View File

@@ -0,0 +1,17 @@
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Models
{
public struct LyricsLayoutMetrics
{
public float MainLyricsSize;
public float TranslationSize;
public float TransliterationSize;
public float SongTitleSize;
public float ArtistNameSize;
public float AlbumNameSize;
public Thickness AlbumArtPadding;
}
}

View File

@@ -6,64 +6,57 @@ using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System.Collections.Generic;
using System.Numerics;
using Windows.UI;
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> PhoneticOpacityTransition { get; set; }
public ValueTransition<double> PlayedOriginalOpacityTransition { get; set; }
public ValueTransition<double> UnplayedOriginalOpacityTransition { get; set; }
public ValueTransition<double> TranslatedOpacityTransition { get; set; }
public ValueTransition<double> ScaleTransition { get; set; }
public ValueTransition<double> YOffsetTransition { get; set; }
public ValueTransition<Color> ColorTransition { get; set; }
public CanvasTextLayout? OriginalCanvasTextLayout { get; private set; }
public CanvasTextLayout? TranslatedCanvasTextLayout { get; private set; }
public CanvasTextLayout? PhoneticCanvasTextLayout { get; private set; }
public Vector2 CenterPosition { get; private set; }
/// <summary>
/// 原文位置
/// 原文坐标(相对于坐标原点)
/// </summary>
public Vector2 OriginalPosition { get; set; }
/// <summary>
/// 译文位置
/// 译文坐标(相对于坐标原点)
/// </summary>
public Vector2 TranslatedPosition { get; set; }
/// <summary>
/// 注音位置
/// 注音坐标(相对于坐标原点)
/// </summary>
public Vector2 PhoneticPosition { get; set; }
public List<LyricsChar> LyricsChars { get; set; } = [];
/// <summary>
/// 顶部坐标(相对于坐标原点)
/// </summary>
public Vector2 TopLeftPosition { get; set; }
/// <summary>
/// 中心坐标(相对于坐标原点)
/// </summary>
public Vector2 CenterPosition { get; private set; }
/// <summary>
/// 底部坐标(相对于坐标原点)
/// </summary>
public Vector2 BottomRightPosition { get; set; }
public List<LyricsSyllable> LyricsSyllables { get; set; } = [];
public int? DurationMs => EndMs - StartMs;
public int? EndMs { get; set; }
@@ -86,6 +79,55 @@ 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.EaseInOutSine
);
BlurAmountTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
PhoneticOpacityTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
PlayedOriginalOpacityTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
UnplayedOriginalOpacityTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
TranslatedOpacityTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
ScaleTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
YOffsetTransition = new(
initialValue: 0,
durationSeconds: AnimationDuration,
easingType: EasingType.EaseInOutSine
);
ColorTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
}
public void UpdateCenterPosition(double maxWidth, TextAlignmentType type)
{
if (OriginalCanvasTextLayout == null)
@@ -93,13 +135,13 @@ namespace BetterLyrics.WinUI3.Models
return;
}
double centerY = OriginalPosition.Y + (OriginalCanvasTextLayout?.LayoutBounds.Height ?? 0) / 2;
double centerY = (TopLeftPosition.Y + BottomRightPosition.Y) / 2;
CenterPosition = type switch
{
TextAlignmentType.Left => new Vector2(OriginalPosition.X, (float)centerY),
TextAlignmentType.Center => new Vector2((float)(OriginalPosition.X + maxWidth / 2.0), (float)centerY),
TextAlignmentType.Right => new Vector2((float)(OriginalPosition.X + maxWidth), (float)centerY),
TextAlignmentType.Left => new Vector2(0, (float)centerY),
TextAlignmentType.Center => new Vector2((float)(0 + maxWidth / 2.0), (float)centerY),
TextAlignmentType.Right => new Vector2((float)(0 + maxWidth), (float)centerY),
_ => throw new System.ArgumentOutOfRangeException(nameof(type), type, 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;
@@ -10,8 +11,17 @@ namespace BetterLyrics.WinUI3.Models
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[]? Artists { get; set; }
public string? Album { get; set; }
@@ -19,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

@@ -2,11 +2,13 @@
namespace BetterLyrics.WinUI3.Models
{
public class LyricsChar
public class LyricsSyllable
{
public int? EndMs { get; set; }
public int StartIndex { get; set; }
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty;
public int? DurationMs => EndMs - StartMs;
public bool IsLongDuration => DurationMs >= 700;
}
}

View File

@@ -35,6 +35,7 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial WindowPixelSampleMode EnvironmentSampleMode { get; set; } = WindowPixelSampleMode.WindowEdge;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool AutoShowOrHideWindow { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TitleBarArea TitleBarArea { get; set; } = TitleBarArea.Top;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double WindowX { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double WindowY { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double WindowWidth { get; set; } = 800;
@@ -102,7 +103,7 @@ namespace BetterLyrics.WinUI3.Models
public void UpdateMonitorNameAndBounds()
{
var lyricsWindow = WindowHook.GetWindow<LyricsWindow>();
var lyricsWindow = WindowHook.GetWindow<NowPlayingWindow>();
if (lyricsWindow == null) return;
var mointor = MonitorHook.GetMonitorInfoExFromWindow(lyricsWindow);
@@ -191,11 +192,13 @@ namespace BetterLyrics.WinUI3.Models
EnvironmentSampleMode = this.EnvironmentSampleMode,
AutoShowOrHideWindow = this.AutoShowOrHideWindow,
TitleBarArea = this.TitleBarArea,
WindowX = this.WindowX,
WindowY = this.WindowY,
WindowWidth = this.WindowWidth,
WindowHeight = this.WindowHeight,
};
}
}
}

View File

@@ -1,17 +1,11 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Collections;
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
@@ -38,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

@@ -7,15 +7,15 @@ namespace BetterLyrics.WinUI3.Models.Settings
public partial class AlbumArtLayoutSettings : ObservableRecipient, ICloneable
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TextAlignmentType SongInfoAlignmentType { get; set; } = TextAlignmentType.Left;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverImageRadius { get; set; } = 12; // 12 % of the cover image size
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverImageShadowAmount { get; set; } = 12;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAutoSongInfoFontSize { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SongInfoFontSize { get; set; } = 18;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ShowTitle { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ShowArtists { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ShowAlbum { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int AlbumArtSize { get; set; } = 64;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool AutoAlbumArtSize { get; set; } = true;
public AlbumArtLayoutSettings() { }
@@ -31,8 +31,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
ShowTitle = this.ShowTitle,
ShowArtists = this.ShowArtists,
ShowAlbum = this.ShowAlbum,
AlbumArtSize = this.AlbumArtSize,
AutoAlbumArtSize = this.AutoAlbumArtSize,
};
}
}

View File

@@ -12,18 +12,14 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PureColorOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsCoverOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayBlurAmount { get; set; } = 100; // 100 % of the cover image size
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlaySpeed { get; set; } = 50; // 50 % of the base rotate speed
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverAcrylicEffectAmount { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FluidOverlayOpacity { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.MedianCut;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsSpectrumOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SpectrumPlacement SpectrumPlacement { get; set; } = SpectrumPlacement.Bottom;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SpectrumStyle SpectrumStyle { get; set; } = SpectrumStyle.Bar;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SpectrumCount { get; set; } = 128;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsSnowFlakeOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SnowFlakeOverlayAmount { get; set; } = 10;
@@ -43,12 +39,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
IsPureColorOverlayEnabled = this.IsPureColorOverlayEnabled,
PureColorOverlayOpacity = this.PureColorOverlayOpacity,
IsCoverOverlayEnabled = this.IsCoverOverlayEnabled,
CoverOverlayBlurAmount = this.CoverOverlayBlurAmount,
CoverOverlayOpacity = this.CoverOverlayOpacity,
CoverOverlaySpeed = this.CoverOverlaySpeed,
CoverAcrylicEffectAmount = this.CoverAcrylicEffectAmount,
IsFluidOverlayEnabled = this.IsFluidOverlayEnabled,
FluidOverlayOpacity = this.FluidOverlayOpacity,
PaletteGeneratorType = this.PaletteGeneratorType,

View File

@@ -6,25 +6,9 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class LyricsEffectSettings : ObservableRecipient, ICloneable
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsBlurAmount { get; set; } = 5;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsLineFadeEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsGlowEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LineRenderingType LyricsGlowEffectScope { get; set; } = LineRenderingType.CurrentChar;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsGlowEffectAmount { get; set; } = 8;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsShadowEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LineRenderingType LyricsShadowScope { get; set; } = LineRenderingType.LineStartToCurrentChar;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsShadowAmount { get; set; } = 8;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LineRenderingType OriginalLyricsHighlightScope { get; set; } = LineRenderingType.LineStartToCurrentChar;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PhoneticLyricsHighlightAmount { get; set; } = 60; // 100% 是上界
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int OriginalLyricsHighlightAmount { get; set; } = 100; // 100% 是上界
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int TranslatedLyricsHighlightAmount { get; set; } = 60; // 100% 是上界
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsScaleEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsFloatAnimationEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsFloatAmount { get; set; } = 1;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial EasingType LyricsScrollEasingType { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollDuration { get; set; }
@@ -33,8 +17,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollTopDelay { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollBottomDelay { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsVerticalEdgeOpacity { get; set; } = 0; // 0% opacity
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFanLyricsEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FanLyricsAngle { get; set; } = 30;
@@ -56,25 +38,9 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
return new LyricsEffectSettings(this.LyricsScrollTopDuration, this.LyricsScrollDuration, this.LyricsScrollBottomDuration, this.LyricsScrollEasingType)
{
LyricsBlurAmount = this.LyricsBlurAmount,
IsLyricsLineFadeEnabled = this.IsLyricsLineFadeEnabled,
IsLyricsGlowEffectEnabled = this.IsLyricsGlowEffectEnabled,
LyricsGlowEffectScope = this.LyricsGlowEffectScope,
LyricsGlowEffectAmount = this.LyricsGlowEffectAmount,
IsLyricsShadowEnabled = this.IsLyricsShadowEnabled,
LyricsShadowScope = this.LyricsShadowScope,
LyricsShadowAmount = this.LyricsShadowAmount,
OriginalLyricsHighlightScope = this.OriginalLyricsHighlightScope,
PhoneticLyricsHighlightAmount = this.PhoneticLyricsHighlightAmount,
OriginalLyricsHighlightAmount = this.OriginalLyricsHighlightAmount,
TranslatedLyricsHighlightAmount = this.TranslatedLyricsHighlightAmount,
IsLyricsScaleEffectEnabled = this.IsLyricsScaleEffectEnabled,
IsLyricsFloatAnimationEnabled = this.IsLyricsFloatAnimationEnabled,
LyricsFloatAmount = this.LyricsFloatAmount,
LyricsScrollEasingType = this.LyricsScrollEasingType,
LyricsScrollDuration = this.LyricsScrollDuration,
@@ -83,8 +49,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
LyricsScrollTopDelay = this.LyricsScrollTopDelay,
LyricsScrollBottomDelay = this.LyricsScrollBottomDelay,
LyricsVerticalEdgeOpacity = this.LyricsVerticalEdgeOpacity,
IsFanLyricsEnabled = this.IsFanLyricsEnabled,
FanLyricsAngle = this.FanLyricsAngle,

View File

@@ -15,7 +15,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int OriginalLyricsFontSize { get; set; } = 24;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int TranslatedLyricsFontSize { get; set; } = 12;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TextAlignmentType LyricsAlignmentType { get; set; } = TextAlignmentType.Left;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsBgFontOpacity { get; set; } = 30; // 30% opacity
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsFontStrokeWidth { get; set; } = 0;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Color LyricsCustomBgFontColor { get; set; } = Colors.White;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Color LyricsCustomFgFontColor { get; set; } = Colors.White;
@@ -25,7 +24,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsFontColorType LyricsStrokeFontColorType { get; set; } = LyricsFontColorType.AdaptiveGrayed;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsFontWeight LyricsFontWeight { get; set; } = LyricsFontWeight.Bold;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsLineSpacingFactor { get; set; } = 0.5;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsTranslationSeparator { get; set; } = StringHelper.NewLine;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsCJKFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsWesternFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
@@ -40,7 +38,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
OriginalLyricsFontSize = this.OriginalLyricsFontSize,
TranslatedLyricsFontSize = this.TranslatedLyricsFontSize,
LyricsAlignmentType = this.LyricsAlignmentType,
LyricsBgFontOpacity = this.LyricsBgFontOpacity,
LyricsFontStrokeWidth = this.LyricsFontStrokeWidth,
LyricsCustomBgFontColor = this.LyricsCustomBgFontColor,
LyricsCustomFgFontColor = this.LyricsCustomFgFontColor,
@@ -50,7 +47,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
LyricsStrokeFontColorType = this.LyricsStrokeFontColorType,
LyricsFontWeight = this.LyricsFontWeight,
LyricsLineSpacingFactor = this.LyricsLineSpacingFactor,
LyricsTranslationSeparator = this.LyricsTranslationSeparator,
LyricsCJKFontFamily = this.LyricsCJKFontFamily,
LyricsWesternFontFamily = this.LyricsWesternFontFamily,
};

View File

@@ -0,0 +1,114 @@
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, bool single)
{
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines = new List<LyricsLine>();
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
var syllableRegex = SyllableRegex();
foreach (var line in lines)
{
var matches = syllableRegex.Matches(line);
var syllables = new List<LyricsSyllable>();
int startIndex = 0;
for (int i = 0; i < matches.Count; i++)
{
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 = match.Groups[6].Value;
syllables.Add(new LyricsSyllable { StartMs = totalMs, Text = text, StartIndex = startIndex });
startIndex += text.Length;
}
if (syllables.Count > 1)
{
lrcLines.Add(new LyricsLine
{
StartMs = syllables[0].StartMs,
OriginalText = string.Concat(syllables.Select(s => s.Text)),
LyricsSyllables = syllables
});
}
else
{
// 普通LRC行
Regex? bracketRegex = LrcRegex();
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 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(new LyricsLine { StartMs = lineStartTime.Value, OriginalText = content });
}
}
}
if (single)
{
LyricsDataArr.Add(new LyricsData(lrcLines));
}
else
{
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.StartMs).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 lyricsLine = linesInGroup[langIdx];
LyricsDataArr[langStartIndex + langIdx].LyricsLines.Add(lyricsLine);
}
// 没有翻译行则不补原文,直接跳过
}
}
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
using BetterLyrics.WinUI3.Models;
using System.Collections.Generic;
using System.Linq;
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
private void ParseQrcKrc(List<Lyricify.Lyrics.Models.ILineInfo>? lines)
{
lines = lines?.Where(x => x.Text != string.Empty).ToList();
List<LyricsLine> lyricsLines = [];
if (lines != null && lines.Count > 0)
{
lyricsLines = [];
for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
{
var lineRead = lines[lineIndex];
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
EndMs = lineRead.EndTime ?? 0,
OriginalText = lineRead.Text,
LyricsSyllables = [],
};
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 LyricsSyllable
{
StartMs = syllable.StartTime,
EndMs = syllable.EndTime,
Text = syllable.Text,
StartIndex = startIndex,
};
lineWrite.LyricsSyllables.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
lyricsLines.Add(lineWrite);
}
}
LyricsDataArr.Add(new LyricsData(lyricsLines));
}
}
}

View File

@@ -0,0 +1,202 @@
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 originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == null)
.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<LyricsSyllable>();
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 LyricsSyllable
{
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,
LyricsSyllables = 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<LyricsSyllable>();
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 LyricsSyllable
{
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,
LyricsSyllables = charTimings,
});
}
}
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
}
}

View File

@@ -0,0 +1,174 @@
// 2025/6/23 by Zhe Fang
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;
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 (string.IsNullOrWhiteSpace(lyricsSearchResult?.Raw))
{
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder());
}
else
{
switch (lyricsSearchResult.Raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(lyricsSearchResult.Raw, lyricsSearchResult.Provider.IsRemote());
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;
}
if (LyricsDataArr.Count == 0)
{
LyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder());
}
}
LoadTranslation(lyricsSearchResult);
LoadTransliteration(lyricsSearchResult);
GenerateTransliterationLyricsData();
}
private void LoadTranslation(LyricsSearchResult? lyricsSearchResult)
{
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Translation))
{
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.QQ:
case LyricsSearchProvider.Kugou:
case LyricsSearchProvider.Netease:
ParseLrc(lyricsSearchResult.Translation, true);
break;
default:
break;
}
}
}
private void LoadTransliteration(LyricsSearchResult? lyricsSearchResult)
{
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Transliteration))
{
switch (lyricsSearchResult.Provider)
{
case LyricsSearchProvider.Netease:
ParseLrc(lyricsSearchResult.Transliteration, true);
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),
LyricsSyllables = line.LyricsSyllables.Select(c => new LyricsSyllable
{
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),
LyricsSyllables = line.LyricsSyllables.Select(c => new LyricsSyllable
{
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),
LyricsSyllables = line.LyricsSyllables.Select(c => new LyricsSyllable
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = PhoneticHelper.ToRomaji(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using System;
using System.Net;
using System.Net.Http;
@@ -67,7 +69,7 @@ namespace BetterLyrics.WinUI3.Providers
_client.DefaultRequestHeaders.Add("Accept-Language", $"{_language},en;q=0.9");
}
public async Task<string?> GetLyricsAsync(string id)
private async Task<string?> GetLyricsAsync(string id)
{
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/songs/{id}";
var url = apiUrl + $"?include[songs]=lyrics,syllable-lyrics&l={_language}";
@@ -109,9 +111,14 @@ namespace BetterLyrics.WinUI3.Providers
return null;
}
public 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);
@@ -120,9 +127,26 @@ namespace BetterLyrics.WinUI3.Providers
if (results.TryGetProperty("songs", out var songs) && songs.GetProperty("data").GetArrayLength() > 0)
{
var song = songs.GetProperty("data")[0];
return song.GetProperty("id").ToString();
var id = song.GetProperty("id").ToString();
var attr = song.GetProperty("attributes");
lyricsSearchResult.Title = attr.GetProperty("name").ToString();
lyricsSearchResult.Artists = attr.GetProperty("artistName").ToString().SplitByCommonSplitter();
lyricsSearchResult.Album = attr.GetProperty("albumName").ToString();
lyricsSearchResult.Duration = attr.GetProperty("durationInMillis").GetInt32() / 1000.0;
lyricsSearchResult.Reference = $"https://music.apple.com/song/{id}";
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
if (id != null)
{
lyricsSearchResult.Raw = await GetLyricsAsync(id);
}
}
return string.Empty;
return lyricsSearchResult;
}
}
}

View File

@@ -0,0 +1,110 @@
using BetterLyrics.WinUI3.Extensions;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public partial class FluidBackgroundRenderer : IDisposable
{
private PixelShaderEffect? _fluidEffect;
private float _timeAccumulator = 0f;
private Vector3 _c1, _c2, _c3, _c4;
public bool IsEnabled { get; set; } = false;
public double Opacity { get; set; } = 1.0;
public bool EnableLightWave { get; set; } = false;
public async Task LoadResourcesAsync()
{
Dispose();
try
{
var uri = new Uri("ms-appx:///Assets/FluidEffect.bin");
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(uri);
using (var stream = await file.OpenReadAsync())
{
var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
await stream.ReadAsync(buffer, (uint)stream.Size, Windows.Storage.Streams.InputStreamOptions.None);
byte[] bytes = buffer.ToArray();
_fluidEffect = new PixelShaderEffect(bytes);
_fluidEffect.Properties["EnableLightWave"] = EnableLightWave;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[FluidRenderer] Load Failed: {ex.Message}");
_fluidEffect = null;
}
}
public void UpdateColors(Color c1, Color c2, Color c3, Color c4)
{
_c1 = c1.ToVector3RGB();
_c2 = c2.ToVector3RGB();
_c3 = c3.ToVector3RGB();
_c4 = c4.ToVector3RGB();
}
public void Update(TimeSpan deltaTime)
{
if (_fluidEffect == null || !IsEnabled) return;
_timeAccumulator += (float)deltaTime.TotalSeconds;
_fluidEffect.Properties["iTime"] = _timeAccumulator;
_fluidEffect.Properties["color1"] = _c1;
_fluidEffect.Properties["color2"] = _c2;
_fluidEffect.Properties["color3"] = _c3;
_fluidEffect.Properties["color4"] = _c4;
_fluidEffect.Properties["EnableLightWave"] = EnableLightWave;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_fluidEffect == null || !IsEnabled || Opacity <= 0) return;
float pixelWidth = control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
float pixelHeight = control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_fluidEffect.Properties["Width"] = pixelWidth;
_fluidEffect.Properties["Height"] = pixelHeight;
if (Opacity >= 1.0)
{
ds.DrawImage(_fluidEffect);
}
else
{
using (var opacityEffect = new OpacityEffect
{
Source = _fluidEffect,
Opacity = (float)Opacity
})
{
ds.DrawImage(opacityEffect);
}
}
}
public void Dispose()
{
_fluidEffect?.Dispose();
_fluidEffect = null;
}
}
}

View File

@@ -0,0 +1,49 @@
using BetterLyrics.WinUI3.Shaders;
using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
namespace BetterLyrics.WinUI3.Renderer
{
public partial class FogRenderer : IDisposable
{
private PixelShaderEffect<FogEffect>? _fogEffect;
private float _timeAccumulator = 0f;
public bool IsEnabled { get; set; } = false;
public void LoadResources()
{
Dispose();
_fogEffect = new PixelShaderEffect<FogEffect>();
}
public void Update(double deltaTime)
{
if (_fogEffect == null || !IsEnabled) return;
_timeAccumulator += (float)deltaTime;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_fogEffect == null || !IsEnabled) return;
float width = control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
float height = control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_fogEffect.ConstantBuffer = new FogEffect(
_timeAccumulator,
new float2(width, height)
);
ds.DrawImage(_fogEffect);
}
public void Dispose()
{
_fogEffect?.Dispose();
_fogEffect = null;
}
}
}

View File

@@ -0,0 +1,258 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Linq;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class LyricsRenderer
{
private readonly PlayingLineRenderer _playingRenderer = new();
private readonly UnplayingLineRenderer _unplayingRenderer = new();
private Matrix4x4 _threeDimMatrix = Matrix4x4.Identity;
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
LyricsData? lyricsData,
int playingLineIndex,
int mouseHoverLineIndex,
bool isMousePressing,
int startVisibleIndex,
int endVisibleIndex,
double lyricsX,
double lyricsY,
double lyricsWidth,
double lyricsHeight,
double userScrollOffset,
double lyricsOpacity,
LyricsWindowStatus windowStatus,
Color strokeColor,
Color bgColor,
Color fgColor,
Func<int, LinePlaybackState> getPlaybackState)
{
using (var opacityLayer = ds.CreateLayer((float)lyricsOpacity))
{
if (windowStatus.LyricsEffectSettings.Is3DLyricsEnabled)
{
using (var layer = new CanvasCommandList(control))
{
using (var layerDs = layer.CreateDrawingSession())
{
DrawLyrics(
control,
layerDs,
lyricsData,
playingLineIndex,
mouseHoverLineIndex,
isMousePressing,
startVisibleIndex,
endVisibleIndex,
lyricsX,
lyricsY,
lyricsWidth,
lyricsHeight,
userScrollOffset,
windowStatus,
strokeColor,
bgColor,
fgColor,
getPlaybackState);
}
ds.DrawImage(new Transform3DEffect
{
Source = layer,
TransformMatrix = _threeDimMatrix
});
}
}
else
{
DrawLyrics(
control,
ds,
lyricsData,
playingLineIndex,
mouseHoverLineIndex,
isMousePressing,
startVisibleIndex,
endVisibleIndex,
lyricsX,
lyricsY,
lyricsWidth,
lyricsHeight,
userScrollOffset,
windowStatus,
strokeColor,
bgColor,
fgColor,
getPlaybackState);
}
}
}
private void DrawLyrics(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
LyricsData? lyricsData,
int playingLineIndex,
int mouseHoverLineIndex,
bool isMousePressing,
int startVisibleIndex,
int endVisibleIndex,
double lyricsX,
double lyricsY,
double lyricsWidth,
double lyricsHeight,
double userScrollOffset,
LyricsWindowStatus windowStatus,
Color strokeColor,
Color bgColor,
Color fgColor,
Func<int, LinePlaybackState> getPlaybackState)
{
if (lyricsData == null) return;
var currentPlayingLine = lyricsData.LyricsLines.ElementAtOrDefault(playingLineIndex);
if (currentPlayingLine == null) return;
var effectSettings = windowStatus.LyricsEffectSettings;
var styleSettings = windowStatus.LyricsStyleSettings;
var rotationY = currentPlayingLine.OriginalPosition.WithX(effectSettings.FanLyricsAngle < 0 ? (float)lyricsWidth : 0);
for (int i = startVisibleIndex; i <= endVisibleIndex; i++)
{
var line = lyricsData.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
if (line.OriginalCanvasTextLayout == null) continue;
if (line.OriginalCanvasTextLayout.LayoutBounds.Width <= 0) continue;
double yOffset = line.YOffsetTransition.Value + userScrollOffset + lyricsY + lyricsHeight / 2;
var transform =
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition) *
Matrix3x2.CreateRotation((float)line.AngleTransition.Value, rotationY) *
Matrix3x2.CreateTranslation((float)lyricsX, (float)yOffset);
ds.Transform = transform;
using (var textOnlyLayer = RenderBaseTextLayer(control, line, styleSettings.LyricsFontStrokeWidth, strokeColor, line.ColorTransition.Value))
{
if (i == playingLineIndex)
{
var state = getPlaybackState(i);
_playingRenderer.Draw(control, ds, textOnlyLayer, line, state, bgColor, fgColor, effectSettings);
}
else
{
_unplayingRenderer.Draw(ds, textOnlyLayer, line);
}
if (i == mouseHoverLineIndex)
{
byte opacity = isMousePressing ? (byte)32 : (byte)16;
double scale = isMousePressing ? 1.09 : 1.10;
ds.FillRoundedRectangle(
new Windows.Foundation.Rect(line.TopLeftPosition.ToPoint(), line.BottomRightPosition.ToPoint()).Scale(scale),
8, 8, Color.FromArgb(opacity, 255, 255, 255));
}
}
ds.Transform = Matrix3x2.Identity;
}
}
private CanvasCommandList RenderBaseTextLayer(
ICanvasResourceCreator resourceCreator,
LyricsLine line,
double strokeWidth,
Color strokeColor,
Color fillColor)
{
var commandList = new CanvasCommandList(resourceCreator);
using (var clds = commandList.CreateDrawingSession())
{
if (strokeWidth > 0)
{
DrawGeometrySafely(clds, line.PhoneticCanvasGeometry, line.PhoneticPosition, strokeColor, strokeWidth);
DrawGeometrySafely(clds, line.OriginalCanvasGeometry, line.OriginalPosition, strokeColor, strokeWidth);
DrawGeometrySafely(clds, line.TranslatedCanvasGeometry, line.TranslatedPosition, strokeColor, strokeWidth);
}
DrawTextLayoutSafely(clds, line.PhoneticCanvasTextLayout, line.PhoneticPosition, fillColor);
DrawTextLayoutSafely(clds, line.OriginalCanvasTextLayout, line.OriginalPosition, fillColor);
DrawTextLayoutSafely(clds, line.TranslatedCanvasTextLayout, line.TranslatedPosition, fillColor);
}
return commandList;
}
private void DrawGeometrySafely(CanvasDrawingSession ds, CanvasGeometry? geo, Vector2 pos, Color color, double width)
{
if (geo == null) return;
try
{
ds.DrawGeometry(geo, pos, color, (float)width);
}
catch (Exception) { }
}
private void DrawTextLayoutSafely(CanvasDrawingSession ds, CanvasTextLayout? layout, Vector2 pos, Color color)
{
if (layout == null) return;
try
{
ds.DrawTextLayout(layout, pos, color);
}
catch (Exception) { }
}
public void CalculateLyrics3DMatrix(LyricsEffectSettings lyricsEffect, double lyricsX, double lyricsY, double lyricsWidth, double canvasHeight)
{
if (!lyricsEffect.Is3DLyricsEnabled) return;
Vector3 center = new(
(float)(lyricsX + lyricsWidth / 2),
(float)(lyricsY + canvasHeight / 2),
0);
float rotationX = (float)(Math.PI * lyricsEffect.Lyrics3DXAngle / 180.0);
float rotationY = (float)(Math.PI * lyricsEffect.Lyrics3DYAngle / 180.0);
float rotationZ = (float)(Math.PI * lyricsEffect.Lyrics3DZAngle / 180.0);
Matrix4x4 rotation =
Matrix4x4.CreateRotationX(rotationX) *
Matrix4x4.CreateRotationY(rotationY) *
Matrix4x4.CreateRotationZ(rotationZ);
Matrix4x4 perspective = Matrix4x4.Identity;
perspective.M34 = 1.0f / lyricsEffect.Lyrics3DDepth;
// 组合变换:
// 1. 将中心移到原点
// 2. 旋转
// 3. 应用透视
// 4. 将中心移回原位
_threeDimMatrix =
Matrix4x4.CreateTranslation(-center) *
rotation *
perspective *
Matrix4x4.CreateTranslation(center);
}
}
}

View File

@@ -1,40 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Renderer
{
public sealed partial class LyricsRenderer : UserControl
{
public LyricsRendererViewModel ViewModel { get; set; }
public LyricsRenderer()
{
InitializeComponent();
ViewModel = Ioc.Default.GetRequiredService<LyricsRendererViewModel>();
}
private void LyricsCanvas_Draw(Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedDrawEventArgs args)
{
ViewModel.Draw(sender, args.DrawingSession);
}
private void LyricsCanvas_Update(Microsoft.Graphics.Canvas.UI.Xaml.ICanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedUpdateEventArgs args)
{
ViewModel.Update(sender, args);
}
private void LyricsCanvas_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
LyricsCanvas.RemoveFromVisualTree();
LyricsCanvas = null;
}
private void LyricsCanvas_CreateResources(Microsoft.Graphics.Canvas.UI.Xaml.CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
{
ViewModel.CreateResources(sender, args);
}
}
}

View File

@@ -0,0 +1,303 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class PlayingLineRenderer
{
public void Draw(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
ICanvasImage textOnlyLayer,
LyricsLine line,
LinePlaybackState playbackState,
Color bgColor,
Color fgColor,
LyricsEffectSettings settings)
{
DrawPhonetic(ds, textOnlyLayer, line);
DrawOriginalText(control, ds, textOnlyLayer, line, playbackState, bgColor, fgColor, settings);
DrawTranslated(ds, textOnlyLayer, line);
}
private void DrawPhonetic(CanvasDrawingSession ds, ICanvasImage source, LyricsLine line)
{
if (line.PhoneticCanvasTextLayout == null) return;
var opacity = line.PhoneticOpacityTransition.Value;
var blur = line.BlurAmountTransition.Value;
var bounds = line.PhoneticCanvasTextLayout.LayoutBounds;
var destRect = new Rect(
bounds.X + line.PhoneticPosition.X,
bounds.Y + line.PhoneticPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new OpacityEffect
{
Source = new GaussianBlurEffect
{
BlurAmount = (float)blur,
Source = new CropEffect
{
Source = source,
BorderMode = EffectBorderMode.Hard,
SourceRectangle = destRect,
},
BorderMode = EffectBorderMode.Soft
},
Opacity = (float)opacity,
});
}
private void DrawTranslated(CanvasDrawingSession ds, ICanvasImage source, LyricsLine line)
{
if (line.TranslatedCanvasTextLayout == null) return;
var opacity = line.TranslatedOpacityTransition.Value;
var blur = line.BlurAmountTransition.Value;
var bounds = line.TranslatedCanvasTextLayout.LayoutBounds;
var destRect = new Rect(
bounds.X + line.TranslatedPosition.X,
bounds.Y + line.TranslatedPosition.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new OpacityEffect
{
Source = new GaussianBlurEffect
{
BlurAmount = (float)blur,
Source = new CropEffect
{
Source = source,
BorderMode = EffectBorderMode.Hard,
SourceRectangle = destRect,
},
BorderMode = EffectBorderMode.Soft
},
Opacity = (float)opacity,
});
}
private void DrawOriginalText(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
ICanvasImage source,
LyricsLine line,
LinePlaybackState state,
Color bgColor,
Color fgColor,
LyricsEffectSettings settings)
{
if (line.OriginalCanvasTextLayout == null) return;
var curCharIndex = state.SyllableStartIndex + state.SyllableLength * state.SyllableProgress;
float fadeWidth = (1f / Math.Max(1, line.OriginalText.Length)) * 0.5f;
var lineRegions = line.OriginalCanvasTextLayout.GetCharacterRegions(0, line.OriginalText.Length);
foreach (var subLineRegion in lineRegions)
{
DrawSubLineRegion(resourceCreator, ds, source, line, subLineRegion, curCharIndex, fadeWidth, bgColor, fgColor, state, settings);
}
}
private void DrawSubLineRegion(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
ICanvasImage source,
LyricsLine line,
CanvasTextLayoutRegion subLineRegion,
double curCharIndex,
float fadeWidth,
Color bgColor,
Color fgColor,
LinePlaybackState state,
LyricsEffectSettings settings)
{
var blur = line.BlurAmountTransition.Value;
var playedOpacity = line.PlayedOriginalOpacityTransition.Value;
var unplayedOpacity = line.UnplayedOriginalOpacityTransition.Value;
var subLineLayoutBounds = subLineRegion.LayoutBounds;
Rect subLineRect = new(
subLineLayoutBounds.X + line.OriginalPosition.X,
subLineLayoutBounds.Y + line.OriginalPosition.Y,
subLineLayoutBounds.Width,
subLineLayoutBounds.Height
);
using (var gradientLayer = new CanvasCommandList(resourceCreator))
{
using (var gradientLayerDs = gradientLayer.CreateDrawingSession())
{
float progressInRegion = (float)((curCharIndex - subLineRegion.CharacterIndex) / subLineRegion.CharacterCount);
progressInRegion = Math.Clamp(progressInRegion, 0, 1 + fadeWidth);
var stop1 = fgColor.WithAlpha((byte)(255 * playedOpacity));
var stop2 = bgColor.WithAlpha((byte)(255 * unplayedOpacity));
using (var gradientBrush = new CanvasLinearGradientBrush(resourceCreator,
[
new CanvasGradientStop { Position = 0, Color = stop1 },
new CanvasGradientStop { Position = progressInRegion, Color = stop1 },
// 这里做判断是防止子行未播放时左侧出现渐变的问题
new CanvasGradientStop { Position = progressInRegion == 0 ? 0 : (progressInRegion + fadeWidth), Color = stop2 },
new CanvasGradientStop { Position = 1 + fadeWidth, Color = stop2 }
]))
{
gradientBrush.StartPoint = new Vector2((float)subLineRect.X, (float)subLineRect.Y);
gradientBrush.EndPoint = new Vector2((float)(subLineRect.X + subLineRect.Width), (float)subLineRect.Y);
gradientLayerDs.FillRectangle(subLineRect, gradientBrush);
}
}
// 这里 gradientLayer 上色的时候已经限制了 Rect 区域,不用再套一个 CropEffect
using (var textWithOpacityLayer = new AlphaMaskEffect
{
Source = source,
AlphaMask = gradientLayer
})
{
if (!settings.IsLyricsFloatAnimationEnabled && !settings.IsLyricsGlowEffectEnabled && !settings.IsLyricsScaleEffectEnabled)
{
ds.DrawImage(textWithOpacityLayer);
}
else
{
int endCharIndex = subLineRegion.CharacterIndex + subLineRegion.CharacterCount;
for (int i = subLineRegion.CharacterIndex; i < endCharIndex; i++)
{
DrawSingleCharacter(ds, line, i, curCharIndex, textWithOpacityLayer, state, settings);
}
}
}
}
}
private void DrawSingleCharacter(
CanvasDrawingSession ds,
LyricsLine line,
int charIndex,
double exactProgressIndex,
ICanvasImage source,
LinePlaybackState state,
LyricsEffectSettings settings)
{
var curCharIndexInt = (int)Math.Floor(exactProgressIndex);
if (line.OriginalCanvasTextLayout == null) return;
var charRegions = line.OriginalCanvasTextLayout.GetCharacterRegions(charIndex, 1);
if (charRegions.Length == 0) return;
var charRegion = charRegions[0];
var charLayoutBounds = charRegion.LayoutBounds;
var sourceCharRect = new Rect(
charLayoutBounds.X + line.OriginalPosition.X,
charLayoutBounds.Y + line.OriginalPosition.Y,
charLayoutBounds.Width,
charLayoutBounds.Height
);
double floatOffset = 0;
double scale = 1;
double glow = 0;
bool drawGlow = false;
if (settings.IsLyricsFloatAnimationEnabled)
{
double targetFloatOffset = sourceCharRect.Height * 0.1;
// 已经浮完了的
if (charIndex < curCharIndexInt)
{
floatOffset = 0;
}
// 正在浮的
else if (charIndex == curCharIndexInt)
{
var p = exactProgressIndex - curCharIndexInt;
floatOffset = -targetFloatOffset + p * targetFloatOffset;
}
// 还没浮的
else
{
floatOffset = -targetFloatOffset;
}
// 制造句间上浮过度动画,这里用任何一个 Transition 都行,主要是获取当前行的进入视野的 Progress
floatOffset *= line.YOffsetTransition.Progress;
}
var parentSyllable = line.LyricsSyllables.FirstOrDefault(x => x.StartIndex <= charIndex && charIndex < x.StartIndex + x.Text.Length);
if (parentSyllable != null && parentSyllable.IsLongDuration && parentSyllable.StartIndex == state.SyllableStartIndex)
{
if (settings.IsLyricsScaleEffectEnabled)
{
scale += Math.Sin(state.SyllableProgress * Math.PI) * 0.15;
}
if (settings.IsLyricsGlowEffectEnabled)
{
glow = Math.Sin(state.SyllableProgress * Math.PI) * sourceCharRect.Height * 0.2;
drawGlow = true;
}
}
var destCharRect = sourceCharRect.Scale(scale).AddY(-floatOffset);
if (drawGlow)
{
var sourcePlayedCharRect = new Rect(
sourceCharRect.X,
sourceCharRect.Y,
sourceCharRect.Width,
sourceCharRect.Height
);
if (charIndex == curCharIndexInt)
{
var p = exactProgressIndex - curCharIndexInt;
sourcePlayedCharRect.Width *= p;
}
else if (charIndex > curCharIndexInt)
{
sourcePlayedCharRect.Width = 0;
}
using (var glowEffect = new GaussianBlurEffect
{
Source = new CropEffect
{
Source = source,
SourceRectangle = sourcePlayedCharRect,
BorderMode = EffectBorderMode.Hard
},
BlurAmount = (float)glow,
BorderMode = EffectBorderMode.Soft
})
{
ds.DrawImage(glowEffect, destCharRect.Extend(sourceCharRect.Height), sourceCharRect.Extend(sourceCharRect.Height));
}
}
ds.DrawImage(source, destCharRect, sourceCharRect);
}
}
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Extensions;
using Microsoft.Graphics.Canvas;
using Windows.Foundation;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public class PureColorBackgroundRenderer
{
public void Draw(
CanvasDrawingSession ds,
Rect bounds,
Color color,
double opacity,
bool isEnabled)
{
if (!isEnabled || opacity <= 0) return;
ds.FillRectangle(
bounds,
color.WithAlpha((byte)(opacity * 255))
);
}
}
}

View File

@@ -0,0 +1,53 @@
using BetterLyrics.WinUI3.Shaders;
using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
namespace BetterLyrics.WinUI3.Renderer
{
public partial class SnowRenderer : IDisposable
{
private PixelShaderEffect<SnowEffect>? _snowEffect;
private float _timeAccumulator = 0f;
public bool IsEnabled { get; set; } = false;
public float Amount { get; set; } = 0.5f;
public float Speed { get; set; } = 1.0f;
public void LoadResources()
{
Dispose();
_snowEffect = new PixelShaderEffect<SnowEffect>();
}
public void Update(double deltaTime)
{
if (_snowEffect == null || !IsEnabled) return;
_timeAccumulator += (float)deltaTime;
}
public void Draw(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_snowEffect == null || !IsEnabled) return;
float width = control.ConvertDipsToPixels((float)control.Size.Width, CanvasDpiRounding.Round);
float height = control.ConvertDipsToPixels((float)control.Size.Height, CanvasDpiRounding.Round);
_snowEffect.ConstantBuffer = new SnowEffect(
_timeAccumulator,
new float2(width, height),
Amount, // 0.0 ~ 1.0
Speed
);
ds.DrawImage(_snowEffect);
}
public void Dispose()
{
_snowEffect?.Dispose();
_snowEffect = null;
}
}
}

View File

@@ -0,0 +1,195 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.UI;
using System;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.Renderer
{
public partial class SpectrumRenderer : IDisposable
{
private CanvasGeometry? _spectrumGeometry;
public void Draw(
ICanvasResourceCreator resourceCreator,
CanvasDrawingSession ds,
float[]? spectrumData,
int barCount,
bool isEnabled,
SpectrumPlacement placement,
SpectrumStyle style,
double canvasWidth,
double canvasHeight,
Color fillColor
)
{
_spectrumGeometry?.Dispose();
_spectrumGeometry = null;
if (!isEnabled || spectrumData == null || spectrumData.Length == 0) return;
_spectrumGeometry = CreateGeometry(resourceCreator, spectrumData, barCount, placement, style, canvasWidth, canvasHeight);
if (_spectrumGeometry != null)
{
DrawGeometry(ds, _spectrumGeometry, fillColor, placement, canvasHeight);
}
}
private CanvasGeometry? CreateGeometry(
ICanvasResourceCreator creator,
float[] data,
int barCount,
SpectrumPlacement placement,
SpectrumStyle style,
double width,
double height)
{
if (barCount < 2) return null;
float maxDataVal = 0;
int checkCount = Math.Min(barCount, data.Length);
for (int i = 0; i < checkCount; i++)
{
if (data[i] > maxDataVal) maxDataVal = data[i];
}
float limitY = (float)height * 0.2f; // 高度限制为总高度的 20%
float scaleRatio = 1.0f;
if (maxDataVal > limitY)
{
scaleRatio = limitY / maxDataVal;
}
using var pathBuilder = new CanvasPathBuilder(creator);
if (style == SpectrumStyle.Bar)
{
float totalStep = (float)width / barCount;
float gap = 2.0f;
float barWidth = totalStep - gap;
if (barWidth < 1.0f) { barWidth = totalStep; gap = 0f; }
for (int i = 0; i < barCount; i++)
{
float rawVal = i < data.Length ? data[i] : 0;
float barHeight = rawVal * scaleRatio;
if (barHeight < 0.5f) continue;
float x = i * totalStep;
float topY, bottomY;
if (placement == SpectrumPlacement.Top)
{
topY = 0;
bottomY = barHeight;
}
else // Bottom
{
topY = (float)height - barHeight;
bottomY = (float)height;
}
// 绘制独立矩形
pathBuilder.BeginFigure(new Vector2(x, topY));
pathBuilder.AddLine(new Vector2(x + barWidth, topY));
pathBuilder.AddLine(new Vector2(x + barWidth, bottomY));
pathBuilder.AddLine(new Vector2(x, bottomY));
pathBuilder.EndFigure(CanvasFigureLoop.Closed);
}
}
else
{
var points = new Vector2[barCount];
float pointSpacing = (float)width / (barCount - 1);
for (int i = 0; i < barCount; i++)
{
float rawVal = i < data.Length ? data[i] : 0;
float y = rawVal * scaleRatio;
// 处理翻转
if (placement == SpectrumPlacement.Bottom)
{
y = (float)height - y;
}
points[i] = new Vector2(i * pointSpacing, y);
}
// 绘制曲线
pathBuilder.BeginFigure(points[0]);
for (int i = 0; i < barCount - 1; i++)
{
Vector2 p0 = points[Math.Max(i - 1, 0)];
Vector2 p1 = points[i];
Vector2 p2 = points[i + 1];
Vector2 p3 = points[Math.Min(i + 2, barCount - 1)];
Vector2 cp1 = p1 + (p2 - p0) / 6.0f;
Vector2 cp2 = p2 - (p3 - p1) / 6.0f;
pathBuilder.AddCubicBezier(cp1, cp2, p2);
}
// 封口
if (placement == SpectrumPlacement.Top)
{
pathBuilder.AddLine(new Vector2(points[barCount - 1].X, 0));
pathBuilder.AddLine(new Vector2(points[0].X, 0));
}
else
{
pathBuilder.AddLine(new Vector2(points[barCount - 1].X, (float)height));
pathBuilder.AddLine(new Vector2(points[0].X, (float)height));
}
pathBuilder.EndFigure(CanvasFigureLoop.Closed);
}
return CanvasGeometry.CreatePath(pathBuilder);
}
private void DrawGeometry(
CanvasDrawingSession ds,
CanvasGeometry geometry,
Color color,
SpectrumPlacement placement,
double height)
{
var stops = new CanvasGradientStop[]
{
new() { Position = 0.0f, Color = Colors.Transparent },
new() { Position = 0.7f, Color = Colors.Transparent },
new() { Position = 1.0f, Color = color }
};
using var brush = new CanvasLinearGradientBrush(ds, stops);
if (placement == SpectrumPlacement.Top)
{
brush.StartPoint = new Vector2(0, (float)height);
brush.EndPoint = new Vector2(0, 0);
}
else
{
brush.StartPoint = new Vector2(0, 0);
brush.EndPoint = new Vector2(0, (float)height);
}
ds.FillGeometry(geometry, brush);
}
public void Dispose()
{
_spectrumGeometry?.Dispose();
_spectrumGeometry = null;
}
}
}

View File

@@ -0,0 +1,94 @@
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using System.Numerics;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Renderer
{
public class UnplayingLineRenderer
{
public void Draw(
CanvasDrawingSession ds,
ICanvasImage textOnlyLayer,
LyricsLine line)
{
var blurAmount = (float)line.BlurAmountTransition.Value;
if (line.PhoneticCanvasTextLayout != null)
{
var opacity = line.PhoneticOpacityTransition.Value;
DrawPart(ds, textOnlyLayer,
line.PhoneticCanvasTextLayout,
line.PhoneticPosition,
blurAmount,
(float)opacity);
}
if (line.OriginalCanvasTextLayout != null)
{
double opacity;
if (line.PlayedOriginalOpacityTransition.StartValue > line.UnplayedOriginalOpacityTransition.StartValue)
{
opacity = line.PlayedOriginalOpacityTransition.Value;
}
else
{
opacity = line.UnplayedOriginalOpacityTransition.Value;
}
DrawPart(ds, textOnlyLayer,
line.OriginalCanvasTextLayout,
line.OriginalPosition,
blurAmount,
(float)opacity);
}
if (line.TranslatedCanvasTextLayout != null)
{
var opacity = line.TranslatedOpacityTransition.Value;
DrawPart(ds, textOnlyLayer,
line.TranslatedCanvasTextLayout,
line.TranslatedPosition,
blurAmount,
(float)opacity);
}
}
private void DrawPart(
CanvasDrawingSession ds,
ICanvasImage source,
CanvasTextLayout layout,
Vector2 position,
float blur,
float opacity)
{
if (opacity <= 0) return;
var bounds = layout.LayoutBounds;
var destRect = new Rect(
bounds.X + position.X,
bounds.Y + position.Y,
bounds.Width,
bounds.Height
);
ds.DrawImage(new OpacityEffect
{
Source = new GaussianBlurEffect
{
BlurAmount = blur,
Source = new CropEffect
{
Source = source,
SourceRectangle = destRect,
BorderMode = EffectBorderMode.Hard,
},
BorderMode = EffectBorderMode.Soft
},
Opacity = opacity
});
}
}
}

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

@@ -47,7 +47,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
}
catch (Exception)
{
DevWinUI.Growl.Error(_resourceService.GetLocalizedString("LastFMAuthFailed") ?? "");
ToastHelper.ShowToast("LastFMAuthFailed", null, InfoBarSeverity.Error);
}
}

View File

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

View File

@@ -18,10 +18,8 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Documents;
namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
@@ -42,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();
@@ -146,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;
}
}
}
}
@@ -205,8 +213,6 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
try
{
LyricsFormat lyricsFormat = provider.GetLyricsFormat();
// Check cache first if allowed
if (checkCache && provider.IsRemote())
{
@@ -218,47 +224,40 @@ 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
{
lyricsSearchResult = await SearchFile(songInfo, lyricsFormat);
}
}
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.LocalLrcFile:
case LyricsSearchProvider.LocalEslrcFile:
case LyricsSearchProvider.LocalTtmlFile:
lyricsSearchResult = await SearchFile(songInfo, provider.GetLyricsFormat());
break;
case LyricsSearchProvider.AppleMusic:
lyricsSearchResult = await SearchAppleMusicAsync(songInfo);
break;
default:
break;
}
if (token.IsCancellationRequested)
{
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
return lyricsSearchResult;
}
@@ -267,14 +266,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
}
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
if (lyricsSearchResult.IsFound)
if (provider.IsRemote())
{
if (provider.IsRemote())
{
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
}
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
}
return lyricsSearchResult;
@@ -282,6 +276,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private async Task<LyricsSearchResult> SearchFile(SongInfo songInfo, LyricsFormat format)
{
int maxScore = 0;
string? bestFile = null;
var lyricsSearchResult = new LyricsSearchResult();
if (format.ToLyricsSearchProvider() is LyricsSearchProvider lyricsSearchProvider)
@@ -297,18 +294,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
{
var fileName = Path.GetFileNameWithoutExtension(file);
if (StringHelper.IsSwitchableNormalizedMatch(fileName, songInfo.Title, songInfo.DisplayArtists) || songInfo.LinkedFileName == fileName)
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = file });
if (score > maxScore)
{
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
{
lyricsSearchResult.Raw = raw;
lyricsSearchResult.CopyFromSongInfo(songInfo);
lyricsSearchResult.Reference = file;
return lyricsSearchResult;
}
bestFile = file;
maxScore = score;
}
}
}
@@ -317,11 +307,28 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
}
if (bestFile != null)
{
lyricsSearchResult.Reference = bestFile;
lyricsSearchResult.MatchPercentage = maxScore;
string? raw = await File.ReadAllTextAsync(bestFile, FileHelper.GetEncoding(bestFile));
if (raw != null)
{
lyricsSearchResult.Raw = raw;
}
}
return lyricsSearchResult;
}
private LyricsSearchResult SearchEmbedded(SongInfo songInfo)
{
int bestScore = 0;
string? bestFile = null;
string? bestRaw = null;
var lyricsSearchResult = new LyricsSearchResult
{
Provider = LyricsSearchProvider.LocalMusicFile,
@@ -336,24 +343,45 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
{
var track = new Track(file);
if ((songInfo.Album != "" && track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists && track.Album == songInfo.Album)
|| (songInfo.Album == "" && track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists)
|| (songInfo.Album == "" && StringHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), songInfo.Title, songInfo.DisplayArtists)))
{
var plain = track.GetRawLyrics();
if (!plain.IsNullOrEmpty())
{
lyricsSearchResult.Raw = plain;
lyricsSearchResult.CopyFromSongInfo(songInfo);
lyricsSearchResult.Reference = file;
var raw = track.GetRawLyrics();
return lyricsSearchResult;
if (!string.IsNullOrEmpty(raw))
{
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;
}
}
}
}
}
}
if (bestFile != null)
{
var track = new Track(bestFile);
lyricsSearchResult.Title = track.Title;
lyricsSearchResult.Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator);
lyricsSearchResult.Album = track.Album;
lyricsSearchResult.Duration = track.Duration;
lyricsSearchResult.Raw = bestRaw;
lyricsSearchResult.Reference = bestFile;
lyricsSearchResult.MatchPercentage = bestScore;
}
return lyricsSearchResult;
}
@@ -425,6 +453,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
catch { }
}
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
if (string.IsNullOrWhiteSpace(rawLyricFile))
{
return lyricsSearchResult;
@@ -503,6 +533,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult.Reference = url;
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
return lyricsSearchResult;
}
@@ -530,7 +562,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
ISearchResult? result;
if (searcher == Searchers.Netease && songInfo.SongId != null)
{
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, songInfo.Artists, (int)songInfo.DurationMs, songInfo.SongId);
result = new NeteaseSearchResult("", [], "", [], 0, songInfo.SongId);
}
else if (searcher == Searchers.QQMusic && songInfo.SongId != null)
{
result = new QQMusicSearchResult("", [], "", [], 0, songInfo.SongId, "");
}
else
{
@@ -560,6 +596,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult.Raw = response?.Lrc?.Lyric;
lyricsSearchResult.Translation = response?.Tlyric?.Lyric;
lyricsSearchResult.Transliteration = response?.Romalrc?.Lyric;
lyricsSearchResult.Reference = $"https://music.163.com/song?id={neteaseResult.Id}";
}
else if (result is KugouSearchResult kugouResult)
@@ -602,26 +639,23 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult.Album = result?.Album;
lyricsSearchResult.Duration = result?.DurationMs / 1000;
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchAppleMusicAsync(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
LyricsSearchResult lyricsSearchResult = new()
{
Provider = LyricsSearchProvider.AppleMusic,
Provider = LyricsSearchProvider.AppleMusic
};
_logger.LogInformation("SearchAppleMusicAsync");
if (await _appleMusic.InitAsync())
{
string id = await _appleMusic.SearchSongInfoAsync(songInfo.DisplayArtists, songInfo.Title);
string? raw = await _appleMusic.GetLyricsAsync(id);
_logger.LogInformation("SearchAppleMusicAsync");
lyricsSearchResult.Raw = raw;
lyricsSearchResult.Title = songInfo.Title;
lyricsSearchResult.Artists = songInfo.Artists;
lyricsSearchResult.Album = "";
lyricsSearchResult.Reference = $"https://music.apple.com/song/{id}";
lyricsSearchResult = await _appleMusic.SearchSongInfoAsync(songInfo);
}
return lyricsSearchResult;

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