Compare commits

..

74 Commits

Author SHA1 Message Date
Zhe Fang
94b22552e5 update doc 2025-10-10 11:40:44 -04:00
Zhe Fang
6deb16f6cb update version code 2025-10-10 11:34:39 -04:00
Zhe Fang
e467ab9c73 替换 SourceAppUserModelId 为 PlayerId 并新增 SongId
将 SongInfo 中的 SourceAppUserModelId 属性替换为 PlayerId,新增可选属性 SongId 以支持更精确的歌曲标识。
更新了相关服务和方法的参数签名,增加对 SongId 的支持,包括 SearchSmartlyAsync 和 SearchSingleAsync 方法。
调整了 MediaSessionsService 的逻辑,支持不同播放器的 SongId 处理。
优化了代码的灵活性和可扩展性,便于处理多播放器和歌曲标识。
2025-10-10 11:29:50 -04:00
Zhe Fang
ea038c9c56 更新 README.md 2025-10-05 07:59:44 -04:00
Zhe Fang
560250ad30 更新 README.md 2025-10-05 07:57:35 -04:00
Zhe Fang
536acc69a5 更新 README.md 2025-10-05 07:56:00 -04:00
Zhe Fang
0bbb379912 update LICENSE.txt 2025-09-21 21:15:15 -04:00
Zhe Fang
1f4d29e6f2 更新 README.md 2025-09-13 06:49:20 -04:00
Zhe Fang
5d1d7476c9 update screenshots 2025-09-11 21:10:24 -04:00
Zhe Fang
e016baefe1 doc 2025-09-11 20:44:17 -04:00
Zhe Fang
70b6194788 Improve formatting and clarity in README.md 2025-09-11 20:32:26 -04:00
Zhe Fang
9f103b0ea3 Update README with QQ 音乐 details and grammar fixes 2025-09-10 09:30:39 -04:00
Zhe Fang
2924140f95 Refine README.md for grammar and clarity
Corrected grammar and punctuation throughout the README file for improved clarity and consistency.
2025-09-08 18:33:06 -04:00
Zhe Fang
3d3f168926 更新 README.md 2025-09-05 07:23:52 -04:00
Zhe Fang
63b2285a36 Update download link for app in README 2025-09-05 07:03:26 -04:00
Zhe Fang
780689fa05 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-09-04 15:19:36 -04:00
Zhe Fang
01384717c4 更新应用版本和图像处理逻辑
在 `Package.appxmanifest` 中,将版本号更新为 `1.0.73.0`,发布者显示名称更改为 `Zhe Fang`。
在 `ImageHelper.cs` 中新增 `DataUrlToByteArray` 和 `GetImageBytesFromUrlAsync` 方法,以支持数据 URL 和网络图片的处理。
在 `MediaSessionsService.cs` 中,更新了音乐专辑封面图像的下载逻辑,改用新的图像获取方法并记录 URL 信息。
2025-09-04 15:19:33 -04:00
Zhe Fang
e18d78170a Add Apple Music configuration instructions
Added instructions for configuring Apple Music in the README.
2025-09-04 09:38:31 -04:00
Zhe Fang
023bf77afc Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-09-04 09:19:18 -04:00
Zhe Fang
2877ac2101 更新版本并增强媒体处理功能
- 更新 `Package.appxmanifest` 版本号至 `1.0.72.0`。
- 修改 `LXMusic.cs` 中的 `QuerySuffix`,新增 `picUrl` 过滤器。
- 清理 `MediaSourceProviderToLogoUriConverter.cs` 中的 `using` 语句。
- 在 `ImageHelper.cs` 中添加异步方法 `DownloadImageAsByteArrayAsync`,用于下载图像。
- 在 `MediaSessionsService.cs` 中添加 `_lxMusicAlbumArtBytes` 字节数组以存储专辑封面。
- 更新媒体属性处理逻辑,以支持从 `picUrl` 下载专辑封面。
- 修改 SSE 消息接收逻辑,支持异步操作以处理图像下载和更新。
2025-09-04 09:19:16 -04:00
Zhe Fang
16d82109bb 更新 README.md 2025-09-03 12:19:09 -04:00
Zhe Fang
c703f04119 Revise README for Microsoft Store and download options 2025-09-02 18:30:31 -04:00
Zhe Fang
998853f9d2 更新 README.md 2025-09-02 13:40:45 -04:00
Zhe Fang
f560735da0 更新 README.md 2025-09-02 13:35:09 -04:00
Zhe Fang
ab9da73b49 更新 README.md 2025-09-02 13:00:38 -04:00
Zhe Fang
dc364edf75 更新 README.md 2025-09-02 12:59:27 -04:00
Zhe Fang
7fbc8fbfe7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-09-02 07:38:47 -04:00
Zhe Fang
4a00bb2ddf 更新版本并添加对 Apple Music 的支持
- 将 `Package.appxmanifest` 的版本号更新为 `1.0.71.0`。
- 移除 `BetterLyrics.WinUI3.csproj` 中对 `TinyPinyin.Net` 的引用。
- 在 `PlaybackSettingsControl.xaml` 中更新目标语言选择,移除中文选项并添加多个新语言。
- 新增 Apple Music 令牌输入框和按钮,允许用户保存令牌。
- 在 `LyricsSearchProviderToDisplayNameConverter.cs` 和 `TranslationSearchProviderToDisplayNameConverter.cs` 中添加对 Apple Music 的支持。
- 在 `LyricsSearchProvider.cs` 中新增 `AppleMusic` 作为歌词搜索提供者,并添加相关缓存目录和格式。
- 更新 `LanguageHelper.cs` 中的目标语言列表。
- 将 `TranslationSettings.cs` 中的 `SelectedTargetLanguageIndex` 属性更改为 `SelectedTargetLanguageCode`。
- 在 `LyricsSearchService.cs` 中添加 Apple Music 的歌词搜索功能。
- 更新 `MediaSessionsService.cs` 中的翻译和歌词更新逻辑。
- 移除 `SettingsPageViewModel.cs` 中的库信息支持,添加对媒体会话服务的引用。
- 新增 `AppleMusic.cs` 文件,包含与 Apple Music API 交互的逻辑。
2025-09-02 07:38:45 -04:00
Zhe Fang
7472aa048f 更新 README.md 2025-08-27 22:07:14 -04:00
Zhe Fang
49b0f7a692 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-08-27 08:28:44 -04:00
Zhe Fang
4d0602ebef 更新版本号并优化歌词窗口边界计算
在 `Package.appxmanifest` 文件中,将应用程序版本号从 `1.0.66.0` 更新为 `1.0.67.0`。
在 `LiveStates.cs` 文件中,修改了 `DemoLyricsWindowBounds` 的计算方式,以更准确地反映歌词窗口在监视器上的位置。
2025-08-27 08:28:42 -04:00
Zhe Fang
626395d93a Update instructions from 'Clone' to 'Fork' 2025-08-26 22:34:18 -04:00
Zhe Fang
0ab5602569 Revise translation contribution instructions
Updated translation instructions for contributors.
2025-08-26 16:50:34 -04:00
Zhe Fang
37a7528762 更新 FAQ.md 2025-08-25 08:56:13 -04:00
Zhe Fang
3ca391a509 更新 FAQ.md 2025-08-25 08:54:43 -04:00
Zhe Fang
f5e542d2f3 更新 How2Install.md 2025-08-25 08:53:10 -04:00
Zhe Fang
107bdf8bee 更新 README.md 2025-08-25 08:51:19 -04:00
Zhe Fang
3e9e56f5cc ignore ttml x-roman span 2025-08-25 07:12:24 -04:00
Zhe Fang
b0fd43ead5 fix input issue in search panel; fix display issue in lyrics window manager 2025-08-24 16:29:28 -04:00
Zhe Fang
81c59495c0 improvement 2025-08-23 21:12:57 -04:00
Zhe Fang
7c5f1a804e Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-08-23 18:09:14 -04:00
Zhe Fang
13ee4227f7 fix #101 fix #100 2025-08-23 18:09:12 -04:00
Zhe Fang
c1360ac972 更新 README.md 2025-08-22 17:48:53 -04:00
Zhe Fang
c72aa8a58f 更新 FAQ.md 2025-08-22 15:33:46 -04:00
Zhe Fang
9545ed610b update readme 2025-08-22 15:26:51 -04:00
Zhe Fang
1c4515acb9 update readme 2025-08-22 15:18:40 -04:00
Zhe Fang
92a6fe46de update docs 2025-08-22 15:00:57 -04:00
Zhe Fang
2f9fa02214 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-08-21 17:56:24 -04:00
Zhe Fang
c174363c07 fix #99 ;add thumb for timeline slider; add margin for top command area 2025-08-21 17:56:13 -04:00
Zhe Fang
8506062c9a Update FUNDING.yml 2025-08-21 13:19:24 -04:00
Zhe Fang
52711cba1f showing ordered system font family list; fix issue that timeline change can not be invoked sometimes; add jump to specific lyrics line when clikcing in searching flyout 2025-08-21 11:01:39 -04:00
Zhe Fang
7eda076920 fix #85 and some other bugs fixed 2025-08-20 20:12:18 -04:00
Zhe Fang
282c2f5ac0 update readme for demo video url changed 2025-08-18 15:06:17 -04:00
Zhe Fang
94e76a6ac9 fix #84 2025-08-18 10:51:37 -04:00
Zhe Fang
516d388009 fix 2025-08-17 20:49:45 -04:00
Zhe Fang
ad33eed57c fix lyrics update issue and lyrics search provider info issue 2025-08-17 15:21:04 -04:00
Zhe Fang
64bf2dc3d9 fix pause music hide window issue 2025-08-17 12:15:05 -04:00
Zhe Fang
b86e4a3d12 split lyrics updater from render to media sessions service 2025-08-17 10:50:48 -04:00
Zhe Fang
411506b9cd fix lyrics display issue 2025-08-16 22:27:38 -04:00
Zhe Fang
f681b43e96 update shortcuts, add background task runner 2025-08-16 20:23:11 -04:00
Zhe Fang
133acf5592 add display type settings for every mode 2025-08-16 11:36:52 -04:00
Zhe Fang
cbaa81b9bb fix #90 2025-08-16 10:51:48 -04:00
Zhe Fang
b834be49ce add compare condition - using metadata 2025-08-15 20:13:09 -04:00
Zhe Fang
8abe6d7f01 add settings port for adjusting fps and isfixedtimestep 2025-08-15 18:38:28 -04:00
Zhe Fang
8a73ba9e6a add icons 2025-08-15 17:29:40 -04:00
Zhe Fang
49a090b0c7 add support for quick lyrics settings 2025-08-15 17:10:14 -04:00
Zhe Fang
a47dd67056 support hide lyrics window to system tray 2025-08-15 17:06:14 -04:00
Zhe Fang
900ecc9776 fix 2025-08-15 17:02:38 -04:00
Zhe Fang
464742d7c5 fix #82 and now have the ability to load original album art 2025-08-14 19:45:35 -04:00
Zhe Fang
5f3aad4e99 rollback local album and lyrics search methos to avoid slow response 2025-08-12 15:15:04 -04:00
Zhe Fang
7b6eca6ff6 fix memory leaking 2025-08-12 11:43:14 -04:00
Zhe Fang
9fcb1ac869 fix 2025-08-12 09:52:17 -04:00
Zhe Fang
74ebda2b6d fix translation 2025-08-11 21:05:19 -04:00
Zhe Fang
e194dfaa70 fix #79 fix #80 2025-08-11 09:03:19 -04:00
182 changed files with 6522 additions and 3810 deletions

2
.github/FUNDING.yml vendored
View File

@@ -12,4 +12,4 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl
polar: # Replace with a single Polar username
buy_me_a_coffee: founchoo
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
custom: ['https://paypal.me/zhefangpay']

View File

@@ -12,13 +12,13 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.46.0" />
Version="1.0.75.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<PublisherDisplayName>Zhe Fang</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>

View File

@@ -10,10 +10,10 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Merged dictionaries here -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Theme -->
@@ -51,8 +51,15 @@
<converter:TranslationSearchProviderToDisplayNameConverter x:Key="TranslationSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converter:MillisecondsToFormattedTimeConverter x:Key="MillisecondsToFormattedTimeConverter" />
<converter:MediaSourceProviderToLogoUriConverter x:Key="MediaSourceProviderToLogoUriConverter" />
<converter:MediaSourceProviderToDisplayedNameConverter x:Key="MediaSourceProviderToDisplayedNameConverter" />
<converter:FPSToTimeSpanConverter x:Key="FPSToTimeSpanConverter" />
<converter:ShortcutToStringConverter x:Key="ShortcutToStringConverter" />
<converter:BoolNegationToVisibilityConverter x:Key="BoolNegationToVisibilityConverter" />
<converter:BoolToOpacityConverter x:Key="BoolToOpacityConverter" />
<converter:RectToMarginConverter x:Key="RectToMarginConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
@@ -308,6 +315,18 @@
<Setter Property="Padding" Value="0,0,0,36" />
</Style>
<Style
x:Key="FlyoutPageStyle"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Opacity" Value="0.9" />
<Setter Property="MinWidth" Value="850" />
<Setter Property="Padding" Value="0" />
<Setter Property="ScrollViewer.VerticalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />

View File

@@ -6,6 +6,7 @@ using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
@@ -84,13 +85,6 @@ namespace BetterLyrics.WinUI3
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenWindow<LyricsWindow>();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
lyricsWindow.ViewModel.InitLockHotKey();
lyricsWindow.AutoSelectLyricsMode();
}
}
private static void ConfigureServices()
@@ -109,6 +103,7 @@ namespace BetterLyrics.WinUI3
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<ILiveStatesService, LiveStatesService>()
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
@@ -123,6 +118,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<PlaybackSettingsControlViewModel>()
.AddSingleton<MediaSettingsControlViewModel>()
.AddSingleton<AllLyricsSettingsControlViewModel>()
.AddSingleton<LyricsSearchControlViewModel>()
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View File

@@ -26,11 +26,13 @@
<None Remove="Controls\AllLyricsSettingsControl.xaml" />
<None Remove="Controls\AppSettingsControl.xaml" />
<None Remove="Controls\ExtendedSlider.xaml" />
<None Remove="Controls\LyricsBavkgroundSettingsControl.xaml" />
<None Remove="Controls\LyricsSearchControl.xaml" />
<None Remove="Controls\LyricsSettingsControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
<None Remove="Views\MusicGalleryWindow.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
@@ -58,8 +60,10 @@
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" />
@@ -69,6 +73,7 @@
<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="RomajiConverter.Core" Version="1.0.9" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.3-dev-02320" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
@@ -76,7 +81,6 @@
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.8" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="TinyPinyin.Net" Version="1.0.2" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.6" />
@@ -86,11 +90,6 @@
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Hqub.Lastfm">
<HintPath>..\..\..\Last.fm\src\Hqub.Lastfm\bin\Release\netstandard2.0\Hqub.Lastfm.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -115,10 +114,10 @@
<Content Update="Assets\Chrome.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Discord.png">
<Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Edge.png">
<Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
@@ -160,10 +159,10 @@
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PotPlayer.png">
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQ.png">
<Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQMusic.png">
@@ -178,13 +177,25 @@
<Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Telegram.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Views\LyricsSearchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ExtendedSlider.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -216,7 +227,7 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsBavkgroundSettingsControl.xaml">
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Constants
{
public static class AppleMusic
{
public const string MediaUserTokenKey = "AppleMusicMediaUserToken";
}
}

View File

@@ -8,6 +8,6 @@ namespace BetterLyrics.WinUI3.Constants
{
public static class LXMusic
{
public const string QuerySuffix = "/subscribe-player-status?filter=progress,duration";
public const string QuerySuffix = "/subscribe-player-status?filter=progress,duration,picUrl";
}
}

View File

@@ -8,7 +8,8 @@ namespace BetterLyrics.WinUI3.Constants
{
public static class Link
{
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string GitHubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string FAQUrl = $"{GitHubUrl}/blob/dev/FAQ/FAQ.md";
public const string QQGroupUrl = "https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info";
public const string DiscordUrl = "https://discord.gg/5yAQPnyCKv";
public const string TelegramUrl = "https://t.me/+svhSLZ7awPsxNGY1";

View File

@@ -17,6 +17,7 @@ namespace BetterLyrics.WinUI3.Constants
public const string PotPlayer = "PotPlayerMini64.exe";
public const string Spotify = "Spotify.exe";
public const string AppleMusic = "AppleInc.AppleMusicWin_nzyj5cx40ttqa!App";
public const string AppleMusicAlternative = "AppleMusic.exe";
public const string NetEaseCloudMusic = "cloudmusic.exe";
public const string KugouMusic = "kugou";
public const string QQMusic = "QQMusic.exe";

View File

@@ -16,6 +16,7 @@ namespace BetterLyrics.WinUI3.Constants
public const string PotPlayer = "PotPlayer";
public const string Spotify = "Spotify";
public const string AppleMusic = "Apple Music";
public const string AppleMusicAlternative = "Apple Music";
public const string NetEaseCloudMusic = "网易云音乐";
public const string KugouMusic = "酷狗音乐";
public const string QQMusic = "QQ 音乐";

View File

@@ -17,7 +17,7 @@
<TextBlock x:Uid="SettingsPageAlbumArt" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE71A;}">
<controls:SettingsCard x:Uid="SettingsPageAlbumRadius" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEA3A;}">
<local:ExtendedSlider
Default="12"
Frequency="1"
@@ -27,6 +27,15 @@
Value="{x:Bind ViewModel.AppSettings.AlbumArtLayoutSettings.CoverImageRadius, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAlbumShadowAmount" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF5EF;}">
<local:ExtendedSlider
Default="12"
Frequency="1"
Maximum="64"
Minimum="0"
Value="{x:Bind ViewModel.AppSettings.AlbumArtLayoutSettings.CoverImageShadowAmount, Mode=TwoWay}" />
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageSongInfo" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageSongInfoAlignment" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E3;}">
@@ -37,6 +46,15 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<local:ExtendedSlider
Default="18"
Frequency="2"
Maximum="48"
Minimum="12"
Value="{x:Bind ViewModel.AppSettings.AlbumArtLayoutSettings.SongInfoFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -21,16 +21,20 @@
<controls:Case Value="Dock">
<uc:LyricsSettingsControl LyricsEffectSettings="{x:Bind ViewModel.AppSettings.DockLyricsEffectSettings, Mode=OneWay}" LyricsStyleSettings="{x:Bind ViewModel.AppSettings.DockLyricsStyleSettings, Mode=OneWay}" />
</controls:Case>
<controls:Case Value="PictureInPicture">
<uc:LyricsSettingsControl LyricsEffectSettings="{x:Bind ViewModel.AppSettings.PictureInPictureLyricsEffectSettings, Mode=OneWay}" LyricsStyleSettings="{x:Bind ViewModel.AppSettings.PictureInPictureLyricsStyleSettings, Mode=OneWay}" />
</controls:Case>
</controls:SwitchPresenter>
<controls:Segmented
x:Name="LyricsSettingsSegmentedControl"
Margin="36,36,36,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SelectedIndex="0">
SelectedIndex="{x:Bind ViewModel.SelectedTabIndex, Mode=OneWay}">
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlStandard" Tag="Standard" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlDesktop" Tag="Desktop" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlDock" Tag="Dock" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlDesktop" Tag="Desktop" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlPictureInPicture" Tag="PictureInPicture" />
</controls:Segmented>
</Grid>
</UserControl>

View File

