Compare commits

...

29 Commits

Author SHA1 Message Date
Zhe Fang
fa6da81988 Merge pull request #53 from jayfunc/dev
fix
2025-07-21 17:18:37 -04:00
Zhe Fang
aa692e2735 fix auto transparent issue after restart and lock when hover on non-transparent window 2025-07-21 17:17:57 -04:00
Zhe Fang
c7ee26f284 fix timeline update strategy 2025-07-21 11:29:52 -04:00
Zhe Fang
b103e6efd1 update readme 2025-07-21 10:28:44 -04:00
Zhe Fang
09e8cce69a Merge pull request #52 from jayfunc/dev
fix
2025-07-21 09:51:41 -04:00
Zhe Fang
16cd12e22b fix 2025-07-21 09:51:14 -04:00
Zhe Fang
e0121bee64 Merge pull request #51 from jayfunc/dev
v1.0.20.0
2025-07-20 23:28:59 -04:00
Zhe Fang
34bdbc89bc fix #50 2025-07-20 23:28:37 -04:00
Zhe Fang
b649e9761d update readme 2025-07-19 14:50:47 -04:00
Zhe Fang
f5638c6880 Merge pull request #49 from jayfunc/dev
v1.0.18.0
2025-07-19 14:34:34 -04:00
Zhe Fang
a9807f4f09 fix #41 2025-07-19 14:33:45 -04:00
Zhe Fang
def287715d fix 2025-07-19 12:54:20 -04:00
Zhe Fang
966f926112 fix #47 fix #44 2025-07-19 08:30:29 -04:00
Zhe Fang
4568293b51 update readme 2025-07-18 11:05:51 -04:00
Zhe Fang
10115ab0a8 update readme 2025-07-18 11:00:42 -04:00
Zhe Fang
ecefaedcb9 update readme 2025-07-17 21:06:15 -04:00
Zhe Fang
b9aae0866d update readme 2025-07-17 21:05:25 -04:00
Zhe Fang
afbfcc921e update readme 2025-07-17 20:55:53 -04:00
Zhe Fang
0606711023 Merge pull request #43 from jayfunc/dev
v1.0.17.0
2025-07-17 14:54:12 -04:00
Zhe Fang
8f997ca3d9 fix #42 2025-07-17 14:45:00 -04:00
Zhe Fang
042d557eb1 fix #40 fix #14 2025-07-17 14:29:47 -04:00
Zhe Fang
6ad79180e4 Merge pull request #19 from jayfunc/dev
github actions
2025-07-11 19:49:17 -04:00
Zhe Fang
42af22a7e3 Merge pull request #18 from jayfunc/dev
v1.0.11.0
2025-07-11 18:08:49 -04:00
Zhe Fang
509079e8c7 Merge pull request #10 from jayfunc/dev
1.0.9.0
2025-07-07 17:32:48 -04:00
Zhe Fang
c50c180aa0 Merge pull request #9 from jayfunc/dev
v1.0.7.0 release
2025-06-30 20:56:34 -04:00
Zhe Fang
5e74468194 Merge pull request #8 from jayfunc/dev
add multiple online lyrics providers; add desktop mode; improve blur/shadow/scrolling effect performance; fix bugs
2025-06-26 21:51:36 -04:00
Zhe Fang
a93b535667 Merge pull request #7 from jayfunc/dev
update to v1.0.5.0
2025-06-23 13:41:33 -04:00
Zhe Fang
2c55b11e70 Merge pull request #6 from jayfunc/dev
fix
2025-06-18 17:16:06 -04:00
Zhe Fang
7bca1d1205 Merge pull request #5 from jayfunc/dev
Add dock mode, improve glow effect, fix bugs ...
2025-06-17 21:50:22 -04:00
59 changed files with 2249 additions and 741 deletions

View File

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

View File

@@ -12,6 +12,7 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
@@ -48,6 +49,7 @@
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
@@ -82,7 +84,7 @@
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
@@ -99,6 +101,374 @@
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TransparentSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />

View File

@@ -16,6 +16,7 @@ using ShadowViewer.Controls;
using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3
@@ -55,7 +56,7 @@ namespace BetterLyrics.WinUI3
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
WindowHelper.OpenWindow<LyricsWindow>();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow == null) return;
@@ -85,13 +86,13 @@ namespace BetterLyrics.WinUI3
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ITranslateService, TranslateService>()
// Manager
// ViewModels
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsPageViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<MusicGalleryViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.BuildServiceProvider()
);

View File

@@ -9,6 +9,7 @@
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
@@ -22,6 +23,8 @@
<None Remove="Assets\Core14.profile.xml" />
<None Remove="Assets\Segoe Fluent Icons.ttf" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
<None Remove="Views\MusicGalleryWindow.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
</ItemGroup>
<ItemGroup>
@@ -33,7 +36,8 @@
<ItemGroup>
<PackageReference Include="3v.EvtSource" Version="2.0.0" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
@@ -46,10 +50,10 @@
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
@@ -58,16 +62,17 @@
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.7" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.0.0" />
<PackageReference Include="z440.atl.core" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
@@ -88,6 +93,16 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
<Generator>MSBuild:Compile</Generator>

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 SecondsToFormattedTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double seconds)
{
return TimeSpan.FromSeconds(seconds).ToString(@"mm\:ss");
}
else if (value is int secondsInt)
{
return TimeSpan.FromSeconds(secondsInt).ToString(@"mm\:ss");
}
return value?.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -19,10 +19,8 @@ namespace BetterLyrics.WinUI3.Helper
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<IntPtr, nint> _oldWndProcs = new();
private static readonly Dictionary<IntPtr, (double X, double Y, double Width, double Height)> _originalWindowBounds = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyles = [];
private static List<Rectangle> _interactiveRects = new();
private delegate nint WndProcDelegate(nint hWnd, uint msg, nint wParam, nint lParam);
@@ -75,6 +73,14 @@ namespace BetterLyrics.WinUI3.Helper
int targetX = _settingsService.DesktopWindowLeft;
int targetY = _settingsService.DesktopWindowTop;
if (targetWidth <= 0 || targetHeight <= 0 || targetX < 0 || targetY < 0)
{
targetWidth = 1200;
targetHeight = 600;
targetX = 200;
targetY = 200;
}
// <20><><EFBFBD>ô<EFBFBD><C3B4>ڴ<EFBFBD>С<EFBFBD><D0A1>λ<EFBFBD><CEBB>
window.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(targetX, targetY, targetWidth, targetHeight)
@@ -103,16 +109,6 @@ namespace BetterLyrics.WinUI3.Helper
window.ToggleWindowStyle(true, WindowStyle.Popup | WindowStyle.Visible);
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT | (int)User32.WindowStylesEx.WS_EX_LAYERED);
//// <20><>װ<EFBFBD>Զ<EFBFBD><D4B6><EFBFBD>WndProc
//if (!_oldWndProcs.ContainsKey(hwnd))
//{
// nint newWndProc = Marshal.GetFunctionPointerForDelegate((WndProcDelegate)((hWnd, msg, wParam, lParam) =>
// CustomWndProc(hWnd, msg, wParam, lParam, hwnd)
// ));
// nint oldWndProc = User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWLP_WNDPROC, newWndProc);
// _oldWndProcs[hwnd] = oldWndProc;
//}
}
else
{
@@ -123,51 +119,7 @@ namespace BetterLyrics.WinUI3.Helper
window.SetWindowStyle(style);
_originalWindowStyles.Remove(hwnd);
}
//// <20>ָ<EFBFBD>ԭWndProc
//if (_oldWndProcs.TryGetValue(hwnd, out var oldWndProc))
//{
// User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWLP_WNDPROC, oldWndProc);
// _oldWndProcs.Remove(hwnd);
//}
}
}
private static nint CustomWndProc(nint hWnd, uint msg, nint wParam, nint lParam, IntPtr hwnd)
{
const int WM_NCHITTEST = 0x84;
const int HTCLIENT = 1;
const int HTTRANSPARENT = -1;
if (msg == WM_NCHITTEST)
{
int x = (short)(lParam.ToInt32() & 0xFFFF);
int y = (short)((lParam.ToInt32() >> 16) & 0xFFFF);
// תΪ<D7AA><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POINT pt = new() { x = x, y = y };
User32.ScreenToClient(hWnd, ref pt);
foreach (var rect in _interactiveRects)
{
if (rect.Contains(pt.x, pt.y))
return HTCLIENT;
}
return HTTRANSPARENT;
}
// <20><><EFBFBD><EFBFBD>ԭWndProc
if (_oldWndProcs.TryGetValue(hwnd, out var oldWndProc))
{
return User32.CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
}
return User32.DefWindowProc(hWnd, msg, wParam, lParam);
}
public static void SetInteractiveRects(IEnumerable<Rectangle> rects)
{
_interactiveRects = rects.ToList();
}
}
}

View File

@@ -31,11 +31,12 @@ namespace BetterLyrics.WinUI3.Helper
window.SetIsAlwaysOnTop(false);
UnregisterAppBar(hwnd);
RefreshWorkArea();
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
window.ExtendsContentIntoTitleBar = true;
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
User32.SetWindowPos(
@@ -54,7 +55,7 @@ namespace BetterLyrics.WinUI3.Helper
public static void Enable(Window window, int appBarHeight, DockPlacement dockPlacement)
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
//window.ExtendsContentIntoTitleBar = false;
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
@@ -63,7 +64,6 @@ namespace BetterLyrics.WinUI3.Helper
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
if (!_originalPositions.ContainsKey(hwnd))
{
@@ -78,6 +78,7 @@ namespace BetterLyrics.WinUI3.Helper
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int screenHeight = User32.GetSystemMetrics(User32.SystemMetric.SM_CYSCREEN);
int y = dockPlacement == DockPlacement.Top ? 0 : screenHeight - appBarHeight;
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
@@ -85,10 +86,11 @@ namespace BetterLyrics.WinUI3.Helper
y,
screenWidth,
appBarHeight,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
User32.SetWindowPosFlags.SWP_HIDEWINDOW
);
RefreshWorkArea();
window.ExtendsContentIntoTitleBar = false;
window.ToggleWindowStyle(true, WindowStyle.Popup);
window.Show();
}
private static void RegisterAppBar(IntPtr hwnd, int height, DockPlacement dockPlacement)
@@ -114,7 +116,9 @@ namespace BetterLyrics.WinUI3.Helper
},
};
// Ref: https://github.com/TwilightLemon/AppBarTest/blob/master/AppBarCreator.cs
Shell32.SHAppBarMessage(Shell32.ABM.ABM_NEW, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
_registered.Add(hwnd);
@@ -167,6 +171,7 @@ namespace BetterLyrics.WinUI3.Helper
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
// 同步窗口实际高度和位置
@@ -178,10 +183,8 @@ namespace BetterLyrics.WinUI3.Helper
y,
User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
newHeight,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
newHeight == 0 ? User32.SetWindowPosFlags.SWP_HIDEWINDOW : User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
RefreshWorkArea();
}, TimeSpan.FromMilliseconds(100));
}
}

View File

@@ -0,0 +1,20 @@
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Graphics.Canvas.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class FontHelper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies();
public static string GetUserPreferredFontFamily() => SystemFontFamilies.ElementAtOrDefault(_settingsService.SelectedFontFamilyIndex) ?? "Segoe UI";
}
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
using Windows.System;
@@ -16,6 +17,7 @@ namespace BetterLyrics.WinUI3.Helper
private readonly List<User32.HWINEVENTHOOK> _hooks = new();
private HWND _currentForeground = HWND.NULL;
private readonly IntPtr _selfHwnd;
private readonly ThrottleHelper _winEventProcThrottle = new(TimeSpan.FromSeconds(1));
public delegate void WindowChangedHandler(HWND hwnd);
private readonly WindowChangedHandler _onWindowChanged;

View File

