Compare commits

...

7 Commits

Author SHA1 Message Date
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
69 changed files with 2258 additions and 923 deletions

View File

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

View File

@@ -51,10 +51,13 @@
<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" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />

View File

@@ -118,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: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -26,7 +26,7 @@
<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" />
@@ -61,6 +61,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<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" />
@@ -87,11 +88,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>
@@ -122,6 +118,9 @@
<Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -161,6 +160,9 @@
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -186,6 +188,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -222,7 +229,7 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsBavkgroundSettingsControl.xaml">
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>

View File

@@ -55,6 +55,7 @@
<ComboBoxItem x:Uid="SettingsPageAutoStartInAppLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDockLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartDesktopLyrics" />
<ComboBoxItem x:Uid="SettingsPageAutoStartPIPLyrics" />
</ComboBox>
</controls:SettingsCard>

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"

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,271 @@
<?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: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="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,6">
<TextBlock x:Uid="LyricsSearchControlSongInfoMapping" Style="{StaticResource SubtitleTextBlockStyle}" />
</Grid>
<Grid Grid.Row="1" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsSearchControlTitle"
Grid.Column="0"
VerticalAlignment="Center"
Text="Title" />
<TextBox
Grid.Column="1"
IsReadOnly="True"
Text="{x:Bind ViewModel.MappedSongSearchQuery.OriginalTitle, Mode=OneWay}"
TextWrapping="Wrap" />
<TextBlock
x:Uid="LyricsSearchControlMappedAs"
Grid.Column="2"
VerticalAlignment="Center" />
<TextBox
Grid.Column="3"
Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedTitle, Mode=TwoWay}"
TextWrapping="Wrap" />
</Grid>
<Grid Grid.Row="2" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LyricsSearchControlArtist"
Grid.Column="0"
VerticalAlignment="Center" />
<TextBox
Grid.Column="1"
IsReadOnly="True"
Text="{x:Bind ViewModel.MappedSongSearchQuery.OriginalArtist, Mode=OneWay}"
TextWrapping="Wrap" />
<TextBlock
x:Uid="LyricsSearchControlMappedAs"
Grid.Column="2"
VerticalAlignment="Center" />
<TextBox
Grid.Column="3"
Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}"
TextWrapping="Wrap" />
</Grid>
<!-- 搜索及其他按钮 -->
<Grid Grid.Row="3">
<RelativePanel>
<CheckBox
x:Uid="LyricsSearchControlMarkAsPureMusic"
IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True" />
<Button
x:Uid="LyricsSearchControlSearch"
Command="{x:Bind ViewModel.SearchCommand}"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource AccentButtonStyle}" />
</RelativePanel>
</Grid>
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<TextBlock x:Uid="LyricsSearchControlTargetSearchProvider" Style="{StaticResource SubtitleTextBlockStyle}" />
</Grid>
<Grid Grid.Column="1">
<TextBlock
x:Uid="LyricsSearchControlLyricsPreview"
Margin="6,0,0,0"
Style="{StaticResource SubtitleTextBlockStyle}" />
</Grid>
</Grid>
<!-- 结果列表及原始歌词文件展示区域 -->
<Grid Grid.Row="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" RowSpacing="12">
<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="1">
<ListView ItemsSource="{x:Bind ViewModel.LyricsData.LyricsLines, Mode=OneWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SelectedLyricsSearchResult, 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.SelectedLyricsSearchResult, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="6">
<RelativePanel>
<TextBlock
x:Uid="LyricsSearchControlHelp"
Margin="0,0,24,0"
FontSize="12"
Foreground="{StaticResource 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

@@ -234,14 +234,46 @@
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsLineFadeEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 高亮 -->
<controls:SettingsCard x:Uid="SettingsPageLyricsHighlightScope" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7E6;}">
<!-- 译文高亮 -->
<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
@@ -319,6 +351,7 @@
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsFanLyricsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 滚动动画 -->
<controls:SettingsExpander
x:Uid="SettingsPageScrollEasing"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -341,7 +374,6 @@
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageScrollTopDuration">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsScrollTopDurationExtendedSlider"
Default="500"
Frequency="50"
Maximum="1000"
@@ -351,7 +383,6 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageScrollDuration">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsScrollDurationExtendedSlider"
Default="500"
Frequency="50"
Maximum="1000"
@@ -361,7 +392,6 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageScrollBottomDuration">
<local:ExtendedSlider
x:Uid="SettingsPageLyricsScrollBottomDurationExtendedSlider"
Default="500"
Frequency="50"
Maximum="1000"
@@ -369,6 +399,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

@@ -31,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"

View File

@@ -24,6 +24,14 @@
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

@@ -1,3 +1,4 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -91,5 +92,22 @@ namespace BetterLyrics.WinUI3.Controls
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

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

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

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

@@ -262,17 +262,50 @@ namespace BetterLyrics.WinUI3.Helper
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 AlphaMaskEffect CreateForegroundHighlightEffect(CanvasCommandList foregroundFontEffect, IGraphicsEffectSource mask)
public static OpacityEffect CreateForegroundHighlightEffect(CanvasCommandList foregroundFontEffect, IGraphicsEffectSource mask, double opacity)
{
return new AlphaMaskEffect
return new OpacityEffect
{
Source = foregroundFontEffect,
AlphaMask = mask,
Source = new AlphaMaskEffect
{
Source = foregroundFontEffect,
AlphaMask = mask,
},
Opacity = (float)opacity,
};
}
@@ -291,6 +324,19 @@ namespace BetterLyrics.WinUI3.Helper
};
}
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

View File

@@ -13,7 +13,8 @@ namespace BetterLyrics.WinUI3.Helper
{
public class GlobalHotKeyHelper
{
private static Dictionary<int, Action> _hotKeyActions = [];
private static Dictionary<int, Action> _actions = [];
private static Dictionary<int, List<string>> _keys = [];
/// <summary>
/// Register a global hotkey for a specific window type
@@ -22,7 +23,7 @@ namespace BetterLyrics.WinUI3.Helper
/// <param name="id"></param>
/// <param name="keys"></param>
/// <param name="action"></param>
public static void RegisterHotKey<T>(ShortcutID id, List<string> keys, Action action)
private static void RegisterHotKey<T>(ShortcutID id, List<string> keys, Action action)
{
if (keys.Count == 0) return;
@@ -55,18 +56,23 @@ namespace BetterLyrics.WinUI3.Helper
key = (VirtualKey)Enum.Parse(typeof(VirtualKey), item, true);
}
}
User32.RegisterHotKey(hwnd, (int)id, modifiers, (uint)key);
_hotKeyActions[(int)id] = action;
bool success = User32.RegisterHotKey(hwnd, (int)id, modifiers, (uint)key);
if (success)
{
_actions[(int)id] = action;
_keys[(int)id] = keys;
}
}
public static void UnregisterHotKey<T>(ShortcutID id)
private static void UnregisterHotKey<T>(ShortcutID id)
{
var window = WindowHelper.GetWindowByWindowType<T>();
if (window == null) return;
HWND hwnd = WindowNative.GetWindowHandle(window);
User32.UnregisterHotKey(hwnd, (int)id);
_hotKeyActions.Remove((int)id);
_actions.Remove((int)id);
_keys.Remove((int)id);
}
public static void UpdateHotKey<T>(ShortcutID id, List<string> keys, Action action)
@@ -75,6 +81,16 @@ namespace BetterLyrics.WinUI3.Helper
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);
@@ -82,7 +98,7 @@ namespace BetterLyrics.WinUI3.Helper
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

@@ -177,7 +177,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();
}

View File

@@ -58,6 +58,11 @@ namespace BetterLyrics.WinUI3.Services
_identifier = _factory.Load(PathHelper.LanguageProfilePath);
}
private static string SimplifiedChineseOrTraditionalChinese(string text)
{
return text == ChineseConverter.ConvertToSimplifiedChinese(text) ? "zh-Hans" : "zh-Hant";
}
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
@@ -67,9 +72,9 @@ namespace BetterLyrics.WinUI3.Services
code = code switch
{
"simple" => "en",
"zh_classical" => "zh-Hant",
"zh_yue" => "zh-Hant",
"zh" => text == ChineseConverter.ConvertToSimplifiedChinese(text) ? "zh-Hans" : "zh-Hant",
"zh_classical" => SimplifiedChineseOrTraditionalChinese(text),
"zh_yue" => SimplifiedChineseOrTraditionalChinese(text),
"zh" => SimplifiedChineseOrTraditionalChinese(text),
_ => code
};
return code;

View File

@@ -7,11 +7,11 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class BackgroundTaskRunner
public class LatestOnlyTaskRunner
{
private CancellationTokenSource? _cts;
public void Run(Func<CancellationToken, Task> taskFactory)
public async Task RunAsync(Func<CancellationToken, Task> taskFactory)
{
_cts?.Cancel();
_cts?.Dispose();
@@ -19,19 +19,16 @@ namespace BetterLyrics.WinUI3.Helper
_cts = new CancellationTokenSource();
var token = _cts.Token;
_ = Task.Run(async () =>
try
{
try
{
await taskFactory(token);
}
catch (OperationCanceledException)
{
}
catch (Exception)
{
}
}, token);
await taskFactory(token);
}
catch (OperationCanceledException)
{
}
catch (Exception)
{
}
}
}
}

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

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

@@ -35,6 +35,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
[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; } = [];
public AppSettings() { }
}
}

View File

@@ -23,7 +23,10 @@ namespace BetterLyrics.WinUI3.Models.Settings
[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;
@@ -31,6 +34,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
[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;

View File

@@ -4,11 +4,13 @@ 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()

View File

@@ -13,7 +13,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
public partial class StandardModeSettings : BaseModeSettings
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial Rect WindowBounds { get; set; } = new Rect(100, 100, 1000, 600);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsMaximized { get; set; } = false;
public StandardModeSettings()
{
LyricsDisplayType = LyricsDisplayType.SplitView;

View File

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

@@ -12,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
@@ -30,7 +31,7 @@ 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;
@@ -55,6 +56,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
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;

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

@@ -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, CancellationToken token);
Task<List<LyricsSearchResult>> SearchAllAsync(string title, string artist, string album, double durationMs, CancellationToken token);
}
}

