Compare commits

...

6 Commits

49 changed files with 1395 additions and 241 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.56.0" />
Version="1.0.61.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,6 +26,7 @@
<None Remove="Controls\AllLyricsSettingsControl.xaml" />
<None Remove="Controls\AppSettingsControl.xaml" />
<None Remove="Controls\ExtendedSlider.xaml" />
<None Remove="Controls\LyricsSearchControl.xaml" />
<None Remove="Controls\LyricsSettingsControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
@@ -117,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>
@@ -156,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>
@@ -181,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>

View File

@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.LyricsSearchControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid Padding="16" RowSpacing="6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,6">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<controls:SettingsExpander x:Uid="LyricsSearchControlSongInfoMapping" IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="LyricsSearchControlTitle" Description="{x:Bind ViewModel.MappedSongSearchQuery.OriginalTitle, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedTitle, Mode=TwoWay}" TextWrapping="Wrap" />
<Button
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedTitleCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="LyricsSearchControlArtist" Description="{x:Bind ViewModel.MappedSongSearchQuery.OriginalArtist, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Uid="LyricsSearchControlMappedAs" VerticalAlignment="Center" />
<TextBox Text="{x:Bind ViewModel.MappedSongSearchQuery.MappedArtist, Mode=TwoWay}" TextWrapping="Wrap" />
<Button
VerticalAlignment="Center"
Command="{x:Bind ViewModel.ResetMappedArtistCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE777;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard>
<CheckBox x:Uid="LyricsSearchControlMarkAsPureMusic" IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="LyricsSearchControlTargetSearchProvider">
<Button
x:Uid="LyricsSearchControlSearch"
Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" />
</controls:SettingsCard>
</StackPanel>
</Grid>
<!-- 结果列表及原始歌词文件展示区域 -->
<Grid Grid.Row="1">
<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}" SelectedItem="{x:Bind ViewModel.SelectedLyricsLine, Mode=TwoWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock
Margin="1,0"
Foreground="{ThemeResource SystemFillColorNeutralBrush}"
Text="-" />
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{Binding EndMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock Margin="6,0" Text="{Binding OriginalText}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Padding="0,36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/Page.png" />
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LyricsData, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="2">
<RelativePanel>
<TextBlock
x:Uid="LyricsSearchControlHelp"
Margin="0,0,24,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
RelativePanel.AlignVerticalCenterWithPanel="True"
RelativePanel.LeftOf="Reset"
TextWrapping="Wrap" />
<Button
x:Name="Reset"
x:Uid="LyricsSearchControlReset"
Margin="0,0,6,0"
Command="{x:Bind ViewModel.ResetCommand}"
RelativePanel.AlignVerticalCenterWithPanel="True"
RelativePanel.LeftOf="SaveChanges" />
<Button
x:Name="SaveChanges"
x:Uid="LyricsSearchControlSaveChanges"
Command="{x:Bind ViewModel.SaveCommand}"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource AccentButtonStyle}" />
</RelativePanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,35 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Ude.Core;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class LyricsSearchControl : UserControl
{
public LyricsSearchControlViewModel ViewModel => (LyricsSearchControlViewModel)DataContext;
public LyricsSearchControl()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<LyricsSearchControlViewModel>();
}
}
}

View File

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

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

@@ -74,9 +74,17 @@ namespace BetterLyrics.WinUI3.Helper
using var ds = list.CreateDrawingSession();
if (strokeWidth > 0)
{
if (lyricsLine.TextGeometry == null)
{
return list;
}
ds.DrawGeometry(lyricsLine.TextGeometry, lyricsLine.Position, strokeColor, strokeWidth); // 描边
}
ds.FillGeometry(lyricsLine.TextGeometry, lyricsLine.Position, fontColor); // 填充
if (lyricsLine.CanvasTextLayout == null)
{
return list;
}
ds.DrawTextLayout(lyricsLine.CanvasTextLayout, lyricsLine.Position, fontColor); // 绘制文本(填充)
return list;
}

View File