@@ -35,6 +35,17 @@ namespace BetterLyrics.WinUI3.Helper
return stream;
}
public static RandomAccessStreamReference ByteArrayToRandomAccessStreamReference(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
var writer = new DataWriter(stream);
writer.WriteBytes(bytes);
writer.StoreAsync().GetAwaiter().GetResult();
writer.FlushAsync().GetAwaiter().GetResult();
writer.DetachStream();
return RandomAccessStreamReference.CreateFromStream(stream);
}
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(int width, int height)
{
var device = CanvasDevice.GetSharedDevice();

View File

@@ -47,7 +47,7 @@ namespace BetterLyrics.WinUI3.Helper
break;
}
}
PostProcessLyricsLines(durationMs.Value);
_lyricsDataArr.Add(new LyricsData()); // 为机翻预留
return _lyricsDataArr;
}
@@ -113,8 +113,7 @@ namespace BetterLyrics.WinUI3.Helper
// 初始化每种语言的歌词列表
_lyricsDataArr.Clear();
for (int i = 0; i < languageCount; i++)
_lyricsDataArr.Add(new LyricsData());
for (int i = 0; i < languageCount; i++) _lyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
foreach (var group in grouped)
@@ -374,29 +373,5 @@ namespace BetterLyrics.WinUI3.Helper
_lyricsDataArr.Add(new LyricsData(lyricsLines));
}
private void PostProcessLyricsLines(int durationMs)
{
for (int langIdx = 0; langIdx < _lyricsDataArr.Count; langIdx++)
{
var lines = _lyricsDataArr[langIdx].LyricsLines;
if (lines.Count > 0)
{
if (lines[0].StartMs > 0)
{
lines.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = lines[0].StartMs,
OriginalText = "● ● ●",
LyricsChars = [],
}
);
}
}
}
}
}
}

View File

@@ -63,7 +63,7 @@ namespace BetterLyrics.WinUI3.Helper
{
var data = pNotify.ToStructure<AUDIO_VOLUME_NOTIFICATION_DATA>();
_masterVolume = (int)(data.fMasterVolume * 100);
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
VolumeChanged?.Invoke(_masterVolume);
});

View File

@@ -0,0 +1,37 @@
using System;
namespace BetterLyrics.WinUI3.Helper
{
public class ThrottleHelper
{
private DateTime _lastTriggerTime = DateTime.MinValue;
private readonly TimeSpan _interval;
public ThrottleHelper(TimeSpan interval)
{
_interval = interval;
}
/// <summary>
/// 判断是否可以触发(距离上次触发已超过设定间隔),如果可以则更新时间戳并返回 true否则返回 false。
/// </summary>
public bool CanTrigger()
{
var now = DateTime.Now;
if ((now - _lastTriggerTime) >= _interval)
{
_lastTriggerTime = now;
return true;
}
return false;
}
/// <summary>
/// 重置触发时间
/// </summary>
public void Reset()
{
_lastTriggerTime = DateTime.MinValue;
}
}
}

View File

@@ -26,18 +26,6 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static void ExitAllWindows()
{
while (_activeWindows.Count > 0)
{
var window = (Window)_activeWindows[0];
DockModeHelper.Disable(window);
window.Close();
_activeWindows.Remove(window);
}
App.Current.Exit();
}
public static T? GetWindowByWindowType<T>()
{
foreach (var window in _activeWindows)
@@ -49,33 +37,32 @@ namespace BetterLyrics.WinUI3.Helper
}
return default;
}
public static void OpenOrShowWindow<T>()
public static void OpenWindow<T>()
{
var window = _activeWindows.Find(w => w is T);
if (window != null)
if (window == null)
{
var castedWindow = (Window)window;
castedWindow.Restore();
}
else
{
object newWindow;
if (typeof(T) == typeof(LyricsWindow))
{
newWindow = new LyricsWindow();
((LyricsWindow)newWindow).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
window = new LyricsWindow();
((LyricsWindow)window).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
}
else if (typeof(T) == typeof(SettingsWindow))
{
newWindow = new SettingsWindow();
window = new SettingsWindow();
}
else if (typeof(T) == typeof(MusicGalleryWindow))
{
window = new MusicGalleryWindow();
}
else
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
((Window)newWindow).Activate();
TrackWindow(newWindow);
TrackWindow(window);
}
var castedWindow = (Window)window;
castedWindow.Restore();
}
public static void RestartApp(string args = "")
@@ -101,7 +88,19 @@ namespace BetterLyrics.WinUI3.Helper
private static void TrackWindow(object window)
{
if (!_activeWindows.Contains(window))
{
_activeWindows.Add(window);
var castedWindow = (Window)window;
castedWindow.Closed += WindowHelper_Closed;
}
}
private static void WindowHelper_Closed(object sender, WindowEventArgs args)
{
if (_activeWindows.Contains(sender))
{
_activeWindows.Remove(sender);
}
}
}
}

View File

@@ -4,7 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LocalLyricsFolder : ObservableObject
public partial class LocalMediaFolder : ObservableObject
{
[ObservableProperty]
public partial bool IsEnabled { get; set; }
@@ -12,9 +12,9 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string Path { get; set; }
public LocalLyricsFolder() { }
public LocalMediaFolder() { }
public LocalLyricsFolder(string path, bool isEnabled)
public LocalMediaFolder(string path, bool isEnabled)
{
Path = path;
IsEnabled = isEnabled;

View File

@@ -71,7 +71,7 @@ namespace BetterLyrics.WinUI3.Models
}
else
{
line.DisplayedText = $"{line.OriginalText}{StringHelper.NewLine}({translationArr[i]})";
line.DisplayedText = $"{line.OriginalText}{StringHelper.NewLine}{translationArr[i]}";
}
i++;
}
@@ -85,6 +85,30 @@ namespace BetterLyrics.WinUI3.Models
}
}
public LyricsData CreateLyricsDataFrom(string translation)
{
var result = new LyricsData(LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
}).ToList());
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in result.LyricsLines)
{
if (i >= translationArr.Count)
{
break;
}
else
{
line.OriginalText = translationArr[i];
}
i++;
}
return result;
}
public static LyricsData GetNotfoundPlaceholder(int durationMs)
{
return new LyricsData([new LyricsLine

View File

@@ -14,6 +14,9 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string Artist { get; set; }
[ObservableProperty]
public partial int? Duration { get; set; }
[ObservableProperty]
public partial double? DurationMs { get; set; }

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class TrimmedTrack
{
public string Title { get; set; }
public string Artist { get; set; }
public string Album { get; set; }
public int? Year { get; set; }
public string Genre { get; set; }
public string FilePath { get; set; }
public int Duration { get; set; }
public byte[]? AlbumArt { get; set; }
}
}

View File

@@ -11,7 +11,7 @@ namespace BetterLyrics.WinUI3.Serialization
[JsonSerializable(typeof(List<AlbumArtSearchProviderInfo>))]
[JsonSerializable(typeof(List<LyricsSearchProviderInfo>))]
[JsonSerializable(typeof(List<MediaSourceProviderInfo>))]
[JsonSerializable(typeof(List<LocalLyricsFolder>))]
[JsonSerializable(typeof(List<LocalMediaFolder>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(TranslateResponse))]
[JsonSerializable(typeof(JsonElement))]

View File

@@ -67,7 +67,7 @@ namespace BetterLyrics.WinUI3.Services
private byte[]? SearchFile(string artist, string album)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{

View File

@@ -14,6 +14,6 @@ namespace BetterLyrics.WinUI3.Services
{
event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public void UpdateWatchers(List<LocalLyricsFolder> folders);
public void UpdateWatchers(List<LocalMediaFolder> folders);
}
}

View File

@@ -19,8 +19,9 @@ namespace BetterLyrics.WinUI3.Services
Task PauseAsync();
Task PreviousAsync();
Task NextAsync();
Task ChangePosition(double seconds);
bool IsPlaying { get; }
SongInfo? SongInfo { get; }
}
}

View File

@@ -41,7 +41,7 @@ namespace BetterLyrics.WinUI3.Services
int PositionOffset { get; set; }
// Lyrics lib
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
List<LocalMediaFolder> LocalMediaFolders { get; set; }
// Lyrics style and effetc
@@ -96,5 +96,10 @@ namespace BetterLyrics.WinUI3.Services
bool IsImmersiveMode { get; set; }
string LXMusicServer { get; set; }
DockPlacement DockPlacement { get; set; }
bool HideWindowWhenNotPlaying { get; set; }
int DockWindowHeight { get; set; }
int SelectedFontFamilyIndex { get; set; }
string LyricsFontFamily { get; set; }
bool IsDragEverywhereEnabled { get; set; }
}
}

View File

@@ -6,19 +6,18 @@ using System.IO;
using System.Linq;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.UI.Dispatching;
namespace BetterLyrics.WinUI3.Services
{
public class LibWatcherService : IDisposable, ILibWatcherService
public class LibWatcherService : BaseViewModel, IDisposable, ILibWatcherService
{
private readonly ISettingsService _settingsService;
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
public LibWatcherService(ISettingsService settingsService)
public LibWatcherService(ISettingsService settingsService) : base(settingsService)
{
_settingsService = settingsService;
UpdateWatchers(_settingsService.LocalLyricsFolders);
UpdateWatchers(_settingsService.LocalMediaFolders);
}
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
@@ -32,7 +31,7 @@ namespace BetterLyrics.WinUI3.Services
_watchers.Clear();
}
public void UpdateWatchers(List<LocalLyricsFolder> folders)
public void UpdateWatchers(List<LocalMediaFolder> folders)
{
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
@@ -69,16 +68,13 @@ namespace BetterLyrics.WinUI3.Services
private void OnChanged(string folder, FileSystemEventArgs e)
{
App.DispatcherQueue!.TryEnqueue(
Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
MusicLibraryFilesChanged?.Invoke(
this,
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
);
}
);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
MusicLibraryFilesChanged?.Invoke(
this,
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
);
});
}
}
}

View File

@@ -164,7 +164,7 @@ namespace BetterLyrics.WinUI3.Services
private async Task<string?> SearchFile(string title, string artist, LyricsFormat format)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
@@ -186,7 +186,7 @@ namespace BetterLyrics.WinUI3.Services
private string? SearchEmbedded(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{

View File

@@ -64,6 +64,7 @@ namespace BetterLyrics.WinUI3.Services
}
public bool IsPlaying => _cachedIsPlaying;
public SongInfo? SongInfo => _cachedSongInfo;
private bool IsMediaSourceEnabled(string id)
{
@@ -109,10 +110,8 @@ namespace BetterLyrics.WinUI3.Services
{
if (!IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId) || mediaSession != _mediaManager.GetFocusedSession()) return;
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(timelineProperties.Position));
}
);
@@ -129,9 +128,8 @@ namespace BetterLyrics.WinUI3.Services
_ => false,
};
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(_cachedIsPlaying));
}
);
@@ -153,6 +151,8 @@ namespace BetterLyrics.WinUI3.Services
SourceAppUserModelId = id,
};
_cachedSongInfo.Duration = (int)(_cachedSongInfo.DurationMs / 1000f);
await _onAnyMediaPropertyChangedRunner.RunAsync(async token =>
{
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
@@ -184,7 +184,7 @@ namespace BetterLyrics.WinUI3.Services
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
@@ -215,8 +215,7 @@ namespace BetterLyrics.WinUI3.Services
{
_mediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, true));
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
MediaSourceProvidersInfoChanged?.Invoke(this, new MediaSourceProvidersInfoEventArgs(_mediaSourceProvidersInfo));
});
@@ -225,8 +224,7 @@ namespace BetterLyrics.WinUI3.Services
private void SendNullMessages()
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
_cachedSongInfo = null;
_cachedIsPlaying = false;
@@ -272,7 +270,7 @@ namespace BetterLyrics.WinUI3.Services
var _albumArtAccentColor = ImageHelper.GetAccentColorsFromByte(bytes).FirstOrDefault();
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
AlbumArtChangedChanged?.Invoke(this, new AlbumArtChangedEventArgs(_albumArtSwBitmap, _albumArtAccentColor));
});
@@ -289,7 +287,7 @@ namespace BetterLyrics.WinUI3.Services
catch (Exception)
{
_logger.LogError("Failed to start SSE connection for LX Music.");
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.LyricsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("FailToStartLXMusicServer"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
});
@@ -366,6 +364,15 @@ namespace BetterLyrics.WinUI3.Services
}
}
public async Task ChangePosition(double seconds)
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
{
await focusedSession.ControlSession.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
}
public void Receive(PropertyChangedMessage<ObservableCollection<MediaSourceProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)

View File