@@ -11,145 +11,375 @@
mc:Ignorable="d">
<Grid>
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SwitchPresenter Margin="0,72,0,0" Value="{Binding SelectedItem.Tag, ElementName=SegmentedControl}">
<!-- App appearance -->
<controls:Case Value="General">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<!-- App appearance -->
<controls:SettingsExpander
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF2B7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.GeneralSettings.Language, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
<ComboBoxItem x:Uid="SettingsPageTC" />
<ComboBoxItem x:Uid="SettingsPageJA" />
<ComboBoxItem x:Uid="SettingsPageKO" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF2B7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.GeneralSettings.Language, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
<ComboBoxItem x:Uid="SettingsPageTC" />
<ComboBoxItem x:Uid="SettingsPageJA" />
<ComboBoxItem x:Uid="SettingsPageKO" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- App behavior -->
<TextBlock x:Uid="SettingsPageAppBehavior" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageAutoStart" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF71C;}">
<ToggleSwitch
x:Name="AutoStartupToggleSwitch"
Loaded="AutoStartupToggleSwitch_Loaded"
Unloaded="AutoStartupToggleSwitch_Unloaded" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- App behavior -->
<controls:SettingsCard x:Uid="SettingsPageAutoStartWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE736;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.GeneralSettings.AutoStartWindowType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageAutoStartInAppLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDockLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDesktopLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartPIPLyrics" />
</ComboBox>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageAppBehavior" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Name="LyricsWindowManagerExpander"
x:Uid="SettingsPageLyricsWindowManager"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE61F;}"
IsExpanded="True" />
<controls:SettingsCard x:Uid="SettingsPageAutoStart" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF71C;}">
<ToggleSwitch
x:Name="AutoStartupToggleSwitch"
Loaded="AutoStartupToggleSwitch_Loaded"
Unloaded="AutoStartupToggleSwitch_Unloaded" />
</controls:SettingsCard>
<Grid
Margin="0,-20,0,0"
Padding="60,16"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="4"
Visibility="{Binding ElementName=LyricsWindowManagerExpander, Path=IsExpanded, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Spacing="16">
<TextBlock x:Uid="SettingsPageCurrentLyricsWindowStatus" />
<StackPanel HorizontalAlignment="Left" Spacing="4">
<Grid
Width="{x:Bind ViewModel.LiveStates.DemoLyricsWindowMonitorBounds.Width, Mode=OneWay}"
Height="{x:Bind ViewModel.LiveStates.DemoLyricsWindowMonitorBounds.Height, Mode=OneWay}"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}">
<Grid
Width="{x:Bind ViewModel.LiveStates.DemoLyricsWindowBounds.Width, Mode=OneWay}"
Height="{x:Bind ViewModel.LiveStates.DemoLyricsWindowBounds.Height, Mode=OneWay}"
Margin="{x:Bind ViewModel.LiveStates.DemoLyricsWindowBounds, Converter={StaticResource RectToMarginConverter}, Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="ExtraBlack"
Opacity="0.7"
Text="{x:Bind ViewModel.LiveStates.LyricsWindowMonitorName, Mode=OneWay}"
TextWrapping="Wrap" />
</Grid>
<Grid>
<Button
x:Uid="SettingsPageRecord"
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.RecordCurrentWindowBoundsCommand}"
Style="{StaticResource AccentButtonStyle}" />
</Grid>
</StackPanel>
</StackPanel>
</Grid>
<controls:SettingsCard x:Uid="SettingsPageAutoStartWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE736;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.GeneralSettings.AutoStartWindowType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageAutoStartInAppLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDockLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDesktopLyrics" />
</ComboBox>
</controls:SettingsCard>
<StackPanel
Margin="0,-3,0,0"
Padding="60,16"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="4"
Spacing="16"
Visibility="{Binding ElementName=LyricsWindowManagerExpander, Path=IsExpanded, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock x:Uid="SettingsPageRecordedWindowStatus" />
<GridView
Margin="-2"
CornerRadius="4"
ItemsSource="{x:Bind ViewModel.AppSettings.WindowBoundsRecords, Mode=OneWay}"
SelectionMode="None">
<GridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
<Setter Property="Margin" Value="5,5,5,5" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel Spacing="4">
<Grid
Width="{Binding DemoMonitorBounds.Width}"
Height="{Binding DemoMonitorBounds.Height}"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}">
<Grid
Width="{Binding DemoWindowBounds.Width}"
Height="{Binding DemoWindowBounds.Height}"
Margin="{Binding DemoWindowBounds, Converter={StaticResource RectToMarginConverter}}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="4" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="ExtraBlack"
Opacity="0.7"
Text="{Binding MonitorDeviceName}"
TextWrapping="Wrap" />
</Grid>
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
x:Uid="SettingsPageDelete"
Grid.Column="0"
HorizontalAlignment="Stretch"
Click="DeleteWindowBoundsRecordButton_Click" />
<Button
x:Uid="SettingsPageApply"
Grid.Column="1"
HorizontalAlignment="Stretch"
Click="ApplyWindowBoundsRecordButton_Click"
Style="{StaticResource AccentButtonStyle}" />
</Grid>
</StackPanel>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
<controls:SettingsCard x:Uid="SettingsPageIgnoreFullscreenWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE967;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreFullscreenWindow, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageIgnoreFullscreenWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE967;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreFullscreenWindow, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageHideWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED1A;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.HideWindowWhenNotPlaying, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageHideWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED1A;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.HideWindowWhenNotPlaying, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageGlobalDrag" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7C2;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsDragEverywhereEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageShowHideHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.GeneralSettings.ShowOrHideLyricsWindowShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Desktop mode -->
<controls:SettingsCard x:Uid="SettingsPageGlobalDrag" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7C2;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsDragEverywhereEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageAppDesktop" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageExitOnLyricsWindowClosed" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7E8;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ExitOnLyricsWindowClosed, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAutoLock" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE755;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.DesktopModeSettings.AutoLockOnDesktopMode, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Playback shortcut -->
<TextBlock x:Uid="SettingsPagePlaybackShortcut" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPagePlayOrPauseSongHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.GeneralSettings.PlayOrPauseShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageNextSongHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.GeneralSettings.NextSongShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPagePreviousSongHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.GeneralSettings.PreviousSongShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLockHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Center"
Text="Ctrl + Alt + " />
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.DesktopModeSettings.LockHotKeyIndex, Mode=TwoWay}">
<ComboBoxItem Content="A" />
<ComboBoxItem Content="B" />
<ComboBoxItem Content="C" />
<ComboBoxItem Content="D" />
<ComboBoxItem Content="E" />
<ComboBoxItem Content="F" />
<ComboBoxItem Content="G" />
<ComboBoxItem Content="H" />
<ComboBoxItem Content="I" />
<ComboBoxItem Content="J" />
<ComboBoxItem Content="K" />
<ComboBoxItem Content="L" />
<ComboBoxItem Content="M" />
<ComboBoxItem Content="N" />
<ComboBoxItem Content="O" />
<ComboBoxItem Content="P" />
<ComboBoxItem Content="Q" />
<ComboBoxItem Content="R" />
<ComboBoxItem Content="S" />
<ComboBoxItem Content="T" />
<ComboBoxItem Content="U" />
<ComboBoxItem Content="V" />
<ComboBoxItem Content="W" />
<ComboBoxItem Content="X" />
<ComboBoxItem Content="Y" />
<ComboBoxItem Content="Z" />
</ComboBox>
</StackPanel>
</controls:SettingsCard>
</Grid>
</ScrollViewer>
<!-- Dock mode -->
</controls:Case>
<TextBlock x:Uid="SettingsPageAppDock" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:Case Value="Standard">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Standard mode -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageDisplayTypeSwitcher" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF246;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.StandardModeSettings.LyricsDisplayType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MainPageAlbumArtOnly" />
<ComboBoxItem x:Uid="MainPageLyriscOnly" />
<ComboBoxItem x:Uid="MainPageSplitView" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAOT" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE718;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.StandardModeSettings.IsAlwaysOnTop, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDockMonitor" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7F4;}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ComboBox ItemsSource="{x:Bind ViewModel.MonitorDeviceNames, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.AppSettings.DockModeSettings.DockMonitorDeviceName, Mode=TwoWay}" />
<Button
Command="{x:Bind ViewModel.RefreshMonitorDeviceNamesCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE72C;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</controls:SettingsCard>
</Grid>
</ScrollViewer>
</controls:Case>
<controls:SettingsCard x:Uid="SettingsPageDockWindowHeight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED5E;}">
<local:ExtendedSlider
Default="64"
Frequency="1"
Maximum="200"
Minimum="64"
Unit="px"
Value="{x:Bind ViewModel.AppSettings.DockModeSettings.DockWindowHeight, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:Case Value="Desktop">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsCard x:Uid="SettingsPageDockPlacement" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.DockModeSettings.DockPlacement, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageDockPlacementTop" />
<ComboBoxItem x:Uid="SettingsPageDockPlacementBottom" />
</ComboBox>
</controls:SettingsCard>
<!-- Desktop mode -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageDisplayTypeSwitcher" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF246;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.DesktopModeSettings.LyricsDisplayType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MainPageAlbumArtOnly" />
<ComboBoxItem x:Uid="MainPageLyriscOnly" />
<ComboBoxItem x:Uid="MainPageSplitView" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAOT" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE718;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.DesktopModeSettings.IsAlwaysOnTop, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageToggleHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.DesktopModeSettings.ToggleShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAutoLock" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE755;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.DesktopModeSettings.AutoLockOnDesktopMode, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLockHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.DesktopModeSettings.LockShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</controls:Case>
<controls:Case Value="Dock">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Dock mode -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageDisplayTypeSwitcher" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF246;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.DockModeSettings.LyricsDisplayType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MainPageAlbumArtOnly" />
<ComboBoxItem x:Uid="MainPageLyriscOnly" />
<ComboBoxItem x:Uid="MainPageSplitView" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageToggleHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.DockModeSettings.ToggleShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDockMonitor" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7F4;}">
<StackPanel Orientation="Horizontal" Spacing="6">
<ComboBox ItemsSource="{x:Bind ViewModel.MonitorDeviceNames, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.AppSettings.DockModeSettings.DockMonitorDeviceName, Mode=TwoWay}" />
<Button
Command="{x:Bind ViewModel.RefreshMonitorDeviceNamesCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE72C;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDockWindowHeight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED5E;}">
<local:ExtendedSlider
Default="64"
Frequency="1"
Maximum="200"
Minimum="64"
Unit="px"
Value="{x:Bind ViewModel.AppSettings.DockModeSettings.DockWindowHeight, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDockPlacement" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.DockModeSettings.DockPlacement, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageDockPlacementTop" />
<ComboBoxItem x:Uid="SettingsPageDockPlacementBottom" />
</ComboBox>
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</controls:Case>
<controls:Case Value="PictureInPicture">
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<!-- Picture in picture mode -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageDisplayTypeSwitcher" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF246;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.PictureInPictureModeSettings.LyricsDisplayType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MainPageAlbumArtOnly" />
<ComboBoxItem x:Uid="MainPageLyriscOnly" />
<ComboBoxItem x:Uid="MainPageSplitView" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageToggleHotKey" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEDA7;}">
<local:ShortcutTextBox Shortcut="{x:Bind ViewModel.AppSettings.PictureInPictureModeSettings.ToggleShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</controls:Case>
</controls:SwitchPresenter>
<controls:Segmented
x:Name="SegmentedControl"
Margin="36,36,36,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SelectedIndex="0">
<controls:SegmentedItem x:Uid="AppSettingsControlGeneral" Tag="General" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlStandard" Tag="Standard" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlDock" Tag="Dock" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlDesktop" Tag="Desktop" />
<controls:SegmentedItem x:Uid="AllLyricsSettingsControlPictureInPicture" Tag="PictureInPicture" />
</controls:Segmented>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -1,5 +1,8 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -46,5 +49,33 @@ namespace BetterLyrics.WinUI3.Controls
{
AutoStartupToggleSwitch.Toggled -= AutoStartupToggleSwitch_Toggled;
}
private void DeleteWindowBoundsRecordButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button)
{
var data = button.DataContext as WindowBoundsRecord;
if (data != null)
{
ViewModel.AppSettings.WindowBoundsRecords.Remove(data);
}
}
}
private void ApplyWindowBoundsRecordButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button)
{
var data = button.DataContext as WindowBoundsRecord;
if (data != null)
{
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
lyricsWindow.AppWindow.MoveAndResize(data.WindowBounds.ToRectInt32());
}
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.LyricsBavkgroundSettingsControl"
x:Class="BetterLyrics.WinUI3.Controls.LyricsBackgroundSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
@@ -25,7 +25,7 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsPureColorBgOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF573;}">
<controls:SettingsCard x:Uid="SettingsPageLyricsPureColorBgOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB42;}">
<uc:ExtendedSlider
Default="100"
Frequency="1"
@@ -35,7 +35,7 @@
Value="{x:Bind ViewModel.AppSettings.LyricsBackgroundSettings.PureColorOverlayOpacity, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsBackgroundOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF573;}">
<controls:SettingsCard x:Uid="SettingsPageLyricsBackgroundOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB42;}">
<uc:ExtendedSlider
Default="100"
Frequency="1"
@@ -64,7 +64,7 @@
Value="{x:Bind ViewModel.AppSettings.LyricsBackgroundSettings.CoverOverlayBlurAmount, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageBackgroundAcrylicEffectAmount" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<controls:SettingsCard x:Uid="SettingsPageBackgroundAcrylicEffectAmount" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE80A;}">
<uc:ExtendedSlider
Default="0"
Frequency="1"

View File

@@ -20,11 +20,11 @@ using Windows.Foundation.Collections;
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class LyricsBavkgroundSettingsControl : UserControl
public sealed partial class LyricsBackgroundSettingsControl : UserControl
{
public LyricsBackgroundSettingsControlViewModel ViewModel => (LyricsBackgroundSettingsControlViewModel)DataContext;
public LyricsBavkgroundSettingsControl()
public LyricsBackgroundSettingsControl()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<LyricsBackgroundSettingsControlViewModel>();

View File

@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.LyricsSearchControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid Padding="16" RowSpacing="6">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsExpander x:Uid="LyricsSearchControlSongInfoMapping" IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="LyricsSearchControlTitle" Description="{x:Bind ViewModel.MappedSongSearchQuery.OriginalTitle, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6">
<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}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="LyricsSearchControlArtist" Description="{x:Bind ViewModel.MappedSongSearchQuery.OriginalArtist, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}" TextWrapping="Wrap" />
<Button
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedArtistCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard>
<CheckBox x:Uid="LyricsSearchControlMarkAsPureMusic" IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="LyricsSearchControlTargetSearchProvider">
<Button
x:Uid="LyricsSearchControlSearch"
Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" />
</controls:SettingsCard>
</StackPanel>
</Grid>
<Grid Grid.Column="1">
<ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<ListViewItem IsEnabled="{Binding IsFound}">
<Grid Opacity="{Binding IsFound, Converter={StaticResource BoolToOpacityConverter}}">
<RelativePanel Padding="0,6">
<TextBlock
x:Name="SearchedTitle"
RelativePanel.AlignLeftWithPanel="True"
Text="{Binding Title}"
Visibility="{Binding IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
x:Name="SearchedArtists"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.Below="SearchedTitle"
Text="{Binding Artist}"
Visibility="{Binding IsFound, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
x:Name="SearchedProvider"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Text="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
</RelativePanel>
<TextBlock
x:Uid="LyricsSearchControlNotFound"
VerticalAlignment="Center"
Text="Not found"
Visibility="{Binding IsFound, Converter={StaticResource BoolNegationToVisibilityConverter}}" />
</Grid>
</ListViewItem>
</DataTemplate>
</ListView.ItemTemplate>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsSearchResults.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsSearchResults.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ListView>
<StackPanel
Padding="0,36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/Empty.png" />
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsSearchResults.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsSearchResults.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</StackPanel>
<ProgressBar
VerticalAlignment="Top"
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.IsSearching, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</Grid>
<Grid Grid.Column="2">
<ListView ItemsSource="{x:Bind ViewModel.LyricsData.LyricsLines, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsLine, Mode=TwoWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding StartMs, 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}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Padding="0,36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/Page.png" />
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</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>
</UserControl>

View File

@@ -0,0 +1,35 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Ude.Core;
using Windows.Foundation;
using Windows.Foundation.Collections;
// 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 LyricsSearchControl : UserControl
{
public LyricsSearchControlViewModel ViewModel => (LyricsSearchControlViewModel)DataContext;
public LyricsSearchControl()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<LyricsSearchControlViewModel>();
}
}
}

View File

@@ -29,7 +29,7 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontFamily" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8D2;}">
<ComboBox ItemsSource="{x:Bind SystemFontNames, Mode=OneWay}" SelectedIndex="{x:Bind LyricsStyleSettings.SelectedFontFamilyIndex, Mode=TwoWay}">
<ComboBox ItemsSource="{x:Bind SystemFontNames, Mode=OneWay}" SelectedItem="{x:Bind LyricsStyleSettings.LyricsFontFamily, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
@@ -54,7 +54,7 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsBgFontOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE799;}">
<controls:SettingsCard x:Uid="SettingsPageLyricsBgFontOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB42;}">
<local:ExtendedSlider
Default="30"
Frequency="1"
@@ -175,14 +175,25 @@
</interactivity:Interaction.Behaviors>
</ColorPicker>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<local:ExtendedSlider
Frequency="2"
Maximum="96"
Minimum="12"
ResetButtonVisibility="Collapsed"
Value="{x:Bind LyricsStyleSettings.LyricsFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLyricsFontSize"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8E9;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageDynamicLyricsFontSize">
<ToggleSwitch IsOn="{x:Bind LyricsStyleSettings.IsDynamicLyricsFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard IsEnabled="{x:Bind LyricsStyleSettings.IsDynamicLyricsFontSize, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<local:ExtendedSlider
Frequency="2"
Maximum="96"
Minimum="12"
ResetButtonVisibility="Collapsed"
Value="{x:Bind LyricsStyleSettings.LyricsFontSize, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageLyricsLineSpacingFactor" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF579;}">
<local:ExtendedSlider
@@ -195,7 +206,7 @@
Value="{x:Bind LyricsStyleSettings.LyricsLineSpacingFactor, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsTranslationSeparator" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF57B;}">
<controls: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}" />
@@ -209,7 +220,7 @@
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Effect" />
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF573;}">
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEB42;}">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsVerticalEdgeOpacitySlider"
Default="0"
@@ -230,14 +241,78 @@
Value="{x:Bind LyricsEffectSettings.LyricsBlurAmount, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsHighlightScope" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7E6;}">
<controls:SettingsCard x:Uid="SettingsPageLyricsLineFade" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED3A;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsLineFadeEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 译文高亮 -->
<controls:SettingsExpander
x:Uid="SettingsPageLyricsTranslationHighlight"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7E6;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageAmount">
<local:ExtendedSlider
Default="60"
Frequency="5"
Maximum="100"
Minimum="0"
Value="{x:Bind LyricsEffectSettings.LyricsTranslationHighlightAmount, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- 原文高亮 -->
<controls:SettingsExpander
x:Uid="SettingsPageLyricsHighlightScope"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7E6;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsHighlightScope, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeLineStartToCurrentChar" />
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentLine" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageAmount">
<local:ExtendedSlider
Default="100"
Frequency="5"
Maximum="100"
Minimum="0"
Value="{x:Bind LyricsEffectSettings.LyricsHighlightAmount, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- 阴影 -->
<controls: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}" />
<controls:SettingsExpander.Items>
<controls: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>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsShadowEnabled, Mode=OneWay}">
<local:ExtendedSlider
Default="8"
Frequency="1"
Maximum="20"
Minimum="1"
Value="{x:Bind LyricsEffectSettings.LyricsShadowAmount, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- 辉光效果 -->
<controls:SettingsExpander
x:Uid="SettingsPageLyricsGlowEffect"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -252,17 +327,42 @@
<ComboBoxItem x:Uid="SettingsPageLyricsRendingScopeCurrentLine" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<local:ExtendedSlider
Default="8"
Frequency="1"
Maximum="20"
Minimum="1"
Value="{x:Bind LyricsEffectSettings.LyricsGlowEffectAmount, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageLyricsFloatAnimation" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8C5;}">
<!-- 浮动动画 -->
<controls:SettingsExpander
x:Uid="SettingsPageLyricsFloatAnimation"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8C5;}"
IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsFloatAnimationEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageAmount" IsEnabled="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<local:ExtendedSlider
Default="1"
Frequency="1"
Maximum="4"
Minimum="1"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAmount, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- 扇形歌词 -->
<controls:SettingsCard x:Uid="SettingsPageFan" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEBC5;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsFanLyricsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 滚动动画 -->
<controls:SettingsExpander
x:Uid="SettingsPageScrollEasing"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -285,7 +385,6 @@
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageScrollTopDuration">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsScrollTopDurationExtendedSlider"
Default="500"
Frequency="50"
Maximum="1000"
@@ -295,7 +394,6 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageScrollDuration">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsScrollDurationExtendedSlider"
Default="500"
Frequency="50"
Maximum="1000"
@@ -305,7 +403,6 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageScrollBottomDuration">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsScrollBottomDurationExtendedSlider"
Default="500"
Frequency="50"
Maximum="1000"
@@ -313,6 +410,24 @@
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollBottomDuration, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageScrollTopDelay">
<local:ExtendedSlider
Default="0"
Frequency="50"
Maximum="2000"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollTopDelay, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageScrollBottomDelay">
<local:ExtendedSlider
Default="0"
Frequency="50"
Maximum="2000"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsScrollBottomDelay, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>

View File