@@ -116,15 +116,11 @@ namespace BetterLyrics.WinUI3.Helper
{
case WindowPixelSampleMode.BelowWindow:
{
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(myRect.Left, sampleY, screenWidth, sampleHeight);
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Bottom + 2, screenWidth, 1);
}
case WindowPixelSampleMode.AboveWindow:
{
int sampleHeight = 1;
int sampleY = myRect.Top - 1;
return GetAverageColorFromScreenRegion(myRect.Left, sampleY, screenWidth, sampleHeight);
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top - 3, screenWidth, 1);
}
case WindowPixelSampleMode.WindowArea:
{

View File

@@ -11,6 +11,6 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class FontHelper
{
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies();
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies().Order().ToArray();
}
}

View File

@@ -120,5 +120,18 @@ namespace BetterLyrics.WinUI3.Models
},
]);
}
public LyricsLine? GetLyricsLine(double sec)
{
for (int i = 0; i < LyricsLines.Count; i++)
{
var line = LyricsLines[i];
if (line.StartMs > sec * 1000)
{
return LyricsLines.ElementAtOrDefault(i - 1);
}
}
return null;
}
}
}

View File

@@ -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()
{
@@ -47,11 +53,6 @@ namespace BetterLyrics.WinUI3.Models
}
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

@@ -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,12 +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)
{
return await Task.Run(async () => await SearchAsyncCore(mediaSessionId, title, artist, album, bytesFromSMTC));
}
public async Task<byte[]?> SearchAsyncCore(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;
@@ -60,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,94 +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)
{
return await Task.Run(async () => await SearchAsyncCore(mediaSessionId, title, artist, album, durationMs, token), 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;
}
private async Task<(string?, LyricsSearchProvider?)> SearchAsyncCore(string mediaSessionId, string title, string artist, string album, double durationMs, CancellationToken token)
public async Task<List<LyricsSearchResult>> SearchAllAsync(string title, string artist, string album, double durationMs, CancellationToken token)
{
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
_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)
@@ -188,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;
}
}
}
@@ -198,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)
@@ -217,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;
@@ -276,7 +358,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
if (string.IsNullOrWhiteSpace(rawLyricFile))
return null;
{
return lyricsSearchResult;
}
// 下载歌词内容
var url = $"{Constants.AmllTTmlDB.QueryPrefix}{rawLyricFile}";
@@ -284,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?" +
@@ -305,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();
@@ -313,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()
{
@@ -354,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))
{
@@ -370,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

@@ -30,6 +30,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
bool IsPlaying { get; }
SongInfo? SongInfo { get; }
TimeSpan Position { get; }
LyricsData? CurrentLyricsData { get; }
LyricsSearchProvider? LyricsSearchProvider { get; }
TranslationSearchProvider? TranslationSearchProvider { get; }

View File

@@ -33,14 +33,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
return;
}
byte[]? bytes = await _albumArtSearchService.SearchAsync(
byte[]? bytes = await Task.Run(async () => await _albumArtSearchService.SearchAsync(
SongInfo?.SourceAppUserModelId ?? "",
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo?.Album ?? string.Empty,
_SMTCAlbumArtBytes
);
token.ThrowIfCancellationRequested();
_SMTCAlbumArtBytes,
token
), token);
if (token.IsCancellationRequested) return;
if (bytes == null)
{
@@ -57,7 +58,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var decoder = await BitmapDecoder.CreateAsync(stream);
token.ThrowIfCancellationRequested();
var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba16, BitmapAlphaMode.Premultiplied);
var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap);
token.ThrowIfCancellationRequested();
var albumArtLightAccentColor = ImageHelper.GetAccentColorsFromByte(bytes, 1, false).FirstOrDefault();

View File