@@ -96,6 +96,12 @@ namespace BetterLyrics.WinUI3.Services
private const string LockHotKeyIndexKey = "LockHotKeyIndex";
private const string DockPlacementKey = "DockPlacement";
private const string LyricsBgFontOpacityKey = "LyricsBgFontOpacity";
private const string HideWindowWhenNotPlayingKey = "HideWindowWhenNotPlaying";
private const string DockWindowHeightKey = "DockWindowHeight";
private const string SelectedFontFamilyIndexKey = "SelectedFontFamilyIndex";
private const string LyricsFontFamilyKey = "LyricsFontFamily";
private const string IsDragEverywhereEnabledKey = "IsDragEverywhereEnabled";
private readonly ApplicationDataContainer _localSettings;
@@ -221,6 +227,41 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(LockHotKeyIndexKey, 'U' - 'A');
SetDefault(DockPlacementKey, (int)DockPlacement.Top);
SetDefault(LyricsBgFontOpacityKey, 30); // 30%
SetDefault(HideWindowWhenNotPlayingKey, false);
SetDefault(DockWindowHeightKey, 64); // 64px
SetDefault(SelectedFontFamilyIndexKey, 0);
SetDefault(LyricsFontFamilyKey, FontHelper.SystemFontFamilies.ElementAtOrDefault(0));
SetDefault(IsDragEverywhereEnabledKey, false);
}
public bool IsDragEverywhereEnabled
{
get => GetValue<bool>(IsDragEverywhereEnabledKey);
set => SetValue(IsDragEverywhereEnabledKey, value);
}
public string LyricsFontFamily
{
get => GetValue<string>(LyricsFontFamilyKey)!;
set => SetValue(LyricsFontFamilyKey, value);
}
public int SelectedFontFamilyIndex
{
get => GetValue<int>(SelectedFontFamilyIndexKey);
set => SetValue(SelectedFontFamilyIndexKey, value);
}
public bool HideWindowWhenNotPlaying
{
get => GetValue<bool>(HideWindowWhenNotPlayingKey);
set => SetValue(HideWindowWhenNotPlayingKey, value);
}
public int DockWindowHeight
{
get => GetValue<int>(DockWindowHeightKey);
set => SetValue(DockWindowHeightKey, value);
}
public int LyricsBgFontOpacity
@@ -379,19 +420,19 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(LanguageKey, (int)value);
}
public List<LocalLyricsFolder> LocalLyricsFolders
public List<LocalMediaFolder> LocalMediaFolders
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LocalLyricsFoldersKey) ?? "[]",
SourceGenerationContext.Default.ListLocalLyricsFolder
SourceGenerationContext.Default.ListLocalMediaFolder
)!;
set =>
SetValue(
LocalLyricsFoldersKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLocalLyricsFolder
SourceGenerationContext.Default.ListLocalMediaFolder
)
);
}

View File

@@ -3,6 +3,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.ViewModels;
using Lyricify.Lyrics.Helpers.General;
using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -46,7 +47,7 @@ namespace BetterLyrics.WinUI3.Services
if (string.IsNullOrEmpty(_settingsService.LibreTranslateServer))
{
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.LyricsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString("TranslateServerNotSet"),

View File

@@ -775,4 +775,25 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>Unable to connect to LX Music server, please go to Settings - Advanced options to check if the link is entered correctly</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>Auto-hide window</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>Automatically hide the window when no songs are playing in dock mode or desktop mode</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Window height</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>Lyrics font family</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>Global drag</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>Extend the title bar to the entire page so that the window can be dragged in any non-interactive area</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Music gallery</value>
</data>
</root>

View File

@@ -775,4 +775,25 @@
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>LX Music Serverに接続できない場合は、設定に移動してください。リンクが正しく入力されているかどうかを確認するための高度なオプションに移動してください</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自動ハイドウィンドウ</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>ドックモードやデスクトップモードで再生されている曲がないときにウィンドウを自動的に非表示にする</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窓の高さ</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌詞フォントファミリー</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>グローバルドラッグ</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>タイトルバーをページ全体に拡張して、ウィンドウを非対話領域でドラッグできるようにします</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音楽ギャラリー</value>
</data>
</root>

View File

@@ -775,4 +775,25 @@
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>LX Music Server에 연결할 수 없습니다. 설정으로 이동하십시오 - 고급 옵션이 링크가 올바르게 입력되었는지 확인하십시오.</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>자동 가죽 창</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>도크 모드 또는 데스크탑 모드에서 재생되지 않을 때는 창을 자동으로 숨기고 있습니다.</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>창 높이</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>가사 글꼴 가족</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>글로벌 드래그</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>비 중과 영역에서 창을 드래그 할 수 있도록 제목 표시 줄을 전체 페이지로 확장하십시오.</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>음악 갤러리</value>
</data>
</root>

View File

@@ -775,4 +775,25 @@
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>无法连接到 LX 音乐服务器,请转到设置 - 高级选项以检查是否正确输入链接</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自动隐藏窗口</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>停靠模式或桌面模式下,无歌曲正在播放时,自动隐藏窗口</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窗口高度</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌词字体</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>全局拖拽</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>将标题栏扩展至整个页面使得在任意非交互区域均可拖拽窗口</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音乐库</value>
</data>
</root>

View File

@@ -775,4 +775,25 @@
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>無法連接到 LX 音樂服務器,請轉到設置 - 高級選項以檢查是否正確輸入鏈接</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自動隱藏窗口</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>停靠模式或桌面模式下,無歌曲正在播放時,自動隱藏窗口</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窗口高度</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌詞字體</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>全域拖曳</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>將標題列擴展至整個頁面使得在任意非互動區域均可拖曳窗口</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音樂庫</value>
</data>
</root>

View File

@@ -7,22 +7,20 @@ using Microsoft.UI.Dispatching;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseViewModel : ObservableRecipient, IDisposable
public partial class BaseViewModel : ObservableRecipient
{
private protected readonly DispatcherQueue _dispatcherQueue =
DispatcherQueue.GetForCurrentThread();
private protected readonly DispatcherQueue _dispatcherQueue;
private protected readonly DispatcherQueueTimer _dispatcherQueueTimer;
private protected readonly ISettingsService _settingsService;
public BaseViewModel(ISettingsService settingsService)
{
IsActive = true;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_dispatcherQueueTimer = _dispatcherQueue.CreateTimer();
_settingsService = settingsService;
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
}

View File

@@ -9,14 +9,23 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using System;
using System.Numerics;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsPageViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<bool>>
public partial class LyricsPageViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<TimeSpan>>
{
private readonly IPlaybackService _playbackService;
private readonly ThrottleHelper _timelineThrottle = new(TimeSpan.FromSeconds(1));
public LyricsPageViewModel(ISettingsService settingsService, IPlaybackService playbackService) : base(settingsService)
{
@@ -27,6 +36,8 @@ namespace BetterLyrics.WinUI3.ViewModels
PositionOffset = _settingsService.PositionOffset;
IsImmersiveMode = _settingsService.IsImmersiveMode;
ShowTranslationOnly = _settingsService.ShowTranslationOnly;
LyricsFontSize = _settingsService.LyricsFontSize;
LyricsFontFamily = _settingsService.LyricsFontFamily;
OnIsImmersiveModeChanged(IsImmersiveMode);
@@ -53,14 +64,27 @@ namespace BetterLyrics.WinUI3.ViewModels
private void PlaybackService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
SongDurationSeconds = SongInfo?.Duration ?? 0;
if (ResetPositionOffsetOnSongChanged)
{
PositionOffset = 0;
}
}
//[ObservableProperty]
//public partial int Volume { get; set; }
[ObservableProperty]
public partial double TimelinePositionSeconds { get; set; }
[ObservableProperty]
public partial int SongDurationSeconds { get; set; }
[ObservableProperty]
public partial int Volume { get; set; }
[ObservableProperty]
public partial string LyricsFontFamily { get; set; }
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial bool IsImmersiveMode { get; set; }
@@ -69,7 +93,7 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial float BottomCommandGridOpacity { get; set; }
[ObservableProperty]
public partial Thickness BottomCommandGridMargin { get; set; } = new Thickness(12);
public partial float BottomCommandFlyoutTriggerOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
@@ -117,7 +141,6 @@ namespace BetterLyrics.WinUI3.ViewModels
{
DisplayType = _settingsService.DisplayType;
}
BottomCommandGridMargin = message.NewValue ? new Thickness(0) : new Thickness(12);
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
@@ -140,7 +163,7 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private static void OpenSettingsWindow()
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
[RelayCommand]
@@ -188,10 +211,12 @@ namespace BetterLyrics.WinUI3.ViewModels
if (value)
{
BottomCommandGridOpacity = 0f;
BottomCommandFlyoutTriggerOpacity = 0f;
}
else
{
BottomCommandGridOpacity = 1f;
BottomCommandFlyoutTriggerOpacity = 1f;
}
}
@@ -200,9 +225,48 @@ namespace BetterLyrics.WinUI3.ViewModels
_settingsService.ShowTranslationOnly = value;
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontFamily))
{
LyricsFontFamily = message.NewValue;
}
}
}
//partial void OnVolumeChanged(int value)
//{
// SystemVolumeHelper.SetMasterVolume(value);
//}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.TotalTime))
{
if (_timelineThrottle.CanTrigger())
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelinePositionSeconds = message.NewValue.TotalSeconds;
});
}
}
}
}
}
}

View File

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -26,6 +27,8 @@ namespace BetterLyrics.WinUI3.ViewModels
_lyricsTextFormat.FontWeight = _settingsService.LyricsFontWeight.ToFontWeight();
_lyricsTextFormat.FontFamily = _artistTextFormat.FontFamily = _titleTextFormat.FontFamily = _settingsService.LyricsFontFamily;
_lyricsAlignmentType = _settingsService.LyricsAlignmentType;
_lyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
_lyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
@@ -64,6 +67,8 @@ namespace BetterLyrics.WinUI3.ViewModels
_playbackService.AlbumArtChangedChanged += PlaybackService_AlbumArtChangedChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
_isPlaying = _playbackService.IsPlaying;
UpdateColorConfig();
}
}

View File

@@ -78,7 +78,7 @@ namespace BetterLyrics.WinUI3.ViewModels
$"[DEBUG]\n" +
$"Cur playing {_playingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n" +
$"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
$"Cur time {_totalTime + _positionOffset}\n" +
$"Cur time {TotalTime + _positionOffset}\n" +
$"Lang size {_lyricsDataArr.Count}\n" +
$"Song duration {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}",
new Vector2(10, 10),
@@ -443,7 +443,8 @@ namespace BetterLyrics.WinUI3.ViewModels
else
{
float height = 0f;
var regions = textLayout.GetCharacterRegions(0, string.Join("", line.LyricsChars.Select(x => x.Text)).Length);
//var regions = textLayout.GetCharacterRegions(0, string.Join("", line.LyricsChars.Select(x => x.Text)).Length);
var regions = textLayout.GetCharacterRegions(0, line.OriginalText.Length);
if (regions.Length > 0)
{
height = (float)regions[^1].LayoutBounds.Bottom - (float)regions[0].LayoutBounds.Top;

View File

@@ -6,12 +6,14 @@ using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
: IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<float>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
@@ -23,13 +25,13 @@ namespace BetterLyrics.WinUI3.ViewModels
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<EasingType>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
IRecipient<PropertyChangedMessage<ObservableCollection<LocalMediaFolder>>>
{
public void Receive(PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>> message)
public void Receive(PropertyChangedMessage<ObservableCollection<LocalMediaFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalLyricsFolders))
if (message.PropertyName == nameof(SettingsPageViewModel.LocalMediaFolders))
{
// Music lib changed, re-fetch lyrics
_logger.LogInformation("Local lyrics folders changed, refreshing lyrics.");
@@ -89,20 +91,23 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_isDockMode = message.NewValue;
UpdateColorConfig();
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
_isDesktopMode = message.NewValue;
UpdateColorConfig();
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsLyricsWindowLocked))
{
_isLyricsWindowLocked = message.NewValue;
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsMouseWithinWindow))
{
_isMouseWithinWindow = message.NewValue;
_immersiveBgOpacityTransition.StartTransition(_isDesktopMode ? (_isMouseWithinWindow ? 1f : 0f) : 1f);
UpdateImmersiveBackgroundOpacity();
}
}
else if (message.Sender is LyricsPageViewModel)
@@ -320,5 +325,18 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontFamily))
{
_lyricsTextFormat.FontFamily = _artistTextFormat.FontFamily = _titleTextFormat.FontFamily = message.NewValue;
_isLayoutChanged = true;
}
}
}
}
}