@@ -14,71 +14,74 @@
<Grid>
<ScrollViewer Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="SettingsPageMusicLib"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8B7;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<controls:SettingsCard x:Uid="SettingsPageMusicLib" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8B7;}" />
<InfoBar
x:Uid="SettingsPageRemoveInfo"
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Success">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AppSettings.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AppSettings.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
<ListView
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate>
<controls:SettingsCard>
<controls:SettingsCard.Header>
<controls:SettingsExpander>
<controls:SettingsExpander.Header>
<HyperlinkButton
Click="LocalFolderHyperlinkButton_Click"
Content="{Binding Path, Mode=OneWay}"
Tag="{Binding Path, Mode=OneWay}" />
</controls:SettingsCard.Header>
<StackPanel Orientation="Horizontal">
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
<ToggleSwitch DataContext="{Binding}" IsOn="{Binding IsEnabled, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Header>
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<controls:SettingsCard.Header>
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
</controls:SettingsCard.Header>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageMusicLibRealTimeWatch">
<ToggleSwitch IsOn="{Binding IsRealTimeWatchEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</DataTemplate>
</controls:SettingsExpander.ItemTemplate>
<controls:SettingsExpander.ItemsHeader>
<InfoBar
x:Uid="SettingsPageRemoveInfo"
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Success">
</ListView.ItemTemplate>
</ListView>
<interactivity:Interaction.Behaviors>
<controls:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button
x:Uid="SettingsPageAddFolderButton"
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
CommandParameter="{Binding ElementName=RootGrid}" />
</controls:SettingsCard>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AppSettings.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AppSettings.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
</controls:SettingsExpander.ItemsHeader>
<controls:SettingsExpander.ItemsFooter>
<controls:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button
x:Uid="SettingsPageAddFolderButton"
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
CommandParameter="{Binding ElementName=RootGrid}" />
</controls:SettingsCard>
</controls:SettingsExpander.ItemsFooter>
</controls:SettingsExpander>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -18,31 +18,6 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<NavigationView
VerticalAlignment="Top"
Background="Transparent"
IsBackButtonVisible="Collapsed"
IsBackEnabled="False"
IsSettingsVisible="False"
MenuItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}"
PaneDisplayMode="Top"
SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}">
<NavigationView.MenuItemTemplate>
<DataTemplate x:DataType="models:MediaSourceProviderInfo">
<NavigationViewItem>
<NavigationViewItem.Icon>
<ImageIcon Source="{Binding Provider, Converter={StaticResource MediaSourceProviderToLogoUriConverter}, Mode=OneWay}" />
</NavigationViewItem.Icon>
<NavigationViewItem.Content>
<TextBlock
MaxWidth="200"
Text="{Binding Provider, Converter={StaticResource MediaSourceProviderToDisplayedNameConverter}, Mode=OneWay}"
TextWrapping="Wrap" />
</NavigationViewItem.Content>
</NavigationViewItem>
</DataTemplate>
</NavigationView.MenuItemTemplate>
</NavigationView>
<ScrollViewer Margin="0,72,0,0" Style="{StaticResource SettingsScrollViewerStyle}">
<Grid Style="{StaticResource SettingsGridStyle}">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
@@ -56,16 +31,20 @@
</controls:SettingsCard>
<!-- 时间轴相关配置 -->
<controls:SettingsCard x:Uid="SettingsPageLyricsTimelineThreshold">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsTimelineThresholdReset"
Frequency="100"
Maximum="1000"
Minimum="0"
ResetButtonVisibility="Collapsed"
Unit="ms"
Value="{x:Bind ViewModel.SelectedMediaSourceProvider.TimelineSyncThreshold, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsExpander x:Uid="SettingsPageLyricsTimeline" IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind ViewModel.SelectedMediaSourceProvider.IsTimelineSyncEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLyricsTimelineThreshold" IsEnabled="{x:Bind ViewModel.SelectedMediaSourceProvider.IsTimelineSyncEnabled, Mode=OneWay}">
<local:ExtendedSlider
Frequency="100"
Maximum="1000"
Minimum="0"
ResetButtonVisibility="Collapsed"
Unit="ms"
Value="{x:Bind ViewModel.SelectedMediaSourceProvider.TimelineSyncThreshold, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsExpander x:Uid="MainPagePositionOffsetSlider" IsExpanded="True">
<local:ExtendedSlider
x:Uid="SettingsPagePositionOffsetReset"
@@ -143,6 +122,31 @@
</StackPanel>
</Grid>
</ScrollViewer>
<ListView
VerticalAlignment="Top"
ItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollMode="Disabled"
SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:MediaSourceProviderInfo">
<StackPanel Orientation="Horizontal" Spacing="6">
<ImageIcon Height="16" Source="{Binding Provider, Converter={StaticResource MediaSourceProviderToLogoUriConverter}, Mode=OneWay}" />
<TextBlock
MaxWidth="200"
Text="{Binding Provider, Converter={StaticResource MediaSourceProviderToDisplayedNameConverter}, Mode=OneWay}"
TextWrapping="Wrap" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo.Count, Mode=OneWay}"
@@ -227,42 +231,60 @@
IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageTargetLanguage">
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.TranslationSettings.SelectedTargetLanguageIndex, Mode=TwoWay}">
<controls:SettingsCard x:Uid="SettingsPageTargetLanguage" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=OneWay}">
<ComboBox SelectedIndex="{x:Bind ViewModel.SelectedTargetLanguageIndex, Mode=TwoWay}">
<ComboBoxItem Content="العربية" Tag="ar" />
<ComboBoxItem Content="Azərbaycan dili" Tag="az" />
<ComboBoxItem Content="简体中文" Tag="zh-Hans" />
<ComboBoxItem Content="繁體中文" Tag="zh-Hant" />
<ComboBoxItem Content="Български" Tag="bg" />
<ComboBoxItem Content="বাংলা" Tag="bn" />
<ComboBoxItem Content="Català" Tag="ca" />
<ComboBoxItem Content="Čeština" Tag="cs" />
<ComboBoxItem Content="Dansk" Tag="da" />
<ComboBoxItem Content="Nederlands" Tag="nl" />
<ComboBoxItem Content="English" Tag="en" />
<ComboBoxItem Content="Esperanto" Tag="eo" />
<ComboBoxItem Content="Suomi" Tag="fi" />
<ComboBoxItem Content="Français" Tag="fr" />
<ComboBoxItem Content="Deutsch" Tag="de" />
<ComboBoxItem Content="Ελληνικά" Tag="el" />
<ComboBoxItem Content="English" Tag="en" />
<ComboBoxItem Content="Esperanto" Tag="eo" />
<ComboBoxItem Content="Español" Tag="es" />
<ComboBoxItem Content="Eesti" Tag="et" />
<ComboBoxItem Content="Euskara" Tag="eu" />
<ComboBoxItem Content="فارسی" Tag="fa" />
<ComboBoxItem Content="Suomi" Tag="fi" />
<ComboBoxItem Content="Français" Tag="fr" />
<ComboBoxItem Content="Gaeilge" Tag="ga" />
<ComboBoxItem Content="Galego" Tag="gl" />
<ComboBoxItem Content="עברית" Tag="he" />
<ComboBoxItem Content="हिन्दी" Tag="hi" />
<ComboBoxItem Content="Magyar" Tag="hu" />
<ComboBoxItem Content="Bahasa Indonesia" Tag="id" />
<ComboBoxItem Content="Gaeilge" Tag="ga" />
<ComboBoxItem Content="Italiano" Tag="it" />
<ComboBoxItem Content="日本語" Tag="ja" />
<ComboBoxItem Content="한국어" Tag="ko" />
<ComboBoxItem Content="فارسی" Tag="fa" />
<ComboBoxItem Content="Кыргызча" Tag="ky" />
<ComboBoxItem Content="Lietuvių" Tag="lt" />
<ComboBoxItem Content="Latviešu" Tag="lv" />
<ComboBoxItem Content="Bahasa Melayu" Tag="ms" />
<ComboBoxItem Content="Norsk bokmål" Tag="nb" />
<ComboBoxItem Content="Nederlands" Tag="nl" />
<ComboBoxItem Content="Português (Brasil)" Tag="pt-BR" />
<ComboBoxItem Content="Polski" Tag="pl" />
<ComboBoxItem Content="Português" Tag="pt" />
<ComboBoxItem Content="Română" Tag="ro" />
<ComboBoxItem Content="Русский" Tag="ru" />
<ComboBoxItem Content="Slovenčina" Tag="sk" />
<ComboBoxItem Content="Español" Tag="es" />
<ComboBoxItem Content="Slovenščina" Tag="sl" />
<ComboBoxItem Content="Shqip" Tag="sq" />
<ComboBoxItem Content="Српски" Tag="sr" />
<ComboBoxItem Content="Svenska" Tag="sv" />
<ComboBoxItem Content="ไทย" Tag="th" />
<ComboBoxItem Content="Filipino" Tag="tl" />
<ComboBoxItem Content="Türkçe" Tag="tr" />
<ComboBoxItem Content="Українська" Tag="uk" />
<ComboBoxItem Content="اردو" Tag="ur" />
<ComboBoxItem Content="Tiếng Việt" Tag="vi" />
<ComboBoxItem Content="中文" Tag="zh" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageTranslationConfig">
<controls:SettingsCard x:Uid="SettingsPageTranslationConfig" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=OneWay}">
<controls:SettingsCard.Description>
<HyperlinkButton Margin="0,6,0,0" NavigateUri="https://github.com/LibreTranslate/LibreTranslate">
<TextBlock
@@ -291,6 +313,29 @@
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- Lyrics phonetic -->
<controls:SettingsExpander
x:Uid="SettingsPageChinese"
IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=OneWay}"
IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsChineseRomanizationEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<ComboBox SelectedIndex="{x:Bind ViewModel.AppSettings.TranslationSettings.ChineseRomanization, Converter={StaticResource EnumToIntConverter}, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPagePinyin" />
<ComboBoxItem x:Uid="SettingsPageJyutping" />
</ComboBox>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="SettingsPageJapanese" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTranslationEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 中文简体繁体偏好 -->
<controls:SettingsCard x:Uid="SettingsPageChinesePreference">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsTraditionalChineseEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Provider info -->
<controls:SettingsCard x:Uid="LyricsPageLyricsProviderPrefix">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.LyricsSearchProvider, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
</controls:SettingsCard>
@@ -311,6 +356,26 @@
IsEnabled="{x:Bind ViewModel.IsLXMusicServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
</StackPanel>
</controls:SettingsCard>
<!-- Apple Music token -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="media-user-token (for Apple Muisc)" />
<controls:SettingsCard
Background="{ThemeResource SystemFillColorCautionBackgroundBrush}"
Foreground="{ThemeResource SystemFillColorCautionBrush}"
Header="Use at your own risk">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBox
MaxWidth="250"
PlaceholderText="media-user-token"
Text="{x:Bind ViewModel.AppleMusicMediaUserToken, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
Command="{x:Bind ViewModel.SaveAppleMusicMediaUserTokenCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE8FB;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.ShortcutTextBox"
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>
<StackPanel Orientation="Horizontal">
<TextBox
x:Name="TextBox"
IsReadOnly="True"
KeyDown="TextBox_KeyDown"
Loaded="TextBox_Loaded" />
<Button
Margin="3,0,0,0"
HorizontalAlignment="Right"
Click="ClearButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE894;}"
Style="{StaticResource GhostButtonStyle}" />
<Button
Margin="3,0,0,0"
HorizontalAlignment="Right"
Click="CheckButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE721;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,113 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Core;
// 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 ShortcutTextBox : UserControl
{
public ShortcutTextBox()
{
InitializeComponent();
}
public static readonly DependencyProperty ShortcutProperty =
DependencyProperty.Register(nameof(Shortcut), typeof(List<string>), typeof(ShortcutTextBox), new PropertyMetadata(default));
public List<string> Shortcut
{
get => (List<string>)GetValue(ShortcutProperty);
set => SetValue(ShortcutProperty, value);
}
private void TextBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
List<string> shortcut = [];
bool ctrl = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
bool shift = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
bool alt = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
bool win = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ctrl)
{
shortcut.Add("Ctrl");
}
if (shift)
{
shortcut.Add("Shift");
}
if (alt)
{
shortcut.Add("Alt");
}
if (win)
{
shortcut.Add("Win");
}
if (e.Key != Windows.System.VirtualKey.Control &&
e.Key != Windows.System.VirtualKey.Shift &&
e.Key != Windows.System.VirtualKey.Menu &&
e.Key != Windows.System.VirtualKey.LeftWindows &&
e.Key != Windows.System.VirtualKey.RightWindows)
{
shortcut.Add(e.Key.ToString());
}
Shortcut = shortcut;
UpdateTextBox();
}
private void UpdateTextBox()
{
TextBox.Text = string.Join(" + ", Shortcut);
}
private void TextBox_Loaded(object sender, RoutedEventArgs e)
{
UpdateTextBox();
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
Shortcut = [];
UpdateTextBox();
}
private void CheckButton_Click(object sender, RoutedEventArgs e)
{
bool registered = GlobalHotKeyHelper.IsHotKeyRegistered(Shortcut);
if (registered)
{
App.Current.SettingsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString("SettingsPageShortcutRegSuccessInfo"),
InfoBarSeverity.Success);
}
else
{
App.Current.SettingsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString("SettingsPageShortcutRegFailInfo"),
InfoBarSeverity.Error);
}
}
}
}

View File

@@ -14,9 +14,9 @@
x:Name="TrayIcon"
x:FieldModifier="public"
ContextMenuMode="SecondWindow"
DoubleClickCommand="{x:Bind ViewModel.OpenLyricsWindowCommand}"
DoubleClickCommand="{x:Bind ViewModel.OpenLyricsCommand}"
IconSource="ms-appx:///Assets/Logo.ico"
LeftClickCommand="{x:Bind ViewModel.OpenLyricsWindowCommand}"
LeftClickCommand="{x:Bind ViewModel.OpenLyricsCommand}"
NoLeftClickDelay="True"
ToolTipText="{x:Bind ViewModel.ToolTipText, Mode=OneWay}">
<tb:TaskbarIcon.ContextFlyout>
@@ -24,15 +24,49 @@
AreOpenCloseAnimationsEnabled="True"
LightDismissOverlayMode="On"
ShowMode="TransientWithDismissOnPointerMoveAway">
<MenuFlyoutItem x:Uid="SystemTrayMusicGallery" Command="{x:Bind ViewModel.OpenMusicGalleryCommand}" />
<MenuFlyoutItem x:Uid="SystemTraySettings" Command="{x:Bind ViewModel.OpenSettingsCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayResetWindowPosition" Command="{x:Bind ViewModel.ResetWindowPositionCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayExit" Command="{x:Bind ViewModel.ExitAppCommand}" />
<MenuFlyout.MenuFlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultMenuFlyoutPresenterStyle}" TargetType="MenuFlyoutPresenter">
<Setter Property="MinWidth" Value="600" />
</Style>
</MenuFlyout.MenuFlyoutPresenterStyle>
<MenuFlyoutItem
x:Uid="SystemTrayLyrics"
Command="{x:Bind ViewModel.OpenLyricsCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE90B;}" />
<MenuFlyoutItem
x:Uid="SystemTrayMusicGallery"
Command="{x:Bind ViewModel.OpenMusicGalleryCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xEA69;}" />
<MenuFlyoutItem
x:Uid="SystemTraySettings"
Command="{x:Bind ViewModel.OpenSettingsCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE713;}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="SystemTrayResetWindowPosition"
Command="{x:Bind ViewModel.ResetWindowPositionCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE923;}" />
<MenuFlyoutItem
x:Uid="SystemTrayUnlock"
Command="{x:Bind ViewModel.UnlockWindowCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE785;}"
Visibility="{x:Bind ViewModel.IsLyricsWindowLocked, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<MenuFlyoutSeparator />
<MenuFlyoutItem
x:Uid="SystemTrayRestart"
Command="{x:Bind ViewModel.RestartAppCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE777;}" />
<MenuFlyoutItem
x:Uid="SystemTrayExit"
Command="{x:Bind ViewModel.ExitAppCommand}"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7E8;}" />
</MenuFlyout>
</tb:TaskbarIcon.ContextFlyout>
</tb:TaskbarIcon>

View File

@@ -0,0 +1,23 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class BoolNegationToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Collapsed : Visibility.Visible;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class BoolToOpacityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
return boolValue ? 1.0 : 0.3;
}
return 1.0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class FPSToTimeSpanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int fps)
{
return TimeSpan.FromSeconds(1.0 / fps);
}
return TimeSpan.FromSeconds(1.0 / 60);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -19,6 +19,7 @@ namespace BetterLyrics.WinUI3.Converter
LyricsSearchProvider.Netease => "网易云音乐",
LyricsSearchProvider.Kugou => "酷狗音乐",
LyricsSearchProvider.AmllTtmlDb => "amll-ttml-db",
LyricsSearchProvider.AppleMusic => "Apple Music",
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalLrcFile"),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalMusicFile"),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderEslrcFile"),

View File

@@ -2,10 +2,6 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
@@ -19,6 +15,7 @@ namespace BetterLyrics.WinUI3.Converter
{
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,

View File

@@ -0,0 +1,30 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class MillisecondsToFormattedTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int milliseconds)
{
return TimeSpan.FromMilliseconds(milliseconds).ToString(@"mm\:ss\.fff");
}
else if (value is double doubleMilliseconds)
{
return TimeSpan.FromMilliseconds(doubleMilliseconds).ToString(@"mm\:ss\.fff");
}
return value?.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class RectToMarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Windows.Foundation.Rect rect)
{
return new Microsoft.UI.Xaml.Thickness(rect.X, rect.Y, 0, 0);
}
return new Microsoft.UI.Xaml.Thickness(0);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class ShortcutToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is List<string> shortcut)
{
return string.Join(" + ", shortcut);
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -19,6 +19,7 @@ namespace BetterLyrics.WinUI3.Converter
TranslationSearchProvider.Netease => "网易云音乐",
TranslationSearchProvider.Kugou => "酷狗音乐",
TranslationSearchProvider.AmllTtmlDb => "amll-ttml-db",
TranslationSearchProvider.AppleMusic => "Apple Music",
TranslationSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalLrcFile"),
TranslationSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalMusicFile"),
TranslationSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderEslrcFile"),

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum ChineseRomanization
{
Pinyin,
Jyutping,
}
}

View File

@@ -67,5 +67,16 @@ namespace BetterLyrics.WinUI3.Enums
_ => ".*",
};
}
public static LyricsSearchProvider? ToLyricsSearchProvider(this LyricsFormat format)
{
return format switch
{
LyricsFormat.Lrc => LyricsSearchProvider.LocalLrcFile,
LyricsFormat.Eslrc => LyricsSearchProvider.LocalEslrcFile,
LyricsFormat.Ttml => LyricsSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}

View File

@@ -15,6 +15,7 @@ namespace BetterLyrics.WinUI3.Enums
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
AppleMusic,
}
public static class LyricsSearchProviderExtensions
@@ -28,6 +29,7 @@ namespace BetterLyrics.WinUI3.Enums
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
_ => throw new System.ArgumentOutOfRangeException(nameof(provider)),
};
}
@@ -41,6 +43,7 @@ namespace BetterLyrics.WinUI3.Enums
LyricsSearchProvider.Kugou => LyricsFormat.Krc,
LyricsSearchProvider.Netease => LyricsFormat.Lrc,
LyricsSearchProvider.AmllTtmlDb => LyricsFormat.Ttml,
LyricsSearchProvider.AppleMusic => LyricsFormat.Ttml,
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
LyricsSearchProvider.LocalEslrcFile => LyricsFormat.Eslrc,
LyricsSearchProvider.LocalTtmlFile => LyricsFormat.Ttml,
@@ -71,6 +74,7 @@ namespace BetterLyrics.WinUI3.Enums
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
LyricsSearchProvider.AppleMusic => TranslationSearchProvider.AppleMusic,
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,

View File

@@ -2,10 +2,11 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum AutoStartWindowType
public enum LyricsWindowMode
{
StandardMode,
DockMode,
DesktopMode,
PictureInPictureMode,
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum ShortcutID
{
LyricsWindowShowOrHide,
DesktopLockOrUnlock,
DesktopToggle,
DockToggle,
PictureInPictureToggle,
PlayOrPauseSong,
NextSong,
PreviousSong,
}
}

View File

@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Enums
Netease,
LrcLib,
AmllTtmlDb,
AppleMusic,
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,

View File

@@ -0,0 +1,14 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class LyricsChangedEventArgs(LyricsData? lyricsData) : EventArgs
{
public LyricsData? LyricsData { get; } = lyricsData;
}
}

View File