View File

@@ -4,6 +4,7 @@ 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;
@@ -11,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;
@@ -87,89 +89,158 @@ 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, 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, 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, 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, token);
results.Add(searchResult);
}
return results;
}
private async Task<LyricsSearchResult> SearchSingleAsync(LyricsSearchProvider provider, string title, string artist, string album, double durationMs, 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, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
lyricsSearchResult = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
lyricsSearchResult = await SearchAmllTtmlDbAsync(title, artist);
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)
@@ -183,7 +254,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
{
return raw;
lyricsSearchResult.Raw = raw;
lyricsSearchResult.Title = title;
lyricsSearchResult.Artist = artist;
}
}
}
@@ -193,11 +266,16 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
}
return null;
return lyricsSearchResult;
}
private string? SearchEmbedded(string title, string artist)
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)
@@ -212,23 +290,32 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var plain = TagLib.File.Create(file).Tag.Lyrics;
if (!plain.IsNullOrEmpty())
{
return plain;
lyricsSearchResult.Raw = plain;
lyricsSearchResult.Title = track.Title;
lyricsSearchResult.Artist = artist;
}
}
}
}
}
}
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;
@@ -271,7 +358,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
if (string.IsNullOrWhiteSpace(rawLyricFile))
return null;
{
return lyricsSearchResult;
}
// 下载歌词内容
var url = $"{Constants.AmllTTmlDB.QueryPrefix}{rawLyricFile}";
@@ -279,17 +368,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?" +
@@ -300,7 +401,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();
@@ -308,22 +411,47 @@ 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, Searchers searchers)
{
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;
}
var result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
@@ -349,11 +477,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))
{
@@ -365,21 +497,26 @@ 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 = Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyrics(candidate.Id, candidate.AccessKey);
}
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = kugouResult.Title;
lyricsSearchResult.Artist = kugouResult.Artists.Join(" | ");
}
return null;
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();
@@ -26,5 +30,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
bool IsPlaying { get; }
SongInfo? SongInfo { get; }
TimeSpan Position { get; }
LyricsSearchProvider? LyricsSearchProvider { get; }
TranslationSearchProvider? TranslationSearchProvider { get; }
}
}

View File

@@ -0,0 +1,70 @@
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?.SourceAppUserModelId ?? "",
_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.Rgba16, BitmapAlphaMode.Premultiplied);
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,227 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.ComponentModel;
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 = [];
private 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 = LanguageHelper.SupportedTargetLanguages[_settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageIndex].Code;
_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 == 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);
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.CurrentLyricsStyleSettings.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.CurrentLyricsStyleSettings.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.SourceAppUserModelId ?? "",
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",
SongInfo.DurationMs ?? 0,
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);
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));
UpdateTranslations();
}
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:
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();
}
}
private void UpdateLyrics()
{
_refreshLyricsRunner.RunAsync(RefreshLyricsAsync);
}
private void UpdateTranslations()
{
_refreshTranslationRunner.RunAsync(RefreshTranslationAsync);
}
}
}

View File

@@ -8,7 +8,11 @@ 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;
@@ -36,13 +40,19 @@ using WindowsMediaController;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<List<string>>>,
IRecipient<PropertyChangedMessage<FullyObservableCollection<AlbumArtSearchProviderInfo>>>
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<LyricsWindowMode>>,
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;
@@ -54,32 +64,58 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly MediaManager _mediaManager = new();
private readonly BackgroundTaskRunner _albumArtRefreshRunner = new();
private readonly BackgroundTaskRunner _onAnyMediaPropertyChangedRunner = new();
private SongInfo? _cachedSongInfo;
private byte[]? _SMTCAlbumArtBytes = null;
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();
@@ -120,12 +156,14 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateAlbumArtRelated();
UpdateAlbumArt();
UpdateLyrics();
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateAlbumArtRelated();
UpdateAlbumArt();
UpdateLyrics();
}
private void MediaSourceProvidersInfo_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
@@ -133,22 +171,38 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
switch (e.PropertyName)
{
case nameof(MediaSourceProviderInfo.AlbumArtSearchProvidersInfo):
UpdateAlbumArtRelated();
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;
@@ -159,18 +213,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)
@@ -178,9 +230,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))
{
@@ -192,61 +244,64 @@ 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.Run(async token =>
string id = mediaSession.Id;
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(id))
{
_cachedSongInfo = null;
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties.Title, mediaProperties.Artist, mediaProperties.AlbumTitle);
@@ -256,42 +311,26 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
_SMTCAlbumArtBytes = null;
UpdateAlbumArtRelated();
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
}
});
}
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,
};
_cachedSongInfo = new SongInfo
{
Title = mediaProperties.Title,
Artist = mediaProperties.Artist,
Album = mediaProperties.AlbumTitle,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
SourceAppUserModelId = id,
};
_cachedSongInfo.Duration = (int)(_cachedSongInfo.DurationMs / 1000f);
_cachedSongInfo.Duration = (int)(_cachedSongInfo.DurationMs / 1000f);
_onAnyMediaPropertyChangedRunner.Run(async token =>
{
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties.Title, mediaProperties.Artist, mediaProperties.AlbumTitle);
@@ -312,18 +351,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_SMTCAlbumArtBytes = null;
}
}
UpdateAlbumArtRelated();
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
}
});
}
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
UpdateAlbumArt();
UpdateLyrics();
});
}
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
@@ -346,6 +379,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;
@@ -378,61 +435,14 @@ 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 void UpdateAlbumArtRelated()
{
_albumArtRefreshRunner.Run(async (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(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);
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()
@@ -467,7 +477,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();
@@ -476,77 +486,76 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void Sse_MessageReceived(object sender, EventSourceMessageEventArgs e)
{
if (_cachedSongInfo?.SourceAppUserModelId == Constants.PlayerID.LXMusic)
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
if (_cachedSongInfo?.SourceAppUserModelId == 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")
{
_lxMusicDurationSeconds = data.GetDouble();
}
_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 void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is MediaSourceProviderInfo)
@@ -556,19 +565,23 @@ 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))
{
UpdateAlbumArtRelated();
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.IsTranslationEnabled))
{
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.ShowTranslationOnly))
{
UpdateTranslations();
}
}
}
public void Receive(PropertyChangedMessage<FullyObservableCollection<AlbumArtSearchProviderInfo>> message)
{
}
public void Receive(PropertyChangedMessage<List<string>> message)
{
if (message.Sender is GeneralSettings)
@@ -587,5 +600,39 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.SelectedTargetLanguageIndex))
{
_logger.LogInformation("Target language index changed: {Index}", _settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageIndex);
UpdateTranslations();
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is LyricsStyleSettings)
{
if (message.PropertyName == nameof(LyricsStyleSettings.LyricsTranslationSeparator))
{
UpdateTranslations();
}
}
}
public void Receive(PropertyChangedMessage<LyricsWindowMode> message)
{
if (message.Sender is LiveStates)
{
if (message.PropertyName == nameof(LiveStates.CurrentLyricsWindowMode))
{
UpdateTranslations();
}
}
}
}
}

View File

@@ -57,6 +57,9 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
AppSettings.LocalMediaFolders.CollectionChanged += AppSettings_CollectionChanged;
AppSettings.LocalMediaFolders.ItemPropertyChanged += AppSettings_ItemPropertyChanged;
AppSettings.MappedSongSearchQueries.CollectionChanged += AppSettings_CollectionChanged;
AppSettings.MappedSongSearchQueries.ItemPropertyChanged += AppSettings_ItemPropertyChanged;
AppSettings.Version = MetadataHelper.AppVersion;
EnsureMediaSourceProvidersInfo();

View File

@@ -10,7 +10,7 @@ namespace BetterLyrics.WinUI3.Services.TranslateService
{
public interface ITranslateService
{
Task<string> TranslateTextAsync(string text, string targetLangCode, CancellationToken? token);
Task<string> TranslateTextAsync(string text, string targetLangCode, CancellationToken token);
int SearchTranslatedLyricsItself(List<LyricsData> lyricsDataArr);
}

View File

@@ -25,7 +25,7 @@ namespace BetterLyrics.WinUI3.Services.TranslateService
_httpClient = new HttpClient();
}
public async Task<string> TranslateTextAsync(string text, string targetLangCode, CancellationToken? token)
public async Task<string> TranslateTextAsync(string text, string targetLangCode, CancellationToken token)
{
if (string.IsNullOrWhiteSpace(text))
{
@@ -49,12 +49,10 @@ namespace BetterLyrics.WinUI3.Services.TranslateService
new("q", text),
new("source", originalLangCode),
new("target", targetLangCode),
]));
token?.ThrowIfCancellationRequested();
]), token);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
token?.ThrowIfCancellationRequested();
var json = await response.Content.ReadAsStringAsync(token);
var result = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.TranslateResponse);
return result?.TranslatedText ?? string.Empty;

View File