View File

@@ -7,6 +7,8 @@ using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Windows.UI;
@@ -29,7 +31,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (_isPlaying)
{
_totalTime += _elapsedTime;
TotalTime += _elapsedTime;
}
var playingLineIndex = GetCurrentPlayingLineIndex();
@@ -194,48 +196,13 @@ namespace BetterLyrics.WinUI3.ViewModels
_canvasYScrollTransition.Update(_elapsedTime);
int startVisibleLineIndex = -1;
int endVisibleLineIndex = -1;
// Update visible line indices
for (int i = startLineIndex; i <= endLineIndex; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
var lines = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines;
if (lines == null || lines.Count == 0) return;
if (line == null || line.CanvasTextLayout == null)
{
continue;
}
var textLayout = line.CanvasTextLayout;
if (
_canvasYScrollTransition.Value
+ _canvasHeight / 2
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= 0
)
{
if (startVisibleLineIndex == -1)
{
startVisibleLineIndex = i;
}
}
if (
_canvasYScrollTransition.Value
+ _canvasHeight / 2
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (endVisibleLineIndex == -1)
{
endVisibleLineIndex = i;
}
}
}
float offset = _canvasYScrollTransition.Value + _canvasHeight / 2;
int startVisibleLineIndex = FindFirstVisibleLine(lines, offset);
int endVisibleLineIndex = FindLastVisibleLine(lines, offset, _canvasHeight);
if (startVisibleLineIndex != -1 && endVisibleLineIndex == -1)
{
@@ -248,6 +215,52 @@ namespace BetterLyrics.WinUI3.ViewModels
_endVisibleLineIndex = endVisibleLineIndex;
}
private int FindFirstVisibleLine(IList<LyricsLine> lines, float offset)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
var layout = line.CanvasTextLayout;
if (layout == null) break;
float value = offset + line.Position.Y + (float)layout.LayoutBounds.Height;
if (value >= 0)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private int FindLastVisibleLine(IList<LyricsLine> lines, float offset, float canvasHeight)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
var layout = line.CanvasTextLayout;
if (layout == null) break;
float value = offset + line.Position.Y + (float)layout.LayoutBounds.Height;
if (value >= canvasHeight)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private void UpdateColorConfig()
{
if (_isDesktopMode || _isDockMode)
@@ -398,5 +411,33 @@ namespace BetterLyrics.WinUI3.ViewModels
line.HighlightOpacityTransition.Update(_elapsedTime);
}
}
private void UpdateImmersiveBackgroundOpacity()
{
float targetOpacity;
if (_isDesktopMode)
{
if (_isLyricsWindowLocked)
{
targetOpacity = 0;
}
else
{
if (_isMouseWithinWindow)
{
targetOpacity = 1f;
}
else
{
targetOpacity = 0f;
}
}
}
else
{
targetOpacity = 1f;
}
_immersiveBgOpacityTransition.StartTransition(targetOpacity);
}
}
}

View File

@@ -31,7 +31,11 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial class LyricsRendererViewModel : BaseViewModel
{
private TimeSpan _elapsedTime = TimeSpan.Zero;
private TimeSpan _totalTime = TimeSpan.Zero;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
private TimeSpan _positionOffset = TimeSpan.Zero;
private int _songDurationMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
@@ -193,12 +197,14 @@ namespace BetterLyrics.WinUI3.ViewModels
private int GetCurrentPlayingLineIndex()
{
var totalMs = TotalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (totalMs < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
for (int i = 0; i < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.Count; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var nextLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i + 1);
var totalMs = _totalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (nextLine != null && line.StartMs <= totalMs && totalMs < nextLine.StartMs)
{
return i;
@@ -227,7 +233,7 @@ namespace BetterLyrics.WinUI3.ViewModels
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = _songDurationMs;
float now = (float)_totalTime.TotalMilliseconds + (float)_positionOffset.TotalMilliseconds;
float now = (float)TotalTime.TotalMilliseconds + (float)_positionOffset.TotalMilliseconds;
// 1. 还没到本句
if (now < line.StartMs)
@@ -283,11 +289,21 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
// 没有逐字时间轴,直接线性,模拟逐字高亮
// 没有逐字时间轴,均匀分配每个字的高亮时间
int textLength = line.OriginalText.Length;
if (textLength == 0) return;
float lineProgress = (now - line.StartMs) / (lineEndMs - line.StartMs);
charStartIndex = (int)(lineProgress * line.OriginalText.Length);
charProgress = lineProgress - (int)(lineProgress);
lineProgress = Math.Clamp(lineProgress, 0f, 1f);
// 计算当前高亮到第几个字
float charFloatIndex = lineProgress * textLength;
int charIndex = (int)charFloatIndex;
charStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
charLength = 1;
// 当前字的进度0~1
charProgress = charFloatIndex - charIndex;
}
}
@@ -321,9 +337,9 @@ namespace BetterLyrics.WinUI3.ViewModels
private void PlaybackService_PositionChanged(object? sender, PositionChangedEventArgs e)
{
if (Math.Abs(_totalTime.TotalMilliseconds - e.Position.TotalMilliseconds) >= _timelineSyncThreshold)
if (Math.Abs(TotalTime.TotalMilliseconds - e.Position.TotalMilliseconds) >= _timelineSyncThreshold)
{
_totalTime = e.Position;
TotalTime = e.Position;
}
}
@@ -350,7 +366,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
await RefreshLyricsAsync(token);
});
_totalTime = TimeSpan.Zero;
TotalTime = TimeSpan.Zero;
}
}
@@ -401,7 +417,9 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_logger.LogInformation("Showing translation for lyrics...");
string targetLangCode = LanguageHelper.GetUserTargetLanguageCode();
string originalText = _lyricsDataArr[0].WrappedOriginalText;
string? originalText = _lyricsDataArr.FirstOrDefault()?.WrappedOriginalText;
if (originalText == null) return;
string? originalLangCode = LanguageHelper.DetectLanguageCode(originalText);
if (originalLangCode == targetLangCode)
@@ -434,9 +452,9 @@ namespace BetterLyrics.WinUI3.ViewModels
token.ThrowIfCancellationRequested();
if (_showTranslationOnly)
{
// TODO
_lyricsDataArr[0].SetDisplayedTextAlongWith(translated);
_langIndex = 0;
_lyricsDataArr[^1] = _lyricsDataArr[0].CreateLyricsDataFrom(translated);
_lyricsDataArr[^1].SetDisplayedTextInOriginalText();
_langIndex = _lyricsDataArr.Count - 1;
}
else
{
@@ -520,6 +538,10 @@ namespace BetterLyrics.WinUI3.ViewModels
foreach (var data in translationData)
{
data.LyricsLines = data.LyricsLines.Where(line => !string.IsNullOrWhiteSpace(line.OriginalText)).ToList();
foreach (var item in data.LyricsLines)
{
if (item.OriginalText == "//") item.OriginalText = "";
}
}
_lyricsDataArr = _lyricsDataArr.Concat(translationData).ToList();
}

View File

@@ -7,9 +7,11 @@ using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
@@ -31,20 +33,29 @@ namespace BetterLyrics.WinUI3
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<DockPlacement>>
{
private readonly IPlaybackService _playbackService = Ioc.Default.GetRequiredService<IPlaybackService>();
private ForegroundWindowWatcher? _windowWatcher = null;
private bool _ignoreFullscreenWindow = false;
private int _dockWindowMinHeight = 96;
private bool _ignoreFullscreenWindow;
private bool _hideWindowWhenNotPlaying;
private DockPlacement _dockPlacement;
private int _lyricsFontSize;
private int _dockWindowHeight;
public LyricsWindowViewModel(ISettingsService settingsService) : base(settingsService)
{
_ignoreFullscreenWindow = _settingsService.IgnoreFullscreenWindow;
_hideWindowWhenNotPlaying = _settingsService.HideWindowWhenNotPlaying;
IsImmersiveMode = _settingsService.IsImmersiveMode;
_dockPlacement = _settingsService.DockPlacement;
_lyricsFontSize = _settingsService.LyricsFontSize;
_dockWindowHeight = _settingsService.DockWindowHeight;
OnIsImmersiveModeChanged(_settingsService.IsImmersiveMode);
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
}
private void PlaybackService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
AutoHideOrShowWindow();
}
[ObservableProperty]
@@ -83,12 +94,43 @@ namespace BetterLyrics.WinUI3
[ObservableProperty]
public partial string LockHotKey { get; set; } = "";
private void AutoHideOrShowWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
if (IsDockMode || IsDesktopMode)
{
if (_hideWindowWhenNotPlaying && _playbackService.SongInfo == null)
{
if (IsDockMode)
{
DockModeHelper.UpdateAppBarHeight(hwnd, 0, _dockPlacement);
}
window.Hide();
}
else
{
if (IsDockMode)
{
DockModeHelper.UpdateAppBarHeight(hwnd, _dockWindowHeight, _dockPlacement);
}
window.Show();
}
}
}
private void UpdateDockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
DockModeHelper.UpdateAppBarHeight(WindowNative.GetWindowHandle(window), Math.Max(_dockWindowMinHeight, _lyricsFontSize * 4), _dockPlacement);
if (!_hideWindowWhenNotPlaying || _playbackService.SongInfo != null)
{
DockModeHelper.UpdateAppBarHeight(WindowNative.GetWindowHandle(window), _dockWindowHeight, _dockPlacement);
}
}
partial void OnIsImmersiveModeChanged(bool value)
@@ -121,6 +163,11 @@ namespace BetterLyrics.WinUI3
{
_ignoreFullscreenWindow = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.HideWindowWhenNotPlaying))
{
_hideWindowWhenNotPlaying = message.NewValue;
AutoHideOrShowWindow();
}
}
}
@@ -139,9 +186,9 @@ namespace BetterLyrics.WinUI3
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
if (message.PropertyName == nameof(SettingsPageViewModel.DockWindowHeight))
{
_lyricsFontSize = message.NewValue;
_dockWindowHeight = message.NewValue;
UpdateDockWindow();
}
else if (message.Sender is SettingsPageViewModel)
@@ -185,11 +232,14 @@ namespace BetterLyrics.WinUI3
hwnd,
onWindowChanged =>
{
if (_ignoreFullscreenWindow && window.AppWindow.Presenter is OverlappedPresenter presenter)
_dispatcherQueueTimer.Debounce(() =>
{
presenter.IsAlwaysOnTop = true;
}
UpdateAccentColor(hwnd);
if (_ignoreFullscreenWindow && window.AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsAlwaysOnTop = true;
}
UpdateAccentColor(hwnd);
}, TimeSpan.FromMilliseconds(300));
}
);
_windowWatcher.Start();
@@ -231,6 +281,8 @@ namespace BetterLyrics.WinUI3
IsLyricsWindowLocked = true;
IsImmersiveMode = true;
}
AutoHideOrShowWindow();
}
[RelayCommand]
@@ -264,13 +316,15 @@ namespace BetterLyrics.WinUI3
IsDockMode = !IsDockMode;
if (IsDockMode)
{
DockModeHelper.Enable(window, Math.Max(_dockWindowMinHeight, _lyricsFontSize * 4), _dockPlacement);
DockModeHelper.Enable(window, _dockWindowHeight, _dockPlacement);
StartWatchWindowColorChange();
}
else
{
DockModeHelper.Disable(window);
}
AutoHideOrShowWindow();
}
[RelayCommand]

View File