@@ -0,0 +1,126 @@
using BetterLyrics.WinUI3.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class AppleMusic
{
private readonly HttpClient _client;
private string _accessToken = "";
private string _storefront = "";
private string _language = "";
public AppleMusic()
{
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36");
_client.DefaultRequestHeaders.Add("Accept", "application/json");
_client.DefaultRequestHeaders.Add("Origin", "https://music.apple.com");
_client.DefaultRequestHeaders.Add("Referer", "https://music.apple.com/");
}
public async Task<bool> InitAsync()
{
await GetAccessTokenAsync();
await SetMediaUserTokenAsync();
return
!string.IsNullOrEmpty(_accessToken) &&
!string.IsNullOrEmpty(PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey));
}
private async Task GetAccessTokenAsync()
{
var resp = await _client.GetStringAsync("https://music.apple.com/us/browse");
var jsMatch = Regex.Match(resp, "(?<=index)(.*?)(?=\\.js\")");
if (!jsMatch.Success) throw new Exception("Failed to find index.js");
var jsUrl = $"https://music.apple.com/assets/index{jsMatch.Value}.js";
var jsResp = await _client.GetStringAsync(jsUrl);
var tokenMatch = Regex.Match(jsResp, "(?=eyJh)(.*?)(?=\")");
if (!tokenMatch.Success) throw new Exception("Failed to find access token");
_accessToken = tokenMatch.Value;
_client.DefaultRequestHeaders.Remove("Authorization");
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_accessToken}");
}
private async Task SetMediaUserTokenAsync()
{
_client.DefaultRequestHeaders.Remove("media-user-token");
_client.DefaultRequestHeaders.Add("media-user-token",
PasswordVaultHelper.Get(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey));
var resp = await _client.GetStringAsync("https://amp-api.music.apple.com/v1/me/storefront");
var json = JsonSerializer.Deserialize(resp, Serialization.SourceGenerationContext.Default.JsonElement);
_storefront = json.GetProperty("data")[0].GetProperty("id").ToString();
_language = json.GetProperty("data")[0].GetProperty("attributes").GetProperty("defaultLanguageTag").ToString();
_client.DefaultRequestHeaders.Remove("Accept-Language");
_client.DefaultRequestHeaders.Add("Accept-Language", $"{_language},en;q=0.9");
}
public async Task<string?> GetLyricsAsync(string title, string artist)
{
string id = await SearchSongInfoAsync(artist, title);
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/songs/{id}";
var url = apiUrl + $"?include[songs]=lyrics,syllable-lyrics&l={_language}";
var resp = await _client.GetStringAsync(url);
var json = JsonSerializer.Deserialize(resp, Serialization.SourceGenerationContext.Default.JsonElement);
var data = json.GetProperty("data");
if (data.GetArrayLength() == 0) return string.Empty;
var song = data[0];
if (!song.TryGetProperty("relationships", out var relationships))
return string.Empty;
if (relationships.TryGetProperty("syllable-lyrics", out var syllableLyrics) &&
syllableLyrics.GetProperty("data").GetArrayLength() > 0)
{
var syllableLyric = syllableLyrics.GetProperty("data")[0];
if (syllableLyric.TryGetProperty("attributes", out var attributes) &&
attributes.TryGetProperty("ttml", out var ttml))
{
string? raw = ttml.GetString();
if (raw != null && raw.Contains("begin=") && raw.Contains("end="))
{
return raw;
}
}
}
//if (relationships.TryGetProperty("lyrics", out var lyrics) &&
// lyrics.GetProperty("data").GetArrayLength() > 0)
//{
// var lyric = lyrics.GetProperty("data")[0];
// if (lyric.TryGetProperty("attributes", out var attributes) &&
// attributes.TryGetProperty("ttml", out var ttml))
// {
// return ttml.GetString();
// }
//}
return null;
}
private async Task<string> SearchSongInfoAsync(string artist, string title)
{
var query = $"{artist} {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);
var json = JsonSerializer.Deserialize(resp, Serialization.SourceGenerationContext.Default.JsonElement);
var results = json.GetProperty("results");
if (results.TryGetProperty("songs", out var songs) && songs.GetProperty("data").GetArrayLength() > 0)
{
var song = songs.GetProperty("data")[0];
return song.GetProperty("id").ToString();
}
return string.Empty;
}
}
}

View File

@@ -1,11 +1,19 @@
using Microsoft.Graphics.Canvas.Brushes;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
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 Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Graphics.Effects;
using Windows.UI;
namespace BetterLyrics.WinUI3.Helper
@@ -29,5 +37,324 @@ namespace BetterLyrics.WinUI3.Helper
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.TextGeometry == null)
{
return list;
}
ds.DrawGeometry(lyricsLine.TextGeometry, lyricsLine.Position, strokeColor, strokeWidth); // 描边
}
if (lyricsLine.CanvasTextLayout == null)
{
return list;
}
ds.DrawTextLayout(lyricsLine.CanvasTextLayout, lyricsLine.Position, 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.CanvasTextLayout == null)
{
return mask;
}
var highlightRegion = lyricsLine.CanvasTextLayout.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.Position.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + lyricsLine.Position.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
var fadeOutRect = new Rect(
highlightRect.Right,
highlightRegion.LayoutBounds.Y + lyricsLine.Position.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.CanvasTextLayout == null)
{
return mask;
}
using var ds = mask.CreateDrawingSession();
var regions = lyricsLine.CanvasTextLayout.GetCharacterRegions(0, charStartIndex);
var highlightRegion = lyricsLine.CanvasTextLayout
.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.Position.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.Position.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + lyricsLine.Position.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.Position.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.CanvasTextLayout == null)
{
return mask;
}
var regions = lyricsLine.CanvasTextLayout.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.Position.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
ds.FillRectangle(rect, Colors.White);
}
}
return mask;
}
public static CanvasCommandList CreateTranslationHighlightMask(ICanvasAnimatedControl control, LyricsLine lyricsLine)
{
var mask = new CanvasCommandList(control);
using var ds = mask.CreateDrawingSession();
if (lyricsLine.CanvasTextLayout == null)
{
return mask;
}
var regions = lyricsLine.CanvasTextLayout.GetCharacterRegions(lyricsLine.OriginalText.Length, lyricsLine.DisplayedText.Length - 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.Position.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
{
Source = new AlphaMaskEffect
{
Source = foregroundFontEffect,
AlphaMask = mask,
},
ShadowColor = shadowColor,
BlurAmount = (float)shadowAmount,
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

@@ -94,6 +94,11 @@ namespace BetterLyrics.WinUI3.Helper
return Color.FromArgb(alpha, color.R, color.G, color.B);
}
public static Color WithOpacity(this Color color, float opacity)
{
return Color.FromArgb((byte)(opacity * 255), color.R, color.G, color.B);
}
public static Color WithBrightness(this Color color, double brightness)
{
// 确保亮度因子在合理范围内
@@ -116,15 +121,11 @@ namespace BetterLyrics.WinUI3.Helper
{
case WindowPixelSampleMode.BelowWindow:
{
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(myRect.Left, sampleY, screenWidth, sampleHeight);
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Bottom + 2, screenWidth, 1);
}
case WindowPixelSampleMode.AboveWindow:
{
int sampleHeight = 1;
int sampleY = myRect.Top - 1;
return GetAverageColorFromScreenRegion(myRect.Left, sampleY, screenWidth, sampleHeight);
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - 3, screenWidth, 1);
}
case WindowPixelSampleMode.WindowArea:
{

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class DirectoryHelper
{
/// <summary>
/// 递归查找指定文件夹下所有文件(包括子文件夹)。
/// </summary>
/// <param name="folderPath">要查找的文件夹路径</param>
/// <returns>所有文件的完整路径列表</returns>
public static List<string> GetAllFiles(string folderPath, string searchPattern = "*")
{
var files = new List<string>();
if (!Directory.Exists(folderPath))
return files;
try
{
files.AddRange(Directory.GetFiles(folderPath, searchPattern));
foreach (var dir in Directory.GetDirectories(folderPath))
{
files.AddRange(GetAllFiles(dir, searchPattern));
}
}
catch (Exception)
{
// 可根据需要处理异常,如权限不足等
}
return files;
}
}
}
}

View File

@@ -24,6 +24,11 @@ namespace BetterLyrics.WinUI3.Helper
private static readonly Dictionary<IntPtr, RECT> _originalPositions = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyle = [];
public static bool IsEnabled(IntPtr hwnd)
{
return _registered.Contains(hwnd);
}
public static void Disable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);

View File

@@ -2,7 +2,9 @@
using BetterLyrics.WinUI3.Enums;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Ude;
@@ -78,5 +80,13 @@ namespace BetterLyrics.WinUI3.Helper
|| normFileName == normQ2 + normQ1;
}
public static readonly string[] MusicExtensions = {
".mp3", ".aac", ".m4a", ".ogg", ".opus", ".wma", ".amr",
".flac", ".alac", ".ape", ".wv", ".tak",
".wav", ".aiff", ".aif", ".pcm", ".cda", ".dsf", ".dff", ".au", ".snd",
".mid", ".midi", ".mod", ".xm", ".it", ".s3m"
};
public static string MusicSearchPattern => string.Join("|", MusicExtensions.Select(x => $"*{x}"));
}
}

View File

@@ -11,10 +11,6 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class FontHelper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies();
public static string GetUserPreferredFontFamily() => SystemFontFamilies.ElementAtOrDefault(_settingsService.AppSettings.StandardLyricsStyleSettings.SelectedFontFamilyIndex) ?? "Segoe UI";
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies().Order().ToArray();
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.UI.Xaml;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -12,30 +13,92 @@ namespace BetterLyrics.WinUI3.Helper
{
public class GlobalHotKeyHelper
{
private static Dictionary<int, Action> _hotKeyActions = [];
private static int _nextId = 0;
private static Dictionary<int, Action> _actions = [];
private static Dictionary<int, List<string>> _keys = [];
public static void RegisterHotKey(Window window, User32.HotKeyModifiers modifiers, uint key, Action action)
/// <summary>
/// Register a global hotkey for a specific window type
/// </summary>
/// <typeparam name="T">Target window type</typeparam>
/// <param name="id"></param>
/// <param name="keys"></param>
/// <param name="action"></param>
private static void RegisterHotKey<T>(ShortcutID id, List<string> keys, Action action)
{
if (keys.Count == 0) return;
var window = WindowHelper.GetWindowByWindowType<T>();
if (window == null) return;
HWND hwnd = WindowNative.GetWindowHandle(window);
int id = _nextId++;
User32.RegisterHotKey(hwnd, id, modifiers, key);
_hotKeyActions[id] = action;
User32.HotKeyModifiers modifiers = User32.HotKeyModifiers.MOD_NONE;
VirtualKey key = VirtualKey.None;
foreach (var item in keys)
{
if (item == "Ctrl")
{
modifiers |= User32.HotKeyModifiers.MOD_CONTROL;
}
else if (item == "Shift")
{
modifiers |= User32.HotKeyModifiers.MOD_SHIFT;
}
else if (item == "Alt")
{
modifiers |= User32.HotKeyModifiers.MOD_ALT;
}
else if (item == "Win")
{
modifiers |= User32.HotKeyModifiers.MOD_WIN;
}
else
{
key = (VirtualKey)Enum.Parse(typeof(VirtualKey), item, true);
}
}
bool success = User32.RegisterHotKey(hwnd, (int)id, modifiers, (uint)key);
if (success)
{
_actions[(int)id] = action;
_keys[(int)id] = keys;
}
}
public static void UnregisterAllHotKeys(Window window)
private static void UnregisterHotKey<T>(ShortcutID id)
{
var window = WindowHelper.GetWindowByWindowType<T>();
if (window == null) return;
HWND hwnd = WindowNative.GetWindowHandle(window);
foreach (var id in _hotKeyActions.Keys.ToList())
{
User32.UnregisterHotKey(hwnd, id);
_hotKeyActions.Remove(id);
}
User32.UnregisterHotKey(hwnd, (int)id);
_actions.Remove((int)id);
_keys.Remove((int)id);
}
public static void UpdateHotKey<T>(ShortcutID id, List<string> keys, Action action)
{
UnregisterHotKey<T>(id);
RegisterHotKey<T>(id, keys, action);
}
public static bool IsHotKeyRegistered(ShortcutID id)
{
return _actions.ContainsKey((int)id);
}
public static bool IsHotKeyRegistered(List<string> keys)
{
return _keys.ContainsValue(keys);
}
public static bool TryInvokeAction(ShortcutID id)
{
return TryInvokeAction((int)id);
}
public static bool TryInvokeAction(int id)
{
if (_hotKeyActions.TryGetValue(id, out var action))
if (_actions.TryGetValue(id, out var action))
{
action?.Invoke();
return true;

View File

@@ -14,6 +14,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
@@ -177,7 +178,7 @@ namespace BetterLyrics.WinUI3.Helper
square.Mutate(ctx => ctx.DrawImage(image, new Point(offsetX, offsetY), 1f));
using var ms = new MemoryStream();
square.Save(ms, new JpegEncoder());
square.Save(ms, new PngEncoder());
return ms.ToArray();
}
@@ -219,5 +220,68 @@ namespace BetterLyrics.WinUI3.Helper
}
return pixelData;
}
public static async Task<byte[]> DownloadImageAsByteArrayAsync(string url)
{
using var httpClient = new HttpClient();
return await httpClient.GetByteArrayAsync(url);
}
public static byte[]? DataUrlToByteArray(string dataUrl)
{
const string base64Marker = ";base64,";
int base64Index = dataUrl.IndexOf(base64Marker, StringComparison.OrdinalIgnoreCase);
if (base64Index >= 0)
{
string base64Data = dataUrl.Substring(base64Index + base64Marker.Length);
return Convert.FromBase64String(base64Data);
}
else
{
// 非 base64直接取逗号后内容并解码
int commaIndex = dataUrl.IndexOf(',');
if (commaIndex >= 0)
{
string rawData = dataUrl.Substring(commaIndex + 1);
return System.Text.Encoding.UTF8.GetBytes(Uri.UnescapeDataString(rawData));
}
else
{
return null;
}
}
}
public static async Task<byte[]?> GetImageBytesFromUrlAsync(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return null;
}
try
{
if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
// data URL直接解析
return DataUrlToByteArray(url);
}
else if (Uri.TryCreate(url, UriKind.Absolute, out var uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
// 普通网络图片,下载
return await DownloadImageAsByteArrayAsync(url);
}
else
{
// 其他类型暂不支持
return null;
}
}
catch (Exception)
{
return null;
}
}
}
}

View File

@@ -1,13 +1,7 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Helpers.General;
using NTextCat;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TinyPinyin;
using Windows.Globalization;
namespace BetterLyrics.WinUI3.Services
@@ -16,46 +10,84 @@ namespace BetterLyrics.WinUI3.Services
{
private static readonly RankedLanguageIdentifierFactory _factory = new();
private static readonly RankedLanguageIdentifier _identifier;
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public static List<Models.LanguageInfo> SupportedTargetLanguages =>
public static List<Models.LanguageInfo> SupportedTargetLanguages { get; set; } =
[
new Models.LanguageInfo("ar", "العربية"),
new Models.LanguageInfo("az", "Azərbaycan dili"),
new Models.LanguageInfo("zh-Hans", "简体中文"),
new Models.LanguageInfo("zh-Hant", "繁體中文"),
new Models.LanguageInfo("bg", "Български"),
new Models.LanguageInfo("bn", "বাংলা"),
new Models.LanguageInfo("ca", "Català"),
new Models.LanguageInfo("cs", "Čeština"),
new Models.LanguageInfo("da", "Dansk"),
new Models.LanguageInfo("nl", "Nederlands"),
new Models.LanguageInfo("de", "Deutsch"),
new Models.LanguageInfo("el", "Ελληνικά"),
new Models.LanguageInfo("en", "English"),
new Models.LanguageInfo("eo", "Esperanto"),
new Models.LanguageInfo("es", "Español"),
new Models.LanguageInfo("et", "Eesti"),
new Models.LanguageInfo("eu", "Euskara"),
new Models.LanguageInfo("fa", "فارسی"),
new Models.LanguageInfo("fi", "Suomi"),
new Models.LanguageInfo("fr", "Français"),
new Models.LanguageInfo("de", "Deutsch"),
new Models.LanguageInfo("el", "Ελληνικά"),
new Models.LanguageInfo("ga", "Gaeilge"),
new Models.LanguageInfo("gl", "Galego"),
new Models.LanguageInfo("he", "עברית"),
new Models.LanguageInfo("hi", "हिन्दी"),
new Models.LanguageInfo("hu", "Magyar"),
new Models.LanguageInfo("id", "Bahasa Indonesia"),
new Models.LanguageInfo("ga", "Gaeilge"),
new Models.LanguageInfo("it", "Italiano"),
new Models.LanguageInfo("ja", "日本語"),
new Models.LanguageInfo("ko", "한국어"),
new Models.LanguageInfo("fa", "فارسی"),
new Models.LanguageInfo("ky", "Кыргызча"),
new Models.LanguageInfo("lt", "Lietuvių"),
new Models.LanguageInfo("lv", "Latviešu"),
new Models.LanguageInfo("ms", "Bahasa Melayu"),
new Models.LanguageInfo("nb", "Norsk bokmål"),
new Models.LanguageInfo("nl", "Nederlands"),
new Models.LanguageInfo("pt-BR", "Português (Brasil)"),
new Models.LanguageInfo("pl", "Polski"),
new Models.LanguageInfo("pt", "Português"),
new Models.LanguageInfo("ro", "Română"),
new Models.LanguageInfo("ru", "Русский"),
new Models.LanguageInfo("sk", "Slovenčina"),
new Models.LanguageInfo("es", "Español"),
new Models.LanguageInfo("sl", "Slovenščina"),
new Models.LanguageInfo("sq", "Shqip"),
new Models.LanguageInfo("sr", "Српски"),
new Models.LanguageInfo("sv", "Svenska"),
new Models.LanguageInfo("th", "ไทย"),
new Models.LanguageInfo("tl", "Filipino"),
new Models.LanguageInfo("tr", "Türkçe"),
new Models.LanguageInfo("uk", "Українська"),
new Models.LanguageInfo("ur", "اردو"),
new Models.LanguageInfo("vi", "Tiếng Việt"),
new Models.LanguageInfo("zh", "中文"),
];
static LanguageHelper()
{
_identifier = _factory.Load(PathHelper.LanguageProfilePath);
RomajiConverter.Core.Helpers.RomajiHelper.Init();
}
public static string? DetectLanguageCode(string? text)
@@ -67,9 +99,8 @@ namespace BetterLyrics.WinUI3.Services
code = code switch
{
"simple" => "en",
"zh_classical" => "zh-Hant",
"zh_yue" => "zh-Hant",
"zh" => "zh-Hans",
"zh_classical" => "zh",
"zh_yue" => "zh",
_ => code
};
return code;
@@ -84,31 +115,17 @@ namespace BetterLyrics.WinUI3.Services
};
}
public static string ConvertToCountryCode(string? languageCode)
public static string GetDefaultTargetLanguageCode()
{
if (languageCode == null) return "us";
return languageCode switch
var found = SupportedTargetLanguages.Find(x => ApplicationLanguages.Languages.FirstOrDefault()?.Contains(x.Code) == true);
if (found == null)
{
"zh" => "cn",
"zh-Hans" => "cn",
"zh-Hant" => "tw",
"ja" => "jp",
"ko" => "kr",
_ => "us"
};
}
public static string GetUserTargetLanguageCode()
{
return SupportedTargetLanguages[_settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageIndex].Code;
}
public static int GetDefaultTargetLanguageIndex()
{
int found = SupportedTargetLanguages.FindIndex(x => ApplicationLanguages.Languages.FirstOrDefault()?.Contains(x.Code) == true);
if (found == -1) found = 7; // 默认使用英语
return found;
return "en";
}
else
{
return found.Code;
}
}
public static string GetOrderChar(string text)
@@ -118,12 +135,17 @@ namespace BetterLyrics.WinUI3.Services
if (char.IsLetter(c) && c < 128)
return char.ToUpper(c).ToString();
if (PinyinHelper.IsChinese(c))
if (Pinyin.Pinyin.Instance.IsHanzi(c.ToString()))
{
return PinyinHelper.GetPinyinInitials($"{c}");
return Pinyin.Pinyin.Instance.HanziToPinyin(c.ToString(), Pinyin.ManTone.Style.NORMAL).ToStr().ToUpper().FirstOrDefault().ToString();
}
return "#";
}
public static string ToRomaji(string text)
{
return string.Join(" ", RomajiConverter.Core.Helpers.RomajiHelper.SentenceToRomaji(text).Select(x => x.Romaji));
}
}
}

View File

@@ -1,5 +1,4 @@
using Nito.AsyncEx;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -10,33 +9,25 @@ namespace BetterLyrics.WinUI3.Helper
{
public class LatestOnlyTaskRunner
{
private readonly AsyncLock _mutex = new();
private CancellationTokenSource _cts;
private CancellationTokenSource? _cts;
public async Task RunAsync(Func<CancellationToken, Task> action)
public async Task RunAsync(Func<CancellationToken, Task> taskFactory)
{
CancellationTokenSource oldCts;
_cts?.Cancel();
_cts?.Dispose();
// 使用 AsyncLock 保证线程安全
using (await _mutex.LockAsync())
{
// 取消旧的
oldCts = _cts;
_cts = new CancellationTokenSource();
}
oldCts?.Cancel();
oldCts?.Dispose();
CancellationToken token = _cts.Token;
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
await action(token);
await taskFactory(token);
}
catch (OperationCanceledException)
{
// 可以选择忽略取消异常
}
catch (Exception)
{
}
}
}

View File