@@ -150,9 +150,6 @@
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-picture mode</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Exit picture-in-picture mode</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
@@ -222,6 +219,9 @@
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>Lyrics provider</value>
</data>
<data name="LyricsPageLyricsSearchButtonToolTip.Content" xml:space="preserve">
<value>Manually search lyrics</value>
</data>
<data name="LyricsPageLyricsSettingsButtonToolTip.Content" xml:space="preserve">
<value>Lyrics style and effect quick settings</value>
</data>
@@ -252,6 +252,42 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Translation provider</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<value>Artist</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* Save changes take effect immediately, after which the track lyrics will be retrieved with mapping information and target lyrics; marking as pure music will directly return to pure music placeholder lyrics. Reset to retrieve by original data.</value>
</data>
<data name="LyricsSearchControlLyricsPreview.Text" xml:space="preserve">
<value>Lyrics preview</value>
</data>
<data name="LyricsSearchControlMappedAs.Text" xml:space="preserve">
<value>mapped as</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>Mark as pure music</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>Not found</value>
</data>
<data name="LyricsSearchControlReset.Content" xml:space="preserve">
<value>Reset</value>
</data>
<data name="LyricsSearchControlSaveChanges.Content" xml:space="preserve">
<value>Save changes</value>
</data>
<data name="LyricsSearchControlSearch.Content" xml:space="preserve">
<value>Search</value>
</data>
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>Song info mapping</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Text" xml:space="preserve">
<value>Target lyrics search provider</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<value>Title</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>Local .ESLRC files</value>
</data>
@@ -478,6 +514,9 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>Activate standard mode</value>
</data>
<data name="SettingsPageAutoStartPIPLyrics.Content" xml:space="preserve">
<value>Start Picture-in-Picture Mode</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>When starting the app</value>
</data>
@@ -485,7 +524,7 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<value>Lyrics backdrop</value>
</data>
<data name="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>Album Background Acrylic Effect Strength!</value>
<value>Album Background Acrylic Effect Strength</value>
</data>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>Lyrics background</value>
@@ -695,13 +734,13 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<value>Lyrics background</value>
</data>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>Album background layer ambiguity!</value>
<value>Album background layer ambiguity</value>
</data>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>Album background layer opacity!</value>
<value>Album background layer opacity</value>
</data>
<data name="SettingsPageLyricsBackgroundSpeed.Header" xml:space="preserve">
<value>Album Background Layer Motion Rate!</value>
<value>Album Background Layer Motion Rate</value>
</data>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>Font color (Non-current playback area)</value>
@@ -779,7 +818,7 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<value>Glow effect</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>Highlight scope</value>
<value>Original highlight range</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Left</value>
@@ -838,12 +877,18 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>Thin</value>
</data>
<data name="SettingsPageLyricsTimeline.Header" xml:space="preserve">
<value>Lyrics timeline sync</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>If the lyrics progress is jittery, try increasing this threshold; changing this value can cause lyrics synchronization to deviate</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>Lyrics timeline sync threshold</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>Translation Highlight</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>Source and translation separator</value>
</data>
@@ -934,17 +979,23 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>Scope</value>
</data>
<data name="SettingsPageScrollBottomDelay.Header" xml:space="preserve">
<value>Tail line delay</value>
</data>
<data name="SettingsPageScrollBottomDuration.Header" xml:space="preserve">
<value>Lyrics scrolling animation duration (Last line)</value>
<value>Final line duration</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>Lyrics scrolling animation duration (Current line)</value>
<value>Current line duration</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>Lyrics scrolling animation type</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>First line delay</value>
</data>
<data name="SettingsPageScrollTopDuration.Header" xml:space="preserve">
<value>Lyrics scrolling animation duration (First line)</value>
<value>The duration of the first line</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>Test server</value>
@@ -958,6 +1009,12 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Settings manager</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>This hotkey was not successfully registered</value>
</data>
<data name="SettingsPageShortcutRegSuccessInfo" xml:space="preserve">
<value>This hotkey has been successfully registered</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Current value: </value>
</data>

View File