@@ -0,0 +1,149 @@
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Media;
using Windows.Media.Core;
using Windows.Media.Playback;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class MusicGalleryViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalMediaFolder>>>
{
private readonly ILibWatcherService _libWatcherService;
private readonly MediaPlayer _mediaPlayer = new();
private readonly SystemMediaTransportControls _smtc;
[ObservableProperty]
public partial ObservableCollection<Track> Tracks { get; set; } = [];
[ObservableProperty]
public partial bool IsDataLoading { get; set; } = false;
public MusicGalleryViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService) : base(settingsService)
{
_smtc = _mediaPlayer.SystemMediaTransportControls;
_mediaPlayer.CommandManager.IsEnabled = false;
_smtc.IsEnabled = true;
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
_smtc.ButtonPressed += Smtc_ButtonPressed;
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
_libWatcherService = libWatcherService;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
}
private void Smtc_PlaybackPositionChangeRequested(SystemMediaTransportControls sender, PlaybackPositionChangeRequestedEventArgs args)
{
_mediaPlayer.TimelineController.Position = args.RequestedPlaybackPosition;
}
private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
switch (args.Button)
{
case SystemMediaTransportControlsButton.Play:
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
_mediaPlayer.Play();
break;
case SystemMediaTransportControlsButton.Pause:
_smtc.PlaybackStatus = MediaPlaybackStatus.Paused;
_mediaPlayer.Pause();
break;
case SystemMediaTransportControlsButton.Next:
//Next
break;
case SystemMediaTransportControlsButton.Previous:
//Previous
break;
}
}
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, Events.LibChangedEventArgs e)
{
RefreshSongs();
}
public void RefreshSongs()
{
IsDataLoading = true;
Tracks.Clear();
Task.Run(() =>
{
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
Track track = new(file);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
Tracks.Add(track);
});
}
}
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
IsDataLoading = false;
});
});
}
public void PlaySongAt(int? index)
{
if (index.HasValue)
{
var track = Tracks.ElementAtOrDefault(index.Value);
if (track != null)
{
_mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(track.Path));
var updater = _smtc.DisplayUpdater;
updater.AppMediaId = Package.Current.Id.FullName;
updater.Type = MediaPlaybackType.Music;
updater.MusicProperties.Title = track.Title;
updater.MusicProperties.Artist = track.Artist;
updater.MusicProperties.AlbumTitle = track.Album;
if (track.EmbeddedPictures.FirstOrDefault()?.PictureData is byte[] pictureData)
{
updater.Thumbnail = ImageHelper.ByteArrayToRandomAccessStreamReference(pictureData);
}
updater.Update();
_mediaPlayer.Play();
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
}
}
}
public void Receive(PropertyChangedMessage<ObservableCollection<LocalMediaFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalMediaFolders))
{
RefreshSongs();
}
}
}
}
}

View File

@@ -7,24 +7,17 @@ using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using ShadowViewer.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Globalization;
using Windows.Media.Playback;
using Windows.System;
using Windows.UI;
using Windows.UI.Popups;
using WinRT.Interop;
using MetadataHelper = BetterLyrics.WinUI3.Helper.MetadataHelper;
@@ -47,7 +40,7 @@ namespace BetterLyrics.WinUI3.ViewModels
LibreTranslateServer = _settingsService.LibreTranslateServer;
SelectedTargetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LocalMediaFolders = [.. _settingsService.LocalMediaFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
AlbumArtSearchProvidersInfo = [.. _settingsService.AlbumArtSearchProvidersInfo];
@@ -97,12 +90,19 @@ namespace BetterLyrics.WinUI3.ViewModels
LXMusicServer = _settingsService.LXMusicServer;
DockPlacement = _settingsService.DockPlacement;
LyricsBgFontOpacity = _settingsService.LyricsBgFontOpacity;
HideWindowWhenNotPlaying = _settingsService.HideWindowWhenNotPlaying;
DockWindowHeight = _settingsService.DockWindowHeight;
SystemFontNames = [.. FontHelper.SystemFontFamilies];
SelectedFontFamilyIndex = _settingsService.SelectedFontFamilyIndex;
LyricsFontFamily = _settingsService.LyricsFontFamily;
IsDragEverywhereEnabled = _settingsService.IsDragEverywhereEnabled;
_playbackService.MediaSourceProvidersInfoChanged += PlaybackService_SessionIdsChanged;
Task.Run(async () =>
{
BuildDate = (await Helper.MetadataHelper.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
BuildDate = (await MetadataHelper.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
});
}
@@ -111,6 +111,21 @@ namespace BetterLyrics.WinUI3.ViewModels
MediaSourceProvidersInfo = [.. e.MediaSourceProviersInfo];
}
[ObservableProperty]
public partial bool IsDragEverywhereEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial string LyricsFontFamily { get; set; }
[ObservableProperty]
public partial ObservableCollection<string> SystemFontNames { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int SelectedFontFamilyIndex { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial DockPlacement DockPlacement { get; set; }
@@ -157,7 +172,7 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial Enums.Language Language { get; set; }
[ObservableProperty]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
public partial ObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
@@ -243,7 +258,7 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial int LyricsVerticalEdgeOpacity { get; set; }
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; }
public partial object NavViewSelectedItemTag { get; set; } = "App";
[ObservableProperty]
[NotifyPropertyChangedRecipients]
@@ -294,6 +309,14 @@ namespace BetterLyrics.WinUI3.ViewModels
[NotifyPropertyChangedRecipients]
public partial string LXMusicServer { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool HideWindowWhenNotPlaying { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int DockWindowHeight { get; set; }
public void OnLyricsSearchProvidersReordered()
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
@@ -314,18 +337,18 @@ namespace BetterLyrics.WinUI3.ViewModels
);
}
public void RemoveFolderAsync(LocalLyricsFolder folder)
public void RemoveFolderAsync(LocalMediaFolder folder)
{
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
LocalMediaFolders.Remove(folder);
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
_libWatcherService.UpdateWatchers([.. LocalMediaFolders]);
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
public void ToggleLocalLyricsFolder(LocalMediaFolder folder)
{
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
@@ -361,16 +384,16 @@ namespace BetterLyrics.WinUI3.ViewModels
{
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (LocalLyricsFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
if (LocalMediaFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"));
}
else if (LocalLyricsFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
else if (LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
{
// 添加的文件夹是现有文件夹的子文件夹
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo"));
}
else if (LocalLyricsFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
else if (LocalMediaFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
)
{
// 添加的文件夹是现有文件夹的父文件夹
@@ -378,23 +401,23 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
LocalMediaFolders.Add(new LocalMediaFolder(path, true));
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
_libWatcherService.UpdateWatchers([.. LocalMediaFolders]);
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(MetadataHelper.GithubUrl));
await Windows.System.Launcher.LaunchUriAsync(new Uri(MetadataHelper.GithubUrl));
}
[RelayCommand]
private static async Task OpenCacheFolderAsync()
{
await Launcher.LaunchFolderPathAsync(PathHelper.CacheFolder);
await Windows.System.Launcher.LaunchFolderPathAsync(PathHelper.CacheFolder);
}
[RelayCommand]
@@ -433,7 +456,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
string targetLangCode = LanguageHelper.SupportedTargetLanguages[SelectedTargetLanguageIndex].Code;
string result = await _libreTranslateService.TranslateTextAsync("Hello, world!", targetLangCode, null);
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestSuccessInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Success);
IsLibreTranslateServerTesting = false;
@@ -441,7 +464,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
catch (Exception)
{
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestFailedInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
IsLibreTranslateServerTesting = false;
@@ -457,7 +480,7 @@ namespace BetterLyrics.WinUI3.ViewModels
Task.Run(async () =>
{
bool testResult = await NetHelper.CheckConnectivity($"{LXMusicServer}/status");
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString($"SettingsPageServerTest{(testResult ? "Success" : "Failed")}Info"),
@@ -670,5 +693,32 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_settingsService.LyricsBgFontOpacity = value;
}
partial void OnHideWindowWhenNotPlayingChanged(bool value)
{
_settingsService.HideWindowWhenNotPlaying = value;
}
partial void OnDockWindowHeightChanged(int value)
{
_settingsService.DockWindowHeight = value;
}
partial void OnSelectedFontFamilyIndexChanged(int value)
{
_settingsService.SelectedFontFamilyIndex = value;
LyricsFontFamily = SystemFontNames[value];
}
partial void OnLyricsFontFamilyChanged(string value)
{
_settingsService.LyricsFontFamily = value;
}
partial void OnIsDragEverywhereEnabledChanged(bool value)
{
_settingsService.IsDragEverywhereEnabled = value;
LyricsWindow? lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
lyricsWindow.UpdateTitleBarArea();
}
}
}
}

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -34,13 +35,18 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private static void ExitApp()
{
WindowHelper.ExitAllWindows();
LyricsWindow? lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
DockModeHelper.Disable(lyricsWindow);
}
Application.Current.Exit();
}
[RelayCommand]
private static void OpenSettings()
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
[RelayCommand]

View File

@@ -18,7 +18,7 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<!-- Lyrics area -->
<renderer:LyricsRenderer />
@@ -28,7 +28,8 @@
x:Uid="MainPageNoMusicPlaying"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}" />
FontFamily="{x:Bind ViewModel.LyricsFontFamily, Mode=OneWay}"
FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
@@ -51,8 +52,9 @@
<!-- Bottom command area -->
<Grid
x:Name="BottomCommandGrid"
Margin="{x:Bind ViewModel.BottomCommandGridMargin, Mode=OneWay}"
Margin="12"
VerticalAlignment="Bottom"
Background="Transparent"
Opacity="{x:Bind ViewModel.BottomCommandGridOpacity, Mode=OneWay}"
PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited">
@@ -60,270 +62,335 @@
<ScalarTransition />
</Grid.OpacityTransition>
<Grid Padding="3" HorizontalAlignment="Left">
<StackPanel
x:Name="BottomLeftCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<!--<Button Style="{StaticResource GhostButtonStyle}">
<Grid>
-->
<!-- Volumn: 0 -->
<!--
<FontIcon
x:Name="VolumeLevel0"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE74F;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 1-32 -->
<!--
<FontIcon
x:Name="VolumeLevel1"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE993;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 33-65 -->
<!--
<FontIcon
x:Name="VolumeLevel2"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE994;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 66-100 -->
<!--
<FontIcon
x:Name="VolumeLevel3"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE995;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
<Button.Flyout>
<Flyout>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.Volume, Mode=OneWay}" />
<TextBlock Margin="0,0,14,0" VerticalAlignment="Center" />
<Slider
Width="150"
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="None"
Value="{x:Bind ViewModel.Volume, Mode=TwoWay}" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>-->
</StackPanel>
<Grid x:Name="BottomCommandContent">
<Grid Padding="3" HorizontalAlignment="Left">
<StackPanel
x:Name="BottomLeftCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<StackPanel
Margin="0,0,0,2"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="2">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding ElementName=TimelineSlider, Path=Value, Converter={StaticResource SecondsToFormattedTimeConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{Binding ElementName=TimelineSlider, Path=Maximum, Converter={StaticResource SecondsToFormattedTimeConverter}}" />
</StackPanel>
<!-- Position offset -->
<Button Click="TimelineOffsetButton_Click" Style="{StaticResource GhostButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xECE7;"
RenderTransformOrigin="0.5,0.5">
<FontIcon.RenderTransform>
<RotateTransform Angle="90" CenterX="0.5" CenterY="0.5" />
</FontIcon.RenderTransform>
</FontIcon>
<ToolTipService.ToolTip>
<ToolTip x:Name="TimelineOffsetToolTip" x:Uid="LyricsPageTimelineOffsetButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="TimelineOffsetFlyout" ShouldConstrainToRootBounds="False">
<StackPanel>
<Slider
x:Uid="MainPagePositionOffsetSlider"
Maximum="5000"
Minimum="-5000"
SnapsTo="Ticks"
StepFrequency="100"
TickFrequency="100"
TickPlacement="Outside"
Value="{x:Bind ViewModel.PositionOffset, Mode=TwoWay}" />
<RelativePanel>
<TextBlock
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Text="{x:Bind ViewModel.PositionOffset, Mode=OneWay}" />
<Button
Click="PositionOffsetResetButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE777;}"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource GhostButtonStyle}" />
</RelativePanel>
<CheckBox IsChecked="{x:Bind ViewModel.ResetPositionOffsetOnSongChanged, Mode=TwoWay}">
<TextBlock x:Uid="LyricsPagePositionOffsetHint" />
</CheckBox>
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>
</StackPanel>
</Grid>
<Grid Padding="3" HorizontalAlignment="Center">
<StackPanel
x:Name="BottomCenterCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE622;}"
Style="{StaticResource GhostButtonStyle}" />
<Button
Command="{x:Bind ViewModel.PauseSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF8AE;}"
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button
Command="{x:Bind ViewModel.PlaySongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF5B0;}"
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button
Command="{x:Bind ViewModel.NextSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</Grid>
<Grid Padding="3" HorizontalAlignment="Right">
<StackPanel
x:Name="BottomRightCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<!-- Volume -->
<!--<Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}">
<Grid>
-->
<!-- Volumn: 0 -->
<!--
<FontIcon
x:Name="VolumeLevel0"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE74F;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 1-32 -->
<!--
<FontIcon
x:Name="VolumeLevel1"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE993;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 33-65 -->
<!--
<FontIcon
x:Name="VolumeLevel2"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE994;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 66-100 -->
<!--
<FontIcon
x:Name="VolumeLevel3"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE995;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
<Button.DataContext>
<Flyout x:Name="VolumeFlyout" ShouldConstrainToRootBounds="False">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.Volume, Mode=OneWay}" />
<TextBlock Margin="0,0,14,0" VerticalAlignment="Center" />
<Slider
Width="150"
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="None"
Value="{x:Bind ViewModel.Volume, Mode=TwoWay}" />
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>-->
<!-- Translation -->
<Button
Click="TranslationButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8BD;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="TranslationToolTip" x:Uid="LyricsPageTranslationButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="TranslationFlyout" ShouldConstrainToRootBounds="False">
<StackPanel>
<ToggleSwitch x:Uid="LyricsPageTranslationEnabled" IsOn="{x:Bind ViewModel.IsTranslationEnabled, Mode=TwoWay}" />
<ToggleSwitch
x:Uid="LyricsPageTranslationOnly"
IsEnabled="{x:Bind ViewModel.IsTranslationEnabled, Mode=OneWay}"
IsOn="{x:Bind ViewModel.ShowTranslationOnly, Mode=TwoWay}" />
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>
<!-- Display type -->
<Button
x:Name="DisplayTypeSwitchButton"
x:Uid="MainPageDisplayTypeSwitcher"
Click="DisplayTypeSwitchButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF246;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="PresentationTypeToolTip" x:Uid="LyricsPageDisplayTypeButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="DisplayTypeSwitchFlyout" ShouldConstrainToRootBounds="false">
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="12,2,12,8" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</Flyout.FlyoutPresenterStyle>
<RadioButtons MaxColumns="1" SelectedIndex="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}">
<RadioButton x:Uid="MainPageAlbumArtOnly" Click="AlbumArtOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageLyriscOnly" Click="LyricsOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageSplitView" Click="SplitViewRadioButton_Click" />
</RadioButtons>
</Flyout>
</Button.DataContext>
</Button>
<!-- Settings -->
<Button
x:Name="SettingsButton"
Command="{x:Bind ViewModel.OpenSettingsWindowCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF8B0;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="SettingsToolTip" x:Uid="LyricsPageSettingsButtonToolTip" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
<Slider
x:Name="TimelineSlider"
Margin="0,-32,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.SongDurationSeconds, Mode=OneWay}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.TimelinePositionSeconds, Mode=OneWay}" />
<Slider
x:Name="TimelineSliderOverlay"
Margin="0,-32,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.SongDurationSeconds, Mode=OneWay}"
Minimum="0"
Style="{StaticResource TransparentSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
ValueChanged="TimelineSliderOverlay_ValueChanged" />
</Grid>
</Grid>
<!-- Bottom command flyout trigger -->
<Grid
x:Name="BottomCommandFlyoutTrigger"
Height="12"
VerticalAlignment="Bottom"
Background="Transparent"
CornerRadius="3,3,0,0"
Opacity="{x:Bind ViewModel.BottomCommandFlyoutTriggerOpacity, Mode=OneWay}"
PointerEntered="BottomCommandFlyoutTrigger_PointerEntered"
PointerExited="BottomCommandFlyoutTrigger_PointerExited"
Tapped="BottomCommandFlyoutTrigger_Tapped">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<Grid
Padding="3"
HorizontalAlignment="Center"
Style="{StaticResource CardGridStyle}">
<StackPanel
x:Name="BottomCenterCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE622;}"
Style="{StaticResource GhostButtonStyle}" />
<Button
Command="{x:Bind ViewModel.PauseSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF8AE;}"
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button
Command="{x:Bind ViewModel.PlaySongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF5B0;}"
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button
Command="{x:Bind ViewModel.NextSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
x:Name="BottomCommandFlyoutTriggerHint"
Width="150"
Margin="4"
Background="{ThemeResource TextFillColorPrimaryBrush}"
CornerRadius="2"
Translation="0,0,0">
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
</Grid>
<Grid
Padding="3"
HorizontalAlignment="Right"
Style="{StaticResource CardGridStyle}">
<StackPanel
x:Name="BottomRightCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<!-- Position offset -->
<Button Click="TimelineOffsetButton_Click" Style="{StaticResource GhostButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xECE7;"
RenderTransformOrigin="0.5,0.5">
<FontIcon.RenderTransform>
<RotateTransform Angle="90" CenterX="0.5" CenterY="0.5" />
</FontIcon.RenderTransform>
</FontIcon>
<ToolTipService.ToolTip>
<ToolTip x:Name="TimelineOffsetToolTip" x:Uid="LyricsPageTimelineOffsetButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="TimelineOffsetFlyout" ShouldConstrainToRootBounds="False">
<StackPanel>
<Slider
x:Uid="MainPagePositionOffsetSlider"
Maximum="5000"
Minimum="-5000"
SnapsTo="Ticks"
StepFrequency="100"
TickFrequency="100"
TickPlacement="Outside"
Value="{x:Bind ViewModel.PositionOffset, Mode=TwoWay}" />
<RelativePanel>
<TextBlock
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Text="{x:Bind ViewModel.PositionOffset, Mode=OneWay}" />
<Button
Click="PositionOffsetResetButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE777;}"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource GhostButtonStyle}" />
</RelativePanel>
<CheckBox IsChecked="{x:Bind ViewModel.ResetPositionOffsetOnSongChanged, Mode=TwoWay}">
<TextBlock x:Uid="LyricsPagePositionOffsetHint" />
</CheckBox>
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>
<!-- Translation -->
<Button
Click="TranslationButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8BD;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="TranslationToolTip" x:Uid="LyricsPageTranslationButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="TranslationFlyout" ShouldConstrainToRootBounds="False">
<StackPanel>
<ToggleSwitch x:Uid="LyricsPageTranslationEnabled" IsOn="{x:Bind ViewModel.IsTranslationEnabled, Mode=TwoWay}" />
<ToggleSwitch
x:Uid="LyricsPageTranslationOnly"
IsEnabled="{x:Bind ViewModel.IsTranslationEnabled, Mode=OneWay}"
IsOn="{x:Bind ViewModel.ShowTranslationOnly, Mode=TwoWay}" />
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>
<!-- Display type -->
<Button
x:Name="DisplayTypeSwitchButton"
x:Uid="MainPageDisplayTypeSwitcher"
Click="DisplayTypeSwitchButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF246;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="PresentationTypeToolTip" x:Uid="LyricsPageDisplayTypeButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="DisplayTypeSwitchFlyout" ShouldConstrainToRootBounds="false">
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="12,2,12,8" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</Flyout.FlyoutPresenterStyle>
<RadioButtons MaxColumns="1" SelectedIndex="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}">
<RadioButton x:Uid="MainPageAlbumArtOnly" Click="AlbumArtOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageLyriscOnly" Click="LyricsOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageSplitView" Click="SplitViewRadioButton_Click" />
</RadioButtons>
</Flyout>
</Button.DataContext>
</Button>
<!-- Settings -->
<Button
x:Name="SettingsButton"
Command="{x:Bind ViewModel.OpenSettingsWindowCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF8B0;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="SettingsToolTip" x:Uid="LyricsPageSettingsButtonToolTip" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
<Grid.ContextFlyout>
<Flyout x:Name="BottomCommandFlyout" ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="600" />
<Setter Property="MinHeight" Value="100" />
<Setter Property="CornerRadius" Value="12" />
</Style>
</Flyout.FlyoutPresenterStyle>
<Grid x:Name="BottomCommandFlyoutContainer" VerticalAlignment="Bottom" />
</Flyout>
</Grid.ContextFlyout>
</Grid>
<TeachingTip