@@ -9,7 +9,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Windows.Globalization.Fonts;
using LyricsData = BetterLyrics.WinUI3.Models.LyricsData;
namespace BetterLyrics.WinUI3.Helper
@@ -47,10 +46,74 @@ namespace BetterLyrics.WinUI3.Helper
break;
}
}
FillRomanizationLyricsData();
_lyricsDataArr.Add(new LyricsData()); // 为机翻预留
return _lyricsDataArr;
}
private void FillRomanizationLyricsData()
{
var chinese = _lyricsDataArr.Where(x => x.LanguageCode == "zh").FirstOrDefault();
if (chinese != null)
{
_lyricsDataArr.Add(new LyricsData
{
LanguageCode = "pinyin",
LyricsLines = chinese.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = Pinyin.Pinyin.Instance.HanziToPinyin(line.OriginalText).ToStr(),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = Pinyin.Pinyin.Instance.HanziToPinyin(c.Text).ToStr(),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
_lyricsDataArr.Add(new LyricsData
{
LanguageCode = "jyutping",
LyricsLines = chinese.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = Pinyin.Jyutping.Instance.HanziToPinyin(line.OriginalText).ToStr(),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = Pinyin.Jyutping.Instance.HanziToPinyin(c.Text).ToStr(),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
var japanese = _lyricsDataArr.Where(x => x.LanguageCode == "ja").FirstOrDefault();
if (japanese != null)
{
_lyricsDataArr.Add(new LyricsData
{
LanguageCode = "romaji",
LyricsLines = japanese.LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
OriginalText = LanguageHelper.ToRomaji(line.OriginalText),
LyricsChars = line.LyricsChars.Select(c => new LyricsChar
{
StartMs = c.StartMs,
EndMs = c.EndMs,
Text = LanguageHelper.ToRomaji(c.Text),
StartIndex = c.StartIndex
}).ToList()
}).ToList()
});
}
}
private void ParseLrc(string raw)
{
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
@@ -182,10 +245,11 @@ namespace BetterLyrics.WinUI3.Helper
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 只获取一级span且排除ttm:role="x-bg"span
// 只获取一级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-bg" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-roman")
.ToList();
// 原文和翻译分离

View File

@@ -1,17 +1,9 @@
// 2025/6/23 by Zhe Fang
using Windows.ApplicationModel;
namespace BetterLyrics.WinUI3.Helper
{
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.FileProperties;
public static class MetadataHelper
{
public static string AppVersion

View File

@@ -1,4 +1,5 @@
using System;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
@@ -6,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.Helper
{
@@ -60,5 +62,14 @@ namespace BetterLyrics.WinUI3.Helper
var primaryMonitorInfo = GetPrimaryMonitorInfoEx();
return primaryMonitorInfo.szDevice;
}
public static User32.MONITORINFOEX GetMonitorInfoExFromWindow(Window window)
{
var hwnd = WindowNative.GetWindowHandle(window);
var hMonitor = User32.MonitorFromWindow(hwnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST);
User32.MONITORINFOEX monitorInfoEx = new() { cbSize = (uint)Marshal.SizeOf<User32.MONITORINFOEX>() };
User32.GetMonitorInfo(hMonitor, ref monitorInfoEx);
return monitorInfoEx;
}
}
}

View File

@@ -46,6 +46,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string QQLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "qq");
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
public static string AppleMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "apple-music");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.json");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
@@ -53,6 +54,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string QQTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "qq");
public static string NeteaseTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "netease");
public static string KugouTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "kugou");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
@@ -69,9 +71,11 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(AppleMusicCacheDirectory);
Directory.CreateDirectory(QQTranslationCacheDirectory);
Directory.CreateDirectory(NeteaseTranslationCacheDirectory);
Directory.CreateDirectory(KugouTranslationCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Graphics;
namespace BetterLyrics.WinUI3.Helper
{
public static class PointHelper
{
public static PointInt32 ToPointInt32(this Point point)
{
return new PointInt32((int)point.X, (int)point.Y);
}
}
}

View File

@@ -1,7 +1,6 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Diagnostics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
@@ -11,6 +10,8 @@ namespace BetterLyrics.WinUI3.Helper
{
private T _currentValue;
private double _durationSeconds;
private double _delaySeconds;
private double _delayRemaining;
private EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
@@ -19,18 +20,21 @@ namespace BetterLyrics.WinUI3.Helper
private T _targetValue;
public double DurationSeconds => _durationSeconds;
public double DelaySeconds => _delaySeconds;
public bool IsTransitioning => _isTransitioning;
public T Value => _currentValue;
public T TargetValue => _targetValue;
public EasingType? EasingType => _easingType;
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null)
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null, double delaySeconds = 0)
{
_currentValue = initialValue;
_startValue = initialValue;
_targetValue = initialValue;
_durationSeconds = durationSeconds;
_delaySeconds = delaySeconds;
_delayRemaining = 0;
_progress = 1f;
_isTransitioning = false;
@@ -46,7 +50,7 @@ namespace BetterLyrics.WinUI3.Helper
}
else
{
_easingType = Enums.EasingType.Linear;
_easingType = Enums.EasingType.EaseInOutQuad;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
}
@@ -58,12 +62,18 @@ namespace BetterLyrics.WinUI3.Helper
_durationSeconds = seconds;
}
public void SetDelay(double seconds)
{
_delaySeconds = seconds;
}
private void JumpTo(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 1f;
_delayRemaining = 0;
_isTransitioning = false;
}
@@ -73,6 +83,7 @@ namespace BetterLyrics.WinUI3.Helper
_startValue = value;
_targetValue = value;
_progress = 0f;
_delayRemaining = 0;
_isTransitioning = false;
}
@@ -89,6 +100,7 @@ namespace BetterLyrics.WinUI3.Helper
_startValue = _currentValue;
_targetValue = targetValue;
_progress = 0f;
_delayRemaining = _delaySeconds;
_isTransitioning = true;
}
}
@@ -103,7 +115,24 @@ namespace BetterLyrics.WinUI3.Helper
{
if (!_isTransitioning) return;
_progress += (double)(elapsedTime / TimeSpan.FromSeconds(_durationSeconds));
if (_delayRemaining > 0)
{
double consume = Math.Min(_delayRemaining, elapsedTime.TotalSeconds);
_delayRemaining -= consume;
if (_delayRemaining > 0)
return;
elapsedTime = TimeSpan.FromSeconds(elapsedTime.TotalSeconds - consume);
}
if (_durationSeconds <= 0)
{
_progress = 1f;
}
else
{
_progress += elapsedTime.TotalSeconds / _durationSeconds;
}
if (_progress >= 1f)
{
_progress = 1f;
@@ -178,4 +207,4 @@ namespace BetterLyrics.WinUI3.Helper
_interpolator = GetInterpolatorByEasingType(easingType);
}
}
}
}

View File

@@ -18,6 +18,10 @@ namespace BetterLyrics.WinUI3.Helper
public static void CloseWindow<T>()
{
if (typeof(T) == typeof(LyricsWindow))
{
EnsureDockModeReleased();
}
var window = _activeWindows.Find(w => w is T);
if (window is Window w)
{
@@ -55,15 +59,41 @@ namespace BetterLyrics.WinUI3.Helper
{
window = new MusicGalleryWindow();
}
else if (typeof(T) == typeof(LyricsSearchWindow))
{
window = new LyricsSearchWindow();
}
else
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
TrackWindow(window);
var castedWindow = (Window)window;
castedWindow.Restore();
castedWindow.Activate();
if (typeof(T) == typeof(LyricsWindow))
{
var lyricsWindow = (LyricsWindow)window;
lyricsWindow.ViewModel.InitShortcuts();
lyricsWindow.AutoSelectLyricsMode();
lyricsWindow.ViewModel.SetIsAlwaysOnTop();
}
}
else
{
var castedWindow = (Window)window;
if (typeof(T) == typeof(LyricsWindow))
{
var lyricsWindow = (LyricsWindow)window;
lyricsWindow.Show();
}
else
{
castedWindow.Restore();
castedWindow.Activate();
}
}
var castedWindow = (Window)window;
castedWindow.Restore();
castedWindow.Activate();
}
public static void RestartApp(string args = "")
@@ -88,12 +118,17 @@ namespace BetterLyrics.WinUI3.Helper
public static void ExitApp()
{
LyricsWindow? lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
EnsureDockModeReleased();
Environment.Exit(0);
}
private static void EnsureDockModeReleased()
{
LyricsWindow? lyricsWindow = GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
DockModeHelper.Disable(lyricsWindow);
}
Environment.Exit(0);
}
private static void TrackWindow(object window)

View File

@@ -0,0 +1,76 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models
{
public partial class LiveStates : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsWindowMode LyricsWindowMode { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsDisplayType LyricsDisplayType { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysOnTop { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsStyleSettings LyricsStyleSettings { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings LyricsEffectSettings { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect LyricsWindowBounds { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect LyricsWindowMonitorBounds { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect DemoLyricsWindowBounds { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect DemoLyricsWindowMonitorBounds { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsWindowMonitorName { get; set; }
public LiveStates(AppSettings appSettings)
{
LyricsWindowMode = LyricsWindowMode.StandardMode;
LyricsDisplayType = appSettings.StandardModeSettings.LyricsDisplayType;
LyricsStyleSettings = appSettings.StandardLyricsStyleSettings;
LyricsEffectSettings = appSettings.StandardLyricsEffectSettings;
IsAlwaysOnTop = false;
}
public void ToggleLyricsWindowMode(LyricsWindowMode mode)
{
if (LyricsWindowMode == mode)
{
LyricsWindowMode = LyricsWindowMode.StandardMode;
}
else
{
LyricsWindowMode = mode;
}
}
partial void OnLyricsWindowBoundsChanged(Rect value)
{
double factor = 0.1;
var lyricsWindow = WindowHelper.GetWindowByWindowType<Views.LyricsWindow>();
if (lyricsWindow == null) return;
var mointor = MonitorHelper.GetMonitorInfoExFromWindow(lyricsWindow);
LyricsWindowMonitorName = mointor.szDevice;
LyricsWindowMonitorBounds = new Rect(
mointor.rcMonitor.Left,
mointor.rcMonitor.Top,
mointor.rcMonitor.Width,
mointor.rcMonitor.Height
);
DemoLyricsWindowBounds = new Rect(
(value.X - mointor.rcMonitor.Left) * factor,
(value.Y - mointor.rcMonitor.Top) * factor,
value.Width * factor,
value.Height * factor
);
DemoLyricsWindowMonitorBounds = new Rect(
mointor.rcMonitor.Left * factor,
mointor.rcMonitor.Top * factor,
mointor.rcMonitor.Width * factor,
mointor.rcMonitor.Height * factor
);
}
}
}

View File

@@ -6,15 +6,15 @@ namespace BetterLyrics.WinUI3.Models
{
public partial class LocalMediaFolder : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Path { get; set; }
public LocalMediaFolder() { }
public LocalMediaFolder(string path, bool isEnabled)
public LocalMediaFolder(string path)
{
Path = path;
IsEnabled = isEnabled;
}
}
}

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using Lyricify.Lyrics.Helpers.General;
using System;
@@ -13,7 +14,12 @@ namespace BetterLyrics.WinUI3.Models
public class LyricsData
{
public List<LyricsLine> LyricsLines { get; set; }
public string? LanguageCode => LanguageHelper.DetectLanguageCode(WrappedOriginalText);
private string? _languageCode;
public string? LanguageCode
{
get => _languageCode ?? LanguageHelper.DetectLanguageCode(WrappedOriginalText);
set => _languageCode = value;
}
public string WrappedOriginalText => string.Join(StringHelper.NewLine, LyricsLines.Select(line => line.OriginalText));
public LyricsData()
@@ -36,23 +42,7 @@ namespace BetterLyrics.WinUI3.Models
if (transLine != null)
{
if (translationData.LanguageCode?.StartsWith("zh") == true)
{
string tmp = "";
if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hant")
{
tmp = ChineseConverter.ConvertToTraditionalChinese(transLine.OriginalText);
}
else if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hans")
{
tmp = ChineseConverter.ConvertToSimplifiedChinese(transLine.OriginalText);
}
line.DisplayedText = $"{line.OriginalText}{separator}{tmp}";
}
else
{
line.DisplayedText = $"{line.OriginalText}{separator}{transLine.OriginalText}";
}
line.DisplayedText = $"{line.OriginalText}{separator}{transLine.OriginalText}";
}
else
{
@@ -136,5 +126,18 @@ namespace BetterLyrics.WinUI3.Models
},
]);
}
public LyricsLine? GetLyricsLine(double sec)
{
for (int i = 0; i < LyricsLines.Count; i++)
{
var line = LyricsLines[i];
if (line.StartMs > sec * 1000)
{
return LyricsLines.ElementAtOrDefault(i - 1);
}
}
return null;
}
}
}

View File

@@ -25,32 +25,32 @@ namespace BetterLyrics.WinUI3.Models
public ValueTransition<double> AngleTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutSine
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> BlurAmountTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutSine
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> HighlightOpacityTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutSine
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> OpacityTransition { get; set; } = new(
initialValue: 0,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutSine
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> ScaleTransition { get; set; } = new(
initialValue: 0.75,
durationSeconds: _animationDuration,
easingType: EasingType.EaseInOutSine
easingType: EasingType.EaseInOutQuad
);
public ValueTransition<double> YOffsetTransition { get; set; } = new(
initialValue: 0,
durationSeconds: 0.5,
easingType: EasingType.EaseInOutSine
easingType: EasingType.EaseInOutQuad
);
public CanvasTextLayout? CanvasTextLayout { get; private set; }
@@ -69,39 +69,6 @@ namespace BetterLyrics.WinUI3.Models
public CanvasGeometry? TextGeometry { get; private set; }
/// <summary>
/// 背景文字层(底字)
/// </summary>
public CanvasCommandList? BackgroundFontEffect { get; private set; }
/// <summary>
/// 背景层
/// </summary>
public OpacityEffect? BackgroundEffect { get; private set; }
/// <summary>
/// 辉光层
/// </summary>
public GaussianBlurEffect? ForegroundBlurEffect { get; private set; }
/// <summary>
/// 高亮层
/// </summary>
public AlphaMaskEffect? ForegroundHighlightEffect { get; private set; }
/// <summary>
/// 前景文字层
/// </summary>
public CanvasCommandList? ForegroundFontEffect { get; private set; }
public CanvasCommandList? ComposedLineEffect { get; private set; }
public CanvasCommandList? CurrentCharMask { get; private set; }
public CanvasCommandList? LineStartToCurrentCharMask { get; private set; }
public CanvasCommandList? CurrentLineMask { get; private set; }
public CanvasCommandList? PlaceholderEffect { get; private set; }
public void UpdateCenterPosition(double maxWidth, TextAlignmentType type)
{
if (CanvasTextLayout == null)
@@ -118,10 +85,15 @@ namespace BetterLyrics.WinUI3.Models
};
}
public void RecreateTextLayout(ICanvasAnimatedControl control, CanvasTextFormat textFormat, double maxWidth, double maxHeight, TextAlignmentType type)
public void DisposeTextLayout()
{
CanvasTextLayout?.Dispose();
CanvasTextLayout = null;
}
public void RecreateTextLayout(ICanvasAnimatedControl control, CanvasTextFormat textFormat, double maxWidth, double maxHeight, TextAlignmentType type)
{
DisposeTextLayout();
CanvasTextLayout = new CanvasTextLayout(control, DisplayedText, textFormat, (float)maxWidth, (float)maxHeight);
CanvasTextLayout.HorizontalAlignment = type.ToCanvasHorizontalAlignment();
}
@@ -141,392 +113,5 @@ namespace BetterLyrics.WinUI3.Models
}
TextGeometry = CanvasGeometry.CreateText(CanvasTextLayout);
}
public void DisposeFontEffects()
{
BackgroundFontEffect?.Dispose();
BackgroundFontEffect = null;
ForegroundFontEffect?.Dispose();
ForegroundFontEffect = null;
}
public void RecreateFontEffect(ICanvasAnimatedControl control, Color strokeColor, int strokeWidth, Color bgFontColor, Color fgFontColor)
{
DisposeFontEffects();
if (TextGeometry == null)
{
return;
}
BackgroundFontEffect = new CanvasCommandList(control);
using var bgFontEffectDs = BackgroundFontEffect.CreateDrawingSession();
ForegroundFontEffect = new CanvasCommandList(control);
using var fgFontEffectDs = ForegroundFontEffect.CreateDrawingSession();
// 大于 0 才描边,避免不必要的资源浪费
if (strokeWidth > 0)
{
bgFontEffectDs.DrawGeometry(TextGeometry, Position, strokeColor, strokeWidth); // 描边
fgFontEffectDs.DrawGeometry(TextGeometry, Position, strokeColor, strokeWidth); // 描边
}
bgFontEffectDs.FillGeometry(TextGeometry, Position, bgFontColor); // 填充
fgFontEffectDs.FillGeometry(TextGeometry, Position, fgFontColor); // 填充
}
/// <summary>
/// 背景层
/// </summary>
/// <param name="lyricsLayerOpacity">_lyricsOpacityTransition.Value</param>
public void RecreateBackgroundEffect(double lyricsLayerOpacity)
{
BackgroundEffect?.Dispose();
BackgroundEffect = null;
if (BackgroundFontEffect == null)
{
return;
}
BackgroundEffect = new OpacityEffect
{
Source = new GaussianBlurEffect
{
Source = BackgroundFontEffect,
BlurAmount = (float)BlurAmountTransition.Value,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Speed,
},
Opacity = (float)(OpacityTransition.Value * lyricsLayerOpacity),
};
}
public void UpdateBackgroundEffect(double lyricsLayerOpacity)
{
BackgroundEffect?.Opacity = (float)(OpacityTransition.Value * lyricsLayerOpacity);
GaussianBlurEffect? blurEffect = (GaussianBlurEffect?)(BackgroundEffect?.Source);
blurEffect?.BlurAmount = (float)BlurAmountTransition.Value;
}
private IGraphicsEffectSource GetAlphaMask(ICanvasAnimatedControl control, LineRenderingType lineRenderingType)
{
if (PlaceholderEffect == null)
{
RecreatePlaceholder(control);
}
var result = lineRenderingType switch
{
LineRenderingType.CurrentChar => CurrentCharMask,
LineRenderingType.LineStartToCurrentChar => LineStartToCurrentCharMask,
// Here, cuz AlphaMask only takes care of alpha channel
// so ForegroundFontEffect can be a mask for CurrentLine
// And we don't need to create a new mask for CurrentLine
LineRenderingType.CurrentLine => CurrentLineMask,
_ => PlaceholderEffect
};
return result ?? PlaceholderEffect!;
}
/// <summary>
/// 销毁并重新创建辉光效果层
/// 仅需在布局重构 (Relayout) 时调用
/// </summary>
/// <param name="lineRenderingType">_lyricsGlowEffectScope</param>
/// <param name="glowEffectAmount">_lyricsGlowEffectAmount</param>
public void RecreateForegroundBlurEffect(ICanvasAnimatedControl control, LineRenderingType lineRenderingType, double glowEffectAmount)
{
ForegroundBlurEffect?.Dispose();
ForegroundBlurEffect = null;
if (ForegroundFontEffect == null)
{
return;
}
var mask = GetAlphaMask(control, lineRenderingType);
if (mask == null)
{
return;
}
ForegroundBlurEffect = new GaussianBlurEffect
{
Source = new AlphaMaskEffect
{
Source = ForegroundFontEffect,
AlphaMask = mask,
},
BlurAmount = (float)glowEffectAmount,
Optimization = EffectOptimization.Speed,
};
}
/// <summary>
/// 仅当前行需要调用此方法(每次 Update 都调用一次)
/// </summary>
/// <param name="control"></param>
/// <param name="lineRenderingType"></param>
/// <param name="glowEffectAmount"></param>
public void UpdateForegroundBlurEffect(ICanvasAnimatedControl control, LineRenderingType lineRenderingType, double glowEffectAmount)
{
if (ForegroundBlurEffect == null)
{
return;
}
if (ForegroundFontEffect == null)
{
return;
}
var mask = GetAlphaMask(control, lineRenderingType);
if (mask == null)
{
return;
}
ForegroundBlurEffect.BlurAmount = (float)glowEffectAmount;
var alphaMaskEffect = (AlphaMaskEffect)ForegroundBlurEffect.Source;
alphaMaskEffect.Source = ForegroundFontEffect;
alphaMaskEffect.AlphaMask = mask;
}
/// <summary>
/// 销毁并重新创建高亮效果层
/// 仅需在布局重构 (Relayout) 时调用
/// </summary>
/// <param name="control"></param>
/// <param name="lineRenderingType"></param>
public void RecreateForegroundHighlightEffect(ICanvasAnimatedControl control, LineRenderingType lineRenderingType)
{
ForegroundHighlightEffect?.Dispose();
ForegroundHighlightEffect = null;
if (ForegroundFontEffect == null)
{
return;
}
var mask = GetAlphaMask(control, lineRenderingType);
if (mask == null)
{
return;
}
ForegroundHighlightEffect = new AlphaMaskEffect
{
Source = ForegroundFontEffect,
AlphaMask = mask,
};
}
/// <summary>
/// 仅当前行需要调用此方法(每次 Update 都调用一次)
/// </summary>
/// <param name="control"></param>
/// <param name="lineRenderingType"></param>
public void UpdateForegroundHighlightEffect(ICanvasAnimatedControl control, LineRenderingType lineRenderingType)
{
if (ForegroundHighlightEffect == null)
{
return;
}
if (ForegroundFontEffect == null)
{
return;
}
var mask = GetAlphaMask(control, lineRenderingType);
if (mask == null)
{
return;
}
ForegroundHighlightEffect.Source = ForegroundFontEffect;
ForegroundHighlightEffect.AlphaMask = mask;
}
/// <summary>
/// 仅当前播放行需要调用此方法(每次 Update 都调用一次)
/// </summary>
/// <param name="control"></param>
/// <param name="playingLineIndex"></param>
/// <param name="charStartIndex"></param>
/// <param name="charLength"></param>
/// <param name="charProgress"></param>
public void RecreateCurrentCharMask(ICanvasAnimatedControl control, int charStartIndex, int charLength, double charProgress)
{
CurrentCharMask?.Dispose();
CurrentCharMask = null;
CurrentCharMask = new CanvasCommandList(control);
if (CanvasTextLayout == null)
{
return;
}
using var ds = CurrentCharMask.CreateDrawingSession();
var highlightRegion = CanvasTextLayout
.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 + Position.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + Position.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
var fadeOutRect = new Rect(
highlightRect.Right,
highlightRegion.LayoutBounds.Y + Position.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);
}
/// <summary>
/// 仅当前播放行需要调用此方法(每次 Update 都调用一次)
/// </summary>
/// <param name="control"></param>
/// <param name="playingLineIndex"></param>
/// <param name="charStartIndex"></param>
/// <param name="charLength"></param>
/// <param name="charProgress"></param>
public void RecreateLineStartToCurrentCharMask(ICanvasAnimatedControl control, int charStartIndex, int charLength, double charProgress)
{
LineStartToCurrentCharMask?.Dispose();
LineStartToCurrentCharMask = null;
LineStartToCurrentCharMask = new CanvasCommandList(control);
if (CanvasTextLayout == null)
{
return;
}
using var ds = LineStartToCurrentCharMask.CreateDrawingSession();
var regions = CanvasTextLayout.GetCharacterRegions(0, charStartIndex);
var highlightRegion = CanvasTextLayout
.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 + Position.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 + Position.Y,
highlightWidth,
highlightRegion.LayoutBounds.Height
);
var fadeInRect = new Rect(
highlightRect.Right - fadingWidth,
highlightRegion.LayoutBounds.Y + Position.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
var fadeOutRect = new Rect(
highlightRect.Right,
highlightRegion.LayoutBounds.Y + Position.Y,
fadingWidth,
highlightRegion.LayoutBounds.Height
);
// Brushes
using var fadeOutBrush = CanvasHelper.CreateHorizontalFillBrush(
control,
[(0f, 1f), (1f, 0f)],
(double)highlightRect.Right,
fadingWidth
);
ds.FillRectangle(highlightRect, Color.FromArgb(255, 128, 128, 128));
ds.FillRectangle(fadeOutRect, fadeOutBrush);
}
/// <summary>
/// 重建当前行遮罩
/// 仅需在布局重构 (Relayout) 时调用
/// </summary>
/// <param name="control"></param>
public void RecreateCurrentLineMask(ICanvasAnimatedControl control)
{
CurrentLineMask?.Dispose();
CurrentLineMask = null;
if (CanvasTextLayout == null)
{
return;
}
CurrentLineMask = new CanvasCommandList(control);
using var ds = CurrentLineMask.CreateDrawingSession();
var regions = CanvasTextLayout.GetCharacterRegions(0, 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 + Position.Y,
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
ds.FillRectangle(rect, Colors.White);
}
}
}
public void RecreatePlaceholder(ICanvasAnimatedControl control)
{
PlaceholderEffect?.Dispose();
PlaceholderEffect = null;
PlaceholderEffect = new CanvasCommandList(control);
}
}
}

View File

@@ -0,0 +1,20 @@
using BetterLyrics.WinUI3.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsSearchResult
{
public bool IsFound => !string.IsNullOrEmpty(Raw);
public LyricsSearchProvider? Provider { get; set; }
public string? Raw { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
}
}

View File

@@ -0,0 +1,36 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class MappedSongSearchQuery : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalTitle { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalArtist { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string MappedTitle { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string MappedArtist { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsMarkedAsPureMusic { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchProvider? LyricsSearchProvider { get; set; }
public MappedSongSearchQuery Clone()
{
return new MappedSongSearchQuery
{
OriginalTitle = this.OriginalTitle,
OriginalArtist = this.OriginalArtist,
MappedTitle = this.MappedTitle,
MappedArtist = this.MappedArtist,
IsMarkedAsPureMusic = this.IsMarkedAsPureMusic,
LyricsSearchProvider = this.LyricsSearchProvider
};
}
}
}

View File

@@ -12,23 +12,29 @@ namespace BetterLyrics.WinUI3.Models
{
public partial class MediaSourceProviderInfo : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Provider { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLastFMTrackEnabled { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLastFMTrackEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTimelineSyncEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int TimelineSyncThreshold { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PositionOffset { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ResetPositionOffsetOnSongChanged { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ResetPositionOffsetOnSongChanged { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; } = [.. Enum.GetValues<LyricsSearchProvider>().Select(p => new LyricsSearchProviderInfo(p, true))];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; } = [.. Enum.GetValues<AlbumArtSearchProvider>().Select(p => new AlbumArtSearchProviderInfo(p, true))];
public MediaSourceProviderInfo() { }
public MediaSourceProviderInfo()
{
Provider = string.Empty;
TimelineSyncThreshold = 0;
PositionOffset = 0;
}
public MediaSourceProviderInfo(string provider) : base()
{
@@ -40,18 +46,13 @@ namespace BetterLyrics.WinUI3.Models
PositionOffset = 1000;
break;
default:
// 设置 100 以防不必要的重复同步
TimelineSyncThreshold = 100;
// 设置 300 以防不必要的重复同步
TimelineSyncThreshold = 300;
PositionOffset = 0;
break;
}
Provider = provider;
IsEnabled = true;
IsLastFMTrackEnabled = false;
ResetPositionOffsetOnSongChanged = false;
LyricsSearchProvidersInfo = [.. Enum.GetValues<LyricsSearchProvider>().Select(p => new LyricsSearchProviderInfo(p, true))];
AlbumArtSearchProvidersInfo = [.. Enum.GetValues<AlbumArtSearchProvider>().Select(p => new AlbumArtSearchProviderInfo(p, true))];
}
partial void OnAlbumArtSearchProvidersInfoChanged(FullyObservableCollection<AlbumArtSearchProviderInfo> oldValue, FullyObservableCollection<AlbumArtSearchProviderInfo> newValue)

View File

@@ -0,0 +1,16 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class AdvancedSettings : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFixedTimeStep { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FPS { get; set; } = 60;
}
}

View File

@@ -12,6 +12,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
[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 int SongInfoFontSize { get; set; } = 18;
public AlbumArtLayoutSettings() { }
}

View File

@@ -9,26 +9,33 @@ namespace BetterLyrics.WinUI3.Models.Settings
public partial class AppSettings : ObservableRecipient
{
public string Version { get; set; } = Helper.MetadataHelper.AppVersion;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsStyleSettings StandardLyricsStyleSettings { get; set; } = new LyricsStyleSettings(32, TextAlignmentType.Left, 0);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsStyleSettings DesktopLyricsStyleSettings { get; set; } = new LyricsStyleSettings(28, TextAlignmentType.Center, 2);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsStyleSettings DockLyricsStyleSettings { get; set; } = new LyricsStyleSettings(16, TextAlignmentType.Center, 0);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsStyleSettings PictureInPictureLyricsStyleSettings { get; set; } = new LyricsStyleSettings(28, TextAlignmentType.Left, 0);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings StandardLyricsEffectSettings { get; set; } = new LyricsEffectSettings(100, 500, 1000, EasingType.EaseInOutSine);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings DesktopLyricsEffectSettings { get; set; } = new LyricsEffectSettings(500, 500, 500, EasingType.EaseInOutSine);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings DockLyricsEffectSettings { get; set; } = new LyricsEffectSettings(500, 500, 500, EasingType.EaseInOutSine);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings StandardLyricsEffectSettings { get; set; } = new LyricsEffectSettings(100, 500, 1000, EasingType.EaseInOutQuad);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings DesktopLyricsEffectSettings { get; set; } = new LyricsEffectSettings(500, 500, 500, EasingType.EaseInOutQuad);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings DockLyricsEffectSettings { get; set; } = new LyricsEffectSettings(500, 500, 500, EasingType.EaseInOutQuad);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectSettings PictureInPictureLyricsEffectSettings { get; set; } = new LyricsEffectSettings(500, 500, 500, EasingType.EaseInOutQuad);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial StandardModeSettings StandardModeSettings { get; set; } = new StandardModeSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DesktopModeSettings DesktopModeSettings { get; set; } = new DesktopModeSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DockModeSettings DockModeSettings { get; set; } = new DockModeSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PictureInPictureModeSettings PictureInPictureModeSettings { get; set; } = new PictureInPictureModeSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsBackgroundSettings LyricsBackgroundSettings { get; set; } = new LyricsBackgroundSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AlbumArtLayoutSettings AlbumArtLayoutSettings { get; set; } = new AlbumArtLayoutSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TranslationSettings TranslationSettings { get; set; } = new TranslationSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial GeneralSettings GeneralSettings { get; set; } = new GeneralSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial MusicGallerySettings MusicGallerySettings { get; set; } = new MusicGallerySettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AdvancedSettings AdvancedSettings { get; set; } = new AdvancedSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<WindowBoundsRecord> WindowBoundsRecords { get; set; } = [];
public AppSettings() { }
}

View File

@@ -0,0 +1,15 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class BaseModeSettings : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsDisplayType LyricsDisplayType { get; set; }
}
}

View File

@@ -9,12 +9,17 @@ using Windows.Graphics;
namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class DesktopModeSettings : ObservableRecipient
public partial class DesktopModeSettings : BaseModeSettings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect WindowBounds { get; set; } = new Rect(100, 100, 400, 200);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool AutoLockOnDesktopMode { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LockHotKeyIndex { get; set; } = 'U' - 'A'; // Default to 'U' key
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> LockShortcut { get; set; } = new List<string>() { "Ctrl", "Alt", "U" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ToggleShortcut { get; set; } = new List<string>() { "Ctrl", "Alt", "D" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysOnTop { get; set; } = true;
public DesktopModeSettings() { }
public DesktopModeSettings()
{
LyricsDisplayType = Enums.LyricsDisplayType.LyricsOnly;
}
}
}

View File

@@ -9,12 +9,16 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class DockModeSettings : ObservableRecipient
public partial class DockModeSettings : BaseModeSettings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DockPlacement DockPlacement { get; set; } = DockPlacement.Top;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int DockWindowHeight { get; set; } = 64;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string DockMonitorDeviceName { get; set; } = MonitorHelper.GetPrimaryMonitorDeviceName();
public DockModeSettings() { }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ToggleShortcut { get; set; } = new List<string> { "Ctrl", "Shift", "D" };
public DockModeSettings()
{
LyricsDisplayType = LyricsDisplayType.LyricsOnly;
}
}
}

View File

@@ -10,14 +10,18 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class GeneralSettings : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AutoStartWindowType AutoStartWindowType { get; set; } = AutoStartWindowType.StandardMode;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsWindowMode AutoStartWindowType { get; set; } = LyricsWindowMode.StandardMode;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Language Language { get; set; } = Language.FollowSystem;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IgnoreFullscreenWindow { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LXMusicServer { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool HideWindowWhenNotPlaying { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ShowOrHideLyricsWindowShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "H" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsDragEverywhereEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsDisplayType DisplayType { get; set; } = LyricsDisplayType.SplitView;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsImmersiveMode { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnLyricsWindowClosed { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> PlayOrPauseShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "P" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> NextSongShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "Right" };
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> PreviousSongShortcut { get; set; } = new List<string> { "Ctrl", "Alt", "Left" };
public GeneralSettings() { }
}

View File

@@ -11,18 +11,36 @@ namespace BetterLyrics.WinUI3.Models.Settings
public partial class LyricsEffectSettings : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsBlurAmount { get; set; } = 5;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsLineFadeEnabled { get; set; } = false;
[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 LyricsHighlightScope { get; set; } = LineRenderingType.LineStartToCurrentChar;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsHighlightAmount { get; set; } = 100; // 100% 是上界
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsTranslationHighlightAmount { get; set; } = 60; // 100% 是上界
[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; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollTopDuration { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollBottomDuration { get; set; }
[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 bool IsLyricsGlowEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial EasingType LyricsScrollEasingType { get; set; }
public LyricsEffectSettings(int lyricsScrollTopDuration, int lyricsScrollDuration, int lyricsScrollBottomDuration, EasingType lyricsScrollEasingType)
public LyricsEffectSettings(int lyricsScrollTopDuration, int lyricsScrollDuration, int lyricsScrollBottomDuration, EasingType lyricsScrollEasingType)
{
LyricsScrollTopDuration = lyricsScrollTopDuration;
LyricsScrollDuration = lyricsScrollDuration;

View File

@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class LyricsStyleSettings : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsDynamicLyricsFontSize { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsFontSize { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TextAlignmentType LyricsAlignmentType { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsBgFontOpacity { get; set; } = 30; // 30% opacity
@@ -27,7 +28,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[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 LyricsFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SelectedFontFamilyIndex { get; set; } = 0;
public LyricsStyleSettings() { }

View File

@@ -0,0 +1,21 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class PictureInPictureModeSettings : BaseModeSettings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Point WindowPosition { get; set; } = new Point(100, 100);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial List<string> ToggleShortcut { get; set; } = new List<string>() { "Ctrl", "Shift", "P" };
public PictureInPictureModeSettings()
{
LyricsDisplayType = Enums.LyricsDisplayType.SplitView;
}
}
}

View File

@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -9,10 +10,15 @@ using Windows.Graphics;
namespace BetterLyrics.WinUI3.Models.Settings
{
public partial class StandardModeSettings : ObservableRecipient
public partial class StandardModeSettings : BaseModeSettings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect WindowBounds { get; set; } = new Rect(100, 100, 1000, 600);
public StandardModeSettings() { }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsMaximized { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysOnTop { get; set; } = false;
public StandardModeSettings()
{
LyricsDisplayType = LyricsDisplayType.SplitView;
}
}
}

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
@@ -14,7 +15,12 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LibreTranslateServer { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTranslationEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ShowTranslationOnly { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int SelectedTargetLanguageIndex { get; set; } = LanguageHelper.GetDefaultTargetLanguageIndex();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string SelectedTargetLanguageCode { get; set; } = LanguageHelper.GetDefaultTargetLanguageCode();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsJyutpingEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial ChineseRomanization ChineseRomanization { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsChineseRomanizationEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsJapaneseRomanizationEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTraditionalChineseEnabled { get; set; } = false;
public TranslationSettings() { }
}

View File

@@ -21,11 +21,14 @@ namespace BetterLyrics.WinUI3.Models
public partial double? DurationMs { get; set; }
[ObservableProperty]
public partial string? SourceAppUserModelId { get; set; } = null;
public partial string? PlayerId { get; set; } = null;
[ObservableProperty]
public partial string Title { get; set; }
[ObservableProperty]
public partial string? SongId { get; set; } = null;
public SongInfo() { }
}
}

View File

@@ -0,0 +1,19 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models
{
public partial class WindowBoundsRecord : ObservableObject
{
[ObservableProperty] public partial string MonitorDeviceName { get; set; } = string.Empty;
[ObservableProperty] public partial Rect WindowBounds { get; set; }
[ObservableProperty] public partial Rect DemoWindowBounds { get; set; }
[ObservableProperty] public partial Rect MonitorBounds { get; set; }
[ObservableProperty] public partial Rect DemoMonitorBounds { get; set; }
}
}

View File

@@ -8,6 +8,7 @@
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Renderer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Unloaded="LyricsCanvas_Unloaded"
mc:Ignorable="d">
<Grid>
@@ -15,15 +16,5 @@
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Update="LyricsCanvas_Update" />
<Grid
Margin="36"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Visibility="{x:Bind ViewModel.IsTranslating, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<FontIcon
x:Name="RotatingIcon"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE8C1;" />
</Grid>
</Grid>
</UserControl>

View File

@@ -25,5 +25,11 @@ namespace BetterLyrics.WinUI3.Renderer
{
ViewModel.Update(sender, args);
}
private void LyricsCanvas_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
LyricsCanvas.RemoveFromVisualTree();
LyricsCanvas = null;
}
}
}

View File

@@ -1,6 +1,7 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -11,6 +12,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
@@ -29,38 +31,46 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
_iTunesHttpClinet = new();
}
public async Task<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC = null)
public async Task<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token)
{
byte[]? result = null;
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == mediaSessionId).FirstOrDefault()?.AlbumArtSearchProvidersInfo ?? [])
try
{
if (!provider.IsEnabled)
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == mediaSessionId).FirstOrDefault()?.AlbumArtSearchProvidersInfo ?? [])
{
continue;
}
if (!provider.IsEnabled)
{
continue;
}
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchFile(artist, title);
break;
case AlbumArtSearchProvider.SMTC:
result = bytesFromSMTC;
break;
case AlbumArtSearchProvider.iTunes:
foreach (string countryCode in new List<string>() { "us", "cn", "jp", "kr" })
{
result = await SearchiTunesAsync(artist, album, title, countryCode);
if (result != null) break;
}
break;
default:
break;
}
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchFile(artist, title);
break;
case AlbumArtSearchProvider.SMTC:
result = bytesFromSMTC;
break;
case AlbumArtSearchProvider.iTunes:
foreach (string countryCode in new List<string>() { "us", "cn", "jp", "kr" })
{
result = await SearchiTunesAsync(artist, album, title, countryCode);
if (token.IsCancellationRequested) return result;
if (result != null) break;
}
break;
default:
break;
}
if (result != null) return result;
if (result != null) return result;
}
}
catch (Exception)
{
}
return null;
}
@@ -70,15 +80,18 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
{
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
if ((track.Title == title && track.Artist == artist) || FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), artist, title))
{
return bytes;
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
}
}
}

View File

@@ -2,12 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
{
public interface IAlbumArtSearchService
{
Task<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC = null);
Task<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token);
}
}

View File