@@ -17,12 +17,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
public partial class MediaSessionsService : IMediaSessionsService
{
private LatestOnlyTaskRunner _refreshLyricsRunner = new();
private LatestOnlyTaskRunner _showTranslationsRunner = new();
private LatestOnlyTaskRunner _refreshTranslationRunner = new();
private int _langIndex = 0;
private List<LyricsData> _lyricsDataArr = [];
private LyricsData? CurrentLyricsData => _lyricsDataArr.ElementAtOrDefault(_langIndex);
public LyricsData? CurrentLyricsData => _lyricsDataArr.ElementAtOrDefault(_langIndex);
public event EventHandler<LyricsChangedEventArgs>? LyricsChanged;
@@ -32,7 +32,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
[ObservableProperty] public partial bool IsTranslating { get; set; } = false;
private void UpdateTranslations()
private async Task RefreshTranslationAsync(CancellationToken token)
{
TranslationSearchProvider = null;
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
@@ -41,20 +41,17 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (_settingsService.AppSettings.TranslationSettings.IsTranslationEnabled)
{
_showTranslationsRunner.RunAsync(async token =>
{
await SetDisplayedAlongWithTranslationsAsync(token);
IsTranslating = false;
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
});
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));
}
IsTranslating = false;
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
}
private async Task SetDisplayedAlongWithTranslationsAsync(CancellationToken token)
@@ -133,27 +130,25 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
LyricsChanged?.Invoke(this, new LyricsChangedEventArgs(CurrentLyricsData));
string? lyricsRaw = null;
LyricsSearchProvider? lyricsSearchProvider = null;
LyricsSearchProvider = lyricsSearchProvider;
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(
var lyricsSearchResult = await Task.Run(async () => await _lyrcsSearchService.SearchSmartlyAsync(
SongInfo.SourceAppUserModelId ?? "",
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",
SongInfo.DurationMs ?? 0,
token
);
LyricsSearchProvider = lyricsSearchProvider;
_logger.LogInformation("Lyrics was found? {Found}, Provider: {LyricsSearchProvider}", lyricsRaw != null, LyricsSearchProvider?.ToString() ?? "null");
token.ThrowIfCancellationRequested();
), 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(lyricsRaw, (int?)SongInfo?.DurationMs);
_lyricsDataArr = new LyricsParser().Parse(lyricsSearchResult?.Raw, (int?)SongInfo?.DurationMs);
FillTranslationFromCache(LyricsSearchProvider);
}
else
@@ -176,12 +171,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
switch (provider)
{
case Enums.LyricsSearchProvider.QQ:
translationRaw = Helper.FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, Helper.PathHelper.QQTranslationCacheDirectory);
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, 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);
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.NeteaseTranslationCacheDirectory);
break;
case Enums.LyricsSearchProvider.LrcLib:
break;
@@ -223,5 +218,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_refreshLyricsRunner.RunAsync(RefreshLyricsAsync);
}
private void UpdateTranslations()
{
_refreshTranslationRunner.RunAsync(RefreshTranslationAsync);
}
}
}

View File

@@ -43,6 +43,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<LyricsWindowMode>>,
IRecipient<PropertyChangedMessage<List<string>>>
{
private readonly IAlbumArtSearchService _albumArtSearchService;
@@ -92,15 +93,29 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_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();
@@ -172,11 +187,22 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
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;
@@ -204,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))
{
@@ -218,11 +244,14 @@ 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));
});
}
}
}
@@ -233,10 +262,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var focusedSession = _mediaManager.GetFocusedSession();
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != focusedSession) return;
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
@@ -264,10 +293,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
string id = mediaSession.Id;
var focusedSession = _mediaManager.GetFocusedSession();
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != focusedSession) return;
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(id))
{
@@ -350,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;
@@ -382,13 +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());
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()
@@ -448,7 +502,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_lxMusicDurationSeconds = data.GetDouble();
}
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(TimeSpan.FromSeconds(_lxMusicPositionSeconds), TimeSpan.FromSeconds(_lxMusicDurationSeconds)));
if (IsMediaSourceTimelineSyncEnabled(Constants.PlayerID.LXMusic))
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(TimeSpan.FromSeconds(_lxMusicPositionSeconds), TimeSpan.FromSeconds(_lxMusicDurationSeconds)));
}
}
}
});
@@ -456,54 +513,49 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
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)
@@ -571,5 +623,16 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
}
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

@@ -219,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>
@@ -249,6 +252,39 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Translation provider</value>
</data>
<data name="LyricsSearchControlArtist.Header" 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="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.Header" xml:space="preserve">
<value>Song info mapping</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Target lyrics search provider</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Title</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>Local .ESLRC files</value>
</data>
@@ -485,7 +521,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 +731,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>
@@ -838,6 +874,9 @@ 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>

View File

@@ -196,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>
@@ -205,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>
@@ -219,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>
@@ -249,6 +252,39 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻訳プロバイダー</value>
</data>
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>アーティスト</value>
</data>
<data name="LyricsSearchControlHelp.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.Header" xml:space="preserve">
<value>曲情報マッピング</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>ターゲット歌詞検索プロバイダー</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>タイトル</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>ローカル.ESLRCファイル</value>
</data>
@@ -485,7 +521,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 +731,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>
@@ -838,6 +874,9 @@
<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>
@@ -1058,6 +1097,6 @@
<value>翻訳サーバーは設定されていません。最初に設定で構成してください</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics はすでに実行されています</value>
<value>BetterLyrics はすでに実行されています</value>
</data>
</root>

View File

@@ -196,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>
@@ -205,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>
@@ -219,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>
@@ -249,6 +252,39 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>번역 제공자</value>
</data>
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>아티스트</value>
</data>
<data name="LyricsSearchControlHelp.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.Header" xml:space="preserve">
<value>노래 정보 매핑</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>가사 검색 공급자를 타겟팅하십시오</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>제목</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>로컬 .ESLRC 파일</value>
</data>
@@ -485,7 +521,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 +731,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>
@@ -838,6 +874,9 @@
<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>

View File

@@ -219,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>
@@ -249,6 +252,39 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻译来源</value>
</data>
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>艺术家</value>
</data>
<data name="LyricsSearchControlHelp.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.Header" xml:space="preserve">
<value>歌曲信息映射</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>目标歌词搜索提供商</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>标题</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
@@ -838,6 +874,9 @@
<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>

View File

@@ -219,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>
@@ -249,6 +252,39 @@
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>翻譯來源</value>
</data>
<data name="LyricsSearchControlArtist.Header" xml:space="preserve">
<value>藝術家</value>
</data>
<data name="LyricsSearchControlHelp.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.Header" xml:space="preserve">
<value>歌曲信息映射</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>目標歌詞搜尋提供者</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>標題</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>本地 .ESLRC 文件</value>
</data>
@@ -485,7 +521,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 +731,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>
@@ -838,6 +874,9 @@
<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>

View File

@@ -102,6 +102,15 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial bool IsSongPlaying { get; set; }
[ObservableProperty]
public partial float TimelineSliderThumbOpacity { get; set; } = 0f;
[ObservableProperty]
public partial LyricsLine? TimelineSliderThumbLyricsLine { get; set; }
[ObservableProperty]
public partial double TimelineSliderThumbSeconds { get; set; } = 0;
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is LyricsWindowViewModel)
@@ -157,6 +166,11 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
partial void OnTimelineSliderThumbSecondsChanged(double value)
{
TimelineSliderThumbLyricsLine = _mediaSessionsService.CurrentLyricsData?.GetLyricsLine(value);
}
//partial void OnVolumeChanged(int value)
//{
// SystemVolumeHelper.SetMasterVolume(value);

View File

@@ -93,7 +93,7 @@ namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
if (_isCanvasWidthChanged)
{
if (_canvasWidth < 450)
if (_canvasWidth < 500)
{
_lyricsLayoutOrientation = LyricsLayoutOrientation.Vertical;
}
@@ -673,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

@@ -0,0 +1,193 @@
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.DependencyInjection;
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 LyricsLine? SelectedLyricsLine { 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();
LyricsData = null;
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;
}
[RelayCommand]
private void ResetMappedTitle()
{
MappedSongSearchQuery?.MappedTitle = MappedSongSearchQuery?.OriginalTitle ?? string.Empty;
}
[RelayCommand]
private void ResetMappedArtist()
{
MappedSongSearchQuery?.MappedArtist = MappedSongSearchQuery?.OriginalArtist ?? string.Empty;
}
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;
}
}
partial void OnSelectedLyricsLineChanged(LyricsLine? value)
{
if (value?.StartMs == null)
{
return;
}
_mediaSessionsService.ChangePosition(value.StartMs / 1000.0);
}
}
}

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},
@@ -279,9 +297,40 @@
Maximum="{x:Bind ViewModel.SongDurationSeconds, Mode=OneWay}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
Tapped="TimelineSliderOverlay_Tapped"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.TimelinePositionSeconds, Mode=OneWay}" />
<Grid
x:Name="TimelineSliderLyricsLineInfo"
Margin="0,-48,0,0"
Padding="8,4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="{ThemeResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="6"
Opacity="{x:Bind ViewModel.TimelineSliderThumbOpacity, Mode=OneWay}">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock
Margin="0,0,0,2"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
<TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.DisplayedText, Mode=OneWay}" />
</StackPanel>
</Grid>
<Grid
Height="32"
Margin="0,-32,0,0"
VerticalAlignment="Top"
Background="Transparent"
PointerEntered="TimelineSliderOverlay_PointerEntered"
PointerExited="TimelineSliderOverlay_PointerExited"
PointerMoved="TimelineSliderOverlay_PointerMoved"
PointerPressed="TimelineSliderOverlay_PointerPressed" />
</Grid>
</Grid>
@@ -315,7 +364,7 @@
<Flyout x:Name="BottomCommandFlyout" ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="450" />
<Setter Property="MinWidth" Value="500" />
<Setter Property="MinHeight" Value="100" />
<Setter Property="CornerRadius" Value="12" />
</Style>

View File

@@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@@ -60,7 +61,7 @@ namespace BetterLyrics.WinUI3.Views
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize.Width < 450 || e.NewSize.Height < 100)
if (e.NewSize.Width < 500 || e.NewSize.Height < 100)
{
if (BottomCommandGrid.Children.Count != 0)
{
@@ -110,11 +111,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
private void TimelineSliderOverlay_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
_mediaSessionsService.ChangePosition(TimelineSlider.Value);
}
private void PlaybackSettingsFlyout_Closed(object sender, object e)
{
PlaybackSettingsFlyout.Content = null;
@@ -134,5 +130,56 @@ 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;
}
private void TimelineSliderOverlay_PointerPressed(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
var grid = (Grid)sender;
var pos = e.GetCurrentPoint(grid).Position;
var ratio = pos.X / grid.ActualWidth;
_mediaSessionsService.ChangePosition(TimelineSlider.Maximum * ratio);
}
private void TimelineSliderOverlay_PointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
float targetX;
var grid = (Grid)sender;
var pos = e.GetCurrentPoint(grid).Position;
var ratio = pos.X / grid.ActualWidth;
ViewModel.TimelineSliderThumbSeconds = TimelineSlider.Maximum * ratio;
if (pos.X + TimelineSliderLyricsLineInfo.ActualWidth > grid.ActualWidth)
{
targetX = (float)(grid.ActualWidth - TimelineSliderLyricsLineInfo.ActualWidth);
}
else
{
targetX = (float)pos.X;
}
TimelineSliderLyricsLineInfo.Translation = new Vector3(targetX, 0, 0);
}
private void TimelineSliderOverlay_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ViewModel.TimelineSliderThumbOpacity = 0.7f;
}
private void TimelineSliderOverlay_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
ViewModel.TimelineSliderThumbOpacity = 0f;
}
}
}

View File

@@ -25,6 +25,7 @@
<!-- Top command -->
<Grid
x:Name="TopCommandGrid"
Margin="6"
VerticalAlignment="Top"
Background="Transparent"
Opacity="{x:Bind ViewModel.TopCommandGridOpacity, Mode=OneWay}"

View File

@@ -354,7 +354,7 @@
<StackPanel>
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>

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/)
## 立即體驗