View File

@@ -6,6 +6,8 @@ using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@@ -14,16 +16,17 @@ namespace BetterLyrics.WinUI3.Views
public sealed partial class LyricsPage : Page
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IPlaybackService _playbackService = Ioc.Default.GetRequiredService<IPlaybackService>();
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
public LyricsPage()
{
this.InitializeComponent();
DataContext = Ioc.Default.GetService<LyricsPageViewModel>();
DataContext = Ioc.Default.GetRequiredService<LyricsPageViewModel>();
}
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
{
ViewModel.IsFirstRun = false;
@@ -54,18 +57,20 @@ namespace BetterLyrics.WinUI3.Views
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (ViewModel.IsImmersiveMode)
if (ViewModel.IsImmersiveMode && BottomCommandGrid.Children.Count != 0)
{
ViewModel.BottomCommandGridOpacity = 1f;
}
e.Handled = true;
}
private void BottomCommandGrid_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (ViewModel.IsImmersiveMode)
if (ViewModel.IsImmersiveMode && BottomCommandGrid.Children.Count != 0)
{
ViewModel.BottomCommandGridOpacity = 0f;
}
e.Handled = true;
}
private void DisplayTypeSwitchButton_Click(object sender, RoutedEventArgs e)
@@ -75,12 +80,69 @@ namespace BetterLyrics.WinUI3.Views
private void TimelineOffsetButton_Click(object sender, RoutedEventArgs e)
{
TimelineOffsetFlyout.ShowAt(BottomRightCommandStackPanel);
TimelineOffsetFlyout.ShowAt(BottomLeftCommandStackPanel);
}
private void TranslationButton_Click(object sender, RoutedEventArgs e)
{
TranslationFlyout.ShowAt(BottomRightCommandStackPanel);
}
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize.Width < 500 || e.NewSize.Height < 100)
{
if (BottomCommandGrid.Children.Count != 0)
{
BottomCommandGrid.Children.Remove(BottomCommandContent);
BottomCommandFlyoutContainer.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 0, 0);
}
else
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.Hide();
BottomCommandFlyoutContainer.Children.Remove(BottomCommandContent);
BottomCommandGrid.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 12, 0);
}
}
private async void TimelineSliderOverlay_ValueChanged(object sender, Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
await _playbackService.ChangePosition(e.NewValue);
}
//private void VolumeButton_Click(object sender, RoutedEventArgs e)
//{
// VolumeFlyout.ShowAt(BottomRightCommandStackPanel);
//}
private void BottomCommandFlyoutTrigger_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (ViewModel.IsImmersiveMode && BottomCommandFlyoutContainer.Children.Count != 0)
{
ViewModel.BottomCommandFlyoutTriggerOpacity = 1f;
}
}
private void BottomCommandFlyoutTrigger_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (ViewModel.IsImmersiveMode && BottomCommandFlyoutContainer.Children.Count != 0)
{
ViewModel.BottomCommandFlyoutTriggerOpacity = 0f;
}
}
private void BottomCommandFlyoutTrigger_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.ShowAt(BottomCommandFlyoutTrigger);
}
}
}
}

View File