@@ -150,9 +150,6 @@
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモード</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>ピクチャーインピクチャーモードを終了します</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>キャンセル</value>
</data>
@@ -199,7 +196,7 @@
<value>ブラウザの承認を完了してください</value>
</data>
<data name="LastFMRequestAuthTitle" xml:space="preserve">
<value>BetterLyricsにLast.fmアカウントへのアクセスを許可してください</value>
<value>BetterLyricsにLast.fmアカウントへのアクセスを許可してください</value>
</data>
<data name="LastFMRequestUnAuthConfirm" xml:space="preserve">
<value>認可をキャンセルしました</value>
@@ -208,7 +205,7 @@
<value>ブラウザでキャンセル操作を完了してください</value>
</data>
<data name="LastFMRequestUnAuthTitle" xml:space="preserve">
<value>Last.fmアカウントへのBetterLyricsアクセスを取り消します</value>
<value>Last.fmアカウントへのBetterLyricsアクセスを取り消します</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>リブレットランスレートからの翻訳のリクエストが失敗しました。設定またはネイティブリブレットランレート構成を確認してください</value>
@@ -222,6 +219,9 @@
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞プロバイダー</value>
</data>
<data name="LyricsPageLyricsSearchButtonToolTip.Content" xml:space="preserve">
<value>手動で歌詞を検索します</value>
</data>
<data name="LyricsPageLyricsSettingsButtonToolTip.Content" xml:space="preserve">
<value>歌詞スタイルと効果クイック設定</value>
</data>
@@ -252,6 +252,42 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻訳プロバイダー</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<value>アーティスト</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>*保存変更はすぐに有効になります。その後、トラックの歌詞がマッピング情報とターゲット歌詞で取得されます。純粋な音楽としてのマーキングは、純粋な音楽プレースホルダーの歌詞に直接戻ります。元のデータで取得するためにリセット。</value>
</data>
<data name="LyricsSearchControlLyricsPreview.Text" xml:space="preserve">
<value>歌詞プレビュー</value>
</data>
<data name="LyricsSearchControlMappedAs.Text" xml:space="preserve">
<value>としてマッピングされました</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>純粋な音楽としてマークしてください</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>見つかりませんでした</value>
</data>
<data name="LyricsSearchControlReset.Content" xml:space="preserve">
<value>リセット</value>
</data>
<data name="LyricsSearchControlSaveChanges.Content" xml:space="preserve">
<value>変更を保存</value>
</data>
<data name="LyricsSearchControlSearch.Content" xml:space="preserve">
<value>検索する</value>
</data>
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>曲情報マッピング</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Text" xml:space="preserve">
<value>ターゲット歌詞検索プロバイダー</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<value>タイトル</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>ローカル.ESLRCファイル</value>
</data>
@@ -478,6 +514,9 @@
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>標準モードをアクティブにします</value>
</data>
<data name="SettingsPageAutoStartPIPLyrics.Content" xml:space="preserve">
<value>ピクチャーインピクチャーモードを開始します</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>アプリを起動するとき</value>
</data>
@@ -485,7 +524,7 @@
<value>歌詞の背景素材</value>
</data>
<data name="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>アルバムの背景アクリル効果の強さ</value>
<value>アルバムの背景アクリル効果の強さ</value>
</data>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>歌詞の背景</value>
@@ -695,13 +734,13 @@
<value>歌詞の背景</value>
</data>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>アルバムの背景レイヤーの曖昧さ</value>
<value>アルバムの背景レイヤーの曖昧さ</value>
</data>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>アルバムの背景レイヤーの不透明度</value>
<value>アルバムの背景レイヤーの不透明度</value>
</data>
<data name="SettingsPageLyricsBackgroundSpeed.Header" xml:space="preserve">
<value>アルバムの背景レイヤーのモーションレート</value>
<value>アルバムの背景レイヤーのモーションレート</value>
</data>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>フォントカラー(非電流再生エリア)</value>
@@ -779,7 +818,7 @@
<value>グロー効果</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>ハイライトスコープ</value>
<value>オリジナルのハイライト範囲</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>左</value>
@@ -844,6 +883,12 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌詞タイムライン同期しきい値</value>
</data>
<data name="SettingsPageLyricsTimeline.Header" xml:space="preserve">
<value>歌詞のタイムラインを同期</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>翻訳ハイライト</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>ソースおよび翻訳セパレーター</value>
</data>
@@ -934,17 +979,23 @@
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>範囲</value>
</data>
<data name="SettingsPageScrollBottomDelay.Header" xml:space="preserve">
<value>テールラインの遅延</value>
</data>
<data name="SettingsPageScrollBottomDuration.Header" xml:space="preserve">
<value>歌詞スクロールアニメーション期間(最終行)</value>
<value>最終ライン期間</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌詞スクロールアニメーション期間(現在の行)</value>
<value>現在のライン期間</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞スクロールアニメーションタイプ</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>最初の行の遅延</value>
</data>
<data name="SettingsPageScrollTopDuration.Header" xml:space="preserve">
<value>歌詞スクロールアニメーション期間(最初の行</value>
<value>最初の行の期間</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>テストサーバー</value>
@@ -958,6 +1009,12 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>設定マネージャー</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>このホットキーは正常に登録されていません</value>
</data>
<data name="SettingsPageShortcutRegSuccessInfo" xml:space="preserve">
<value>このホットキーは正常に登録されています</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>現在の値: </value>
</data>
@@ -1043,6 +1100,6 @@
<value>翻訳サーバーは設定されていません。最初に設定で構成してください</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics はすでに実行されています</value>
<value>BetterLyrics はすでに実行されています</value>
</data>
</root>

View File

@@ -150,9 +150,6 @@
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>사진 인당 모드</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-Picture 모드 종료</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>취소</value>
</data>
@@ -199,7 +196,7 @@
<value>브라우저에서 승인을 완료하십시오</value>
</data>
<data name="LastFMRequestAuthTitle" xml:space="preserve">
<value>Last.fm 계정에 BetterLyrics 액세스 권한을 부여하세요!</value>
<value>Last.fm 계정에 BetterLyrics 액세스 권한을 부여하세요</value>
</data>
<data name="LastFMRequestUnAuthConfirm" xml:space="preserve">
<value>내 승인을 취소했습니다</value>
@@ -208,7 +205,7 @@
<value>브라우저에서 취소 작업을 완료하십시오</value>
</data>
<data name="LastFMRequestUnAuthTitle" xml:space="preserve">
<value>Last.fm 계정에 대한 BetterLyrics 액세스를 취소하십시오!</value>
<value>Last.fm 계정에 대한 BetterLyrics 액세스를 취소하십시오</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>LibreTranslate에서 번역 요청 실패, 설정 또는 기본 LibreTranslate 구성을 확인하십시오.</value>
@@ -222,6 +219,9 @@
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>가사 제공자</value>
</data>
<data name="LyricsPageLyricsSearchButtonToolTip.Content" xml:space="preserve">
<value>가사를 수동으로 검색합니다</value>
</data>
<data name="LyricsPageLyricsSettingsButtonToolTip.Content" xml:space="preserve">
<value>가사 스타일과 효과 빠른 설정</value>
</data>
@@ -252,6 +252,42 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>번역 제공자</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<value>아티스트</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* 변경 사항 저장 변경은 즉시 적용되며, 그 후 트랙 가사는 맵핑 정보와 대상 가사로 검색됩니다. 순수한 음악으로 표시하면 순수한 음악 자리 표시 자 가사로 직접 돌아갑니다. 원래 데이터로 검색하도록 재설정하십시오.</value>
</data>
<data name="LyricsSearchControlLyricsPreview.Text" xml:space="preserve">
<value>가사 미리보기</value>
</data>
<data name="LyricsSearchControlMappedAs.Text" xml:space="preserve">
<value>로 매핑됨</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>순수한 음악으로 표시하세요</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>찾을 수 없음</value>
</data>
<data name="LyricsSearchControlReset.Content" xml:space="preserve">
<value>재설정</value>
</data>
<data name="LyricsSearchControlSaveChanges.Content" xml:space="preserve">
<value>변경 사항 저장</value>
</data>
<data name="LyricsSearchControlSearch.Content" xml:space="preserve">
<value>검색</value>
</data>
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>노래 정보 매핑</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Text" xml:space="preserve">
<value>가사 검색 공급자를 타겟팅하십시오</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<value>제목</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>로컬 .ESLRC 파일</value>
</data>
@@ -478,6 +514,9 @@
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>표준 모드를 ​​활성화합니다</value>
</data>
<data name="SettingsPageAutoStartPIPLyrics.Content" xml:space="preserve">
<value>Picture-in-Picture 모드를 시작하십시오</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>앱을 시작할 때</value>
</data>
@@ -485,7 +524,7 @@
<value>가사 배경 자료</value>
</data>
<data name="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>앨범 배경 아크릴 효과 강도!</value>
<value>앨범 배경 아크릴 효과 강도</value>
</data>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>가사 배경</value>
@@ -695,13 +734,13 @@
<value>가사 배경</value>
</data>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>앨범 배경 레이어 모호성!</value>
<value>앨범 배경 레이어 모호성</value>
</data>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>앨범 배경 레이어 불투명도!</value>
<value>앨범 배경 레이어 불투명도</value>
</data>
<data name="SettingsPageLyricsBackgroundSpeed.Header" xml:space="preserve">
<value>앨범 배경 레이어 모션 속도!</value>
<value>앨범 배경 레이어 모션 속도</value>
</data>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>글꼴 색상 (비 전류 재생 영역)</value>
@@ -779,7 +818,7 @@
<value>글로우 효과</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>하이라이트 범위</value>
<value>원래 하이라이트 범위</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>왼쪽</value>
@@ -844,6 +883,12 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>가사 타임 라인 동기화 임계 값</value>
</data>
<data name="SettingsPageLyricsTimeline.Header" xml:space="preserve">
<value>가사 타임라인 동기화</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>번역 하이라이트</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>소스 및 번역 분리기</value>
</data>
@@ -934,17 +979,23 @@
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>범위</value>
</data>
<data name="SettingsPageScrollBottomDelay.Header" xml:space="preserve">
<value>테일 라인 지연</value>
</data>
<data name="SettingsPageScrollBottomDuration.Header" xml:space="preserve">
<value>가사 스크롤 애니메이션 지속 시간 (마지막 줄)</value>
<value>최종 라인 기간</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>가사 스크롤 애니메이션 지속 시간 (현재 줄)</value>
<value>현재 라인 기간</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>가사 스크롤링 애니메이션 유형</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>첫 번째 줄 지연</value>
</data>
<data name="SettingsPageScrollTopDuration.Header" xml:space="preserve">
<value>가사 스크롤 애니메이션 지속 시간 (첫 번째 줄)</value>
<value>첫 번째 줄의 기간</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>테스트 서버</value>
@@ -958,6 +1009,12 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>설정 관리자</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>이 핫키는 성공적으로 등록되지 않았습니다</value>
</data>
<data name="SettingsPageShortcutRegSuccessInfo" xml:space="preserve">
<value>이 핫키는 성공적으로 등록되었습니다</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>현재 가치 : </value>
</data>

View File

@@ -150,9 +150,6 @@
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>画中画模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出画中画模式</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>取消</value>
</data>
@@ -222,6 +219,9 @@
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌词来源</value>
</data>
<data name="LyricsPageLyricsSearchButtonToolTip.Content" xml:space="preserve">
<value>手动检索歌词</value>
</data>
<data name="LyricsPageLyricsSettingsButtonToolTip.Content" xml:space="preserve">
<value>歌词样式与效果快捷设置</value>
</data>
@@ -252,6 +252,42 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻译来源</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<value>艺术家</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* 保存更改立即生效,此后将以映射信息和目标歌词源检索该曲目歌词;标记为纯音乐将直接返回纯音乐占位歌词。重置以按原始数据检索。</value>
</data>
<data name="LyricsSearchControlLyricsPreview.Text" xml:space="preserve">
<value>歌词预览</value>
</data>
<data name="LyricsSearchControlMappedAs.Text" xml:space="preserve">
<value>映射为</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>标记为纯音乐</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>未找到</value>
</data>
<data name="LyricsSearchControlReset.Content" xml:space="preserve">
<value>重置</value>
</data>
<data name="LyricsSearchControlSaveChanges.Content" xml:space="preserve">
<value>保存更改</value>
</data>
<data name="LyricsSearchControlSearch.Content" xml:space="preserve">
<value>搜索</value>
</data>
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>歌曲信息映射</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Text" xml:space="preserve">
<value>目标歌词搜索提供商</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<value>标题</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
@@ -478,6 +514,9 @@
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>启动标准模式</value>
</data>
<data name="SettingsPageAutoStartPIPLyrics.Content" xml:space="preserve">
<value>启动画中画模式</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>启动应用时</value>
</data>
@@ -779,7 +818,7 @@
<value>辉光效果</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>高亮显示范围</value>
<value>原文高亮显示范围</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>靠左</value>
@@ -844,6 +883,12 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌词时间轴同步阈值</value>
</data>
<data name="SettingsPageLyricsTimeline.Header" xml:space="preserve">
<value>歌词时间轴同步</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>翻译高亮</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>原文译文分隔符</value>
</data>
@@ -934,17 +979,23 @@
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>范围</value>
</data>
<data name="SettingsPageScrollBottomDelay.Header" xml:space="preserve">
<value>尾行延时</value>
</data>
<data name="SettingsPageScrollBottomDuration.Header" xml:space="preserve">
<value>歌词滚动动画持续时间(最后一行)</value>
<value>尾行持续时间</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌词滚动动画持续时间(当前行)</value>
<value>当前行持续时间</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌词滚动动画类型</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>首行延时</value>
</data>
<data name="SettingsPageScrollTopDuration.Header" xml:space="preserve">
<value>歌词滚动动画持续时间(第一行)</value>
<value>首行持续时间</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>测试服务器</value>
@@ -958,6 +1009,12 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>设置管理器</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>该热键未成功注册</value>
</data>
<data name="SettingsPageShortcutRegSuccessInfo" xml:space="preserve">
<value>该热键已成功注册</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>当前值: </value>
</data>

View File

@@ -150,9 +150,6 @@
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>畫中畫模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>退出畫中畫模式</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>取消</value>
</data>
@@ -222,6 +219,9 @@
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞來源</value>
</data>
<data name="LyricsPageLyricsSearchButtonToolTip.Content" xml:space="preserve">
<value>手動檢索歌詞</value>
</data>
<data name="LyricsPageLyricsSettingsButtonToolTip.Content" xml:space="preserve">
<value>歌詞樣式與效果快捷設置</value>
</data>
@@ -252,6 +252,42 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻譯來源</value>
</data>
<data name="LyricsSearchControlArtist.Text" xml:space="preserve">
<value>藝術家</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* 保存更改立即生效,此後將以映射信息和目標歌詞源檢索該曲目歌詞;標記為純音樂將直接返回純音樂佔位歌詞。重置以按原始數據檢索。</value>
</data>
<data name="LyricsSearchControlLyricsPreview.Text" xml:space="preserve">
<value>歌詞預覽</value>
</data>
<data name="LyricsSearchControlMappedAs.Text" xml:space="preserve">
<value>映射爲</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>標記為純音樂</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>找不到</value>
</data>
<data name="LyricsSearchControlReset.Content" xml:space="preserve">
<value>重設</value>
</data>
<data name="LyricsSearchControlSaveChanges.Content" xml:space="preserve">
<value>保存更改</value>
</data>
<data name="LyricsSearchControlSearch.Content" xml:space="preserve">
<value>搜索</value>
</data>
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>歌曲信息映射</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Text" xml:space="preserve">
<value>目標歌詞搜尋提供者</value>
</data>
<data name="LyricsSearchControlTitle.Text" xml:space="preserve">
<value>標題</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
@@ -478,6 +514,9 @@
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>啟動標準模式</value>
</data>
<data name="SettingsPageAutoStartPIPLyrics.Content" xml:space="preserve">
<value>啟動畫中畫模式</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>啟動應用程式時</value>
</data>
@@ -485,7 +524,7 @@
<value>歌詞背景材質</value>
</data>
<data name="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>專輯背景壓克力效果強度!</value>
<value>專輯背景壓克力效果強度</value>
</data>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>歌詞背景</value>
@@ -695,13 +734,13 @@
<value>歌詞背景</value>
</data>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>專輯背景層模糊度!</value>
<value>專輯背景層模糊度</value>
</data>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>專輯背景層不透明度!</value>
<value>專輯背景層不透明度</value>
</data>
<data name="SettingsPageLyricsBackgroundSpeed.Header" xml:space="preserve">
<value>專輯背景層運動速率!</value>
<value>專輯背景層運動速率</value>
</data>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>字體顏色(非當前播放區域)</value>
@@ -779,7 +818,7 @@
<value>輝光效果</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>高亮顯示範圍</value>
<value>原文高亮顯示範圍</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>靠左</value>
@@ -838,12 +877,18 @@
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>極細</value>
</data>
<data name="SettingsPageLyricsTimeline.Header" xml:space="preserve">
<value>歌詞時間軌同步閾值</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>當歌詞進度抖動時,請嘗試增加該閾值;更改此值會導致歌詞同步偏差</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌詞時間軌同步閾值</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>翻譯高亮</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>原文譯文分隔符</value>
</data>
@@ -934,17 +979,23 @@
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>範圍</value>
</data>
<data name="SettingsPageScrollBottomDelay.Header" xml:space="preserve">
<value>尾行延時</value>
</data>
<data name="SettingsPageScrollBottomDuration.Header" xml:space="preserve">
<value>歌詞滾動動畫持續時間(最後一行)</value>
<value>尾行持續時間</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌詞滾動動畫持續時間(當前行)</value>
<value>當前行持續時間</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞滾動動畫類型</value>
</data>
<data name="SettingsPageScrollTopDelay.Header" xml:space="preserve">
<value>首行延時</value>
</data>
<data name="SettingsPageScrollTopDuration.Header" xml:space="preserve">
<value>歌詞滾動動畫持續時間(第一行)</value>
<value>首行持續時間</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>測試服務器</value>
@@ -958,6 +1009,12 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>設置管理器</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>該熱鍵未成功註冊</value>
</data>
<data name="SettingsPageShortcutRegSuccessInfo" xml:space="preserve">
<value>該熱鍵已成功註冊</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>目前值: </value>
</data>

View File

@@ -1,99 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
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;
using BetterLyrics.WinUI3.Services.TranslateService;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel
{
public LyricsRendererViewModel(
ISettingsService settingsService,
IMediaSessionsService mediaSessionsService,
ILyricsSearchService musicSearchService,
ILibWatcherService libWatcherService,
ITranslateService libreTranslateService,
ILastFMService lastFMService,
ILiveStatesService liveStatesService
)
{
_settingsService = settingsService;
_lyrcsSearchService = musicSearchService;
_mediaSessionsService = mediaSessionsService;
_libWatcherService = libWatcherService;
_translateService = libreTranslateService;
_liveStatesService = liveStatesService;
_lastFMService = lastFMService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
AppSettings = _settingsService.AppSettings;
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.AppSettings.AlbumArtLayoutSettings.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
_timelineSyncThreshold = 0;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
_mediaSessionsService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_mediaSessionsService.SongInfoChanged += PlaybackService_SongInfoChanged;
_mediaSessionsService.AlbumArtChangedChanged += PlaybackService_AlbumArtChangedChanged;
_mediaSessionsService.TimelineChanged += PlaybackService_TimelineChanged;
IsPlaying = _mediaSessionsService.IsPlaying;
UpdateColorConfig();
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, Extensions.ItemPropertyChangedEventArgs e)
{
// Music lib changed, re-fetch lyrics
_logger.LogInformation("Local lyrics folders changed, refreshing lyrics.");
_refreshLyricsRunner.Run(async tokne =>
{
await RefreshLyricsAsync(tokne);
});
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Music lib changed, re-fetch lyrics
_logger.LogInformation("Local lyrics folders changed, refreshing lyrics.");
_refreshLyricsRunner.Run(async tokne =>
{
await RefreshLyricsAsync(tokne);
});
}
private void MediaSourceProvidersInfo_ItemPropertyChanged(object? sender, Extensions.ItemPropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(MediaSourceProviderInfo.LyricsSearchProvidersInfo):
_logger.LogInformation("MediaSourceProviderInfo.LyricsSearchProvidersInfo changed, refreshing lyrics.");
_refreshLyricsRunner.Run(async token =>
{
await RefreshLyricsAsync(token);
});
break;
default:
break;
}
}
}
}