@@ -52,7 +52,7 @@ namespace BetterLyrics.WinUI3.Services.LibWatcherService
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
{
if (!folders.Any(x => x.Path == key && x.IsEnabled))
if (!folders.Any(x => x.Path == key && x.IsEnabled && x.IsRealTimeWatchEnabled))
{
_watchers[key].Dispose();
_watchers.Remove(key);
@@ -62,11 +62,7 @@ namespace BetterLyrics.WinUI3.Services.LibWatcherService
// 添加新的监听
foreach (var folder in folders)
{
if (
!_watchers.ContainsKey(folder.Path)
&& Directory.Exists(folder.Path)
&& folder.IsEnabled
)
if (!_watchers.ContainsKey(folder.Path) && Directory.Exists(folder.Path) && folder.IsEnabled && folder.IsRealTimeWatchEnabled)
{
var watcher = new FileSystemWatcher(folder.Path)
{

View File

@@ -0,0 +1,14 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.LiveStatesService
{
public interface ILiveStatesService
{
LiveStates LiveStates { get; set; }
}
}

View File

@@ -0,0 +1,139 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.LiveStatesService
{
public class LiveStatesService : BaseViewModel, ILiveStatesService,
IRecipient<PropertyChangedMessage<LyricsWindowMode>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<bool>>
{
private readonly ISettingsService _settingsService;
public LiveStates LiveStates { get; set; }
public LiveStatesService(ISettingsService settingsService)
{
_settingsService = settingsService;
LiveStates = new LiveStates(_settingsService.AppSettings);
}
public void Receive(PropertyChangedMessage<LyricsWindowMode> message)
{
if (message.Sender is LiveStates)
{
if (message.PropertyName == nameof(LiveStates.LyricsWindowMode))
{
switch (message.NewValue)
{
case LyricsWindowMode.StandardMode:
LiveStates.LyricsStyleSettings = _settingsService.AppSettings.StandardLyricsStyleSettings;
LiveStates.LyricsEffectSettings = _settingsService.AppSettings.StandardLyricsEffectSettings;
LiveStates.LyricsDisplayType = _settingsService.AppSettings.StandardModeSettings.LyricsDisplayType;
LiveStates.IsAlwaysOnTop = _settingsService.AppSettings.StandardModeSettings.IsAlwaysOnTop;
break;
case LyricsWindowMode.DockMode:
LiveStates.LyricsStyleSettings = _settingsService.AppSettings.DockLyricsStyleSettings;
LiveStates.LyricsEffectSettings = _settingsService.AppSettings.DockLyricsEffectSettings;
LiveStates.LyricsDisplayType = _settingsService.AppSettings.DockModeSettings.LyricsDisplayType;
LiveStates.IsAlwaysOnTop = true;
break;
case LyricsWindowMode.DesktopMode:
LiveStates.LyricsStyleSettings = _settingsService.AppSettings.DesktopLyricsStyleSettings;
LiveStates.LyricsEffectSettings = _settingsService.AppSettings.DesktopLyricsEffectSettings;
LiveStates.LyricsDisplayType = _settingsService.AppSettings.DesktopModeSettings.LyricsDisplayType;
LiveStates.IsAlwaysOnTop = _settingsService.AppSettings.DesktopModeSettings.IsAlwaysOnTop;
break;
case LyricsWindowMode.PictureInPictureMode:
LiveStates.LyricsStyleSettings = _settingsService.AppSettings.PictureInPictureLyricsStyleSettings;
LiveStates.LyricsEffectSettings = _settingsService.AppSettings.PictureInPictureLyricsEffectSettings;
LiveStates.LyricsDisplayType = _settingsService.AppSettings.PictureInPictureModeSettings.LyricsDisplayType;
// IsAlwaysOnTop 由系统托管
break;
default:
break;
}
}
}
}
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
if (message.Sender is StandardModeSettings)
{
if (message.PropertyName == nameof(StandardModeSettings.LyricsDisplayType))
{
if (LiveStates.LyricsWindowMode == LyricsWindowMode.StandardMode)
{
LiveStates.LyricsDisplayType = message.NewValue;
}
}
}
else if (message.Sender is DockModeSettings)
{
if (message.PropertyName == nameof(DockModeSettings.LyricsDisplayType))
{
if (LiveStates.LyricsWindowMode == LyricsWindowMode.DockMode)
{
LiveStates.LyricsDisplayType = message.NewValue;
}
}
}
else if (message.Sender is DesktopModeSettings)
{
if (message.PropertyName == nameof(DesktopModeSettings.LyricsDisplayType))
{
if (LiveStates.LyricsWindowMode == LyricsWindowMode.DesktopMode)
{
LiveStates.LyricsDisplayType = message.NewValue;
}
}
}
else if (message.Sender is PictureInPictureModeSettings)
{
if (message.PropertyName == nameof(PictureInPictureModeSettings.LyricsDisplayType))
{
if (LiveStates.LyricsWindowMode == LyricsWindowMode.PictureInPictureMode)
{
LiveStates.LyricsDisplayType = message.NewValue;
}
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is StandardModeSettings)
{
if (message.PropertyName == nameof(StandardModeSettings.IsAlwaysOnTop))
{
if (LiveStates.LyricsWindowMode == LyricsWindowMode.StandardMode)
{
LiveStates.IsAlwaysOnTop = message.NewValue;
}
}
}
else if (message.Sender is DesktopModeSettings)
{
if (message.PropertyName == nameof(DesktopModeSettings.IsAlwaysOnTop))
{
if (LiveStates.LyricsWindowMode == LyricsWindowMode.DesktopMode)
{
LiveStates.IsAlwaysOnTop = message.NewValue;
}
}
}
}
}
}

View File

@@ -1,14 +1,18 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
public interface ILyricsSearchService
{
Task<(string?, LyricsSearchProvider?)> SearchAsync(string mediaSessionId, string title, string artist, string album, double durationMs, CancellationToken token);
Task<LyricsSearchResult> SearchSmartlyAsync(string mediaSessionId, string title, string artist, string album, double durationMs, string? songId, CancellationToken token);
Task<List<LyricsSearchResult>> SearchAllAsync(string title, string artist, string album, double durationMs, CancellationToken token);
}
}

View File

@@ -3,6 +3,8 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Providers.Web.Kugou;
@@ -10,6 +12,7 @@ using Lyricify.Lyrics.Searchers;
using Microsoft.Extensions.Logging;
using NTextCat.Commons;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -25,6 +28,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
private readonly HttpClient _amllTtmlDbHttpClient;
private readonly HttpClient _lrcLibHttpClient;
private readonly AppleMusic _appleMusic;
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
@@ -37,9 +41,10 @@ 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.GitHubUrl})"
);
_amllTtmlDbHttpClient = new();
_appleMusic = new AppleMusic();
}
private static bool IsAmllTtmlDbIndexInvalid()
@@ -86,143 +91,236 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
public async Task<(string?, LyricsSearchProvider?)> SearchAsync(string mediaSessionId, string title, string artist, string album, double durationMs, CancellationToken token)
public async Task<LyricsSearchResult> SearchSmartlyAsync(string mediaSessionId, string title, string artist, string album, double durationMs, string? songId, CancellationToken token)
{
var lyricsSearchResult = new LyricsSearchResult();
string overridenTitle = title;
string overridenArtist = artist;
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
var found = _settingsService.AppSettings.MappedSongSearchQueries
.Where(x => x.OriginalTitle == title && x.OriginalArtist == artist)
.FirstOrDefault();
if (found != null)
{
overridenTitle = found.MappedTitle;
overridenArtist = found.MappedArtist;
_logger.LogInformation("Found mapped song search query: {MappedSongSearchQuery}", found);
var pureMusic = found.IsMarkedAsPureMusic;
if (pureMusic)
{
lyricsSearchResult.Title = overridenTitle;
lyricsSearchResult.Artist = overridenArtist;
lyricsSearchResult.Raw = "[99:00.000]🎶🎶🎶";
return lyricsSearchResult;
}
var targetProvider = found.LyricsSearchProvider;
if (targetProvider != null)
{
return await SearchSingleAsync(targetProvider.Value, overridenTitle, overridenArtist, album, durationMs, songId, token);
}
}
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == mediaSessionId).FirstOrDefault()?.LyricsSearchProvidersInfo ?? [])
{
if (!provider.IsEnabled)
{
continue;
}
lyricsSearchResult = await SearchSingleAsync(provider.Provider, overridenTitle, overridenArtist, album, durationMs, null, token);
if (lyricsSearchResult.IsFound)
{
return lyricsSearchResult;
}
}
return lyricsSearchResult;
}
public async Task<List<LyricsSearchResult>> SearchAllAsync(string title, string artist, string album, double durationMs, CancellationToken token)
{
_logger.LogInformation("Searching all lyrics for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
var results = new List<LyricsSearchResult>();
foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
{
var searchResult = await SearchSingleAsync(provider, title, artist, album, durationMs, null, token);
results.Add(searchResult);
}
return results;
}
private async Task<LyricsSearchResult> SearchSingleAsync(LyricsSearchProvider provider, string title, string artist, string album, double durationMs, string? songId, CancellationToken token)
{
var lyricsSearchResult = new LyricsSearchResult
{
Provider = provider,
};
try
{
foreach (var provider in _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == mediaSessionId).FirstOrDefault()?.LyricsSearchProvidersInfo ?? [])
LyricsFormat lyricsFormat = provider.GetLyricsFormat();
// Check cache first
if (provider.IsRemote())
{
if (!provider.IsEnabled)
var cachedLyrics = FileHelper.ReadLyricsCache(title, artist, lyricsFormat, provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
continue;
lyricsSearchResult.Raw = cachedLyrics;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
return lyricsSearchResult;
}
}
string? cachedLyrics;
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
// Check cache first
if (provider.Provider.IsRemote())
if (provider.IsLocal())
{
if (provider == LyricsSearchProvider.LocalMusicFile)
{
cachedLyrics = FileHelper.ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, provider.Provider);
}
}
string? searchedLyrics = null;
if (provider.Provider.IsLocal())
{
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = SearchEmbedded(title, artist);
}
else
{
searchedLyrics = await SearchFile(title, artist, lyricsFormat);
}
lyricsSearchResult = SearchEmbedded(title, artist);
}
else
{
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
break;
case LyricsSearchProvider.QQ:
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
break;
default:
break;
}
lyricsSearchResult = await SearchFile(title, artist, lyricsFormat);
}
token.ThrowIfCancellationRequested();
if (!string.IsNullOrWhiteSpace(searchedLyrics))
}
else
{
switch (provider)
{
if (provider.Provider.IsRemote())
{
FileHelper.WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
}
case LyricsSearchProvider.LrcLib:
lyricsSearchResult = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
break;
case LyricsSearchProvider.QQ:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, songId, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, songId, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, songId, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
lyricsSearchResult = await SearchAmllTtmlDbAsync(title, artist);
break;
case LyricsSearchProvider.AppleMusic:
lyricsSearchResult = await SearchAppleMusicAsync(title, artist, album, (int)durationMs);
break;
default:
break;
}
}
return (searchedLyrics, provider.Provider);
if (token.IsCancellationRequested)
{
return lyricsSearchResult;
}
if (lyricsSearchResult.IsFound)
{
if (provider.IsRemote())
{
FileHelper.WriteLyricsCache(title, artist, lyricsSearchResult.Raw!, lyricsFormat, provider.GetCacheDirectory());
}
}
}
catch (Exception) { }
catch (Exception)
{
}
return (null, null);
return lyricsSearchResult;
}
private async Task<string?> SearchFile(string title, string artist, LyricsFormat format)
private async Task<LyricsSearchResult> SearchFile(string title, string artist, LyricsFormat format)
{
var lyricsSearchResult = new LyricsSearchResult
{
Provider = format.ToLyricsSearchProvider(),
};
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*{format.ToFileExtension()}", SearchOption.AllDirectories))
try
{
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
{
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
return raw;
}
}
}
}
}
return null;
}
private string? SearchEmbedded(string title, string artist)
{
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
try
{
var plain = TagLib.File.Create(file).Tag.Lyrics;
if (plain != null && plain != string.Empty)
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
{
return plain;
lyricsSearchResult.Raw = raw;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
}
}
}
}
catch (Exception)
{
}
}
}
return lyricsSearchResult;
}
private LyricsSearchResult SearchEmbedded(string title, string artist)
{
var lyricsSearchResult = new LyricsSearchResult
{
Provider = LyricsSearchProvider.LocalMusicFile,
};
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
{
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
{
var track = new Track(file);
if ((track.Title == title && track.Artist == artist) || FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
var plain = TagLib.File.Create(file).Tag.Lyrics;
if (!plain.IsNullOrEmpty())
{
lyricsSearchResult.Raw = plain;
lyricsSearchResult.Title = track.Title;
lyricsSearchResult.Artist = artist;
}
}
catch (Exception) { }
}
}
}
}
return null;
return lyricsSearchResult;
}
private async Task<string?> SearchAmllTtmlDbAsync(string title, string artist)
private async Task<LyricsSearchResult> SearchAmllTtmlDbAsync(string title, string artist)
{
var lyricsSearchResult = new LyricsSearchResult
{
Provider = LyricsSearchProvider.AmllTtmlDb,
};
if (IsAmllTtmlDbIndexInvalid())
{
var downloadOk = await DownloadAmllTtmlDbIndexAsync();
if (!downloadOk)
return null;
{
return lyricsSearchResult;
}
}
string? rawLyricFile = null;
@@ -265,7 +363,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
if (string.IsNullOrWhiteSpace(rawLyricFile))
return null;
{
return lyricsSearchResult;
}
// 下载歌词内容
var url = $"{Constants.AmllTTmlDB.QueryPrefix}{rawLyricFile}";
@@ -273,17 +373,29 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
using var response = await _amllTtmlDbHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStringAsync();
{
return lyricsSearchResult;
}
string lyrics = await response.Content.ReadAsStringAsync();
lyricsSearchResult.Raw = lyrics;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
}
catch
{
return null;
}
return lyricsSearchResult;
}
private async Task<string?> SearchLrcLibAsync(string title, string artist, string album, int duration)
private async Task<LyricsSearchResult> SearchLrcLibAsync(string title, string artist, string album, int duration)
{
var lyricsSearchResult = new LyricsSearchResult
{
Provider = LyricsSearchProvider.LrcLib,
};
// Build API query URL
var url =
$"https://lrclib.net/api/search?" +
@@ -294,7 +406,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
using var response = await _lrcLibHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
{
return lyricsSearchResult;
}
var json = await response.Content.ReadAsStringAsync();
@@ -302,31 +416,64 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
json,
Serialization.SourceGenerationContext.Default.JsonElement
);
string? original = null;
string? searchedTitle = null;
string? searchedArtist = null;
if (jArr.ValueKind == JsonValueKind.Array && jArr.GetArrayLength() > 0)
{
var first = jArr[0];
var syncedLyrics = first.GetProperty("syncedLyrics").GetString();
var result = string.IsNullOrWhiteSpace(syncedLyrics) ? null : syncedLyrics;
if (!string.IsNullOrWhiteSpace(result))
{
return result;
}
original = first.GetProperty("syncedLyrics").GetString();
searchedTitle = first.GetProperty("trackName").GetString();
searchedArtist = first.GetProperty("artistName").GetString();
}
return null;
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = searchedTitle;
lyricsSearchResult.Artist = searchedArtist;
return lyricsSearchResult;
}
private static async Task<string?> SearchQQNeteaseKugouAsync(string title, string artist, string album, int durationMs, Searchers searchers)
private static async Task<LyricsSearchResult> SearchQQNeteaseKugouAsync(string title, string artist, string album, int durationMs, string? songId, Searchers searchers)
{
var result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs = durationMs,
Album = album,
Artists = [artist],
Title = title,
}
);
var lyricsSearchResult = new LyricsSearchResult();
switch (searchers)
{
case Searchers.QQMusic:
lyricsSearchResult.Provider = LyricsSearchProvider.QQ;
break;
case Searchers.Netease:
lyricsSearchResult.Provider = LyricsSearchProvider.Netease;
break;
case Searchers.Kugou:
lyricsSearchResult.Provider = LyricsSearchProvider.Kugou;
break;
case Searchers.Musixmatch:
break;
default:
break;
}
ISearchResult? result;
if (searchers == Searchers.Netease && songId != null)
{
result = new NeteaseSearchResult(title, [artist], album, null, durationMs, songId);
}
else
{
result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs = durationMs,
Album = album,
Artists = [artist],
Title = title,
}
);
}
if (result is QQMusicSearchResult qqResult)
{
@@ -343,11 +490,15 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
PathHelper.QQTranslationCacheDirectory
);
}
return original;
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = qqResult.Title;
lyricsSearchResult.Artist = qqResult.Artists.Join(" | ");
}
else if (result is NeteaseSearchResult neteaseResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(neteaseResult.Id);
var original = response?.Lrc?.Lyric;
var translated = response?.Tlyric?.Lyric;
if (!string.IsNullOrEmpty(translated))
{
@@ -359,21 +510,59 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
PathHelper.NeteaseTranslationCacheDirectory
);
}
return response?.Lrc.Lyric;
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = neteaseResult.Title;
lyricsSearchResult.Artist = neteaseResult.Artists.Join(" | ");
}
else if (result is KugouSearchResult kugouResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(hash: kugouResult.Hash);
string? original = null;
if (response?.Candidates.FirstOrDefault() is SearchLyricsResponse.Candidate candidate)
{
return Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyrics(
candidate.Id,
candidate.AccessKey
);
original = await Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyricsAsync(candidate.Id, candidate.AccessKey);
if (candidate.TransId != null)
{
string? translated = await Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyricsAsync(candidate.TransId, candidate.AccessKey);
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(
title,
artist,
translated,
LyricsFormat.Lrc,
PathHelper.KugouTranslationCacheDirectory
);
}
}
}
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = kugouResult.Title;
lyricsSearchResult.Artist = kugouResult.Artists.Join(" | ");
}
return null;
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchAppleMusicAsync(string title, string artist, string album, int durationMs)
{
var lyricsSearchResult = new LyricsSearchResult
{
Provider = LyricsSearchProvider.AppleMusic,
};
if (await _appleMusic.InitAsync())
{
var raw = await _appleMusic.GetLyricsAsync(title, artist);
_logger.LogInformation("Apple Music lyrics search result for {Title} - {Artist}: {Raw}", title, artist, raw ?? "null");
lyricsSearchResult.Raw = raw;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
}
return lyricsSearchResult;
}
}
}

View File

@@ -1,9 +1,12 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TagLib.Riff;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
@@ -12,7 +15,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<TimelineChangedEventArgs>? TimelineChanged;
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChanged;
event EventHandler<LyricsChangedEventArgs>? LyricsChanged;
event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
Task PlayAsync();
@@ -23,8 +27,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo();
void UpdateLyrics();
void UpdateTranslations();
bool IsPlaying { get; }
SongInfo? SongInfo { get; }
TimeSpan Position { get; }
LyricsData? CurrentLyricsData { get; }
LyricsSearchProvider? LyricsSearchProvider { get; }
TranslationSearchProvider? TranslationSearchProvider { get; }
}
}

View File

@@ -0,0 +1,71 @@
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
public partial class MediaSessionsService : IMediaSessionsService
{
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();
public event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChanged;
private void UpdateAlbumArt()
{
_albumArtRefreshRunner.RunAsync(RefreshArtAlbum);
}
private async Task RefreshArtAlbum(CancellationToken token)
{
if (_cachedSongInfo == null)
{
_logger.LogWarning("Cached song info is null, cannot update album art.");
return;
}
byte[]? bytes = await Task.Run(async () => await _albumArtSearchService.SearchAsync(
SongInfo?.PlayerId ?? "",
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo?.Album ?? string.Empty,
_SMTCAlbumArtBytes,
token
), token);
if (token.IsCancellationRequested) return;
if (bytes == null)
{
bytes = await ImageHelper.CreateTextPlaceholderBytesAsync(500, 500);
token.ThrowIfCancellationRequested();
}
bytes = ImageHelper.MakeSquareWithThemeColor(bytes);
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
token.ThrowIfCancellationRequested();
var decoder = await BitmapDecoder.CreateAsync(stream);
token.ThrowIfCancellationRequested();
var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap);
token.ThrowIfCancellationRequested();
var albumArtLightAccentColor = ImageHelper.GetAccentColorsFromByte(bytes, 1, false).FirstOrDefault();
var albumArtDarkAccentColor = ImageHelper.GetAccentColorsFromByte(bytes, 1, true).FirstOrDefault();
AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, albumArtLightAccentColor, albumArtDarkAccentColor));
}
}
}

View File

@@ -0,0 +1,263 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using Lyricify.Lyrics.Helpers.General;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
public partial class MediaSessionsService : IMediaSessionsService
{
private LatestOnlyTaskRunner _refreshLyricsRunner = new();
private LatestOnlyTaskRunner _refreshTranslationRunner = new();
private int _langIndex = 0;
private List<LyricsData> _lyricsDataArr = [];
public LyricsData? CurrentLyricsData => _lyricsDataArr.ElementAtOrDefault(_langIndex);
public event EventHandler<LyricsChangedEventArgs>? LyricsChanged;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchProvider? LyricsSearchProvider { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TranslationSearchProvider? TranslationSearchProvider { get; set; }
[ObservableProperty] public partial bool IsTranslating { get; set; } = false;
private async Task RefreshTranslationAsync(CancellationToken token)
{
TranslationSearchProvider = null;
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
IsTranslating = true;
if (_settingsService.AppSettings.TranslationSettings.IsTranslationEnabled)
{
await SetDisplayedAlongWithTranslationsAsync(token);
if (token.IsCancellationRequested) return;
}
else
{
_logger.LogInformation("Translation is disabled, showing original lyrics only.");
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
_langIndex = 0;
}
IsTranslating = false;
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
}
private async Task SetDisplayedAlongWithTranslationsAsync(CancellationToken token)
{
_logger.LogInformation("Showing translation for lyrics...");
string targetLangCode = _settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageCode;
_logger.LogInformation("Target language code: {TargetLangCode}", targetLangCode);
string? originalText = _lyricsDataArr.FirstOrDefault()?.WrappedOriginalText;
if (originalText == null) return;
string? originalLangCode = LanguageHelper.DetectLanguageCode(originalText);
_logger.LogInformation("Original language code: {OriginalLangCode}", originalLangCode ?? "null");
if (originalLangCode == "zh" && _settingsService.AppSettings.TranslationSettings.IsChineseRomanizationEnabled)
{
switch (_settingsService.AppSettings.TranslationSettings.ChineseRomanization)
{
case ChineseRomanization.Pinyin:
targetLangCode = "pinyin";
break;
case ChineseRomanization.Jyutping:
targetLangCode = "jyutping";
break;
default:
break;
}
}
else if (originalLangCode == "ja" && _settingsService.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled)
{
targetLangCode = "romaji";
}
if (originalLangCode == targetLangCode)
{
_logger.LogInformation("Original lyrics already in target language: {TargetLangCode}", targetLangCode);
_lyricsDataArr[0].SetDisplayedTextInOriginalText();
}
else
{
// Try get translation from itself first
int found = _translateService.SearchTranslatedLyricsItself(_lyricsDataArr, targetLangCode);
if (found >= 0)
{
_logger.LogInformation("Found translation in lyrics data at index {FoundIndex}", found);
if (_settingsService.AppSettings.TranslationSettings.ShowTranslationOnly)
{
_lyricsDataArr[found].SetDisplayedTextInOriginalText();
_langIndex = found;
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(_lyricsDataArr[found], _liveStatesService.LiveStates.LyricsStyleSettings.LyricsTranslationSeparator, 50);
_langIndex = 0;
TranslationSearchProvider = LyricsSearchProvider.ToTranslationSearchProvider();
}
}
else if (_settingsService.AppSettings.TranslationSettings.IsLibreTranslateEnabled)
{
_logger.LogInformation("LibreTranslate is enabled, trying to translate lyrics...");
string translated = string.Empty;
try
{
translated = await _translateService.TranslateTextAsync(originalText, targetLangCode, token);
if (token.IsCancellationRequested) return;
if (translated == string.Empty) return;
if (_settingsService.AppSettings.TranslationSettings.ShowTranslationOnly)
{
_lyricsDataArr[^1] = _lyricsDataArr[0].CreateLyricsDataFrom(translated);
_lyricsDataArr[^1].SetDisplayedTextInOriginalText();
_langIndex = _lyricsDataArr.Count - 1;
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(translated, _liveStatesService.LiveStates.LyricsStyleSettings.LyricsTranslationSeparator);
_langIndex = 0;
}
TranslationSearchProvider = Enums.TranslationSearchProvider.LibreTranslate;
}
catch (Exception)
{
App.Current.LyricsWindowNotificationPanel?.Notify(App.ResourceLoader?.GetString("LibreTranslateFailed")!, Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
}
}
}
}
private async Task RefreshLyricsAsync(CancellationToken token)
{
_logger.LogInformation("Refreshing lyrics...");
LyricsSearchProvider = null;
_lyricsDataArr = [LyricsData.GetLoadingPlaceholder()];
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
if (SongInfo != null)
{
_logger.LogInformation("Searching lyrics for: Title={Title}, Artist={Artist}, Album={Album}, DurationMs={DurationMs}",
SongInfo.Title, SongInfo.Artist, SongInfo.Album, SongInfo.DurationMs);
var lyricsSearchResult = await Task.Run(async () => await _lyrcsSearchService.SearchSmartlyAsync(
SongInfo.PlayerId ?? "",
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",
SongInfo.DurationMs ?? 0,
SongInfo.SongId,
token
), token);
if (token.IsCancellationRequested) return;
LyricsSearchProvider = lyricsSearchResult?.Provider;
_logger.LogInformation("Lyrics was found? {Found}, Provider: {LyricsSearchProvider}", lyricsSearchResult?.IsFound, LyricsSearchProvider?.ToString() ?? "null");
_lyricsDataArr = new LyricsParser().Parse(lyricsSearchResult?.Raw, (int?)SongInfo?.DurationMs);
ApplyChinesePreference();
FillTranslationFromCache(LyricsSearchProvider);
}
else
{
_logger.LogWarning("SongInfo is null, cannot search lyrics.");
}
_logger.LogInformation("Parsed lyrics: {MultiLangLyricsCount} languages", _lyricsDataArr.Count);
// This ensures that original lyrics are always shown while waiting for translations
_lyricsDataArr[0].SetDisplayedTextInOriginalText();
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
ApplyChinesePreference();
UpdateTranslations();
}
private void ApplyChinesePreference()
{
var traditionalChinesePreferred = _settingsService.AppSettings.TranslationSettings.IsTraditionalChineseEnabled;
var found = _lyricsDataArr.FindIndex(x => x.LanguageCode == "zh");
if (found >= 0)
{
foreach (var item in _lyricsDataArr[found].LyricsLines)
{
item.OriginalText = traditionalChinesePreferred ? ChineseHelper.S2T(item.OriginalText) : ChineseHelper.T2S(item.OriginalText);
}
}
}
private void FillTranslationFromCache(LyricsSearchProvider? provider)
{
string? translationRaw = null;
switch (provider)
{
case Enums.LyricsSearchProvider.QQ:
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.QQTranslationCacheDirectory);
break;
case Enums.LyricsSearchProvider.Kugou:
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.KugouTranslationCacheDirectory);
break;
case Enums.LyricsSearchProvider.Netease:
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.NeteaseTranslationCacheDirectory);
break;
case Enums.LyricsSearchProvider.LrcLib:
break;
case Enums.LyricsSearchProvider.AmllTtmlDb:
break;
case Enums.LyricsSearchProvider.LocalMusicFile:
break;
case Enums.LyricsSearchProvider.LocalLrcFile:
break;
case Enums.LyricsSearchProvider.LocalEslrcFile:
break;
case Enums.LyricsSearchProvider.LocalTtmlFile:
break;
default:
break;
}
if (translationRaw != null)
{
var translationData = new LyricsParser().Parse(translationRaw, (int?)SongInfo?.DurationMs);
if (provider == Enums.LyricsSearchProvider.QQ)
{
foreach (var data in translationData)
{
foreach (var item in data.LyricsLines)
{
if (item.OriginalText == "//")
{
item.OriginalText = "";
}
}
}
}
_lyricsDataArr = _lyricsDataArr.Concat(translationData).ToList();
}
}
public void UpdateLyrics()
{
_refreshLyricsRunner.RunAsync(RefreshLyricsAsync);
}
public void UpdateTranslations()
{
_refreshTranslationRunner.RunAsync(RefreshTranslationAsync);
}
}
}

View File

@@ -7,27 +7,24 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using EvtSource;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
using Microsoft.UI.Dispatching;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using WindowsMediaController;
@@ -36,14 +33,22 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<FullyObservableCollection<AlbumArtSearchProviderInfo>>>
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<LyricsWindowMode>>,
IRecipient<PropertyChangedMessage<ChineseRomanization>>,
IRecipient<PropertyChangedMessage<List<string>>>
{
private readonly IAlbumArtSearchService _albumArtSearchService;
private readonly ILogger<MediaSessionsService> _logger;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ITranslateService _translateService;
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly ILiveStatesService _liveStatesService;
private readonly ILogger<MediaSessionsService> _logger;
private double _lxMusicPositionSeconds = 0;
private double _lxMusicDurationSeconds = 0;
private byte[]? _lxMusicAlbumArtBytes = null;
private bool _cachedIsPlaying = false;
private TimeSpan _cachedPosition = TimeSpan.Zero;
@@ -52,28 +57,106 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly MediaManager _mediaManager = new();
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();
private readonly LatestOnlyTaskRunner _onAnyMediaPropertyChangedRunner = new();
private SongInfo? _cachedSongInfo;
private byte[]? _SMTCAlbumArtBytes = null;
private int _targetAlbumArtSize = 500;
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<TimelineChangedEventArgs>? TimelineChanged;
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
public event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
public MediaSessionsService(ISettingsService settingsService, IAlbumArtSearchService albumArtSearchService)
public bool IsPlaying => _cachedIsPlaying;
public SongInfo? SongInfo => _cachedSongInfo;
public TimeSpan Position => _cachedPosition;
public MediaSessionsService(
ISettingsService settingsService,
IAlbumArtSearchService albumArtSearchService,
ILyricsSearchService musicSearchService,
ILibWatcherService libWatcherService,
ILiveStatesService liveStatesService,
ITranslateService libreTranslateService)
{
_settingsService = settingsService;
_albumArtSearchService = albumArtSearchService;
_lyrcsSearchService = musicSearchService;
_libWatcherService = libWatcherService;
_translateService = libreTranslateService;
_liveStatesService = liveStatesService;
_logger = Ioc.Default.GetRequiredService<ILogger<MediaSessionsService>>();
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_settingsService.AppSettings.MappedSongSearchQueries.CollectionChanged += MappedSongSearchQueries_CollectionChanged;
_settingsService.AppSettings.MappedSongSearchQueries.ItemPropertyChanged += MappedSongSearchQueries_ItemPropertyChanged;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
InitMediaManager();
InitPlaybackShortcuts();
}
private void MappedSongSearchQueries_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateLyrics();
}
private void MappedSongSearchQueries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateLyrics();
}
private void InitPlaybackShortcuts()
{
UpdatePlayOrPauseSongShortcut();
UpdatePreviousSongShortcut();
UpdateNextSongShortcut();
}
private void UpdatePlayOrPauseSongShortcut()
{
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.PlayOrPauseSong, _settingsService.AppSettings.GeneralSettings.PlayOrPauseShortcut, () =>
{
if (_cachedIsPlaying)
{
_ = PauseAsync();
}
else
{
_ = PlayAsync();
}
});
}
private void UpdatePreviousSongShortcut()
{
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.PreviousSong, _settingsService.AppSettings.GeneralSettings.PreviousSongShortcut, () =>
{
_ = PreviousAsync();
});
}
private void UpdateNextSongShortcut()
{
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.NextSong, _settingsService.AppSettings.GeneralSettings.NextSongShortcut, () =>
{
_ = NextAsync();
});
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private void MediaSourceProvidersInfo_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
@@ -81,25 +164,38 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
switch (e.PropertyName)
{
case nameof(MediaSourceProviderInfo.AlbumArtSearchProvidersInfo):
_ = _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
UpdateAlbumArt();
break;
case nameof(MediaSourceProviderInfo.LyricsSearchProvidersInfo):
UpdateLyrics();
break;
default:
break;
}
}
public bool IsPlaying => _cachedIsPlaying;
public SongInfo? SongInfo => _cachedSongInfo;
public TimeSpan Position => _cachedPosition;
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
public MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
{
var desiredSession = GetCurrentSession();
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == desiredSession?.Id);
}
private bool IsMediaSourceEnabled(string id)
{
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id)?.IsEnabled ?? true;
}
private bool IsMediaSourceTimelineSyncEnabled(string id)
{
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id)?.IsTimelineSyncEnabled ?? true;
}
private void InitMediaManager()
{
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
@@ -110,18 +206,16 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
_mediaManager.Start();
Task.Run(() =>
{
MediaManager_OnFocusedSessionChanged(null);
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSourceProviderInfo(x.Value));
});
MediaManager_OnFocusedSessionChanged(null);
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSourceProviderInfo(x.Value));
}
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
{
if (!_mediaManager.IsStarted) return;
SendFocusedMessagesAsync().ConfigureAwait(false);
SendFocusedMessagesAsync();
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties timelineProperties)
@@ -129,9 +223,9 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var focusedSession = _mediaManager.GetFocusedSession();
var desiredSession = GetCurrentSession();
if (mediaSession != focusedSession) return;
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
@@ -143,113 +237,115 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
else
{
_cachedPosition = timelineProperties.Position;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
if (IsMediaSourceTimelineSyncEnabled(mediaSession.Id))
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(_cachedPosition, timelineProperties.EndTime));
});
_cachedPosition = timelineProperties.Position;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(_cachedPosition, timelineProperties.EndTime));
});
}
}
}
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo playbackInfo)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var focusedSession = _mediaManager.GetFocusedSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != focusedSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_cachedIsPlaying = false;
}
else
{
_cachedIsPlaying = playbackInfo.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_cachedIsPlaying = false;
}
else
{
_cachedIsPlaying = playbackInfo.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
}
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(_cachedIsPlaying));
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties mediaProperties)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
string id = mediaSession.Id;
var focusedSession = _mediaManager.GetFocusedSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != focusedSession) return;
if (!IsMediaSourceEnabled(id))
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
_cachedSongInfo = null;
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
_onAnyMediaPropertyChangedRunner.RunAsync(async token =>
string sessionId = mediaSession.Id;
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(sessionId))
{
_cachedSongInfo = null;
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties.Title, mediaProperties.Artist, mediaProperties.AlbumTitle);
if (id == Constants.PlayerID.LXMusic)
if (sessionId == Constants.PlayerID.LXMusic)
{
StopSSE();
}
_SMTCAlbumArtBytes = null;
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
}
}).ConfigureAwait(false);
}
else
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
}
else
{
var currentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
if (currentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
});
_cachedSongInfo = new SongInfo
{
Title = mediaProperties.Title,
Artist = mediaProperties.Artist,
Album = mediaProperties.AlbumTitle,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
SourceAppUserModelId = id,
};
string fixedArtist = mediaProperties.Artist;
string fixedAlbum = mediaProperties.AlbumTitle;
string? songId = null;
_cachedSongInfo.Duration = (int)(_cachedSongInfo.DurationMs / 1000f);
if (sessionId == Constants.PlayerID.AppleMusic || sessionId == Constants.PlayerID.AppleMusicAlternative)
{
fixedArtist = mediaProperties.Artist.Split(" — ").FirstOrDefault() ?? mediaProperties.Artist;
fixedAlbum = mediaProperties.Artist.Split(" — ").LastOrDefault() ?? mediaProperties.AlbumTitle;
}
else if (sessionId == Constants.PlayerID.NetEaseCloudMusic)
{
songId = mediaProperties.Genres.FirstOrDefault()?.Replace("NCM-", "");
if (songId != null && songId.Length != 10)
{
songId = null;
}
}
_cachedSongInfo = new SongInfo
{
Title = mediaProperties.Title,
Artist = fixedArtist,
Album = fixedAlbum,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
PlayerId = sessionId,
SongId = songId
};
_cachedSongInfo.Duration = (int)(_cachedSongInfo.DurationMs / 1000f);
_onAnyMediaPropertyChangedRunner.RunAsync(async token =>
{
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties.Title, mediaProperties.Artist, mediaProperties.AlbumTitle);
if (id == Constants.PlayerID.LXMusic)
if (sessionId == Constants.PlayerID.LXMusic)
{
StartSSE();
}
@@ -258,7 +354,11 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
StopSSE();
}
if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
if (sessionId == Constants.PlayerID.LXMusic && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBytes = _lxMusicAlbumArtBytes;
}
else if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBytes = await ImageHelper.ToByteArrayAsync(streamReference);
}
@@ -266,21 +366,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_SMTCAlbumArtBytes = null;
}
}
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
}
}).ConfigureAwait(false);
}
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
UpdateAlbumArt();
UpdateLyrics();
});
}
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
@@ -303,6 +394,30 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
SendFocusedMessagesAsync().ConfigureAwait(false);
}
private MediaManager.MediaSession? GetCurrentSession()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession == null)
{
return null;
}
if (IsMediaSourceEnabled(focusedSession.Id))
{
return focusedSession;
}
else
{
foreach (var session in _mediaManager.CurrentMediaSessions.Values)
{
if (IsMediaSourceEnabled(session.Id))
{
return session;
}
}
}
return null;
}
private void RecordMediaSourceProviderInfo(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
@@ -335,62 +450,23 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private async Task SendFocusedMessagesAsync()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession == null || focusedSession.ControlSession == null) return;
var desiredSession = GetCurrentSession();
if (desiredSession == null || desiredSession.ControlSession == null) return;
var mediaProps = await focusedSession.ControlSession.TryGetMediaPropertiesAsync();
MediaManager_OnAnyTimelinePropertyChanged(focusedSession, focusedSession.ControlSession.GetTimelineProperties());
MediaManager_OnAnyMediaPropertyChanged(focusedSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(focusedSession, focusedSession.ControlSession.GetPlaybackInfo());
}
private async Task UpdateAlbumArtRelated(CancellationToken token)
{
if (_cachedSongInfo == null)
{
_logger.LogWarning("Cached song info is null, cannot update album art.");
return;
}
byte[]? bytes = await _albumArtSearchService.SearchAsync(
SongInfo?.SourceAppUserModelId ?? "",
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo?.Album ?? string.Empty,
_SMTCAlbumArtBytes
);
token.ThrowIfCancellationRequested();
if (bytes == null)
{
bytes = await ImageHelper.CreateTextPlaceholderBytesAsync(_targetAlbumArtSize, _targetAlbumArtSize);
token.ThrowIfCancellationRequested();
}
bytes = ImageHelper.Resize(bytes, _targetAlbumArtSize);
bytes = ImageHelper.MakeSquareWithThemeColor(bytes);
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
token.ThrowIfCancellationRequested();
var decoder = await BitmapDecoder.CreateAsync(stream);
token.ThrowIfCancellationRequested();
var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
token.ThrowIfCancellationRequested();
var albumArtLightAccentColor = ImageHelper.GetAccentColorsFromByte(bytes, 1, false).FirstOrDefault();
var albumArtDarkAccentColor = ImageHelper.GetAccentColorsFromByte(bytes, 1, true).FirstOrDefault();
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
AlbumArtChangedChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, albumArtLightAccentColor, albumArtDarkAccentColor));
});
var mediaProps = await desiredSession.ControlSession.TryGetMediaPropertiesAsync();
if (desiredSession == null || desiredSession.ControlSession == null) return;
MediaManager_OnAnyTimelinePropertyChanged(desiredSession, desiredSession.ControlSession.GetTimelineProperties());
MediaManager_OnAnyMediaPropertyChanged(desiredSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(desiredSession, desiredSession.ControlSession.GetPlaybackInfo());
}
private void StartSSE()
{
if (_sse != null)
{
return;
}
try
{
_sse = new EventSourceReader(new Uri($"{_settingsService.AppSettings.GeneralSettings.LXMusicServer}{Constants.LXMusic.QuerySuffix}")).Start();
@@ -421,7 +497,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void Sse_Disconnected(object sender, DisconnectEventArgs e)
{
Task.Run(async () =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
await Task.Delay(e.ReconnectDelay);
if (_sse != null && !_sse.IsDisposed) _sse.Start();
@@ -430,78 +506,91 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void Sse_MessageReceived(object sender, EventSourceMessageEventArgs e)
{
if (_cachedSongInfo?.SourceAppUserModelId == Constants.PlayerID.LXMusic)
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
if (_cachedSongInfo?.PlayerId == Constants.PlayerID.LXMusic)
{
if (e.Event == "progress")
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
{
_lxMusicPositionSeconds = data.GetDouble();
if (e.Event == "progress")
{
_lxMusicPositionSeconds = data.GetDouble();
}
else if (e.Event == "duration")
{
_lxMusicDurationSeconds = data.GetDouble();
}
if (IsMediaSourceTimelineSyncEnabled(Constants.PlayerID.LXMusic))
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(TimeSpan.FromSeconds(_lxMusicPositionSeconds), TimeSpan.FromSeconds(_lxMusicDurationSeconds)));
}
}
else if (e.Event == "duration")
else if (data.ValueKind == JsonValueKind.String)
{
_lxMusicDurationSeconds = data.GetDouble();
if (e.Event == "picUrl")
{
string? picUrl = data.GetString();
if (picUrl != null)
{
_logger.LogInformation("LX Music Album Art URL: {url}", picUrl);
_lxMusicAlbumArtBytes = await ImageHelper.GetImageBytesFromUrlAsync(picUrl);
_SMTCAlbumArtBytes = _lxMusicAlbumArtBytes;
UpdateAlbumArt();
}
}
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(TimeSpan.FromSeconds(_lxMusicPositionSeconds), TimeSpan.FromSeconds(_lxMusicDurationSeconds)));
});
}
}
});
}
public async Task PlayAsync()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await focusedSession.ControlSession?.TryPlayAsync();
await desiredSession.ControlSession?.TryPlayAsync();
}
}
public async Task PauseAsync()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await focusedSession.ControlSession?.TryPauseAsync();
await desiredSession.ControlSession?.TryPauseAsync();
}
}
public async Task PreviousAsync()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await focusedSession.ControlSession?.TrySkipPreviousAsync();
await desiredSession.ControlSession?.TrySkipPreviousAsync();
}
}
public async Task NextAsync()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await focusedSession.ControlSession?.TrySkipNextAsync();
await desiredSession.ControlSession?.TrySkipNextAsync();
}
}
public async Task ChangePosition(double seconds)
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await focusedSession.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
await desiredSession.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
}
public MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
{
return _settingsService.AppSettings.MediaSourceProvidersInfo.Where(x => x.Provider == _cachedSongInfo?.SourceAppUserModelId)?.FirstOrDefault();
}
public async void Receive(PropertyChangedMessage<bool> message)
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is MediaSourceProviderInfo)
{
@@ -510,20 +599,94 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
MediaManager_OnFocusedSessionChanged(null);
}
}
else if (message.Sender is AlbumArtSearchProviderInfo)
else if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(AlbumArtSearchProviderInfo.IsEnabled))
if (message.PropertyName == nameof(TranslationSettings.IsLibreTranslateEnabled))
{
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.IsTranslationEnabled))
{
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.ShowTranslationOnly))
{
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.IsChineseRomanizationEnabled))
{
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.IsJapaneseRomanizationEnabled))
{
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.IsTraditionalChineseEnabled))
{
UpdateLyrics();
}
}
}
public void Receive(PropertyChangedMessage<FullyObservableCollection<AlbumArtSearchProviderInfo>> message)
public void Receive(PropertyChangedMessage<List<string>> message)
{
if (message.Sender is GeneralSettings)
{
if (message.PropertyName == nameof(GeneralSettings.PlayOrPauseShortcut))
{
UpdatePlayOrPauseSongShortcut();
}
else if (message.PropertyName == nameof(GeneralSettings.PreviousSongShortcut))
{
UpdatePreviousSongShortcut();
}
else if (message.PropertyName == nameof(GeneralSettings.NextSongShortcut))
{
UpdateNextSongShortcut();
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsTranslationSeparator))
{
UpdateTranslations();
}
}
else if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.SelectedTargetLanguageCode))
{
_logger.LogInformation("Target language code changed: {code}", _settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageCode);
UpdateTranslations();
}
}
}
public void Receive(PropertyChangedMessage<LyricsWindowMode> message)
{
if (message.Sender is LiveStates)
{
if (message.PropertyName == nameof(LiveStates.LyricsWindowMode))
{
UpdateTranslations();
}
}
}
public void Receive(PropertyChangedMessage<ChineseRomanization> message)
{
if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.ChineseRomanization))
{
UpdateTranslations();
}
}
}
}
}

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