@@ -25,8 +25,8 @@
<!-- Top command -->
<Grid
x:Name="TopCommandGrid"
Margin="12"
VerticalAlignment="Top"
Background="Transparent"
Opacity="{x:Bind ViewModel.TopCommandGridOpacity, Mode=OneWay}"
PointerEntered="TopCommandGrid_PointerEntered"
PointerExited="TopCommandGrid_PointerExited">
@@ -34,10 +34,7 @@
<ScalarTransition />
</Grid.OpacityTransition>
<Grid
Padding="3"
HorizontalAlignment="Left"
Style="{StaticResource CardGridStyle}">
<Grid Padding="3" HorizontalAlignment="Left">
<StackPanel
x:Name="TopLeftCommandStackPanel"
HorizontalAlignment="Left"
@@ -46,6 +43,18 @@
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<!-- Music gallery -->
<Button
Click="MusicGalleryButton_Click"
Style="{StaticResource TitleBarButtonStyle}"
Visibility="Collapsed">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize}"
Glyph="&#xE8A9;" />
</Button>
<!-- Immersive mode -->
<ToggleButton
x:Name="ImmersiveButton"
Command="{x:Bind ViewModel.ImmersiveToggleButtonEnabledChangedCommand}"
@@ -59,10 +68,7 @@
</StackPanel>
</Grid>
<Grid
Padding="3"
HorizontalAlignment="Right"
Style="{StaticResource CardGridStyle}">
<Grid Padding="3" HorizontalAlignment="Right">
<StackPanel
x:Name="TopRightCommandStackPanel"
Orientation="Horizontal"

View File

@@ -33,13 +33,25 @@ namespace BetterLyrics.WinUI3.Views
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
Title = App.ResourceLoader!.GetString("LyricsPageTitle");
SetTitleBar(TopCommandGrid);
//SetTitleBar(RootGrid);
UpdateTitleBarArea();
_wmm = new WindowMessageMonitor(this);
_wmm.WindowMessageReceived += Wmm_WindowMessageReceived;
}
public void UpdateTitleBarArea()
{
if (_settingsService.IsDragEverywhereEnabled)
{
SetTitleBar(RootGrid);
}
else
{
SetTitleBar(TopCommandGrid);
}
}
private void Wmm_WindowMessageReceived(object? sender, WindowMessageEventArgs e)
{
if (e.Message.MessageId == (uint)User32.WindowMessage.WM_HOTKEY)
@@ -57,6 +69,14 @@ namespace BetterLyrics.WinUI3.Views
switch (type!)
{
case AutoStartWindowType.StandardMode:
if (_settingsService.StandardWindowLeft < 0 || _settingsService.StandardWindowTop < 0 ||
_settingsService.StandardWindowWidth <= 0 || _settingsService.StandardWindowHeight <= 0)
{
_settingsService.StandardWindowLeft = 200;
_settingsService.StandardWindowTop = 200;
_settingsService.StandardWindowWidth = 1600;
_settingsService.StandardWindowHeight = 800;
}
AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(
_settingsService.StandardWindowLeft,
_settingsService.StandardWindowTop,
@@ -97,25 +117,27 @@ namespace BetterLyrics.WinUI3.Views
var rect = AppWindow.Position;
var size = AppWindow.Size;
if (ViewModel.IsDesktopMode)
{
_settingsService.DesktopWindowLeft = rect.X;
_settingsService.DesktopWindowTop = rect.Y;
_settingsService.DesktopWindowWidth = size.Width;
_settingsService.DesktopWindowHeight = size.Height;
}
else if (ViewModel.IsDockMode)
if (rect.X >= 0 && rect.Y >= 0 && size.Width > 0 && size.Height > 0)
{
if (ViewModel.IsDesktopMode)
{
_settingsService.DesktopWindowLeft = rect.X;
_settingsService.DesktopWindowTop = rect.Y;
_settingsService.DesktopWindowWidth = size.Width;
_settingsService.DesktopWindowHeight = size.Height;
}
else if (ViewModel.IsDockMode)
{
}
else
{
_settingsService.StandardWindowLeft = rect.X;
_settingsService.StandardWindowTop = rect.Y;
_settingsService.StandardWindowWidth = size.Width;
_settingsService.StandardWindowHeight = size.Height;
}
}
else
{
_settingsService.StandardWindowLeft = rect.X;
_settingsService.StandardWindowTop = rect.Y;
_settingsService.StandardWindowWidth = size.Width;
_settingsService.StandardWindowHeight = size.Height;
}
}
}
@@ -152,13 +174,11 @@ namespace BetterLyrics.WinUI3.Views
private void SettingsMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
private void UpdateTitleBarWindowButtonsVisibility()
{
TopCommandGrid.Margin = new Thickness(12);
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
@@ -197,7 +217,6 @@ namespace BetterLyrics.WinUI3.Views
Visibility.Collapsed;
ViewModel.IsImmersiveMode = true;
TopCommandGrid.Margin = new Thickness();
}
else if (DesktopFlyoutItem.IsChecked)
{
@@ -250,7 +269,8 @@ namespace BetterLyrics.WinUI3.Views
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
WindowHelper.ExitAllWindows();
DockModeHelper.Disable(this);
Application.Current.Exit();
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
@@ -301,27 +321,22 @@ namespace BetterLyrics.WinUI3.Views
private void RootGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
{
ViewModel.IsMouseWithinWindow = true;
e.Handled = true;
}
private void RootGrid_PointerExited(object sender, PointerRoutedEventArgs e)
{
ViewModel.IsMouseWithinWindow = false;
e.Handled = true;
}
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (ClickThroughButton == null) return;
}
// <20><>ȡ<EFBFBD><C8A1><EFBFBD>ؼ<EFBFBD><D8BC>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD>е<EFBFBD>λ<EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͻǣ<CFBD>
var transform = ClickThroughButton.TransformToVisual(Content);
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
var btnRect = new Rectangle(
(int)point.X,
(int)point.Y,
(int)ClickThroughButton.ActualWidth,
(int)ClickThroughButton.ActualHeight
);
DesktopModeHelper.SetInteractiveRects([btnRect]);
private void MusicGalleryButton_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenWindow<MusicGalleryWindow>();
}
}
}

View File

@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="BetterLyrics.WinUI3.Views.MusicGalleryPage"
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:labs="using:CommunityToolkit.Labs.WinUI"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="Page_Loaded"
mc:Ignorable="d">
<Grid>
<Grid Padding="0,12,0,0">
<controls:Segmented
x:Name="Segmented"
Margin="36,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SelectedIndex="0"
SelectionMode="Single">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=Segmented, Path=SelectedIndex, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Tag" Value="Song" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=Segmented, Path=SelectedIndex, Mode=OneWay}"
ComparisonCondition="Equal"
Value="1">
<interactivity:ChangePropertyAction PropertyName="Tag" Value="Album" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=Segmented, Path=SelectedIndex, Mode=OneWay}"
ComparisonCondition="Equal"
Value="2">
<interactivity:ChangePropertyAction PropertyName="Tag" Value="Artist" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<controls:SegmentedItem Content="歌曲" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEC4F;}" />
<controls:SegmentedItem Content="专辑" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE93C;}" />
<controls:SegmentedItem Content="艺术家" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEFA9;}" />
</controls:Segmented>
<controls:OpacityMaskView Margin="0,36,0,0" HorizontalContentAlignment="Stretch">
<controls:OpacityMaskView.OpacityMask>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.05" Color="White" />
<GradientStop Offset="0.95" Color="White" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</controls:OpacityMaskView.OpacityMask>
<Grid>
<ScrollViewer>
<controls:SwitchPresenter Padding="36,6" Value="{Binding ElementName=Segmented, Path=Tag, Mode=OneWay}">
<controls:SwitchPresenter.ContentTransitions>
<TransitionCollection>
<PopupThemeTransition />
</TransitionCollection>
</controls:SwitchPresenter.ContentTransitions>
<controls:Case Value="Song">
<ListView ItemsSource="{x:Bind ViewModel.Tracks, Mode=OneWay}" SelectionChanged="SongListView_SelectionChanged">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<!-- 歌曲名 -->
<ColumnDefinition Width="1.5*" />
<!-- 歌手名 -->
<ColumnDefinition Width="1.5*" />
<!-- 专辑名 -->
<ColumnDefinition Width="1*" />
<!-- 年份 -->
<ColumnDefinition Width="1.2*" />
<!-- 流派 -->
<ColumnDefinition Width="1*" />
<!-- 歌曲时长 -->
</Grid.ColumnDefinitions>
<!-- 歌曲名 -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Title}"
TextWrapping="Wrap" />
<!-- 歌手名 -->
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Artist}"
TextWrapping="Wrap" />
<!-- 专辑名 -->
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Album}"
TextWrapping="Wrap" />
<!-- 年份 -->
<TextBlock
Grid.Column="3"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Year}"
TextWrapping="Wrap" />
<!-- 流派 -->
<TextBlock
Grid.Column="4"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Genre}"
TextWrapping="Wrap" />
<!-- 歌曲时长 -->
<TextBlock
Grid.Column="5"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Duration, Converter={StaticResource SecondsToFormattedTimeConverter}}"
TextAlignment="Right"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</controls:Case>
<controls:Case Value="Album">
<ListView />
</controls:Case>
<controls:Case Value="Artist">
<ListView />
</controls:Case>
</controls:SwitchPresenter>
</ScrollViewer>
</Grid>
</controls:OpacityMaskView>
</Grid>
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}" Visibility="{x:Bind ViewModel.IsDataLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<labs:Shimmer Grid.Row="0" CornerRadius="12" />
<labs:Shimmer Grid.Row="2" CornerRadius="12" />
<labs:Shimmer Grid.Row="4" CornerRadius="12" />
<labs:Shimmer Grid.Row="6" CornerRadius="12" />
</Grid>
<ProgressRing IsActive="{x:Bind ViewModel.IsDataLoading, Mode=OneWay}" />
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,45 @@
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 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.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MusicGalleryPage : Page
{
public MusicGalleryViewModel ViewModel => (MusicGalleryViewModel)DataContext;
public MusicGalleryPage()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<MusicGalleryViewModel>();
}
private void SongListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ViewModel.PlaySongAt((sender as ListView)?.SelectedIndex);
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.RefreshSongs();
}
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="BetterLyrics.WinUI3.Views.MusicGalleryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MusicGalleryWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<local:MusicGalleryPage />
</Window>

View File

@@ -0,0 +1,36 @@
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using WinUIEx;
// 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.Views
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MusicGalleryWindow : Window
{
public MusicGalleryWindow()
{
InitializeComponent();
Title = App.ResourceLoader!.GetString("MusicGalleryPageTitle");
AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
this.SetIcon(@"Assets/Logo.ico");
}
}
}

View File

@@ -138,6 +138,14 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.IgnoreFullscreenWindow, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageHideWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED1A;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.HideWindowWhenNotPlaying, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageGlobalDrag" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7C2;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsDragEverywhereEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Desktop mode -->
<TextBlock x:Uid="SettingsPageAppDesktop" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -183,6 +191,25 @@
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDockWindowHeight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED5E;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.DockWindowHeight, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" px" />
<Slider
Maximum="200"
Minimum="64"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.DockWindowHeight, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<!-- Dock mode -->
<TextBlock x:Uid="SettingsPageAppDock" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -364,7 +391,7 @@
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8B7;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.LocalLyricsFolders, Mode=OneWay}">
ItemsSource="{x:Bind ViewModel.LocalMediaFolders, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard>
@@ -399,13 +426,13 @@
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -484,6 +511,16 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontFamily" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8D2;}">
<ComboBox ItemsSource="{x:Bind ViewModel.SystemFontNames, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SelectedFontFamilyIndex, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontWeight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8DD;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.LyricsFontWeight, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsThin" />

View File

@@ -26,7 +26,7 @@ namespace BetterLyrics.WinUI3.Views
{
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LocalLyricsFolder localLyricsFolder)
if (toggleSwitch.DataContext is LocalMediaFolder localLyricsFolder)
{
ViewModel.ToggleLocalLyricsFolder(localLyricsFolder);
}
@@ -65,7 +65,7 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.RoutedEventArgs e
)
{
ViewModel.RemoveFolderAsync((LocalLyricsFolder)(sender as HyperlinkButton)!.Tag);
ViewModel.RemoveFolderAsync((LocalMediaFolder)(sender as HyperlinkButton)!.Tag);
}
private void MediaSourceProviderToggleSwitch_Toggled(object sender, RoutedEventArgs e)

View File

@@ -12,7 +12,7 @@
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid x:Name="RootGrid">
<local:SettingsPage />
</Grid>

View File