View File

@@ -63,9 +63,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
_drawFrameCount++;
var currentPlayingLine = _lyricsDataArr
.ElementAtOrDefault(_langIndex)
?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
var currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine != null)
{
@@ -87,7 +85,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
$"Visible lines: [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
$"Total line count: {GetMaxLyricsLineIndexBoundaries().Item2 + 1}\n" +
$"Cur time: {TotalTime + _positionOffset}\n" +
$"Lang size: {_lyricsDataArr.Count}\n" +
$"Song duration: {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}\n" +
$"Y offset: {_canvasYScrollTransition.Value}",
new Vector2(10, 40),
@@ -206,9 +203,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private void DrawBlurredLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var currentPlayingLine = _lyricsDataArr
.ElementAtOrDefault(_langIndex)
?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
var currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null)
{
@@ -217,7 +212,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var textLayout = line.CanvasTextLayout;
@@ -239,10 +234,8 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
using var combined = new CanvasCommandList(control);
using var combinedDs = combined.CreateDrawingSession();
// Mock gradient blurred lyrics layer
// 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层(背景歌词层次)
// Current line will not be blurred
using var backgroundFontEffect = CanvasHelper.CreateFontEffect(line, control, _strokeFontColor,
using var backgroundFontEffect = CanvasHelper.CreateFontEffect(line, control, _strokeFontColor,
_liveStatesService.LiveStates.CurrentLyricsStyleSettings.LyricsFontStrokeWidth, _bgFontColor);
using var backgroundEffect = CanvasHelper.CreateBackgroundEffect(line, backgroundFontEffect, _lyricsOpacityTransition.Value);
combinedDs.DrawImage(backgroundEffect);
@@ -257,16 +250,23 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.IsLyricsLineFadeEnabled);
using var lineMask = CanvasHelper.CreateLineMask(control, line);
using var foregroundFontEffect = CanvasHelper.CreateFontEffect(line, control, _strokeFontColor,
_liveStatesService.LiveStates.CurrentLyricsStyleSettings.LyricsFontStrokeWidth, _bgFontColor);
using var foregroundFontEffect = CanvasHelper.CreateFontEffect(line, control, _strokeFontColor,
_liveStatesService.LiveStates.CurrentLyricsStyleSettings.LyricsFontStrokeWidth, _fgFontColor);
using var effectLayer = new CanvasCommandList(control);
using var effectLayerDs = effectLayer.CreateDrawingSession();
if (line.OriginalText != line.DisplayedText && _liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsTranslationHighlightAmount != 0)
{
using var translationHighlightMask = CanvasHelper.CreateTranslationHighlightMask(control, line);
using var foregroundTranslationHighlightEffect = CanvasHelper.CreateForegroundHighlightEffect(foregroundFontEffect, translationHighlightMask,
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsTranslationHighlightAmount / 100.0);
effectLayerDs.DrawImage(foregroundTranslationHighlightEffect);
}
if (_liveStatesService.LiveStates.CurrentLyricsEffectSettings.IsLyricsShadowEnabled)
{
var shadowEffectMask = CanvasHelper.GetAlphaMask(control, charMask, lineStartToCharMask, lineMask,
var shadowEffectMask = CanvasHelper.GetAlphaMask(control, charMask, lineStartToCharMask, lineMask,
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsShadowScope);
using var foregroundShadowEffect = CanvasHelper.CreateForegroundShadowEffect(foregroundFontEffect, shadowEffectMask,
using var foregroundShadowEffect = CanvasHelper.CreateForegroundShadowEffect(foregroundFontEffect, shadowEffectMask,
_albumArtAccentColorTransition.Value, _liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsShadowAmount);
effectLayerDs.DrawImage(foregroundShadowEffect);
}
@@ -278,10 +278,14 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsGlowEffectAmount);
effectLayerDs.DrawImage(foregroundBlurEffect);
}
var highlightEffectMask = CanvasHelper.GetAlphaMask(control, charMask, lineStartToCharMask, lineMask,
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsHighlightScope);
using var foregroundHighlightEffect = CanvasHelper.CreateForegroundHighlightEffect(foregroundFontEffect, highlightEffectMask);
effectLayerDs.DrawImage(foregroundHighlightEffect);
if (_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsHighlightAmount != 0)
{
var highlightEffectMask = CanvasHelper.GetAlphaMask(control, charMask, lineStartToCharMask, lineMask,
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsHighlightScope);
using var foregroundHighlightEffect = CanvasHelper.CreateForegroundHighlightEffect(foregroundFontEffect, highlightEffectMask,
_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsHighlightAmount / 100.0);
effectLayerDs.DrawImage(foregroundHighlightEffect);
}
combinedDs.DrawImage(new OpacityEffect
{

View File

@@ -53,22 +53,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
}
}
else if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.IsLibreTranslateEnabled))
{
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.IsTranslationEnabled))
{
_logger.LogInformation("Translation enabled state changed: {IsEnabled}", _settingsService.AppSettings.TranslationSettings.IsTranslationEnabled);
UpdateTranslations();
}
else if (message.PropertyName == nameof(TranslationSettings.ShowTranslationOnly))
{
UpdateTranslations();
}
}
else if (message.Sender is LyricsWindowViewModel)
{
if (message.PropertyName == nameof(LyricsWindowViewModel.IsLyricsWindowLocked))
@@ -89,18 +73,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
UpdateIsLastFMTrackEnabled();
}
}
if (message.Sender is LyricsSearchProviderInfo)
{
if (message.PropertyName == nameof(LyricsSearchProviderInfo.IsEnabled))
{
_logger.LogInformation("LyricsSearchProviderInfo.IsEnabled changed, refreshing lyrics.");
_refreshLyricsRunner.Run(async token =>
{
await RefreshLyricsAsync(token);
});
}
}
}
public void Receive(PropertyChangedMessage<Color> message)
@@ -195,6 +167,14 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollTopDelay))
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsEffectSettings.LyricsScrollBottomDelay))
{
_isLayoutChanged = true;
}
}
else if (message.Sender is LyricsStyleSettings)
{
@@ -211,14 +191,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
_isLayoutChanged = true;
}
}
else if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.SelectedTargetLanguageIndex))
{
_logger.LogInformation("Target language index changed: {Index}", _settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageIndex);
UpdateTranslations();
}
}
else if (message.Sender is MediaSourceProviderInfo)
{
if (message.PropertyName == nameof(MediaSourceProviderInfo.TimelineSyncThreshold))
@@ -337,10 +309,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsStyleSettings.LyricsTranslationSeparator))
{
UpdateTranslations();
}
}
}

View File

@@ -336,9 +336,9 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
double y = 0;
// Init Positions
for (int i = 0; i < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.Count; i++)
for (int i = 0; i < _currentLyricsData?.LyricsLines.Count; i++)
{
var line = _lyricsDataArr[_langIndex].LyricsLines.ElementAtOrDefault(i);
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null)
{
@@ -366,7 +366,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
LyricsLine? currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null) return;
@@ -374,7 +374,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
if (playingTextLayout == null) return;
double? targetYScrollOffset = -currentPlayingLine!.Position.Y + _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines[0].Position.Y - playingTextLayout.LayoutBounds.Height / 2.0;
double? targetYScrollOffset = -currentPlayingLine!.Position.Y + _currentLyricsData?.LyricsLines[0].Position.Y - playingTextLayout.LayoutBounds.Height / 2.0;
if (!targetYScrollOffset.HasValue) return;
@@ -385,7 +385,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
var lines = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines;
var lines = _currentLyricsData?.LyricsLines;
if (lines == null || lines.Count == 0) return;
double offset = _canvasYScrollTransition.Value + _canvasHeight / 2;
@@ -555,15 +555,13 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private void UpdateVisibleLinesProps(ICanvasAnimatedControl control)
{
var currentPlayingLine = _lyricsDataArr
.ElementAtOrDefault(_langIndex)
?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
var currentPlayingLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null) return;
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex + 1; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
@@ -591,25 +589,31 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
line.HighlightOpacityTransition.StartTransition(i == _playingLineIndex ? 1f : 0f);
double yScrollDuration;
double yScrollDelay;
if (lineCountDelta < 0)
{
yScrollDuration =
_canvasYScrollTransition.DurationSeconds +
distanceFactor * (_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsScrollTopDuration / 1000.0 - _canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * _liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsScrollTopDelay / 1000.0;
}
else if (lineCountDelta == 0)
{
yScrollDuration = _canvasYScrollTransition.DurationSeconds;
yScrollDelay = 0;
}
else
{
yScrollDuration =
_canvasYScrollTransition.DurationSeconds +
distanceFactor * (_liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsScrollBottomDuration / 1000.0 - _canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * _liveStatesService.LiveStates.CurrentLyricsEffectSettings.LyricsScrollBottomDelay / 1000.0;
}
line.YOffsetTransition.SetEasingType(_canvasYScrollTransition.EasingType ?? EasingType.Linear);
line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay);
line.YOffsetTransition.StartTransition(_canvasTargetYScrollOffset, _isLayoutChanged);
}
@@ -669,7 +673,8 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private void UpdateTimelineSyncThreshold()
{
_timelineSyncThreshold = _mediaSessionsService.GetCurrentMediaSourceProviderInfo()?.TimelineSyncThreshold ?? 0;
var current = _mediaSessionsService.GetCurrentMediaSourceProviderInfo();
_timelineSyncThreshold = current?.TimelineSyncThreshold ?? 0;
}
private void UpdatePositionOffset()

View File

@@ -14,6 +14,7 @@ using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
@@ -55,8 +56,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private int _drawFrameCount = 0;
private int _displayedDrawFrameCount = 0;
private Queue<SoftwareBitmap?> _cachedAlbumArtSwBitmaps = [];
private SoftwareBitmap? _lastAlbumArtSwBitmap = null;
private SoftwareBitmap? _albumArtSwBitmap = null;
@@ -87,21 +86,10 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private double _canvasTargetYScrollOffset = 0;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsSearchProvider? LyricsSearchProvider { get; set; } = null;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TranslationSearchProvider? TranslationSearchProvider { get; set; } = null;
private double _maxLyricsWidth = 0f;
private readonly ISettingsService _settingsService;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ILibWatcherService _libWatcherService;
private readonly IMediaSessionsService _mediaSessionsService;
private readonly ITranslateService _translateService;
private readonly ILastFMService _lastFMService;
private readonly ILiveStatesService _liveStatesService;
private readonly ILogger _logger;
@@ -132,6 +120,8 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private LyricsData? _currentLyricsData;
private bool _isDebugOverlayEnabled = false;
[ObservableProperty]
@@ -142,10 +132,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
private bool _isLayoutChanged = true;
private int _langIndex = 0;
private List<LyricsData> _lyricsDataArr = [];
private int _timelineSyncThreshold = 0;
private CanvasTextFormat _lyricsTextFormat = new()
@@ -178,14 +164,8 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
FontWeight = FontWeights.ExtraBlack,
};
private BackgroundTaskRunner _refreshLyricsRunner = new();
private BackgroundTaskRunner _showTranslationsRunner = new();
private LyricsLayoutOrientation _lyricsLayoutOrientation;
[ObservableProperty]
public partial bool IsTranslating { get; set; } = false;
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; }
@@ -193,16 +173,53 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeTypeSent { get; set; }
public LyricsRendererViewModel(
ISettingsService settingsService,
IMediaSessionsService mediaSessionsService,
ILastFMService lastFMService,
ILiveStatesService liveStatesService)
{
_settingsService = settingsService;
_mediaSessionsService = mediaSessionsService;
_liveStatesService = liveStatesService;
_lastFMService = lastFMService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
AppSettings = _settingsService.AppSettings;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.AppSettings.AlbumArtLayoutSettings.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
_timelineSyncThreshold = 0;
_mediaSessionsService.IsPlayingChanged += MediaSessionsService_IsPlayingChanged;
_mediaSessionsService.SongInfoChanged += MediaSessionsService_SongInfoChanged;
_mediaSessionsService.AlbumArtChanged += MediaSessionsService_AlbumArtChangedChanged;
_mediaSessionsService.LyricsChanged += MediaSessionsService_LyricsChanged;
_mediaSessionsService.TimelineChanged += MediaSessionsService_TimelineChanged;
IsPlaying = _mediaSessionsService.IsPlaying;
UpdateColorConfig();
}
private void MediaSessionsService_LyricsChanged(object? sender, LyricsChangedEventArgs e)
{
_currentLyricsData = e.LyricsData;
_isLayoutChanged = true;
}
private int GetCurrentPlayingLineIndex()
{
var totalMs = TotalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (totalMs < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
if (totalMs < _currentLyricsData?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
for (int i = 0; i < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.Count; i++)
for (int i = 0; i < _currentLyricsData?.LyricsLines.Count; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var nextLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i + 1);
var nextLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(i + 1);
if (nextLine != null && line.StartMs <= totalMs && totalMs < nextLine.StartMs)
{
return i;
@@ -222,9 +239,9 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
charLength = 0;
charProgress = 0f;
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(lineIndex);
var line = _currentLyricsData?.LyricsLines.ElementAtOrDefault(lineIndex);
if (line == null) return;
var nextLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(lineIndex + 1);
var nextLine = _currentLyricsData?.LyricsLines.ElementAtOrDefault(lineIndex + 1);
int lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
@@ -309,31 +326,22 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
if (
SongInfo == null
|| _lyricsDataArr.ElementAtOrDefault(_langIndex) == null
|| _lyricsDataArr[_langIndex].LyricsLines.Count == 0
|| _currentLyricsData == null
|| _currentLyricsData.LyricsLines.Count == 0
)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _lyricsDataArr[_langIndex].LyricsLines.Count - 1);
return new Tuple<int, int>(0, _currentLyricsData.LyricsLines.Count - 1);
}
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
{
_logger.LogInformation("Music library files changed: {ChangeType} {FilePath}, refreshing lyrics...", e.ChangeType, e.FilePath);
_refreshLyricsRunner.Run(async token =>
{
await RefreshLyricsAsync(token);
});
}
private void PlaybackService_IsPlayingChanged(object? sender, IsPlayingChangedEventArgs e)
private void MediaSessionsService_IsPlayingChanged(object? sender, IsPlayingChangedEventArgs e)
{
IsPlaying = e.IsPlaying;
}
private void PlaybackService_TimelineChanged(object? sender, TimelineChangedEventArgs e)
private void MediaSessionsService_TimelineChanged(object? sender, TimelineChangedEventArgs e)
{
var diff = Math.Abs(TotalTime.TotalMilliseconds - e.Position.TotalMilliseconds);
if (diff >= _timelineSyncThreshold)
@@ -352,7 +360,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
}
}
private void PlaybackService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e)
private void MediaSessionsService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
@@ -373,11 +381,6 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
_songInfoOpacityTransition.Reset(0f);
_songInfoOpacityTransition.StartTransition(1f);
_logger.LogInformation("Song info changed: Title={Title}, Artist={Artist}, refreshing lyrics...", _songTitle, _songArtist);
_refreshLyricsRunner.Run(async token =>
{
await RefreshLyricsAsync(token);
});
TotalTime = TimeSpan.Zero;
// 处理 Last.fm 追踪
@@ -386,245 +389,17 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
}
}
private void PlaybackService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
private void MediaSessionsService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
{
if (e.AlbumArtSwBitmap != _albumArtSwBitmap)
{
_cachedAlbumArtSwBitmaps.Append(_albumArtSwBitmap);
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
_albumArtSwBitmap = e.AlbumArtSwBitmap;
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
_albumArtChanged = true;
if (_cachedAlbumArtSwBitmaps.Count > 2)
{
_cachedAlbumArtSwBitmaps.Dequeue()?.Dispose();
}
_albumArtLightAccentColor = e.AlbumArtLightAccentColor ?? Colors.Transparent;
_albumArtDarkAccentColor = e.AlbumArtDarkAccentColor ?? Colors.Transparent;
_cachedAlbumArtSwBitmaps.Append(e.AlbumArtSwBitmap);
_albumArtSwBitmap = e.AlbumArtSwBitmap;
if (_cachedAlbumArtSwBitmaps.Count > 2)
{
_cachedAlbumArtSwBitmaps.Dequeue()?.Dispose();
}
_albumArtChanged = true;
_albumArtLightAccentColor = e.AlbumArtLightAccentColor ?? Colors.Transparent;
_albumArtDarkAccentColor = e.AlbumArtDarkAccentColor ?? Colors.Transparent;
UpdateColorConfig();
}
else
{
e.AlbumArtSwBitmap?.Dispose();
}
}
private void UpdateTranslations()
{
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
TranslationSearchProvider = null;
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
_isLayoutChanged = true;
IsTranslating = true;
});
if (_settingsService.AppSettings.TranslationSettings.IsTranslationEnabled)
{
_showTranslationsRunner.Run(async token =>
{
await SetDisplayedAlongWithTranslationsAsync(token);
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
IsTranslating = false;
_isLayoutChanged = true;
});
});
}
else
{
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
_langIndex = 0;
IsTranslating = false;
_isLayoutChanged = true;
});
}
}
private async Task SetDisplayedAlongWithTranslationsAsync(CancellationToken token)
{
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, async () =>
{
_logger.LogInformation("Showing translation for lyrics...");
string targetLangCode = LanguageHelper.SupportedTargetLanguages[_settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageIndex].Code;
_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 == 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);
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.CurrentLyricsStyleSettings.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);
token.ThrowIfCancellationRequested();
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.CurrentLyricsStyleSettings.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...");
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
LyricsSearchProvider = null;
_lyricsDataArr = [LyricsData.GetLoadingPlaceholder()];
_isLayoutChanged = true;
});
string? lyricsRaw = null;
LyricsSearchProvider? lyricsSearchProvider = null;
if (SongInfo != null)
{
_logger.LogInformation("Searching lyrics for: Title={Title}, Artist={Artist}, Album={Album}, DurationMs={DurationMs}",
SongInfo.Title, SongInfo.Artist, SongInfo.Album, SongInfo.DurationMs);
(lyricsRaw, lyricsSearchProvider) = await _lyrcsSearchService.SearchAsync(
SongInfo.SourceAppUserModelId ?? "",
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",
SongInfo.DurationMs ?? 0,
token
);
_logger.LogInformation("Lyrics was found? {Found}, Provider: {LyricsSearchProvider}", lyricsRaw != null, lyricsSearchProvider?.ToString() ?? "null");
token.ThrowIfCancellationRequested();
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
LyricsSearchProvider = lyricsSearchProvider;
_lyricsDataArr = new LyricsParser().Parse(lyricsRaw, (int?)SongInfo?.DurationMs);
FillTranslationFromCache(LyricsSearchProvider);
});
}
else
{
_logger.LogWarning("SongInfo is null, cannot search lyrics.");
}
_logger.LogInformation("Parsed lyrics: {MultiLangLyricsCount} languages", _lyricsDataArr.Count);
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
// This ensures that original lyrics are always shown while waiting for translations
_lyricsDataArr[0].SetDisplayedTextInOriginalText();
_isLayoutChanged = true;
});
UpdateTranslations();
}
private void FillTranslationFromCache(LyricsSearchProvider? provider)
{
string? translationRaw = null;
switch (provider)
{
case Enums.LyricsSearchProvider.QQ:
translationRaw = Helper.FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, Helper.PathHelper.QQTranslationCacheDirectory);
break;
case Enums.LyricsSearchProvider.Kugou:
break;
case Enums.LyricsSearchProvider.Netease:
translationRaw = Helper.FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, Helper.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();
}
UpdateColorConfig();
}
}
}

View File

@@ -0,0 +1,166 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsSearchControlViewModel : BaseViewModel
{
private readonly ILyricsSearchService _lyricsSearchService;
private readonly IMediaSessionsService _mediaSessionsService;
private readonly ISettingsService _settingsService;
private LatestOnlyTaskRunner _lyricsSearchRunner = new();
[ObservableProperty]
public partial AppSettings AppSettings { get; set; }
[ObservableProperty]
public partial ObservableCollection<LyricsSearchResult> LyricsSearchResults { get; set; } = [];
[ObservableProperty]
public partial LyricsSearchResult? SelectedLyricsSearchResult { get; set; }
[ObservableProperty]
public partial LyricsData? LyricsData { get; set; }
[ObservableProperty]
public partial MappedSongSearchQuery? MappedSongSearchQuery { get; set; }
[ObservableProperty]
public partial bool IsSearching { get; set; } = false;
public LyricsSearchControlViewModel(ILyricsSearchService lyricsSearchService, IMediaSessionsService mediaSessionsService, ISettingsService settingsService)
{
_lyricsSearchService = lyricsSearchService;
_mediaSessionsService = mediaSessionsService;
_settingsService = settingsService;
AppSettings = _settingsService.AppSettings;
_mediaSessionsService.SongInfoChanged += MediaSessionsService_SongInfoChanged;
InitMappedSongSearchQuery();
}
private void MediaSessionsService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
InitMappedSongSearchQuery();
}
private void InitMappedSongSearchQuery()
{
LyricsSearchResults.Clear();
if (_mediaSessionsService.SongInfo != null)
{
var found = GetMappedSongSearchQueryFromSettings();
if (found == null)
{
MappedSongSearchQuery = new MappedSongSearchQuery
{
OriginalTitle = _mediaSessionsService.SongInfo.Title,
OriginalArtist = _mediaSessionsService.SongInfo.Artist,
MappedTitle = _mediaSessionsService.SongInfo.Title,
MappedArtist = _mediaSessionsService.SongInfo.Artist,
};
}
else
{
MappedSongSearchQuery = found.Clone();
}
}
}
private MappedSongSearchQuery? GetMappedSongSearchQueryFromSettings()
{
if (_mediaSessionsService.SongInfo == null)
{
return null;
}
var found = AppSettings.MappedSongSearchQueries
.Where(x => x.OriginalTitle == _mediaSessionsService.SongInfo.Title && x.OriginalArtist == _mediaSessionsService.SongInfo.Artist).FirstOrDefault();
return found;
}
[RelayCommand]
private void Search()
{
if (MappedSongSearchQuery == null)
{
return;
}
IsSearching = true;
LyricsSearchResults.Clear();
MappedSongSearchQuery.LyricsSearchProvider = null;
_ = _lyricsSearchRunner.RunAsync(async (token) =>
{
LyricsSearchResults = [..await Task.Run(async () =>
{
return await _lyricsSearchService.SearchAllAsync(
MappedSongSearchQuery.MappedTitle,
MappedSongSearchQuery.MappedArtist,
_mediaSessionsService.SongInfo?.Album ?? "",
_mediaSessionsService.SongInfo?.DurationMs ?? 0, token);
}, token)];
IsSearching = false;
});
}
[RelayCommand]
private void Save()
{
if (MappedSongSearchQuery == null)
{
return;
}
var existing = GetMappedSongSearchQueryFromSettings();
if (existing != null)
{
AppSettings.MappedSongSearchQueries.Remove(existing);
}
AppSettings.MappedSongSearchQueries.Add(MappedSongSearchQuery);
MappedSongSearchQuery = MappedSongSearchQuery.Clone();
}
[RelayCommand]
private void Reset()
{
var existing = GetMappedSongSearchQueryFromSettings();
if (existing != null)
{
AppSettings.MappedSongSearchQueries.Remove(existing);
}
InitMappedSongSearchQuery();
SelectedLyricsSearchResult = null;
}
partial void OnSelectedLyricsSearchResultChanged(LyricsSearchResult? value)
{
MappedSongSearchQuery?.LyricsSearchProvider = value?.Provider;
if (value?.Raw != null)
{
var lyricsParser = new LyricsParser();
var lyricsDataArr = lyricsParser.Parse(value?.Raw, (int?)_mediaSessionsService.SongInfo?.DurationMs);
LyricsData = lyricsDataArr.FirstOrDefault();
}
else
{
LyricsData = null;
}
}
}
}

View File

@@ -120,6 +120,7 @@ namespace BetterLyrics.WinUI3
private void UpdateDockOrDesktopWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
@@ -137,7 +138,7 @@ namespace BetterLyrics.WinUI3
}
else
{
if (LiveStates.CurrentLyricsWindowMode == LyricsWindowMode.DesktopMode)
if (LiveStates.CurrentLyricsWindowMode == LyricsWindowMode.DockMode)
{
DockModeHelper.UpdateAppBarHeight(hwnd, _dockMonitorDeviceName, _dockWindowHeight, _dockPlacement);
}
@@ -217,8 +218,7 @@ namespace BetterLyrics.WinUI3
private void UpdateDesktopLockShortcut()
{
GlobalHotKeyHelper.UnregisterHotKey<LyricsWindow>(ShortcutID.DesktopLock);
GlobalHotKeyHelper.RegisterHotKey<LyricsWindow>(ShortcutID.DesktopLock,
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.DesktopLock,
_settingsService.AppSettings.DesktopModeSettings.LockShortcut,
() =>
{
@@ -232,8 +232,7 @@ namespace BetterLyrics.WinUI3
private void UpdateDesktopToggleShortcut()
{
GlobalHotKeyHelper.UnregisterHotKey<LyricsWindow>(ShortcutID.DesktopToggle);
GlobalHotKeyHelper.RegisterHotKey<LyricsWindow>(ShortcutID.DesktopToggle,
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.DesktopToggle,
_settingsService.AppSettings.DesktopModeSettings.ToggleShortcut,
() =>
{
@@ -248,8 +247,7 @@ namespace BetterLyrics.WinUI3
private void UpdateDockToggleShortcut()
{
GlobalHotKeyHelper.UnregisterHotKey<LyricsWindow>(ShortcutID.DockToggle);
GlobalHotKeyHelper.RegisterHotKey<LyricsWindow>(ShortcutID.DockToggle,
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.DockToggle,
_settingsService.AppSettings.DockModeSettings.ToggleShortcut,
() =>
{
@@ -264,8 +262,7 @@ namespace BetterLyrics.WinUI3
private void UpdatePictureInPictureToggleShortcut()
{
GlobalHotKeyHelper.UnregisterHotKey<LyricsWindow>(ShortcutID.PictureInPictureToggle);
GlobalHotKeyHelper.RegisterHotKey<LyricsWindow>(ShortcutID.PictureInPictureToggle,
GlobalHotKeyHelper.UpdateHotKey<LyricsWindow>(ShortcutID.PictureInPictureToggle,
_settingsService.AppSettings.PictureInPictureModeSettings.ToggleShortcut,
() =>
{
@@ -462,6 +459,7 @@ namespace BetterLyrics.WinUI3
if (LiveStates.CurrentLyricsWindowMode == LyricsWindowMode.PictureInPictureMode)
{
window.AppWindow.SetPresenter(AppWindowPresenterKind.CompactOverlay);
window.AppWindow.Move(AppSettings.PictureInPictureModeSettings.WindowPosition.ToPointInt32());
SetPIPModeTitleBarControlsStatus();
}
else

View File

@@ -80,6 +80,9 @@ namespace BetterLyrics.WinUI3.ViewModels
IsLastFMAuthenticated = _lastFMService.IsAuthenticated;
LastFMUser = _lastFMService.User;
LyricsSearchProvider = _mediaSessionsService.LyricsSearchProvider;
TranslationSearchProvider = _mediaSessionsService.TranslationSearchProvider;
SelectedMediaSourceProvider = AppSettings.MediaSourceProvidersInfo.FirstOrDefault();
}
@@ -121,7 +124,7 @@ namespace BetterLyrics.WinUI3.ViewModels
try
{
string targetLangCode = LanguageHelper.SupportedTargetLanguages[AppSettings.TranslationSettings.SelectedTargetLanguageIndex].Code;
string result = await _libreTranslateService.TranslateTextAsync("Hello, world!", targetLangCode, null);
string result = await _libreTranslateService.TranslateTextAsync("Hello, world!", targetLangCode, new System.Threading.CancellationToken());
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestSuccessInfo"), InfoBarSeverity.Success);
@@ -183,9 +186,9 @@ namespace BetterLyrics.WinUI3.ViewModels
public void Receive(PropertyChangedMessage<LyricsSearchProvider?> message)
{
if (message.Sender is LyricsRendererViewModel.LyricsRendererViewModel)
if (message.Sender is MediaSessionsService)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsRendererViewModel.LyricsSearchProvider))
if (message.PropertyName == nameof(MediaSessionsService.LyricsSearchProvider))
{
LyricsSearchProvider = message.NewValue;
}
@@ -194,9 +197,9 @@ namespace BetterLyrics.WinUI3.ViewModels
public void Receive(PropertyChangedMessage<TranslationSearchProvider?> message)
{
if (message.Sender is LyricsRendererViewModel.LyricsRendererViewModel)
if (message.Sender is MediaSessionsService)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsRendererViewModel.TranslationSearchProvider))
if (message.PropertyName == nameof(MediaSessionsService.TranslationSearchProvider))
{
TranslationSearchProvider = message.NewValue;
}

View File

@@ -80,14 +80,17 @@ namespace BetterLyrics.WinUI3.ViewModels
var file = await picker.PickSingleFileAsync();
var succeed = _settingsService.ImportSettings(file.Path);
if (succeed)
if (file != null)
{
WindowHelper.RestartApp();
}
else
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader?.GetString("ImportSettingsFailed") ?? "");
var succeed = _settingsService.ImportSettings(file.Path);
if (succeed)
{
WindowHelper.RestartApp();
}
else
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader?.GetString("ImportSettingsFailed") ?? "");
}
}
}

View File

@@ -237,7 +237,25 @@
</Button.ContextFlyout>
</Button>
<!-- Lyrics & Translation -->
<Button
Click="LyricsSearchShortcutButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE721;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Uid="LyricsPageLyricsSearchButtonToolTip" />
</ToolTipService.ToolTip>
<Button.ContextFlyout>
<Flyout
x:Name="LyricsSearchFlyout"
Closed="LyricsSearchFlyout_Closed"
FlyoutPresenterStyle="{StaticResource FlyoutPageStyle}"
Placement="Right"
ShouldConstrainToRootBounds="False" />
</Button.ContextFlyout>
</Button>
<!-- Playback shortcut settings -->
<Button
Click="PlaybackSettingsShortcutButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},

View File

@@ -134,5 +134,20 @@ namespace BetterLyrics.WinUI3.Views
};
LyricsSettingsFlyout.ShowAt(BottomRightCommandStackPanel);
}
private void LyricsSearchShortcutButton_Click(object sender, RoutedEventArgs e)
{
LyricsSearchFlyout.Content = new LyricsSearchControl
{
MaxHeight = 500,
MaxWidth = 850,
};
LyricsSearchFlyout.ShowAt(BottomRightCommandStackPanel);
}
private void LyricsSearchFlyout_Closed(object sender, object e)
{
LyricsSearchFlyout.Content = null;
}
}
}

View File

@@ -20,6 +20,7 @@ namespace BetterLyrics.WinUI3.Views
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly WindowMessageMonitor _wmm;
private bool _autoSelectLyricsModeOnRunning = true;
public LyricsWindow()
{
@@ -75,8 +76,17 @@ namespace BetterLyrics.WinUI3.Views
switch (type!)
{
case LyricsWindowMode.StandardMode:
AppWindow.MoveAndResize(_settingsService.AppSettings.StandardModeSettings.WindowBounds.ToRectInt32());
ViewModel.SetStandardModeTitleBarControlsStatus();
if (_settingsService.AppSettings.StandardModeSettings.IsMaximized)
{
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ô<EFBFBD><C3B4>ڴ<EFBFBD>С<EFBFBD>Ա<EFBFBD><D4B1>˳<EFBFBD><CBB3><EFBFBD><EFBFBD>󻯺<EFBFBD>
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܶ<EFBFBD><DCB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ļ<EFBFBD><C4BB>ԵӰ<D4B5><D3B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
this.Maximize();
}
else
{
AppWindow.MoveAndResize(_settingsService.AppSettings.StandardModeSettings.WindowBounds.ToRectInt32());
}
break;
case LyricsWindowMode.DockMode:
ViewModel.ToggleDockMode();
@@ -84,9 +94,13 @@ namespace BetterLyrics.WinUI3.Views
case LyricsWindowMode.DesktopMode:
ViewModel.ToggleDesktopMode();
break;
case LyricsWindowMode.PictureInPictureMode:
ViewModel.TogglePictureInPictureMode();
break;
default:
break;
}
_autoSelectLyricsModeOnRunning = false;
}
private void AOTFlyoutItem_Click(object sender, RoutedEventArgs e)
@@ -96,6 +110,8 @@ namespace BetterLyrics.WinUI3.Views
private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)
{
if (_autoSelectLyricsModeOnRunning) return;
if (args.DidPositionChange || args.DidSizeChange)
{
var size = AppWindow.Size;
@@ -107,14 +123,28 @@ namespace BetterLyrics.WinUI3.Views
}
else
{
if (ViewModel.LiveStates.CurrentLyricsWindowMode == LyricsWindowMode.DesktopMode)
switch (ViewModel.LiveStates.CurrentLyricsWindowMode)
{
_settingsService.AppSettings.DesktopModeSettings.WindowBounds = new Windows.Foundation.Rect(rect.X, rect.Y, size.Width, size.Height);
}
else if (ViewModel.LiveStates.CurrentLyricsWindowMode == LyricsWindowMode.DockMode) { }
else
{
_settingsService.AppSettings.StandardModeSettings.WindowBounds = new Windows.Foundation.Rect(rect.X, rect.Y, size.Width, size.Height);
case LyricsWindowMode.StandardMode:
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
{
_settingsService.AppSettings.StandardModeSettings.WindowBounds = new Windows.Foundation.Rect(rect.X, rect.Y, size.Width, size.Height);
_settingsService.AppSettings.StandardModeSettings.IsMaximized = overlappedPresenter.State == OverlappedPresenterState.Maximized;
}
break;
case LyricsWindowMode.DockMode:
break;
case LyricsWindowMode.DesktopMode:
_settingsService.AppSettings.DesktopModeSettings.WindowBounds = new Windows.Foundation.Rect(rect.X, rect.Y, size.Width, size.Height);
break;
case LyricsWindowMode.PictureInPictureMode:
if (AppWindow.Presenter is CompactOverlayPresenter compactOverlayPresenter)
{
_settingsService.AppSettings.PictureInPictureModeSettings.WindowPosition = new Windows.Foundation.Point(rect.X, rect.Y);
}
break;
default:
break;
}
}
}

View File

@@ -89,7 +89,7 @@
<!-- Lyrics background -->
<controls:Case Value="Background">
<uc:LyricsBavkgroundSettingsControl />
<uc:LyricsBackgroundSettingsControl />
</controls:Case>
<!-- Album art area style -->

View File

@@ -163,7 +163,7 @@ WinUI 3とWin2Dで構築された動的歌詞表示ツール — ローカル再
## デモ
Bilibiliで紹介動画を見る2025年7月7日アップロード):[こちら](https://www.bilibili.com/video/BV1zjGjzfEXh)
Bilibiliで紹介動画を見る2025年8月18日アップロード):[こちら](https://www.bilibili.com/video/BV1yLYtzQEME/)
## 今すぐ試す

View File

@@ -163,7 +163,7 @@ WinUI 3와 Win2D로 제작된 동적 가사 디스플레이 도구 — 로컬
## 데모
Bilibili에서 소개 영상을 시청하세요(2025년 77일 업로드): [여기서 보기](https://www.bilibili.com/video/BV1zjGjzfEXh)
Bilibili에서 소개 영상을 시청하세요(2025년 818일 업로드): [여기서 보기](https://www.bilibili.com/video/BV1yLYtzQEME/)
## 지금 사용해보세요

View File

@@ -163,7 +163,7 @@ Check out the article: [BetterLyrics An immersive and smooth lyrics display
## Demonstration
Watch our introduction video (uploaded on 7 July 2025) on Bilibili [here](https://www.bilibili.com/video/BV1zjGjzfEXh).
Watch our introduction video (uploaded on 18 Aug 2025) on Bilibili [here](https://www.bilibili.com/video/BV1yLYtzQEME/).
## Try it now

View File

@@ -167,7 +167,7 @@ BetterLyrics
## 演示
在 B 站观看我们的介绍视频2025 年 77 日上传):[点此观看](https://www.bilibili.com/video/BV1zjGjzfEXh)
在 B 站观看我们的介绍视频2025 年 818 日上传):[点此观看](https://www.bilibili.com/video/BV1yLYtzQEME/)
## 立即体验

View File

@@ -163,7 +163,7 @@ BetterLyrics
## 演示
在 B 站觀看我們的介紹影片2025 年 77 日上傳):[點此觀看](https://www.bilibili.com/video/BV1zjGjzfEXh)
在 B 站觀看我們的介紹影片2025 年 818 日上傳):[點此觀看](https://www.bilibili.com/video/BV1yLYtzQEME/)
## 立即體驗