@@ -1,6 +1,6 @@
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using H.NotifyIcon;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinUIEx;
@@ -8,26 +8,14 @@ namespace BetterLyrics.WinUI3.Views
{
public sealed partial class SettingsWindow : Window
{
public SettingsWindowViewModel ViewModel { get; set; } = Ioc.Default.GetRequiredService<SettingsWindowViewModel>();
public SettingsWindow()
{
InitializeComponent();
Title = App.ResourceLoader!.GetString("SettingsPageTitle");
ExtendsContentIntoTitleBar = true;
AppWindow.Closing += AppWindow_Closing;
}
public SettingsWindowViewModel ViewModel { get; set; } =
Ioc.Default.GetRequiredService<SettingsWindowViewModel>();
private void AppWindow_Closing(
Microsoft.UI.Windowing.AppWindow sender,
Microsoft.UI.Windowing.AppWindowClosingEventArgs args
)
{
args.Cancel = true; // Prevent the window from closing
this.Hide(true);
AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
this.SetIcon(@"Assets/Logo.ico");
}
}
}

View File

@@ -1,6 +1,8 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.md">_**Click here to see the English version**_</a>
> 注:以下内容使用 https://claude.ai/ 依照英文原文翻译
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**点此处查看常见问题与解答FAQ**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.md">_**🌐Click here to see the English version**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**❓点击查看常见问题FAQ**_</a>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
@@ -9,143 +11,152 @@
<h2 align="center">
BetterLyrics
</h2>
<h3 align="center">
基于 WinUI 3 打造的丝滑动态歌词显示工具
<h4 align="center">
基于 WinUI 3 构建的流畅动态歌词显示工具
</h3>
---
## 🎉 本项目已获得少数派推荐!
查看文章:[BetterLyrics 专为 Windows 设计的沉浸式流畅歌词显示工具](https://sspai.com/post/101028)
- [「BetterLyrics」反馈交流群简体中文](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (QQ群号1054700388)
- [「BetterLyrics」反馈交流群繁体中文/英文)](https://discord.gg/5yAQPnyCKv) (Discord)
## 反馈交流群
---
- [「BetterLyrics」反馈交流群简体中文](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) QQ群
- [「BetterLyrics」反馈交流群繁体中文/英语)](https://discord.gg/5yAQPnyCKv) Discord服务器
🎉 本项目已被少数派精选推荐!欢迎阅读:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)
## 核心特色功能
---
- 动态模糊专辑封面作为背景
- 流畅的歌词淡入淡出、缩放效果
- 切换歌曲时界面平滑过渡
- 逐字渐变卡拉OK效果带光晕
- 沉浸式桌面歌词(悬浮模式)
- 本地翻译支持30种语言
## 核心特色
- 动态模糊专辑封面背景
- 丝滑的歌词淡入/淡出、缩放效果
- 歌曲切换时的流畅界面过渡
- 逐字渐变的卡拉OK效果带光晕
- 沉浸式桌面歌词(停靠模式)
- 本地化歌词翻译(支持 30 种语言)
> 项目仍在开发中,最新分支可能存在未修复的 Bug 或异常行为
> 本项目仍在开发中最新版本可能存在bug和意外行为。
## 支持的歌词来源
- 本地资源
- 本地存储
- 音乐文件(内嵌歌词)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) 文件(标准格式增强格式)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) 文件(支持标准格式增强格式)
- [.eslrc](https://github.com/ESLyric/release) 文件
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) 文件
歌词下载推荐工具:[LDDC](https://github.com/chenmozhijin/LDDC)
如需下载歌词,可使用 [LDDC](https://github.com/chenmozhijin/LDDC)
- 在线歌词
- 在线歌词提供商
- QQ音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
## 效果展示
## 应用截图
### 标准模式
![标准模式](Screenshots/image.png)
![alt text](Screenshots/image.png)
![光晕浮动效果](Screenshots/glow-float.gif)
![alt text](Screenshots/glow-float.gif)
![歌词面板](Screenshots/fan.png)
![alt text](Screenshots/fan.png)
![纯歌词模式](Screenshots/lyrics-only.png)
![alt text](Screenshots/lyrics-only.png)
![纯专辑封面模式](Screenshots/album-art-only.png)
![alt text](Screenshots/album-art-only.png)
### 停靠模式
### 悬浮模式
![停靠模式1](Screenshots/dock-1.png)
![alt text](Screenshots/dock-1.png)
![停靠模式2](Screenshots/dock-2.png)
![alt text](Screenshots/dock-2.png)
### 桌面模式
![桌面模式1](Screenshots/desktop-1.png)
![alt text](Screenshots/desktop-1.png)
![桌面模式2](Screenshots/desktop-2.png)
![alt text](Screenshots/desktop-2.png)
## 演示视频
观看 B 站演示视频2025年7月7日发布)[点击此处](https://www.bilibili.com/video/BV1zjGjzfEXh)
观看我们的介绍视频2025年7月7日上传):[B站链接](https://www.bilibili.com/video/BV1zjGjzfEXh)
## 立即体验
- 稳定版
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/zh-cn%20dark.svg" width="200"/>
</a>
> **最便捷**的获取方式,提供**无限制**免费试用或购买(免费版与付费版**功能完全一致**,购买视为对开发者的支持)
或通过 Google Drive 获取(链接见[发布页](https://github.com/jayfunc/BetterLyrics/releases)
> 下载的是 ".zip" 压缩包,安装指南请参阅[此文档](How2Install/How2Install.md)
- 开发版
可通过 `git clone` 拉取项目源码自行编译
## 已测试播放器
## 已测试的音乐播放器
- 网易云音乐
- 请先安装 [BetterNCM 插件](https://microblock.cc/betterncm) 安装完成后如若弹出降级指引,请根据指引完成网易云音乐的降级操作(降级至 2.10.13
- 之后请在 PluginMarket 内安装 InfLink 插件,安装完成后请重启网易云音乐。至此,所有预备操作均已完成,尽情享用吧!
- 酷狗音乐
- **时间轴同步限制**:酷狗不广播时间信息,调整进度时歌词无法实时跟随
-广播时间线信息,这意味着当您在酷狗音乐中更改播放进度时BetterLyrics无法检测到此更改。
- Apple Music
- 在设置中调整时间阈值至 600 ms 左右(路径:设置→高级选项)
- 确保您在设置中时间线阈值设置为约600毫秒进入"设置"-"高级选项"进行更改),否则歌词会不断前后跳动。
- foobar2000
- 需配合安装 [foo_mediacontrol](https://github.com/dumbie/foo_mediacontrol) 插件
- 确保您安装了 https://github.com/dumbie/foo_mediacontrol 插件
- Spotify
- QQ音乐
- PotPlayer
- 系统自带媒体播放器
- 媒体播放器(系统自带)
- LX 音乐
- 请确保您已在 LX 音乐设置页面启用“开放 API”
- 然后打开 BetterLyrics进入设置点击“高级选项”输入您的 LX 音乐服务器地址(例如 http://127.0.0.1:23330即可
- MusicBee
- 使用前请安装 https://github.com/HenryPDT/mb_MediaControl
- iTunes
- 请安装 https://github.com/thewizrd/iTunes-SMTC
## 鸣谢
## 立即下载体验
### Microsoft Store
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
**最简单**的获取方式,**无限制**免费试用或购买(免费版与付费版**功能完全相同**
☕ 如果您觉得有用,请考虑在**Microsoft Store**中购买支持🧧,我会非常感激的!🥰
> 请注意Microsoft Store中的版本可能不是最新版本。
### Google Drive
想要体验**最新**版本从Google Drive获取请查看[发布页面](https://github.com/jayfunc/BetterLyrics/releases)获取链接)
> 请注意您下载的是".zip"文件,安装指南请参考[此文档](How2Install/How2Install.md)。
## 特别感谢
- [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper)
- 提供 QQ/网易云/酷狗歌词获取解密
- 提供QQ网易、酷狗音源的歌词获取解密和解析
- [LRCLIB](https://lrclib.net/)
- 歌词 API 服务
- [ATL.NET](https://github.com/Zeugma440/atldotnet)
- 音乐文件封面提取
- LRCLIB歌词API提供商
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- 用于提取音乐文件中的图片
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- Win32 窗口 API 封装
- 提供便捷的Win32 API窗口操作方式
- [TagLib#](https://github.com/mono/taglib-sharp)
- 歌词内容解析
- 用于读取原始歌词内容
- [Vanara](https://github.com/dahall/Vanara)
- Win32 API 封装库
- [Stackoverflow - WPF边距动画实现](https://stackoverflow.com/a/21542882/11048731)
- Win32 API包装器
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [B - WinUI3系统背景效果教程](https://www.bilibili.com/video/BV1PY4FevEkS)
- [博客园 - .NET应用与SMTC交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D游戏循环实现](https://www.cnblogs.com/walterlv/p/10236395.html)
- [Win2D高级示例](https://github.com/r2d2rigo/Win2D-Samples)
- [CommunityToolkit开发指南](https://mvvm.coldwind.top/)
- [Bilibili -WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
## 灵感来源
## 设计灵感来源
- [网易云歌词增强](https://github.com/solstice23/refined-now-playing-netease)
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐播放器](https://moriafly.com/program/salt-player)
- [MyToolBar任务栏工具](https://github.com/TwilightLemon/MyToolBar)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## 项目星标历史
## Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date)
## 欢迎反馈与贡献
## 欢迎提交问题和拉取请求
遇问题请提交 Issue如有改进建议欢迎提交 PR
果您发现bug请在issues中提交如果您有任何想法也欢迎在这里分享。

View File

@@ -1,6 +1,6 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md">_**点此处查看中文说明**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md">_**🌐点此处查看中文说明**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**Click here to view frequently asked questions (FAQ)**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**Click here to view frequently asked questions (FAQ)**_</a>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
@@ -9,22 +9,18 @@
<h2 align="center">
BetterLyrics
</h2>
<h3 align="center">
<h4 align="center">
Your smooth dynamic lyrics display tool built with WinUI 3
</h3>
---
## 🎉 This project was featured by SSPAI!
Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028)
## Feedback and chat group
- [「BetterLyrics」反馈交流群简体中文](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) on QQ
- [「BetterLyrics」Feedback Chat Group (Traditional Chinese / English)](https://discord.gg/5yAQPnyCKv) on Discord
---
🎉 This project was featured by SSPAI! Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028)
---
## Highlighted features
- Dynamic blur album art as background
@@ -83,36 +79,48 @@ Your smooth dynamic lyrics display tool built with WinUI 3
Watch our introduction video (uploaded on 7 July 2025) on Bilibili [here](https://www.bilibili.com/video/BV1zjGjzfEXh).
## Try it now
- Stable version
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
> **Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version, if you like you can purchase to support me)
Or alternatively get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases) page for the link)
> Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
- Latest dev version
You can `git clone` this project and build it yourself.
## Tested music player
- NetEase Cloud Music
- Please install the [BetterNCM plugin](https://microblock.cc/betterncm) first. If a downgrade guide pops up after the installation, please follow the guide to complete the downgrade of NetEase Cloud Music (downgrade to 2.10.13);
- After that, please install the InfLink plugin in PluginMarket. After the installation is complete, please restart NetEase Cloud Music. At this point, all preparatory operations have been completed, enjoy it!
- Kugou Music
- No timeline information broadcasted, which means when you change timeline position in Kugou Music, BetterLyrics has no way to detect this change.
- Apple Music
- Make sure you have set timeline threshold to around 600 ms in settings (Go to "Settings" - "Advanced option" to change), otherwise, the lyrics will be moving forward and afterward constantly.
- foobar2000
- Make sure you have https://github.com/dumbie/foo_mediacontrol installed with it
- Spofity
- Spotify
- QQ Music
- PotPlayer
- Media Player (System)
- LX Music
- Please make sure you have enabled "Open API" in LX Music settings page
- Then open BetterLyrics, go to settings, go to "Advanced options", input your LX Music server address (mostly like http://127.0.0.1:23330) and there you go!
- MusicBee
- Please install https://github.com/HenryPDT/mb_MediaControl before using
- iTunes
- Please install https://github.com/thewizrd/iTunes-SMTC before using
## Try it now
### Microsoft Store
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
**Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version)
☕ If you find it helpful, please consider purchasing 🧧 it in **Microsoft Store**, I'll appreciate it! 🥰
> Please note that the version in Microsoft Store may not be the latest.
### Google Drive
Wanna try the **latest** version? get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases) page for the link)
> Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
## Many thanks to

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB