Compare commits

..

58 Commits

Author SHA1 Message Date
Zhe Fang
9d64f80b97 New translations resources.resw (Chinese Simplified) 2025-12-27 21:53:26 -05:00
Zhe Fang
e9862704ae New translations resources.resw (Malay) 2025-12-27 17:39:41 -05:00
Zhe Fang
85a6be7988 New translations resources.resw (Hindi) 2025-12-27 17:39:40 -05:00
Zhe Fang
dcef2de3a9 New translations resources.resw (Thai) 2025-12-27 17:39:39 -05:00
Zhe Fang
18b3539227 New translations resources.resw (Indonesian) 2025-12-27 17:39:38 -05:00
Zhe Fang
b00c5f9c0e New translations resources.resw (Vietnamese) 2025-12-27 17:39:37 -05:00
Zhe Fang
6c712531e5 New translations resources.resw (Chinese Traditional) 2025-12-27 17:39:36 -05:00
Zhe Fang
5fbcb0cb7b New translations resources.resw (Chinese Simplified) 2025-12-27 17:39:35 -05:00
Zhe Fang
f24e9750ff New translations resources.resw (Russian) 2025-12-27 17:39:34 -05:00
Zhe Fang
e14f8eb01f New translations resources.resw (Portuguese) 2025-12-27 17:39:33 -05:00
Zhe Fang
0065e9a51f New translations resources.resw (Korean) 2025-12-27 17:39:32 -05:00
Zhe Fang
8e8fa8f41f New translations resources.resw (Japanese) 2025-12-27 17:39:31 -05:00
Zhe Fang
52be544cd8 New translations resources.resw (German) 2025-12-27 17:39:30 -05:00
Zhe Fang
04f5ce57bf New translations resources.resw (Arabic) 2025-12-27 17:39:29 -05:00
Zhe Fang
122ee9c969 New translations resources.resw (Spanish) 2025-12-27 17:39:28 -05:00
Zhe Fang
f29fa240fc New translations resources.resw (French) 2025-12-27 17:39:27 -05:00
Zhe Fang
bab3eef211 New translations resources.resw (Thai) 2025-12-27 16:43:28 -05:00
Zhe Fang
fc8793ce96 New translations resources.resw (Indonesian) 2025-12-27 16:43:27 -05:00
Zhe Fang
322357613a New translations resources.resw (Vietnamese) 2025-12-27 16:43:26 -05:00
Zhe Fang
90ce2d2e3e New translations resources.resw (Chinese Traditional) 2025-12-27 16:43:25 -05:00
Zhe Fang
e6a006bc9d New translations resources.resw (Russian) 2025-12-27 16:43:24 -05:00
Zhe Fang
c58c49616c New translations resources.resw (Portuguese) 2025-12-27 16:43:23 -05:00
Zhe Fang
ecf03993b0 New translations resources.resw (Korean) 2025-12-27 16:43:22 -05:00
Zhe Fang
5286c52287 New translations resources.resw (Japanese) 2025-12-27 16:43:21 -05:00
Zhe Fang
c3e07a2e7c New translations resources.resw (German) 2025-12-27 16:43:20 -05:00
Zhe Fang
c02602caf7 New translations resources.resw (Arabic) 2025-12-27 16:43:19 -05:00
Zhe Fang
3cb7a815e0 New translations resources.resw (Spanish) 2025-12-27 16:43:18 -05:00
Zhe Fang
d1f1043190 New translations resources.resw (French) 2025-12-27 16:43:17 -05:00
Zhe Fang
8a552a19dd New translations resources.resw (Malay) 2025-12-27 15:32:32 -05:00
Zhe Fang
48b3c1cb2f New translations resources.resw (Hindi) 2025-12-27 15:32:31 -05:00
Zhe Fang
0ceeaabe68 New translations resources.resw (Thai) 2025-12-27 15:32:30 -05:00
Zhe Fang
ceb37c3765 New translations resources.resw (Indonesian) 2025-12-27 15:32:29 -05:00
Zhe Fang
bd85cc3d2a New translations resources.resw (Vietnamese) 2025-12-27 15:32:28 -05:00
Zhe Fang
656ab8f20e New translations resources.resw (Chinese Traditional) 2025-12-27 15:32:27 -05:00
Zhe Fang
dfdbf03750 New translations resources.resw (Chinese Simplified) 2025-12-27 15:32:26 -05:00
Zhe Fang
f5b949daa3 New translations resources.resw (Russian) 2025-12-27 15:32:25 -05:00
Zhe Fang
6d41396b43 New translations resources.resw (Portuguese) 2025-12-27 15:32:24 -05:00
Zhe Fang
d6ffbc834f New translations resources.resw (Korean) 2025-12-27 15:32:23 -05:00
Zhe Fang
92e05f182e New translations resources.resw (Japanese) 2025-12-27 15:32:22 -05:00
Zhe Fang
d795a00e0c New translations resources.resw (German) 2025-12-27 15:32:21 -05:00
Zhe Fang
925fb1e1f1 New translations resources.resw (Arabic) 2025-12-27 15:32:20 -05:00
Zhe Fang
fa4ccaa5cf New translations resources.resw (Spanish) 2025-12-27 15:32:19 -05:00
Zhe Fang
1342f9a3a7 New translations resources.resw (French) 2025-12-27 15:32:18 -05:00
Zhe Fang
27ccde9166 New translations resources.resw (Malay) 2025-12-25 13:35:08 -05:00
Zhe Fang
d0e04c70f6 New translations resources.resw (Hindi) 2025-12-25 13:35:07 -05:00
Zhe Fang
19e249f8ce New translations resources.resw (Thai) 2025-12-25 13:35:06 -05:00
Zhe Fang
06c98a22b1 New translations resources.resw (Indonesian) 2025-12-25 13:35:05 -05:00
Zhe Fang
3d1cfa456c New translations resources.resw (Vietnamese) 2025-12-25 13:35:04 -05:00
Zhe Fang
c681aec3d3 New translations resources.resw (Chinese Traditional) 2025-12-25 13:35:03 -05:00
Zhe Fang
978008a00c New translations resources.resw (Chinese Simplified) 2025-12-25 13:35:02 -05:00
Zhe Fang
bb0e29bf6d New translations resources.resw (Russian) 2025-12-25 13:35:01 -05:00
Zhe Fang
712ff25d0d New translations resources.resw (Portuguese) 2025-12-25 13:35:00 -05:00
Zhe Fang
e79cb47fcc New translations resources.resw (Korean) 2025-12-25 13:34:59 -05:00
Zhe Fang
6289eca013 New translations resources.resw (Japanese) 2025-12-25 13:34:58 -05:00
Zhe Fang
2de7f40285 New translations resources.resw (German) 2025-12-25 13:34:57 -05:00
Zhe Fang
26274b0ffb New translations resources.resw (Arabic) 2025-12-25 13:34:56 -05:00
Zhe Fang
5d76ef6780 New translations resources.resw (Spanish) 2025-12-25 13:34:55 -05:00
Zhe Fang
8a06fc2cff New translations resources.resw (French) 2025-12-25 13:34:53 -05:00
336 changed files with 27626 additions and 38444 deletions

3
.gitignore vendored
View File

@@ -408,6 +408,3 @@ FodyWeavers.xsd
/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package)_TemporaryKey.pfx
/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/LastFM.cs
/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/Discord.cs
/BetterLyrics.Plugins.Romaji/unidic
/Plugins/BetterLyrics.Plugins.Romaji/unidic
/Plugins/BetterLyrics.Plugins.Transliteration.Romaji/unidic

View File

@@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</PropertyGroup>
</Project>

View File

@@ -1,12 +0,0 @@
using BetterLyrics.Core.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Features
{
public interface ILyricsSource
{
Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration);
}
}

View File

@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Features
{
public interface ILyricsTranslator
{
Task<string?> GetTranslationAsync(string text, string targetLangCode);
}
}

View File

@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Features
{
public interface ILyricsTransliterator
{
Task<string?> GetTransliterationAsync(string text, string targetLangCode);
}
}

View File

@@ -1,20 +0,0 @@
using BetterLyrics.Core.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface IPlugin
{
string Id { get; }
string Name { get; }
string Description { get; }
string Author { get; }
string Version { get; }
DateTime LastUpdated { get; }
void OnLoad(IPluginContext context);
void OnUnload();
}
}

View File

@@ -1,14 +0,0 @@
using BetterLyrics.Core.Interfaces.Services;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface IPluginContext
{
string PluginDirectory { get; }
IAIService? AIService { get; }
}
}

View File

@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Services
{
public interface IAIService
{
Task<string> ChatAsync(string systemPrompt, string userPrompt);
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Models
{
public record LyricsSearchResult(
string? Title,
string? Artist,
string? Album,
double? Duration,
string Raw,
string? Translation = null,
string? Transliteration = null,
string? Reference = null);
}

View File

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

View File

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

View File

@@ -3,6 +3,8 @@
x:Class="BetterLyrics.WinUI3.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:globalization="using:Windows.Globalization"
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:media="using:CommunityToolkit.WinUI.Media">
@@ -11,14 +13,8 @@
<ResourceDictionary.MergedDictionaries>
<!-- Merged dictionaries here -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<ResourceDictionary Source="ms-appx:///DevWinUI.Controls/Themes/Generic.xaml" />
<ResourceDictionary Source="/Styles/Converters.xaml" />
<ResourceDictionary Source="/Styles/InteractiveListViewHeaderStyle.xaml" />
<ResourceDictionary Source="/Styles/GhostSliderStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Theme -->
@@ -46,6 +42,45 @@
<ExponentialEase x:Key="EaseOut" EasingMode="EaseOut" />
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:EnumToIntConverter x:Key="EnumToIntConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converter:TranslationSearchProviderToDisplayNameConverter x:Key="TranslationSearchProviderToDisplayNameConverter" />
<converter:TransliterationSearchProviderToDisplayNameConverter x:Key="TransliterationSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converter:MillisecondsToFormattedTimeConverter x:Key="MillisecondsToFormattedTimeConverter" />
<converter:FPSToTimeSpanConverter x:Key="FPSToTimeSpanConverter" />
<converter:ShortcutToStringConverter x:Key="ShortcutToStringConverter" />
<converter:BoolNegationToVisibilityConverter x:Key="BoolNegationToVisibilityConverter" />
<converter:BoolToOpacityConverter x:Key="BoolToOpacityConverter" />
<converter:BoolToPartialOpacityConverter x:Key="BoolToPartialOpacityConverter" />
<converter:BoolNegationToOpacityConverter x:Key="BoolNegationToOpacityConverter" />
<converter:RectToMarginConverter x:Key="RectToMarginConverter" />
<converter:LanguageCodeToDisplayedNameConverter x:Key="LanguageCodeToDisplayedNameConverter" />
<converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
<converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
<converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
<converter:IntToBoolConverter x:Key="IntToBoolConverter" />
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
<converter:MillisecondsToSecondsConverter x:Key="MillisecondsToSecondsConverter" />
<converter:PictureInfosToImageSourceConverter x:Key="PictureInfosToImageSourceConverter" />
<converter:LyricsFontWeightToFontWeightConverter x:Key="LyricsFontWeightToFontWeightConverter" />
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style -->
@@ -61,7 +96,7 @@
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,9,16,9" />
<Setter Property="Padding" Value="14,6,14,9" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
@@ -73,7 +108,7 @@
</Style>
<Style
x:Key="TitleBarToggleButtonStyle"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
BasedOn="{StaticResource ToggleButtonRevealStyle}"
TargetType="ToggleButton">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
@@ -81,10 +116,7 @@
<Setter Property="Padding" Value="14,6,14,9" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style
x:Key="GhostToggleButtonStyle"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
TargetType="ToggleButton">
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
@@ -98,6 +130,190 @@
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource ControlStrokeColorOnAccentDefaultBrush}" />
<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="ListViewStretchedItemContainerStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
@@ -141,6 +357,10 @@
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />
<!-- Dimensions -->
<!-- Fonts -->

View File

@@ -1,168 +1,81 @@
using BetterLyrics.WinUI3.Helper;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.DbContext;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.Globalization;
using Serilog;
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3
{
public partial class App : Application
{
private Window? m_window;
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
private readonly string _appKey = Windows.ApplicationModel.Package.Current.Id.FamilyName;
private static Mutex? _instanceMutex;
public App()
{
// Must be done before InitializeComponent
if (!TryHandleSingleInstance())
{
// 如果移交成功直接退出当前进程
Environment.Exit(0);
return;
}
this.InitializeComponent();
EnsureSingleInstance();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
// 注册全局异常捕获
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
/// <summary>
/// 处理单实例逻辑。
/// 返回 true 表示我是主实例,继续运行。
/// 返回 false 表示我是第二个实例,已通知主实例,我应该退出。
/// </summary>
private bool TryHandleSingleInstance()
private void EnsureSingleInstance()
{
// 尝试查找或注册当前实例
var mainInstance = AppInstance.FindOrRegisterForKey(_appKey);
_instanceMutex = new Mutex(true, Constants.App.AppName, out bool createdNew);
// 如果当前实例就是注册的那个主实例
if (mainInstance.IsCurrent)
if (!createdNew)
{
// 监听 "Activated" 事件。
// 当第二个实例启动并重定向过来时,这个事件会被触发。
mainInstance.Activated += OnMainInstanceActivated;
return true;
}
else
{
// 我不是主实例,我是后来者。
// 获取当前实例的激活参数(比如是通过文件双击打开的,这里能拿到文件路径)
var args = AppInstance.GetCurrent().GetActivatedEventArgs();
// 将激活请求重定向给主实例
// 注意:这里是同步等待,确保发送成功后再退出
try
{
mainInstance.RedirectActivationToAsync(args).AsTask().Wait();
}
catch (Exception)
{
// 即使重定向失败,作为第二个实例也应该退出
}
return false;
User32.MessageBox(HWND.NULL, new ResourceLoader().GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
}
}
/// <summary>
/// 当第二个实例试图启动时,主实例会收到此回调
/// </summary>
private void OnMainInstanceActivated(object? sender, AppActivationArguments e)
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// 这个事件是在后台线程触发的,必须切回 UI 线程操作窗口
m_window?.DispatcherQueue.TryEnqueue(() =>
{
HandleActivation();
});
}
/// <summary>
/// 唤醒逻辑
/// </summary>
private void HandleActivation()
{
WindowHook.OpenOrShowWindow<LyricsWindowSwitchWindow>();
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
await InitDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
// Migrate MappedSongSearchQueries
var songSearchMapService = Ioc.Default.GetRequiredService<ISongSearchMapService>();
var obsoleteSongSearchMap = settingsService.AppSettings.MappedSongSearchQueries;
if (obsoleteSongSearchMap.Count > 0)
{
foreach (var item in obsoleteSongSearchMap)
{
await songSearchMapService.SaveMappingAsync(item);
}
obsoleteSongSearchMap.Clear();
}
WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// Start scan tasks in background
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
{
_ = Task.Run(async () => await fileSystemService.ScanMediaFolderAsync(item, CancellationToken.None));
}
}
fileSystemService.StartAllFolderTimers();
// Ensure plugins
var pluginService = Ioc.Default.GetRequiredService<IPluginService>();
pluginService.LoadPlugins();
// Init system tray
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// Open lyrics window if set
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -178,83 +91,38 @@ namespace BetterLyrics.WinUI3
}
}
}
// Open music gallery if set
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
private async Task InitDatabasesAsync()
{
// Init databases
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var songSearchMapFactory = Ioc.Default.GetRequiredService<IDbContextFactory<SongSearchMapDbContext>>();
var filesIndexFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
var lyricsCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<LyricsCacheDbContext>>();
using (var playHistoryDb = await playHistoryFactory.CreateDbContextAsync())
{
await playHistoryDb.Database.EnsureCreatedAsync();
}
using (var songSearchMapDb = await songSearchMapFactory.CreateDbContextAsync())
{
await songSearchMapDb.Database.EnsureCreatedAsync();
}
using (var filesIndexDb = await filesIndexFactory.CreateDbContextAsync())
{
await filesIndexDb.Database.EnsureCreatedAsync();
}
using (var lyricsCacheDb = await lyricsCacheFactory.CreateDbContextAsync())
{
await lyricsCacheDb.Database.EnsureCreatedAsync();
}
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Error)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
// 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
.AddDbContextFactory<LyricsCacheDbContext>(options => options.UseSqlite($"Data Source={PathHelper.LyricsCachePath}"))
.AddDbContextFactory<SongSearchMapDbContext>(options => options.UseSqlite($"Data Source={PathHelper.SongSearchMapPath}"))
// 日志
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<ISMTCService, SMTCService>()
.AddSingleton<IGSMTCService, GSMTCService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ITranslationService, TranslationService>()
.AddSingleton<ITransliterationService, TransliterationService>()
.AddSingleton<ILastFMService, LastFMService>()
.AddSingleton<IDiscordService, DiscordService>()
.AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
.AddSingleton<IPluginService, PluginService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>()
@@ -269,8 +137,6 @@ namespace BetterLyrics.WinUI3
.AddSingleton<MusicGalleryPageViewModel>()
.AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddSingleton<PlayQueueViewModel>()
.AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>()
@@ -288,8 +154,7 @@ namespace BetterLyrics.WinUI3
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
// FirstChance 异常非常多(比如内部 try-catch 也会触发),通常建议只在 Debug 模式记录,或者过滤特定类型
// _logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
@@ -302,4 +167,4 @@ namespace BetterLyrics.WinUI3
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 593 KiB

View File

@@ -42,18 +42,12 @@
<None Remove="Controls\LyricsWindowSwitchControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PatronControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PluginManagerControl.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\StatsDashboardControl.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Controls\WindowSettingsControl.xaml" />
<None Remove="Styles\GhostSliderStyle.xaml" />
<None Remove="Styles\InteractiveListViewHeaderStyle.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
<None Remove="Views\LyricsWindowSwitchWindow.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
@@ -75,7 +69,6 @@
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
@@ -83,7 +76,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.251219" />
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="DevWinUI.Controls" Version="9.9.2" />
<PackageReference Include="DevWinUI.Controls" Version="9.8.1" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" />
<PackageReference Include="F23.StringSimilarity" Version="7.0.1" />
<PackageReference Include="FlaUI.UIA3" Version="5.0.0" />
@@ -91,20 +84,11 @@
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.4.1" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc6.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
<PackageReference Include="NTextCat" Version="0.3.65" />
@@ -123,12 +107,11 @@
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
<PackageReference Include="WebDav.Client" Version="2.9.0" />
<PackageReference Include="WinUIEx" Version="2.9.0" />
<PackageReference Include="z440.atl.core" Version="7.10.0" />
<PackageReference Include="z440.atl.core" Version="7.9.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
@@ -140,245 +123,214 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootDescriptor Include="PluginConfigs\**\*_TrimmerRoots.xml" />
<TrimmerRootDescriptor Include="Core_TrimmerRoots.xml" />
<TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
<TrimmerRootAssembly Include="Vanara.PInvoke.Gdi32" />
<TrimmerRootAssembly Include="Vanara.PInvoke.Shell32" />
<TrimmerRootAssembly Include="Vanara.PInvoke.User32" />
<TrimmerRootAssembly Include="Vanara.Windows.Shell" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AIMP.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AlbumArtPlaceholder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Alipay.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AMLLPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AppleMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Chrome.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\FluidEffect.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\HyPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\iTunes.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\KugouMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LastFM.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Leaf.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Listen1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LXMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MediaPlayerWindows11.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MoeKoeMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MusicBee.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PlanetMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\WeChatReward.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AIMP.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AlbumArtPlaceholder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Alipay.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AMLLPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AppleMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Chrome.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\FluidEffect.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\HyPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\iTunes.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\KugouMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LastFM.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Leaf.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Listen1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LXMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MediaPlayerWindows11.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MoeKoeMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MusicBee.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PlanetMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\WeChatReward.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PluginManagerControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\LyricsWindowSwitchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsWindowSwitchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\DemoWindowGrid.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsWindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\LyricsSearchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\LyricsWindowSwitchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsWindowSwitchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ExtendedSlider.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\DemoWindowGrid.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsWindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\MediaSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\LyricsSearchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PlaybackSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AlbumArtAreaStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ExtendedSlider.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AppSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\MediaSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlaybackSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AlbumArtAreaStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AppSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
@@ -391,57 +343,49 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\RemoteServerConfigControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\RemoteServerConfigControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\NowPlayingBar.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\NowPlayingBar.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SystemTrayWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\SystemTrayWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\WindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\WindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AlbumArtAreaEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AlbumArtAreaEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ImageSwitcher.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ImageSwitcher.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ShadowImage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ShadowImage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="PluginConfigs\" />
<Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Constants
{
public static class PlayerId
public static class PlayerID
{
public const string LXMusic = "cn.toside.music.desktop";
public const string LXMusicPortable = "lx-music-desktop.exe";
@@ -25,6 +25,5 @@
public const string MoeKoeMusic = "cn.MoeKoe.Music";
public const string MoeKoeMusicAlternative = "electron.app.MoeKoe Music";
public const string Listen1 = "com.listen1.listen1";
public const string OriginalSoundHQPlayer = "SennpaiStudio.528762A6196EF_z79ft30j24epr!App";
}
}

View File

@@ -24,6 +24,5 @@
public const string SaltPlayerForWindowsSteam = "Salt Player for Windows (Steam)";
public const string MoeKoeMusic = "MoeKoe Music";
public const string Listen1 = "Listen 1";
public const string OriginalSoundHQPlayer = "Original Sound HQ Player";
}
}

View File

@@ -6,7 +6,5 @@ namespace BetterLyrics.WinUI3.Constants
{
public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250);
public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(350);
public static readonly TimeSpan LongAnimationDuration = TimeSpan.FromMilliseconds(650);
public static readonly TimeSpan WaitingDuration = TimeSpan.FromMilliseconds(300);
}
}

View File

@@ -57,12 +57,12 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="GitHub" NavigateUri="{x:Bind const:Link.BetterLyricsGitHub}" />
<HyperlinkButton x:Uid="UserGuide" NavigateUri="{x:Bind const:Link.UserGuide}" />
<HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfService}" />
</dev:WrapPanel>
</StackPanel>
</StackPanel>
</dev:SettingsCard>
@@ -70,18 +70,18 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageFeedback" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="QQ 反馈交流群" NavigateUri="{x:Bind const:Link.QQGroup}" />
<HyperlinkButton Content="Discord" NavigateUri="{x:Bind const:Link.Discord}" />
<HyperlinkButton Content="Telegram" NavigateUri="{x:Bind const:Link.Telegram}" />
</dev:WrapPanel>
</StackPanel>
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDonation" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="Buy Me a Coffee" NavigateUri="{x:Bind const:Link.BuyMeACoffee}" />
<HyperlinkButton Content="PayPal" NavigateUri="{x:Bind const:Link.PayPal}" />
<HyperlinkButton
@@ -117,25 +117,26 @@
</HyperlinkButton.ContextFlyout>
</HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</dev:WrapPanel>
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="*" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="*" />
<TextBlock
x:Uid="SetingsPageThanks"
Grid.Column="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</Grid>
</StackPanel>
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageThanksList">
<Button
Click="Patron_Click"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7FD;}"
Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
<dev:SettingsExpander.ItemsFooter>
<InfoBar
@@ -148,108 +149,6 @@
</dev:SettingsExpander.ItemsFooter>
</dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageThanksList">
<dev:SettingsExpander.HeaderIcon>
<ImageIcon Source="ms-appx:///Assets/RevolvingHearts.gif" />
</dev:SettingsExpander.HeaderIcon>
<dev:SettingsExpander.Items>
<!-- 贡献者 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Code" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="jayfunc" NavigateUri="https://github.com/jayfunc" />
<HyperlinkButton Content="Raspberry-Monster" NavigateUri="https://github.com/Raspberry-Monster" />
<HyperlinkButton Content="ZHider" NavigateUri="https://github.com/ZHider" />
<HyperlinkButton Content="kusutori" NavigateUri="https://github.com/kusutori" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 贡献者 (Translator) -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Translator" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="borcolasky" NavigateUri="https://crowdin.com/profile/borcolasky" />
<HyperlinkButton Content="SuHeAndZl" NavigateUri="https://crowdin.com/profile/SuHeAndZl" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 赞助 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SettingsPagePatrons" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<uc:PatronControl Date="Jan 8, 2026" PatronName="Eureka-K_K" />
<uc:PatronControl Date="Jan 3, 2026" PatronName="**轩" />
<uc:PatronControl Date="Dec 13, 2025" PatronName="&lt;Anonymous&gt;" />
<uc:PatronControl Date="Dec 3, 2025" PatronName="YE" />
<uc:PatronControl Date="Dec 2, 2025" PatronName="&lt;Anonymous&gt;" />
<uc:PatronControl Date="Nov 23, 2025" PatronName="**玄" />
<uc:PatronControl Date="Nov 21, 2025" PatronName="**智" />
<uc:PatronControl Date="Nov 17, 2025" PatronName="SuHeAndZl" />
<uc:PatronControl Date="Nov 2, 2025" PatronName="借过" />
<uc:PatronControl Date="Aug 28, 2025" PatronName="**华" />
<TextBlock
x:Uid="SettingsPageUserWhoPurchased"
Margin="12,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 特别鸣谢 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageSpecialThanks" />
<TextBlock
x:Uid="SettingsPageYouNowUsing"
Margin="0,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</dev:SettingsCard>
<!-- 代码参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDeps" />
<HyperlinkButton Margin="-12,0,0,0" NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<TextBlock x:Uid="SetingsPageDeps" />
</HyperlinkButton>
</StackPanel>
</dev:SettingsCard>
<!-- UI/UX 参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageUIUXRef" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="refined-now-playing-netease" NavigateUri="https://github.com/solstice23/refined-now-playing-netease" />
<HyperlinkButton Content="Lyricify" NavigateUri="https://github.com/WXRIW/Lyricify-App" />
<HyperlinkButton Content="椒盐音乐 Salt Player" NavigateUri="https://moriafly.com/program/salt-player" />
<HyperlinkButton Content="MyToolBar" NavigateUri="https://github.com/TwilightLemon/MyToolBar" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<HyperlinkButton x:Uid="SettingsPagePlayingMockMusicButton" NavigateUri="https://soundcloud.com/carlyraejepsen/cut-to-the-feeling" />
</dev:SettingsCard>
@@ -296,28 +195,194 @@
Value="{x:Bind ViewModel.AppSettings.AdvancedSettings.FPS, Mode=TwoWay}" />
</dev:SettingsCard>
<RichTextBlock
Margin="0,16,0,0"
HorizontalAlignment="Center"
HorizontalTextAlignment="Center"
LineHeight="28">
<Paragraph FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" />
</Paragraph>
<Paragraph>
<Run Text="An elegant and deeply customizable lyrics visualizer &amp; versatile music player" />
</Paragraph>
<Paragraph>
<Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink>
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
</ScrollViewer>
<Grid
x:Name="CreditsReel"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
Opacity="0"
SizeChanged="CreditsReel_SizeChanged"
Tapped="CreditsReel_Tapped"
Visibility="Collapsed">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<ScrollViewer
x:Name="CreditsReelScrollViewer"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollMode="Disabled">
<RichTextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalTextAlignment="Center"
LineHeight="28"
PointerEntered="RichTextBlock_PointerEntered"
PointerExited="RichTextBlock_PointerExited">
<Paragraph x:Name="CreditsReelHeader" />
<!-- 贡献者 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageContributors" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc">
<Run Text="jayfunc" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Raspberry-Monster">
<Run Text="Raspberry-Monster" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/ZHider">
<Run Text="ZHider" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/kusutori">
<Run Text="kusutori" />
</Hyperlink>
</Paragraph>
<!-- 赞助 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SettingsPagePatrons" />
</Paragraph>
<Paragraph>
<Run Text="YE" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Dec 3, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**玄" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 23, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**智" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 21, 2025" />
</Paragraph>
<Paragraph>
<Run Text="*鹤" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 17, 2025" />
</Paragraph>
<Paragraph>
<Run Text="借过" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 2, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**华" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Aug 28, 2025" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageUserWhoPurchased" />
</Paragraph>
<!-- 特别鸣谢 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageSpecialThanks" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageYouNowUsing" />
</Paragraph>
<!-- 代码参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageDeps" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce">
<Run Text="Get album artwork from ITunes (with Python3 or C#)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://stackoverflow.com/a/32013610/11048731">
<Run Text="FullyObservableCollection" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/Impressionist">
<Run Text="Impressionist" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/ColorThief.WinUI3">
<Run Text="ColorThief.WinUI3" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Johnwikix/SpectrumVisualization">
<Run Text="SpectrumVisualization" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/Mdt3Df">
<Run Text="Snow (as shown in sweden)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/lllSR2">
<Run Text="w10" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/mo-jinran/Taskbar-Lyrics">
<Run Text="Taskbar-Lyrics" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<Run Text="..." />
</Hyperlink>
</Paragraph>
<!-- UI/UX 设计参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageUIUXRef" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/solstice23/refined-now-playing-netease">
<Run Text="refined-now-playing-netease" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/WXRIW/Lyricify-App">
<Run Text="Lyricify" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://moriafly.com/program/salt-player">
<Run Text="椒盐音乐 Salt Player" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/TwilightLemon/MyToolBar">
<Run Text="MyToolBar" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="">
<Run Text="" />
</Hyperlink>
</Paragraph>
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" />
</Paragraph>
<Paragraph>
<Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink>
</Paragraph>
<Paragraph x:Name="CreditsReelFooter" />
</RichTextBlock>
</ScrollViewer>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,6 +1,8 @@
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Threading.Tasks;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -9,6 +11,7 @@ namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class AboutControl : UserControl
{
private bool _isCreditsScrolling = false;
public AboutControlViewModel ViewModel => (AboutControlViewModel)DataContext;
public AboutControl()
@@ -17,6 +20,47 @@ namespace BetterLyrics.WinUI3.Controls
DataContext = Ioc.Default.GetRequiredService<AboutControlViewModel>();
}
private async void Patron_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
CreditsReel.Opacity = 1;
_isCreditsScrolling = true;
}
private void CompositionTarget_Rendering(object? sender, object e)
{
if (_isCreditsScrolling)
{
CreditsReelScrollViewer.ChangeView(null, CreditsReelScrollViewer.VerticalOffset + 0.5, null);
}
}
private async void CreditsReel_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
CreditsReel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
CompositionTarget.Rendering -= CompositionTarget_Rendering;
CreditsReelScrollViewer.ChangeView(null, 0, null);
}
private void CreditsReel_SizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
CreditsReelHeader.LineHeight = e.NewSize.Height;
CreditsReelFooter.LineHeight = e.NewSize.Height / 2;
}
private void RichTextBlock_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = false;
}
private void RichTextBlock_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = true;
}
private void WeChat_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
WeChatFlyout.ShowAt(WeChatButton);

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;

View File

@@ -5,10 +5,10 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Logic;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -31,7 +31,7 @@ namespace BetterLyrics.WinUI3.Controls
public sealed partial class LyricsCanvas : UserControl,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsData?>>,
IRecipient<PropertyChangedMessage<SongInfo>>,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
@@ -41,7 +41,8 @@ namespace BetterLyrics.WinUI3.Controls
IRecipient<PropertyChangedMessage<IRandomAccessStream?>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
@@ -58,47 +59,48 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent,
defaultTotalDuration: 0.3f,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
durationSeconds: 0.3f
);
private readonly ValueTransition<Color> _accentColor1Transition = new(
initialValue: Colors.Transparent,
defaultTotalDuration: 0.3f,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor2Transition = new(
initialValue: Colors.Transparent,
defaultTotalDuration: 0.3f,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor3Transition = new(
initialValue: Colors.Transparent,
defaultTotalDuration: 0.3f,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<Color> _accentColor4Transition = new(
initialValue: Colors.Transparent,
defaultTotalDuration: 0.3f,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutSine
);
private readonly ValueTransition<double> _mouseYScrollTransition = new(
initialValue: 0f,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
durationSeconds: 0.3f,
easingType: EasingType.EaseInOutSine
);
private TimeSpan _songPositionWithOffset;
private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1>
private TimeSpan _totalPlayedTime; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD>ŵ<EFBFBD>ʱ<EFBFBD>
private bool _isLastFMTracked = false;
private double _renderLyricsStartX = 0;
private double _renderLyricsStartY = 0;
@@ -120,7 +122,7 @@ namespace BetterLyrics.WinUI3.Controls
private bool _isLayoutChanged = true;
private bool _isMouseScrollingChanged = false;
private int _primaryPlayingLineIndex;
private int _playingLineIndex;
private (int Start, int End) _visibleRange;
private double _canvasTargetScrollOffset;
@@ -293,7 +295,7 @@ namespace BetterLyrics.WinUI3.Controls
}
else if (e.Property == MouseScrollOffsetProperty)
{
canvas._mouseYScrollTransition.Start(Convert.ToDouble(e.NewValue));
canvas._mouseYScrollTransition.StartTransition(Convert.ToDouble(e.NewValue));
}
else if (e.Property == MousePositionProperty)
{
@@ -319,11 +321,11 @@ namespace BetterLyrics.WinUI3.Controls
else if (e.Property == AlbumArtThemeColorsProperty)
{
var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue;
canvas._immersiveBgColorTransition.Start(albumArtThemeColors.EnvColor);
canvas._accentColor1Transition.Start(albumArtThemeColors.AccentColor1);
canvas._accentColor2Transition.Start(albumArtThemeColors.AccentColor2);
canvas._accentColor3Transition.Start(albumArtThemeColors.AccentColor3);
canvas._accentColor4Transition.Start(albumArtThemeColors.AccentColor4);
canvas._immersiveBgColorTransition.StartTransition(albumArtThemeColors.EnvColor);
canvas._accentColor1Transition.StartTransition(albumArtThemeColors.AccentColor1);
canvas._accentColor2Transition.StartTransition(albumArtThemeColors.AccentColor2);
canvas._accentColor3Transition.StartTransition(albumArtThemeColors.AccentColor3);
canvas._accentColor4Transition.StartTransition(albumArtThemeColors.AccentColor4);
canvas._albumArtThemeColors = albumArtThemeColors;
canvas._isLayoutChanged = true;
@@ -344,7 +346,8 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _gsmtcService.CurrentSongInfo.DurationMs;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor;
double finalOpacity;
@@ -382,6 +385,7 @@ namespace BetterLyrics.WinUI3.Controls
control: sender,
ds: args.DrawingSession,
lines: _renderLyricsLines,
playingLineIndex: _playingLineIndex,
mouseHoverLineIndex: _mouseHoverLineIndex,
isMousePressing: _isMousePressing,
startVisibleIndex: _visibleRange.Start,
@@ -397,7 +401,6 @@ namespace BetterLyrics.WinUI3.Controls
strokeColor: _albumArtThemeColors.StrokeFontColor,
bgColor: _albumArtThemeColors.BgFontColor,
fgColor: _albumArtThemeColors.FgFontColor,
currentProgressMs: _songPositionWithOffset.TotalMilliseconds,
getPlaybackState: (lineIndex) =>
{
if (_renderLyricsLines == null) return new LinePlaybackState();
@@ -410,7 +413,9 @@ namespace BetterLyrics.WinUI3.Controls
return _synchronizer.GetLinePlayingProgress(
_songPositionWithOffset.TotalMilliseconds,
line,
lyricsEffect.WordByWordEffectMode
nextLine,
songDuration,
isForceWordByWord
);
}
);
@@ -431,19 +436,19 @@ namespace BetterLyrics.WinUI3.Controls
);
}
#if DEBUG && false
args.DrawingSession.DrawText(
$"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
$"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
$"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
$"Playing line (idx): {_playingLineIndex}\n" +
$"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
$"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
$"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
$"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_gsmtcService.CurrentSongInfo.DurationMs)}\n" +
$"Y offset: {_canvasYScrollTransition.Value}\n" +
$"User scroll offset: {_mouseYScrollTransition.Value}",
new Vector2(0, 0), Colors.Red);
#if DEBUG
//args.DrawingSession.DrawText(
// $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
// $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
// $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
// $"Playing line (idx): {_playingLineIndex}\n" +
// $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
// $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
// $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
// $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" +
// $"Y offset: {_canvasYScrollTransition.Value}\n" +
// $"User scroll offset: {_mouseYScrollTransition.Value}",
// new Vector2(0, 0), Colors.Red);
#endif
}
@@ -455,7 +460,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsBg = _lyricsWindowStatus.LyricsBackgroundSettings;
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
var lyricsData = _gsmtcService.CurrentLyricsData;
var lyricsData = _mediaSessionsService.CurrentLyricsData;
TimeSpan elapsedTime = args.Timing.ElapsedTime;
@@ -473,29 +478,22 @@ namespace BetterLyrics.WinUI3.Controls
#region UpdatePlayingLineIndex
int primaryPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, _renderLyricsLines);
bool isPrimaryPlayingLineChanged = primaryPlayingIndex != _primaryPlayingLineIndex;
_primaryPlayingLineIndex = primaryPlayingIndex;
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, lyricsData);
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex;
_playingLineIndex = newPlayingIndex;
#endregion
#region UpdateTargetScrollOffset
if (isPrimaryPlayingLineChanged || _isLayoutChanged)
if (isPlayingLineChanged || _isLayoutChanged)
{
var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _primaryPlayingLineIndex);
var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _playingLineIndex);
if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value;
if (_isLayoutChanged)
{
_canvasYScrollTransition.JumpTo(_canvasTargetScrollOffset);
}
else
{
_canvasYScrollTransition.SetDurationMs(lyricsEffect.LyricsScrollDuration);
_canvasYScrollTransition.SetInterpolator(EasingHelper.GetInterpolatorByEasingType<double>(lyricsEffect.LyricsScrollEasingType, lyricsEffect.LyricsScrollEasingMode));
_canvasYScrollTransition.Start(_canvasTargetScrollOffset);
}
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType);
_canvasYScrollTransition.SetDuration(lyricsEffect.LyricsScrollDuration / 1000.0);
_canvasYScrollTransition.StartTransition(_canvasTargetScrollOffset, _isLayoutChanged);
}
_canvasYScrollTransition.Update(elapsedTime);
@@ -528,7 +526,7 @@ namespace BetterLyrics.WinUI3.Controls
_renderLyricsLines,
_isMouseScrolling ? maxRange.Start : _visibleRange.Start,
_isMouseScrolling ? maxRange.End : _visibleRange.End,
_primaryPlayingLineIndex,
_playingLineIndex,
sender.Size.Height,
_canvasTargetScrollOffset,
lyricsStyle.PlayingLineTopOffset / 100.0,
@@ -540,9 +538,8 @@ namespace BetterLyrics.WinUI3.Controls
elapsedTime,
_isMouseScrolling,
_isLayoutChanged,
isPrimaryPlayingLineChanged,
_isMouseScrollingChanged,
_songPositionWithOffset.TotalMilliseconds
isPlayingLineChanged,
_isMouseScrollingChanged
);
_isMouseScrollingChanged = false;
@@ -658,32 +655,54 @@ namespace BetterLyrics.WinUI3.Controls
private void UpdatePlaybackState(TimeSpan elapsedTime)
{
if (_gsmtcService.CurrentIsPlaying)
if (_mediaSessionsService.CurrentIsPlaying)
{
_songPosition += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_gsmtcService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
_totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
}
}
private void ResetPlaybackState()
{
_songPosition = TimeSpan.Zero;
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
private void UpdateRenderLyricsLines()
{
_renderLyricsLines = null;
var lines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine(x)).ToList();
if (lines != null)
_renderLyricsLines = _mediaSessionsService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
{
LyricsLayoutManager.CalculateLanes(lines);
}
_renderLyricsLines = lines;
LyricsSyllables = x.LyricsSyllables,
StartMs = x.StartMs,
EndMs = x.EndMs,
PhoneticText = x.PhoneticText,
OriginalText = x.OriginalText,
TranslatedText = x.TranslatedText
}).ToList();
}
private async Task ReloadCoverBackgroundResourcesAsync()
{
if (_gsmtcService.AlbumArtBitmapStream is IRandomAccessStream stream)
if (_mediaSessionsService.AlbumArtBitmapStream is IRandomAccessStream stream)
{
stream.Seek(0);
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
@@ -693,19 +712,26 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IGSMTCService)
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
var realPosition = message.NewValue;
var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds);
var timelineSyncThreshold = _gsmtcService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
// ƫ<><C6AB> or seek
if (diff >= timelineSyncThreshold)
{
_songPosition = realPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˿<EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD> LastFM ͳ<><CDB3>״̬
if (_songPosition.TotalSeconds <= 1)
{
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>
@@ -719,9 +745,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<LyricsData?> message)
{
if (message.Sender is IGSMTCService)
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IGSMTCService.CurrentLyricsData))
if (message.PropertyName == nameof(IMediaSessionsService.CurrentLyricsData))
{
UpdateRenderLyricsLines();
_isLayoutChanged = true;
@@ -729,11 +755,11 @@ namespace BetterLyrics.WinUI3.Controls
}
}
public void Receive(PropertyChangedMessage<SongInfo> message)
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IGSMTCService)
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
ResetPlaybackState();
}
@@ -886,14 +912,13 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IGSMTCService)
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapStream))
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}
}
}
}
}

View File

@@ -23,14 +23,6 @@
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Effect" />
<dev:SettingsCard x:Uid="SettingsPageLyricsWordByWordEffectMode" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF714;}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.WordByWordEffectMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAuto" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeNever" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAlways" />
</ComboBox>
</dev:SettingsCard>
<!-- 模糊效果 -->
<dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" />
@@ -116,14 +108,6 @@
Minimum="0"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationAmount, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsEffectSettingsControlAnimationDuration">
<local:ExtendedSlider
Default="450"
Maximum="2000"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationDuration, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
@@ -196,25 +180,18 @@
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingTypeLinear" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeSmoothStep" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseSine" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuad" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseCubic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuart" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuint" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseExpo" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseCirc" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseBack" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseElastic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseBounce" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutSine" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuad" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCubic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuart" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuint" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutExpo" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCirc" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBack" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutElastic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBounce" />
</ComboBox>
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageEasingMode">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingModeIn" />
<ComboBoxItem x:Uid="SettingsPageEasingModeOut" />
<ComboBoxItem x:Uid="SettingsPageEasingModeInOut" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageScrollTopDuration">
<local:ExtendedSlider
Default="500"

View File

@@ -8,7 +8,6 @@
xmlns:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:lyricsmodels="using:BetterLyrics.WinUI3.Models.Lyrics"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
@@ -26,7 +25,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<ScrollViewer Padding="8,0">
<ScrollViewer>
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="LyricsSearchControlSongInfoMapping" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -98,6 +97,19 @@
Style="{StaticResource GhostButtonStyle}" />
</Grid>
<RichTextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
<Paragraph>
<Run Text="*" />
<Run x:Uid="ArtistsSplitHint" />
</Paragraph>
<Paragraph>
<Run Text="; , / " />
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
@@ -141,11 +153,12 @@
<CheckBox x:Uid="LyricsSearchControlMarkAsPureMusic" IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}" />
</dev:SettingsCard>
<Button
x:Uid="LyricsSearchControlSearch"
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" />
<dev:SettingsCard x:Uid="LyricsSearchControlTargetSearchProvider">
<Button
x:Uid="LyricsSearchControlSearch"
Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreCacheWhenSearching, Mode=TwoWay}" />
@@ -157,7 +170,7 @@
<Grid Grid.Column="1">
<ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsCacheItem">
<DataTemplate x:DataType="models:LyricsSearchResult">
<ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow
@@ -168,13 +181,17 @@
<!-- Lyrics search result -->
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind Artist, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
<!-- NOT FOUND -->
<TextBlock
@@ -224,6 +241,8 @@
<ProgressBar
VerticalAlignment="Top"
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.IsSearching, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</Grid>
<Grid Grid.Column="2">
@@ -243,7 +262,7 @@
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<Pivot.HeaderTemplate>
<DataTemplate x:DataType="lyricsmodels:LyricsData">
<DataTemplate x:DataType="models:LyricsData">
<StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<InfoBadge
@@ -255,13 +274,13 @@
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate x:DataType="lyricsmodels:LyricsData">
<DataTemplate x:DataType="models:LyricsData">
<ListView
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind LyricsLines, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="lyricsmodels:LyricsLine">
<DataTemplate x:DataType="models:LyricsLine">
<Grid Margin="0,6" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -291,7 +310,7 @@
</interactivity:Interaction.Behaviors>
</Button>
</Grid>
<local:PropertyRow Grid.Column="1" Value="{x:Bind PrimaryText, Mode=OneWay}" />
<local:PropertyRow Grid.Column="1" Value="{x:Bind OriginalText, Mode=OneWay}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>

View File

@@ -1,4 +1,4 @@
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;

View File

@@ -187,51 +187,29 @@
<controls:Segmented
x:Name="ConfigSegmented"
HorizontalAlignment="Stretch"
SelectionChanged="ConfigSegmented_SelectionChanged"
Style="{StaticResource PivotSegmentedStyle}">
<controls:SegmentedItem x:Name="WindowSegmentedItem" Tag="Window">
<TextBlock
x:Uid="AppSettingsControlGeneral"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="AppSettingsControlGeneral" />
</controls:SegmentedItem>
<controls:SegmentedItem x:Name="LayoutSegmentedItem" Tag="Layout">
<TextBlock
x:Uid="SettingsPageLayout"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="SettingsPageLayout" />
</controls:SegmentedItem>
<controls:SegmentedItem x:Name="AlbumArtStyleSegmentedItem" Tag="AlbumArtStyle">
<TextBlock
x:Uid="SettingsPageAlbumStyle"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="SettingsPageAlbumStyle" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="AlbumArtEffect">
<TextBlock
x:Uid="SettingsPageAlbumEffect"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="SettingsPageAlbumEffect" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsStyle">
<TextBlock
x:Uid="SettingsPageLyricsStyle"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="SettingsPageLyricsStyle" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsEffect">
<TextBlock
x:Uid="SettingsPageLyricsEffect"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="SettingsPageLyricsEffect" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsBackground">
<TextBlock
x:Uid="SettingsPageBackgroundOverlay"
MaxWidth="120"
TextWrapping="Wrap" />
<TextBlock x:Uid="SettingsPageBackgroundOverlay" />
</controls:SegmentedItem>
</controls:Segmented>

View File

@@ -1,6 +1,6 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;

View File

@@ -5,12 +5,10 @@
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:enums="using:BetterLyrics.WinUI3.Enums"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:settingsmodels="using:BetterLyrics.WinUI3.Models.Settings"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -51,124 +49,80 @@
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="settingsmodels:MediaFolder">
<dev:SettingsExpander IsExpanded="True">
<DataTemplate x:DataType="models:MediaFolder">
<dev:SettingsExpander Description="{x:Bind ConnectionSummary, Mode=OneWay}">
<dev:SettingsExpander.HeaderIcon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="{x:Bind SourceType, Converter={StaticResource FileSourceTypeToIconConverter}, Mode=OneWay}" />
</dev:SettingsExpander.HeaderIcon>
<dev:SettingsExpander.Header>
<TextBlock IsTextSelectionEnabled="True" Text="{x:Bind Name, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
Click="LocalFolderHyperlinkButton_Click"
Content="{x:Bind Path, Mode=OneWay}"
Tag="{x:Bind Path, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ConnectionSummary}" />
</dev:SettingsExpander.Header>
<dev:SettingsExpander.Description>
<TextBlock IsTextSelectionEnabled="True" Text="{x:Bind ConnectionSummary, Mode=OneWay}" />
</dev:SettingsExpander.Description>
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="MediaSettingsControlNameSetting">
<TextBox VerticalAlignment="Center" Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="MediaSettingsControlLastSyncTime" Description="{x:Bind LastSyncTime.ToString(), Mode=OneWay, TargetNullValue=N/A}">
<Button
x:Uid="MediaSettingsControlSyncNow"
Click="SyncNowButton_Click"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="MusicSettingsControlAutoSyncInterval">
<ComboBox IsEnabled="{x:Bind IsEnabled, Mode=OneWay}" SelectedIndex="{x:Bind ScanInterval, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalDisabled" />
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEveryFifteenMin" />
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEveryHour" />
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEverySixHrs" />
<ComboBoxItem x:Uid="MusicSettingsControlAutoSyncIntervalEveryDay" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard>
<Button x:Uid="SettingsPageRemovePath" Click="SettingsPageRemovePathButton_Click" />
<dev:SettingsCard.Header>
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Padding="0"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
</dev:SettingsCard.Header>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageMusicLibRealTimeWatch" IsEnabled="{Binding IsLocal, Mode=OneWay}">
<ToggleSwitch IsOn="{Binding IsRealTimeWatchEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
<dev:SettingsExpander.ItemsHeader>
<StackPanel>
<!-- Index info -->
<InfoBar
IsClosable="False"
IsOpen="True"
Message="{x:Bind StatusText, Mode=OneWay}"
Severity="{x:Bind StatusSeverity, Mode=OneWay}" />
<ProgressBar
Background="Transparent"
Visibility="{x:Bind IsProcessing, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{x:Bind IndexingProgress, Mode=OneWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="IsIndeterminate" Value="True" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="IsIndeterminate" Value="False" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ProgressBar>
</StackPanel>
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Margin="0,6,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="6">
<Button Command="{x:Bind ViewModel.OpenMusicGalleryWindowCommand}">
<TextBlock x:Uid="SystemTrayMusicGallery" />
</Button>
<dev:SettingsCard Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<DropDownButton x:Uid="SettingsPageAddFolderButton">
<DropDownButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="SettingsPageLocalFolder"
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="Local">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE8B7;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
CommandParameter="{Binding ElementName=RootGrid}"
Icon="Folder" />
<MenuFlyoutSeparator />
<MenuFlyoutSeparator Visibility="Collapsed" />
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
CommandParameter="SMB"
Text="SMB">
Text="SMB"
Visibility="Collapsed">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE839;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
CommandParameter="FTP"
Text="FTP">
Text="FTP"
Visibility="Collapsed">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE838;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
Command="{x:Bind ViewModel.AddRemoteSourceCommand}"
CommandParameter="WebDAV"
Text="WebDAV">
Text="WebDAV"
Visibility="Collapsed">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE774;" />
</MenuFlyoutItem.Icon>
@@ -177,7 +131,7 @@
</MenuFlyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
</dev:SettingsCard>
</StackPanel>
</Grid>

View File

@@ -1,8 +1,10 @@
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using Windows.System;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -20,14 +22,18 @@ namespace BetterLyrics.WinUI3.Controls
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
ViewModel.RemoveFolder(folder);
ViewModel.RemoveFolderAsync((MediaFolder)(sender as HyperlinkButton)!.Tag);
}
private void SyncNowButton_Click(object sender, RoutedEventArgs e)
private async void LocalFolderHyperlinkButton_Click(object sender, RoutedEventArgs e)
{
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
ViewModel.SyncFolder(folder);
if (sender is HyperlinkButton button && button.Tag is string uriStr)
{
if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri))
{
await Launcher.LaunchUriAsync(uri);
}
}
}
}
}

View File

@@ -4,7 +4,6 @@
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:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,6 +11,7 @@
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid
x:Name="BottomCommandGrid"
Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
@@ -60,16 +60,16 @@
</interactivity:Interaction.Behaviors>
<Grid VerticalAlignment="Center" CornerRadius="4">
<local:ImageSwitcher
x:Name="AlbumArtImageSwitcher"
Width="36"
Height="36"
Source="{x:Bind ViewModel.GSMTCService.AlbumArtBitmapImage, Mode=OneWay}" />
Height="36" />
</Grid>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<TextBlock x:Name="TitleTextBlock" />
<TextBlock
x:Name="ArtistsTextBlock"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, Mode=OneWay}" />
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</StackPanel>
@@ -113,58 +113,7 @@
x:Name="BottomCenterCommandStackPanel"
Padding="16"
Orientation="Horizontal"
Spacing="12">
<!-- Playback order -->
<Button
Grid.Column="2"
Click="PlaybackOrderButton_Click"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlaybackOrderButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<!-- 上一曲目 -->
Spacing="3">
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -177,13 +126,13 @@
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
@@ -197,13 +146,13 @@
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -215,17 +164,6 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<Button
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPagePlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
@@ -236,19 +174,6 @@
Orientation="Horizontal"
Spacing="3">
<!-- Stop media session -->
<Button
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowStopButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Volume -->
<Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}">
<Grid>
@@ -420,11 +345,10 @@
Margin="0,-14,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Maximum="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.GSMTCService.CurrentPosition.TotalSeconds, Mode=OneWay}" />
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" />
<Grid
x:Name="TimelineSliderLyricsLineInfo"
@@ -447,7 +371,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
<!-- TODO 原文翻译共同显示 -->
<TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.PrimaryText, Mode=OneWay}" />
<TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.OriginalText, Mode=OneWay}" />
</StackPanel>
</Grid>
<Grid
@@ -503,58 +427,7 @@
</Grid.ContextFlyout>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PlaybackOrderState">
<VisualState x:Name="RepeatAll">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="1" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Visible" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="RepeatOne">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="1" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Visible" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Shuffle">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="1" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -1,12 +1,15 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Numerics;
@@ -15,13 +18,15 @@ using System.Numerics;
namespace BetterLyrics.WinUI3.Controls;
public sealed partial class NowPlayingBar : UserControl
public sealed partial class NowPlayingBar : UserControl,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<BitmapImage?>>,
IRecipient<PropertyChangedMessage<TimeSpan>>
{
public NowPlayingBarViewModel ViewModel => (NowPlayingBarViewModel)DataContext;
public event EventHandler? SongInfoTapped;
public event EventHandler? TimeTapped;
public event EventHandler? PlayQueueButtonClick;
public bool ShowTime
{
@@ -41,42 +46,6 @@ public sealed partial class NowPlayingBar : UserControl
public static readonly DependencyProperty ShowSongInfoProperty =
DependencyProperty.Register(nameof(ShowSongInfo), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlayingQueueButton
{
get { return (bool)GetValue(ShowPlayingQueueButtonProperty); }
set { SetValue(ShowPlayingQueueButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlayingQueueButtonProperty =
DependencyProperty.Register(nameof(ShowPlayingQueueButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlaybackOrderButton
{
get { return (bool)GetValue(ShowPlaybackOrderButtonProperty); }
set { SetValue(ShowPlaybackOrderButtonProperty, value); }
}
public static readonly DependencyProperty ShowStopButtonProperty =
DependencyProperty.Register(nameof(ShowStopButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowStopButton
{
get { return (bool)GetValue(ShowStopButtonProperty); }
set { SetValue(ShowStopButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public PlaybackOrder PlaybackOrder
{
get { return (PlaybackOrder)GetValue(PlaybackOrderProperty); }
set { SetValue(PlaybackOrderProperty, value); }
}
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsCompactMode
{
get { return (bool)GetValue(IsCompactModeProperty); }
@@ -101,6 +70,8 @@ public sealed partial class NowPlayingBar : UserControl
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<NowPlayingBarViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
}
private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -196,7 +167,7 @@ public sealed partial class NowPlayingBar : UserControl
var grid = (Grid)sender;
var pos = e.GetCurrentPoint(grid).Position;
var ratio = pos.X / grid.ActualWidth;
ViewModel.GSMTCService.ChangePosition(TimelineSlider.Maximum * ratio);
ViewModel.MediaSessionsService.ChangePosition(TimelineSlider.Maximum * ratio);
}
private void TimelineSliderOverlay_PointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -239,12 +210,12 @@ public sealed partial class NowPlayingBar : UserControl
private void SongInfoStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
SongInfoTapped?.Invoke(sender, EventArgs.Empty);
SongInfoTapped?.Invoke(this, EventArgs.Empty);
}
private void TimeStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
TimeTapped?.Invoke(sender, EventArgs.Empty);
TimeTapped?.Invoke(this, EventArgs.Empty);
}
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -291,13 +262,37 @@ public sealed partial class NowPlayingBar : UserControl
}
}
private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
PlayQueueButtonClick?.Invoke(sender, EventArgs.Empty);
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
TitleTextBlock.Text = message.NewValue?.Title;
ArtistsTextBlock.Text = message.NewValue?.DisplayArtists;
}
}
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
{
AlbumArtImageSwitcher.Source = message.NewValue;
}
}
}
private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
PlaybackOrder = PlaybackOrder.GetNext();
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
}
}
}
}

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PatronControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Margin="12,8">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind PatronName, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorTertiaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,34 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class PatronControl : UserControl
{
public string PatronName
{
get { return (string)GetValue(PatronNameProperty); }
set { SetValue(PatronNameProperty, value); }
}
public static readonly DependencyProperty PatronNameProperty =
DependencyProperty.Register(nameof(PatronName), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public string Date
{
get { return (string)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register(nameof(Date), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public PatronControl()
{
InitializeComponent();
}
}
}

View File

@@ -1,142 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PlayQueue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid>
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="12,12,12,0">
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
</Grid>
<Grid Grid.Row="1" Margin="12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.SMTCService.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<!-- Scroll to playing item -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<NavigationViewItemSeparator Grid.Row="2" />
<ListView
x:Name="PlayingQueueListView"
Grid.Row="3"
ItemsSource="{x:Bind ViewModel.SMTCService.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel Margin="0,0,36,0">
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
VerticalAlignment="Center"
Click="RemoveFromPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="3">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SMTCService.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SMTCService.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -1,94 +0,0 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System;
using System.Linq;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class PlayQueue : UserControl, IRecipient<PropertyChangedMessage<int>>
{
public PlayQueueViewModel ViewModel => (PlayQueueViewModel)DataContext;
public PlayQueue()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<PlayQueueViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
}
private void ScrollToPlayingItem()
{
if (PlayingQueueListView == null) return;
var targetItem = ViewModel.SMTCService.TrackPlayingQueue
.ElementAtOrDefault(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
if (targetItem == null) return;
PlayingQueueListView.ScrollIntoView(targetItem);
}
private void ScrollToPlayingItemButton_Click(object sender, RoutedEventArgs e)
{
ScrollToPlayingItem();
}
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
await ViewModel.SMTCService.PlayTrackAsync(item);
}
private async void RemoveFromPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
bool playNext = false;
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
int index = ViewModel.SMTCService.TrackPlayingQueue.IndexOf(item);
if (item == PlayingQueueListView.SelectedItem)
{
playNext = true;
}
ViewModel.SMTCService.TrackPlayingQueue.Remove(item);
if (playNext)
{
if (ViewModel.SMTCService.TrackPlayingQueue.Count == 0)
{
index = -1;
}
else if (index >= ViewModel.SMTCService.TrackPlayingQueue.Count)
{
index = ViewModel.SMTCService.TrackPlayingQueue.Count - 1;
}
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = index;
await ViewModel.SMTCService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
}
private async void EmptyPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.SMTCService.TrackPlayingQueue.Clear();
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = -1;
await ViewModel.SMTCService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is MusicGallerySettings)
{
if (message.PropertyName == nameof(MusicGallerySettings.PlayQueueIndex))
{
ScrollToPlayingItem();
}
}
}
}
}

View File

@@ -13,7 +13,6 @@
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:settingsmodels="using:BetterLyrics.WinUI3.Models.Settings"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -61,7 +60,7 @@
ItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="settingsmodels:MediaSourceProviderInfo">
<DataTemplate x:DataType="models:MediaSourceProviderInfo">
<Grid Padding="2,4" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -173,7 +172,7 @@
<ScalarTransition />
</ListView.OpacityTransition>
<ListView.ItemTemplate>
<DataTemplate x:DataType="settingsmodels:AlbumArtSearchProviderInfo">
<DataTemplate x:DataType="models:AlbumArtSearchProviderInfo">
<dev:SettingsCard Header="{Binding Provider, Converter={StaticResource AlbumArtSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<dev:SettingsCard.HeaderIcon>
<FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" />
@@ -220,7 +219,7 @@
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="settingsmodels:LyricsSearchProviderInfo">
<DataTemplate x:DataType="models:LyricsSearchProviderInfo">
<Grid>
<dev:SettingsExpander Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}" IsExpanded="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}">
<dev:SettingsExpander.HeaderIcon>
@@ -228,9 +227,6 @@
</dev:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache">
<CheckBox IsChecked="{Binding IgnoreCacheWhenSearching, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageOverwriteMatchingThreshold">
<ToggleSwitch IsOn="{Binding IsMatchingThresholdOverwritten, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -318,8 +314,8 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.GSMTCService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -329,10 +325,10 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -342,22 +338,26 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Artist, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<local:PropertyRow
x:Uid="LyricsPageLyricsProviderPrefix"
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay}"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -365,6 +365,10 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- Lyrics translation -->
<TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True">
@@ -424,6 +428,30 @@
</dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageJapanese" IsExpanded="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageCutletDockerServer" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
<dev:SettingsCard.Description>
<HyperlinkButton Content="https://github.com/jayfunc/cutlet-docker" NavigateUri="https://github.com/jayfunc/cutlet-docker" />
</dev:SettingsCard.Description>
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Uid="CutletServerTextBox"
Grid.Column="0"
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
Text="{x:Bind ViewModel.AppSettings.TranslationSettings.CutletDockerServer, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
x:Uid="SettingsPageServerTestButton"
Grid.Column="1"
Command="{x:Bind ViewModel.CutletDockerServerTestCommand}"
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
</Grid>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<!-- 中文简体繁体偏好 -->

View File

@@ -1,83 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PluginManagerControl"
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:dev="using:DevWinUI"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid Padding="36,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock x:Uid="PluginManagerControlTitle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
</Grid>
<ListView
Grid.Row="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind Plugins, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:PluginDisplayModel">
<dev:SettingsExpander
Description="{x:Bind Description}"
Header="{x:Bind Name}"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE74C;}">
<TextBlock Text="{x:Bind Version}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow Header="Author" Value="{x:Bind Author}" />
<local:PropertyRow Header="ID" Value="{x:Bind Id}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard>
<Button
x:Uid="PluginManagerControlUninstall"
Click="OnUninstallClick"
Tag="{x:Bind Plugin}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="{x:Bind IsListEmpty, Mode=OneWay}">
<FontIcon
FontSize="48"
Glyph="&#xE74C;"
Opacity="0.3" />
<TextBlock
x:Uid="PluginManagerControlNoPluginsInstalled"
Margin="0,12,0,0"
Opacity="0.5" />
</StackPanel>
<StackPanel Grid.Row="2" Margin="0,6,0,20">
<Button
x:Uid="PluginManagerControlInstall"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="OnInstallPluginClick"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,149 +0,0 @@
using BetterLyrics.Core.Interfaces;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Views;
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.Collections.ObjectModel;
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.Controls
{
public sealed partial class PluginManagerControl : UserControl
{
public ObservableCollection<PluginDisplayModel> Plugins { get; } = new();
public Visibility IsListEmpty => Plugins.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
private readonly IPluginService _pluginService;
public PluginManagerControl()
{
this.InitializeComponent();
_pluginService = Ioc.Default.GetRequiredService<IPluginService>();
this.Loaded += (s, e) =>
{
RefreshPluginList();
};
}
private void RefreshPluginList()
{
Plugins.Clear();
var allPlugins = _pluginService.Plugins;
foreach (var plugin in allPlugins)
{
Plugins.Add(new PluginDisplayModel(plugin));
}
Bindings.Update();
}
private async void OnInstallPluginClick(object sender, RoutedEventArgs e)
{
var file = await Helper.PickerHelper.PickSingleFileAsync<SettingsWindow>([".zip"]);
if (file != null)
{
try
{
// 显示加载条...
// 3. 调用我们在上一步写的 InstallPlugin 方法
_pluginService.InstallPlugin(file.Path);
// 4. 重新加载所有插件 (这会触发热重载)
_pluginService.LoadPlugins();
// 5. 刷新界面
RefreshPluginList();
ShowTip("安装成功", $"插件 {file.Name} 已安装。");
}
catch (Exception ex)
{
ShowError("安装失败", ex.Message);
}
}
}
// 卸载按钮点击事件
private async void OnUninstallClick(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is IPlugin plugin)
{
// 二次确认对话框
ContentDialog deleteDialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = "卸载插件",
Content = $"确定要删除 \"{plugin.Name}\" 吗?此操作无法撤销。",
PrimaryButtonText = "删除",
CloseButtonText = "取消",
DefaultButton = ContentDialogButton.Close
};
var result = await deleteDialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
try
{
_pluginService.UninstallPlugin(plugin.Id);
// 暂时我们只能刷新列表演示
RefreshPluginList();
}
catch (Exception ex)
{
ShowError("卸载失败", ex.Message);
}
}
}
}
// 简单的弹窗辅助方法
private async void ShowTip(string title, string content)
{
ContentDialog dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = title,
Content = content,
CloseButtonText = "关闭"
};
await dialog.ShowAsync();
}
private async void ShowError(string title, string content)
{
ContentDialog dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = title,
Content = content,
CloseButtonText = "关闭"
};
await dialog.ShowAsync();
}
}
}

View File

@@ -9,85 +9,56 @@
mc:Ignorable="d">
<Grid>
<ScrollViewer>
<StackPanel Width="400" Spacing="16">
<ProgressBar
x:Name="ProgressBar"
IsIndeterminate="True"
Visibility="Collapsed" />
<InfoBar
x:Name="ErrorInfoBar"
IsClosable="True"
IsOpen="False"
Severity="Error" />
<StackPanel Width="400" Spacing="16">
<ProgressBar
x:Name="ProgressBar"
IsIndeterminate="True"
Visibility="Collapsed" />
<InfoBar
x:Name="ErrorInfoBar"
IsClosable="True"
IsOpen="False"
Severity="Error" />
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="12">
<TextBox
x:Name="NameBox"
x:Uid="RemoteServerConfigControlName"
x:Name="HostBox"
x:Uid="RemoteServerConfigControlServerAddress"
Grid.Column="0"
InputScope="Url"
PlaceholderText="192.168.1.x"
TextWrapping="Wrap" />
<StackPanel x:Name="RemoteFieldsPanel" Spacing="16">
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="12">
<TextBox
x:Name="HostBox"
x:Uid="RemoteServerConfigControlServerAddress"
Grid.Column="0"
InputScope="Url"
PlaceholderText="192.168.1.x"
TextWrapping="Wrap" />
<NumberBox
x:Name="PortBox"
x:Uid="RemoteServerConfigControlPort"
Grid.Column="1"
MinWidth="100"
LargeChange="10"
SmallChange="1"
SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="80"
Value="80" />
</Grid>
<NumberBox
x:Name="PortBox"
x:Uid="RemoteServerConfigControlPort"
Grid.Column="1"
MinWidth="100"
LargeChange="10"
SmallChange="1"
SpinButtonPlacementMode="Inline"
ToolTipService.ToolTip="80"
Value="80" />
</Grid>
</StackPanel>
<TextBox
x:Name="PathBox"
x:Uid="RemoteServerConfigControlPath"
TextWrapping="Wrap" />
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="8">
<TextBox
x:Name="PathBox"
x:Uid="RemoteServerConfigControlPath"
Grid.Column="0"
TextChanged="PathBox_TextChanged"
TextWrapping="Wrap" />
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<TextBox
x:Name="UserBox"
x:Uid="RemoteServerConfigControlUsername"
Grid.Column="0"
TextWrapping="Wrap" />
<Button
x:Name="BrowseButton"
x:Uid="RemoteServerConfigControlBrowse"
Grid.Column="1"
VerticalAlignment="Bottom"
Click="BrowseButton_Click"
Visibility="Collapsed" />
</Grid>
<InfoBar
x:Name="PathWarningBar"
IsClosable="False"
IsOpen="False"
Severity="Warning" />
<StackPanel x:Name="AuthFieldsPanel" Spacing="16">
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<TextBox
x:Name="UserBox"
x:Uid="RemoteServerConfigControlUsername"
Grid.Column="0"
TextWrapping="Wrap" />
<PasswordBox
x:Name="PwdBox"
x:Uid="RemoteServerConfigControlPassword"
Grid.Column="1"
PasswordRevealMode="Peek" />
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>
<PasswordBox
x:Name="PwdBox"
x:Uid="RemoteServerConfigControlPassword"
Grid.Column="1"
PasswordRevealMode="Peek" />
</Grid>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,8 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -12,130 +10,90 @@ namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class RemoteServerConfigControl : UserControl
{
private readonly FileSourceType _fileSourceType;
private readonly string _protocolType;
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public RemoteServerConfigControl(FileSourceType fileSourceType)
public RemoteServerConfigControl(string protocolType)
{
this.InitializeComponent();
_fileSourceType = fileSourceType;
_protocolType = protocolType;
SetupDefaults();
CheckPathForWarning();
}
private void SetupDefaults()
{
if (_fileSourceType == FileSourceType.Local)
switch (_protocolType.ToUpper())
{
RemoteFieldsPanel.Visibility = Visibility.Collapsed;
AuthFieldsPanel.Visibility = Visibility.Collapsed;
BrowseButton.Visibility = Visibility.Visible;
PathBox.PlaceholderText = @"D:\Music";
}
else
{
BrowseButton.Visibility = Visibility.Collapsed;
RemoteFieldsPanel.Visibility = Visibility.Visible;
AuthFieldsPanel.Visibility = Visibility.Visible;
switch (_fileSourceType)
{
case FileSourceType.SMB:
PortBox.Value = 445;
PathBox.PlaceholderText = "SharedMusic";
break;
case FileSourceType.FTP:
PortBox.Value = 21;
PathBox.PlaceholderText = "/pub/music";
break;
case FileSourceType.WebDAV:
PortBox.Value = 80;
PathBox.PlaceholderText = "/dav/music";
break;
}
}
}
private string GetScheme()
{
string scheme = string.Empty;
switch (_fileSourceType)
{
case FileSourceType.SMB:
scheme = "smb";
case "SMB":
PortBox.Value = 445; // SMB Ĭ<>϶˿<CFB6>
PathBox.PlaceholderText = "SharedMusic";
break;
case FileSourceType.FTP:
scheme = "ftp";
case "FTP":
PortBox.Value = 21; // FTP Ĭ<>϶˿<CFB6>
PathBox.PlaceholderText = "/pub/music";
break;
case FileSourceType.WebDAV:
scheme = "https";
case "WEBDAV":
PortBox.Value = 80; // WebDAV Ĭ<>϶˿<CFB6>
PathBox.PlaceholderText = "/dav/music";
break;
}
return scheme;
}
public MediaFolder GetConfig()
{
string finalName = HostBox.Text.Trim();
if (_fileSourceType == FileSourceType.Local)
{
if (string.IsNullOrWhiteSpace(PathBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathRequired"));
if (!string.IsNullOrWhiteSpace(NameBox.Text))
finalName = NameBox.Text.Trim();
else
finalName = PathBox.Text.TrimEnd(System.IO.Path.DirectorySeparatorChar);
return new MediaFolder
{
Name = finalName,
SourceType = FileSourceType.Local,
UriScheme = "file",
UriPath = PathBox.Text.Trim(),
};
}
if (string.IsNullOrWhiteSpace(HostBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlServerAddressRequired"));
if (!string.IsNullOrWhiteSpace(NameBox.Text))
{
finalName = NameBox.Text.Trim();
}
else
{
finalName = $"{_fileSourceType} - {HostBox.Text}";
}
string name = $"{_protocolType} - {HostBox.Text}";
string scheme = GetScheme();
Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
var folder = new MediaFolder
{
Name = finalName,
SourceType = _fileSourceType,
UriScheme = scheme,
UriHost = HostBox.Text.Trim(), // ȥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>β<EFBFBD>ո<EFBFBD>
UriPort = (int)PortBox.Value,
UriPath = PathBox.Text.Trim(),
UserName = UserBox.Text.Trim(),
Password = PwdBox.Password,
Name = name,
Path = HostBox.Text, // <20><><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP/Host
Port = (int)PortBox.Value,
UserName = UserBox.Text,
Password = PwdBox.Password, // <20><> PasswordBox <20><>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
SourceType = sourceType,
IsRealTimeWatchEnabled = false
};
// <20><><EFBFBD><EFBFBD><E2B4A6>·<EFBFBD><C2B7><EFBFBD><EFBFBD>
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><><D4B6>·<EFBFBD><C2B7><>ӵ<EFBFBD> Path <20><EFBFBD><EFA3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>ֶδ<D6B6>
// Ϊ<>˼򵥣<CBBC><F2B5A5A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѭ<EFBFBD><D1AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD>
// <20><><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD><EFBFBD><EFBFBD>ټ<EFBFBD>һ<EFBFBD><D2BB> RemotePath <20>ֶΣ<D6B6><CEA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// *<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> MediaFolder <20><><EFBFBD><EFBFBD><E5A3AC><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD><EFBFBD>
// Path <20>ֶδ洢<CEB4><E6B4A2>ʽ<EFBFBD><CABD> "192.168.1.5/Music"
var rawPath = PathBox.Text.Trim().TrimStart('/', '\\'); // ȥ<><C8A5><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7>б<EFBFBD><D0B1>
if (!string.IsNullOrEmpty(rawPath))
{
// <20>򵥵<EFBFBD>·<EFBFBD><C2B7>ƴ<EFBFBD><C6B4><EFBFBD>߼<EFBFBD>
if (sourceType == FileSourceType.SMB)
{
// SMBLibrary <20><><EFBFBD>߼<EFBFBD>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD> Host <20>ֿ<EFBFBD><D6BF><EFBFBD>ShareName <20>ֿ<EFBFBD>
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP <20><><EFBFBD><EFBFBD> Path <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFA3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA> ShareName ƴ<>ں<EFBFBD><DABA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֶ<EFBFBD>
// Ϊ<>˷<EFBFBD><CBB7><EFBFBD><E3A3AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IP <20><> ShareName ƴ<><C6B4>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Path
// <20><><EFBFBD><EFBFBD>: 192.168.1.5/Music
folder.Path = $"{HostBox.Text}/{rawPath}";
}
else
{
// FTP/WebDAV: 192.168.1.5/pub/music
folder.Path = $"{HostBox.Text}/{rawPath}";
}
}
return folder;
}
public void ShowError(string? message)
public void ShowError(string message)
{
ErrorInfoBar.Message = message;
ErrorInfoBar.IsOpen = !string.IsNullOrWhiteSpace(message);
ErrorInfoBar.IsOpen = true;
}
public void SetProgressBarVisibility(Visibility visibility)
@@ -143,53 +101,5 @@ namespace BetterLyrics.WinUI3.Controls
ProgressBar.Visibility = visibility;
}
private void CheckPathForWarning()
{
string? path = PathBox.Text?.Trim();
bool isSymbolRoot = string.IsNullOrEmpty(path) ||
path == "/" ||
path == "\\";
bool isDriveRoot = false;
if (!string.IsNullOrEmpty(path))
{
var normalized = path.TrimEnd('\\', '/');
isDriveRoot = normalized.EndsWith(":") && normalized.Length == 2;
}
bool isRoot = isSymbolRoot || isDriveRoot;
if (isRoot)
{
PathWarningBar.Message = _localizationService.GetLocalizedString("FileSystemServiceRootDirectoryWarning");
PathWarningBar.IsOpen = true;
}
else
{
PathWarningBar.IsOpen = false;
}
}
private void PathBox_TextChanged(object sender, TextChangedEventArgs e)
{
CheckPathForWarning();
}
private async void BrowseButton_Click(object sender, RoutedEventArgs e)
{
try
{
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
if (folder != null)
{
PathBox.Text = folder.Path;
}
}
catch (Exception ex)
{
ShowError(ex.Message);
}
}
}
}

View File

@@ -1,411 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.StatsDashboardControl"
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:converters="using:BetterLyrics.WinUI3.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.WinUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:statsmodels="using:BetterLyrics.WinUI3.Models.Stats"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="StatsCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,12,12" />
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ProgressBar
Grid.Row="0"
Background="Transparent"
IsIndeterminate="{x:Bind ViewModel.IsLoading, Mode=OneWay}"
Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.GSMTCService.IsScrobbled, Mode=OneWay, Converter={StaticResource BoolNegationToVisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<InfoBar
x:Uid="StatsDashboardControlRecording"
Grid.Row="0"
IsClosable="False"
IsOpen="True"
Message="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<ProgressBar
Grid.Row="1"
Background="Transparent"
Maximum="{x:Bind ViewModel.GSMTCService.TargetScrobbledDuration.TotalSeconds, Mode=OneWay}"
ShowPaused="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
Value="{x:Bind ViewModel.GSMTCService.ScrobbledDuration.TotalSeconds, Mode=OneWay}" />
</Grid>
<controls:WrapPanel
Grid.Row="2"
Margin="36,36,36,12"
HorizontalSpacing="12"
Orientation="Horizontal"
VerticalSpacing="12">
<ComboBox
x:Uid="StatsDashboardControlTimeRange"
Header="Time Range"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlToday" />
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlAllTime" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<CalendarDatePicker
x:Uid="StatsDashboardControlStart"
Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomStartTime, Mode=TwoWay}" />
<CalendarDatePicker
x:Uid="StatsDashboardControlEnd"
Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomEndTime, Mode=TwoWay}" />
<Button
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.RefreshDataCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE72C;}" />
</controls:WrapPanel>
<ScrollViewer Grid.Row="3" Padding="36,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE916;" />
<TextBlock x:Uid="StatsDashboardControlTotalDuration" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<StackPanel
Margin="0,8,0,0"
Orientation="Horizontal"
Spacing="4">
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay, Converter={StaticResource DoubleToDecimalConverter}}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="Hrs" />
</StackPanel>
</StackPanel>
</Border>
<Border Grid.Column="1" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE8D6;" />
<TextBlock x:Uid="StatsDashboardControlTracksPlayed" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalTracksPlayed, Mode=OneWay}" />
</StackPanel>
</Border>
<Border
Grid.Column="2"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xEC4A;" />
<TextBlock x:Uid="StatsDashboardControlTopSource" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TopPlayerName, Mode=OneWay}" />
</StackPanel>
</Border>
</Grid>
<!-- Activity by hour -->
<Border
Grid.Row="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Margin="0,0,0,12"
Orientation="Horizontal"
Spacing="8">
<TextBlock x:Uid="StatsDashboardControlActivityByHour" Style="{ThemeResource SubtitleTextBlockStyle}" />
</StackPanel>
<Grid Grid.Row="1" Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlMostActive"
FontSize="12"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.PeakHourText, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock
x:Uid="StatsDashboardControlLeastActive"
FontSize="12"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.QuietHourText, Mode=OneWay}" />
</StackPanel>
</Grid>
<lvc:CartesianChart
Grid.Row="2"
Height="180"
Margin="0,8,0,0"
Background="Transparent"
TooltipPosition="Top">
<lvc:CartesianChart.XAxes>
<lvc:AxesCollection>
<lvc:XamlAxis Labels="{x:Bind ViewModel.HourlyXAxisLabels, Mode=OneWay}" TextSize="{StaticResource BodyTextBlockFontSize}" />
</lvc:AxesCollection>
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:AxesCollection>
<lvc:XamlAxis
x:Uid="StatsDashboardControlTrackCountAxis"
NameTextSize="{StaticResource BodyTextBlockFontSize}"
ShowSeparatorLines="False"
TextSize="{StaticResource BodyTextBlockFontSize}" />
</lvc:AxesCollection>
</lvc:CartesianChart.YAxes>
<lvc:CartesianChart.Series>
<lvc:SeriesCollection>
<lvc:XamlColumnSeries
x:Name="HourlySeries"
Rx="4"
Ry="4"
Values="{x:Bind ViewModel.HourlySeriesValues, Mode=OneWay}" />
</lvc:SeriesCollection>
</lvc:CartesianChart.Series>
</lvc:CartesianChart>
</Grid>
</Border>
<!-- Top artists and sources -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlTopArtists"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10">
<PersonPicture
Width="32"
Height="32"
DisplayName="{x:Bind Artist}" />
<TextBlock
VerticalAlignment="Center"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
x:Uid="StatsDashboardControlTrackCountText"
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<!-- Top tracks -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Width="40"
Height="40"
Margin="0,0,12,0"
Background="{ThemeResource LayerFillColorAltBrush}"
CornerRadius="4">
<FontIcon
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xE8D6;" />
</Grid>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Style="{ThemeResource BodyStrongTextBlockStyle}" Text="{x:Bind Title}" />
<TextBlock
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
x:Uid="StatsDashboardControlTrackCountText"
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
<!-- 播放源分布 -->
<Border
Grid.Row="3"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<lvc:PieChart
Grid.Row="1"
MinHeight="250"
Background="Transparent"
LegendPosition="Bottom"
LegendTextSize="{StaticResource BodyTextBlockFontSize}"
Series="{x:Bind ViewModel.SourceSeries, Mode=OneWay}"
TooltipPosition="Center" />
</Grid>
</Border>
</Grid>
</ScrollViewer>
<!--<Button
Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data" />-->
</Grid>
</UserControl>

View File

@@ -1,20 +0,0 @@
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls;
public sealed partial class StatsDashboardControl : UserControl
{
public StatsDashboardControlViewModel ViewModel => (StatsDashboardControlViewModel)DataContext;
public StatsDashboardControl()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<StatsDashboardControlViewModel>();
}
}

View File

@@ -18,7 +18,13 @@
<TextBlock x:Uid="AppSettingsControlGeneral" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageConfigName" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8AC;}">
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
<StackPanel
Margin="0,6,0,0"
Orientation="Horizontal"
Spacing="6">
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay}" TextWrapping="Wrap" />
<Button Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, FontSize=12, Glyph=&#xE8FB;}" Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsExpander

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

View File

@@ -17,16 +17,20 @@ namespace BetterLyrics.WinUI3.Converter
using (var ms = new MemoryStream(byteArray))
{
var stream = ms.AsRandomAccessStream();
var bitmapImage = new BitmapImage();
bitmapImage.SetSource(stream);
return bitmapImage;
}
}
catch { }
catch
{
return PathHelper.AlbumArtPlaceholderPath;
}
}
return new BitmapImage(new Uri(PathHelper.AlbumArtPlaceholderPath));
return PathHelper.AlbumArtPlaceholderPath;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)

View File

@@ -1,31 +0,0 @@
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class DoubleToDecimalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null) return string.Empty;
if (double.TryParse(value.ToString(), out double number))
{
int decimalPlaces = 2;
if (parameter != null && int.TryParse(parameter.ToString(), out int parsedParams))
{
decimalPlaces = parsedParams;
}
return number.ToString($"F{decimalPlaces}");
}
return value.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -15,7 +15,7 @@ namespace BetterLyrics.WinUI3.Converter
FileSourceType.Local => "\uE8B7", // Folder
FileSourceType.SMB => "\uE839", // Network
FileSourceType.FTP => "\uE838", // Globe
FileSourceType.WebDAV => "\uE753", // Cloud
FileSourceType.WebDav => "\uE753", // Cloud
_ => "\uE8B7"
};
}

View File

@@ -3,37 +3,18 @@ using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class MillisecondsToFormattedTimeConverter : IValueConverter
public class MillisecondsToFormattedTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
double? milliseconds = null;
if (value is int iVal) milliseconds = iVal;
else if (value is double dVal) milliseconds = dVal;
else if (value is long lVal) milliseconds = lVal;
if (milliseconds.HasValue)
if (value is int milliseconds)
{
var ts = TimeSpan.FromMilliseconds(milliseconds.Value);
string? format = parameter?.ToString();
if (string.IsNullOrEmpty(format))
{
format = @"mm\:ss\.fff";
}
try
{
return ts.ToString(format);
}
catch (FormatException)
{
return ts.ToString();
}
return TimeSpan.FromMilliseconds(milliseconds).ToString(@"mm\:ss\.fff");
}
else if (value is double doubleMilliseconds)
{
return TimeSpan.FromMilliseconds(doubleMilliseconds).ToString(@"mm\:ss\.fff");
}
return value?.ToString() ?? "";
}

View File

@@ -1,29 +0,0 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.IO;
namespace BetterLyrics.WinUI3.Converter
{
public partial class PathToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
string targetPath = PathHelper.AlbumArtPlaceholderPath;
if (value is string path)
{
if (File.Exists(path))
{
targetPath = path;
}
}
return new BitmapImage(new Uri(targetPath));
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,23 +0,0 @@
using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Data;
using System;
namespace BetterLyrics.WinUI3.Converter
{
public partial class UriStringToDecodedAbsoluteUri : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string uriString)
{
return uriString.ToDecodedAbsoluteUri();
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<linker>
<assembly fullname="FlaUI.UIA3" preserve="all" />
<assembly fullname="Interop.UIAutomationClient" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore.Abstractions" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore.Relational" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore.Sqlite" preserve="all" />
<assembly fullname="NAudio.Wasapi" preserve="all" />
<assembly fullname="TagLibSharp" preserve="all" />
<assembly fullname="Vanara.PInvoke.DwmApi" preserve="all" />
<assembly fullname="Vanara.PInvoke.Gdi32" preserve="all" />
<assembly fullname="Vanara.PInvoke.Shell32" preserve="all" />
<assembly fullname="Vanara.PInvoke.User32" preserve="all" />
<assembly fullname="Vanara.Windows.Shell" preserve="all" />
</linker>

View File

@@ -1,11 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum AutoScanInterval
{
Disabled,
Every15Minutes,
EveryHour,
Every6Hours,
Daily
}
}

View File

@@ -1,8 +0,0 @@
namespace BetterLyrics.WinUI3.Enums;
public enum EaseMode
{
In,
Out,
InOut,
}

View File

@@ -6,15 +6,15 @@ namespace BetterLyrics.WinUI3.Enums
{
Linear,
SmoothStep,
Sine,
Quad,
Cubic,
Quart,
Quint,
Expo,
Circle,
Back,
Elastic,
Bounce,
EaseInOutSine,
EaseInOutQuad,
EaseInOutCubic,
EaseInOutQuart,
EaseInOutQuint,
EaseInOutExpo,
EaseInOutCirc,
EaseInOutBack,
EaseInOutElastic,
EaseInOutBounce,
}
}

View File

@@ -5,6 +5,6 @@
Local,
SMB,
FTP,
WebDAV
WebDav
}
}

View File

@@ -0,0 +1,14 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum Language
{
FollowSystem,
English,
SimplifiedChinese,
TraditionalChinese,
Japanese,
Korean,
}
}

View File

@@ -0,0 +1,10 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LocalSearchTargetProps
{
LyricsOnly,
LyricsAndAlbumArt,
}
}

View File

@@ -14,6 +14,5 @@ namespace BetterLyrics.WinUI3.Enums
LocalEslrcFile,
LocalTtmlFile,
AppleMusic,
Plugin = 999,
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum SettingsStoreType
{
Container,
JSON
}
}

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum ShortcutId
public enum ShortcutID
{
LyricsWindowShowOrHide,
LyricsWindowSwitch,

View File

@@ -1,13 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum StatsRange
{
Today,
ThisWeek,
ThisMonth,
ThisQuarter,
ThisYear,
AllTime,
Custom
}
}

View File

@@ -13,6 +13,5 @@
LocalEslrcFile,
LocalTtmlFile,
LibreTranslate,
Plugin = 999,
}
}

View File

@@ -13,7 +13,6 @@
LocalEslrcFile,
LocalTtmlFile,
BetterLyrics,
CutletDocker,
Plugin = 999,
CutletDocker
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum WordByWordEffectMode
{
Auto,
Never,
Always,
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, List<Color> albumArtLightAccentColors, List<Color> albumArtDarkAccentColors) : EventArgs
{
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public List<Color> AlbumArtLightAccentColors { get; set; } = albumArtLightAccentColors;
public List<Color> AlbumArtDarkAccentColors { get; set; } = albumArtDarkAccentColors;
}
}

View File

@@ -0,0 +1,14 @@
// 2025/6/23 by Zhe Fang
using System;
using System.IO;
namespace BetterLyrics.WinUI3.Events
{
public class LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType) : EventArgs
{
public WatcherChangeTypes ChangeType { get; } = changeType;
public string FilePath { get; } = filePath;
public string Folder { get; } = folder;
}
}

View File

@@ -0,0 +1,10 @@
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Events
{
public class LyricsChangedEventArgs(LyricsData? lyricsData) : EventArgs
{
public LyricsData? LyricsData { get; } = lyricsData;
}
}

View File

@@ -0,0 +1,11 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Events
{
public class MediaSourceProvidersInfoEventArgs(List<MediaSourceProviderInfo> sessionIds) : EventArgs
{
public List<MediaSourceProviderInfo> MediaSourceProviersInfo { get; set; } = sessionIds;
}
}

View File

@@ -1,136 +0,0 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Services.LocalizationService;
using System;
using System.Collections.Generic;
using System.Linq;
namespace BetterLyrics.WinUI3.Extensions
{
public static class LyricsDataExtensions
{
extension(LyricsData lyricsData)
{
public static LyricsData GetLoadingPlaceholder()
{
return new LyricsData()
{
LyricsLines = [
new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
PrimaryText = "● ● ●",
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds }],
IsPrimaryHasRealSyllableInfo = true,
},
],
LanguageCode = "N/A",
};
}
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PrimaryText = "N/A",
PrimarySyllables = [new BaseLyrics { Text = "N/A", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }],
}]);
}
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 50)
{
foreach (var line in lyricsData.LyricsLines)
{
// 在翻译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = translationData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
// 此处 transLine.OriginalText 指翻译中的“原文”属性
line.SecondaryText = transLine.PrimaryText;
}
else
{
// 没有匹配的翻译
line.SecondaryText = "";
}
}
}
public void SetPhoneticText(LyricsData phoneticData, int toleranceMs = 50)
{
foreach (var line in lyricsData.LyricsLines)
{
// 在音译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = phoneticData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
// 此处 transLine.OriginalText 指音译中的“原文”属性
line.TertiaryText = transLine.PrimaryText;
}
else
{
// 没有匹配的音译
line.TertiaryText = "";
}
}
}
public void SetTranslation(string translation)
{
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in lyricsData.LyricsLines)
{
if (i >= translationArr.Count)
{
line.SecondaryText = ""; // No translation available, keep empty
}
else
{
line.SecondaryText = translationArr[i];
}
i++;
}
}
public void SetTransliteration(string transliteration)
{
List<string> transliterationArr = transliteration.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in lyricsData.LyricsLines)
{
if (i >= transliterationArr.Count)
{
line.TertiaryText = ""; // No transliteration available, keep empty
}
else
{
line.TertiaryText = transliterationArr[i];
}
i++;
}
}
public LyricsLine? GetLyricsLine(double sec)
{
for (int i = 0; i < lyricsData.LyricsLines.Count; i++)
{
var line = lyricsData.LyricsLines[i];
if (line.StartMs > sec * 1000)
{
return lyricsData.LyricsLines.ElementAtOrDefault(i - 1);
}
}
return null;
}
}
}
}

View File

@@ -1,4 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
namespace BetterLyrics.WinUI3.Extensions
{
@@ -6,6 +8,21 @@ namespace BetterLyrics.WinUI3.Extensions
{
extension(LyricsSearchProvider provider)
{
public string GetCacheDirectory() => provider switch
{
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
LyricsSearchProvider.LocalMusicFile => PathHelper.LocalMusicCacheDirectory,
LyricsSearchProvider.LocalLrcFile => PathHelper.LocalLrcCacheDirectory,
LyricsSearchProvider.LocalEslrcFile => PathHelper.LocalEslrcCacheDirectory,
LyricsSearchProvider.LocalTtmlFile => PathHelper.LocalTtmlCacheDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
};
public LyricsFormat GetLyricsFormat() => provider switch
{
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,

View File

@@ -1,5 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Views;

View File

@@ -1,19 +1,14 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Entities;
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions
{
public static class SongInfoExtensions
{
public static SongInfo Placeholder => new()
public static SongInfo Placeholder => new SongInfo
{
Title = "N/A",
Album = "N/A",
Artist = "N/A",
Artists = ["N/A"],
};
extension(SongInfo songInfo)
@@ -24,9 +19,9 @@ namespace BetterLyrics.WinUI3.Extensions
return songInfo;
}
public SongInfo WithArtist(string value)
public SongInfo WithArtist(string[] value)
{
songInfo.Artist = value;
songInfo.Artists = value;
return songInfo;
}
@@ -35,45 +30,6 @@ namespace BetterLyrics.WinUI3.Extensions
songInfo.Album = value;
return songInfo;
}
public SongInfo WithSongId(string value)
{
songInfo.SongId = value;
return songInfo;
}
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{
if (songInfo == null) return null;
return new PlayHistoryItem
{
Title = songInfo.Title,
Artist = songInfo.Artist,
Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.FromBinary(songInfo.StartedAt)
};
}
public string GetCacheKey()
{
string title = songInfo.Title?.Trim() ?? "";
string album = songInfo.Album?.Trim() ?? "";
string artists = songInfo.Artist?.Trim() ?? "";
long seconds = (long)Math.Round(songInfo.Duration);
string durationPart = seconds.ToString(CultureInfo.InvariantCulture);
string rawKey = $"{title}|{artists}|{album}|{durationPart}";
using var sha256 = SHA256.Create();
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawKey));
return Convert.ToHexString(bytes);
}
}
}
}

View File

@@ -1,5 +1,4 @@
using BetterLyrics.WinUI3.Enums;
using System;
using System.Linq;
namespace BetterLyrics.WinUI3.Extensions
@@ -76,18 +75,6 @@ namespace BetterLyrics.WinUI3.Extensions
return null;
}
}
public string ToDecodedAbsoluteUri()
{
if (string.IsNullOrEmpty(str)) return "";
try
{
var u = new Uri(str);
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsoluteUri);
}
catch { return str; }
}
}
}
}

View File

@@ -15,16 +15,6 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
{
public static Color GetSystemAccentColor()
{
if (Application.Current.Resources.TryGetValue("SystemAccentColor", out var resource) &&
resource is Color uiColor)
{
return uiColor;
}
return Color.FromArgb(255, 0, 120, 215);
}
public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor)
{
// 计算亮度YIQ公式

View File

@@ -56,57 +56,6 @@
}
catch (Exception) { }
}
/// <summary>
/// https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories
/// </summary>
/// <param name="sourceDir"></param>
/// <param name="destinationDir"></param>
/// <param name="recursive"></param>
/// <exception cref="DirectoryNotFoundException"></exception>
public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive)
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
// Check if the source directory exists
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
// Cache directories before we start copying
DirectoryInfo[] dirs = dir.GetDirectories();
// Create the destination directory
Directory.CreateDirectory(destinationDir);
// Get the files in the source directory and copy to the destination directory
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
CopyLockedFile(file.FullName, targetFilePath);
}
// If recursive and copying subdirectories, recursively call this method
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}
private static void CopyLockedFile(string sourcePath, string targetPath)
{
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var destStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
{
sourceStream.CopyTo(destStream);
}
}
}
}
}

View File

@@ -1,170 +1,114 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Numerics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public class EasingHelper
{
#region Interpolators
public static Func<T, T, double, T> GetInterpolatorByEasingType<T>(EasingType? type, EaseMode easingMode = EaseMode.Out)
where T : INumber<T>, IFloatingPointIeee754<T>
public static double EaseInOutSine(double t)
{
return (start, end, progress) =>
return -(Math.Cos(Math.PI * t) - 1f) / 2f;
}
public static double EaseInOutQuad(double t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
public static double EaseInOutCubic(double t)
{
return t < 0.5f ? 4 * t * t * t : 1 - Math.Pow(-2 * t + 2, 3) / 2;
}
public static double EaseInOutQuart(double t)
{
return t < 0.5f ? 8 * t * t * t * t : 1 - Math.Pow(-2 * t + 2, 4) / 2;
}
public static double EaseInOutQuint(double t)
{
return t < 0.5f ? 16 * t * t * t * t * t : 1 - Math.Pow(-2 * t + 2, 5) / 2;
}
public static double EaseInOutExpo(double t)
{
return t == 0
? 0
: t == 1
? 1
: t < 0.5 ? Math.Pow(2, 20 * t - 10) / 2
: (2 - Math.Pow(2, -20 * t + 10)) / 2;
}
public static double EaseInOutCirc(double t)
{
return t < 0.5f
? (1 - Math.Sqrt(1 - Math.Pow(2 * t, 2))) / 2
: (Math.Sqrt(1 - Math.Pow(-2 * t + 2, 2)) + 1) / 2;
}
public static double EaseInOutBack(double t)
{
double c1 = 1.70158f;
double c2 = c1 * 1.525f;
return t < 0.5
? (Math.Pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.Pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
public static double EaseInOutElastic(double t)
{
if (t == 0 || t == 1) return t;
double p = 0.3f;
double s = p / 4;
return t < 0.5f
? -(Math.Pow(2, 20 * t - 10) * Math.Sin((20 * t - 11.125f) * (2 * Math.PI) / p)) / 2
: (Math.Pow(2, -20 * t + 10) * Math.Sin((20 * t - 11.125f) * (2 * Math.PI) / p)) / 2 + 1;
}
private static double EaseOutBounce(double t)
{
if (t < 4 / 11f)
{
Func<T, T> easeInFunc = type switch
{
EasingType.Sine => EaseInSine,
EasingType.Quad => EaseInQuad,
EasingType.Cubic => EaseInCubic,
EasingType.Quart => EaseInQuart,
EasingType.Quint => EaseInQuint,
EasingType.Expo => EaseInExpo,
EasingType.Circle => EaseInCircle,
EasingType.Back => EaseInBack,
EasingType.Elastic => EaseInElastic,
EasingType.Bounce => EaseInBounce,
EasingType.SmoothStep => SmoothStep,
EasingType.Linear => Linear,
_ => EaseInQuad,
};
double t = Ease(progress, easingMode, easeInFunc);
return start + ((end - start) * T.CreateChecked(t));
};
}
#endregion
public static double Ease<T>(double t, EaseMode mode, Func<T, T> easeIn)
where T : IFloatingPointIeee754<T>
{
t = Math.Clamp(t, 0.0, 1.0);
T tt = T.CreateChecked(t);
T half = T.CreateChecked(0.5);
T two = T.CreateChecked(2);
T tResult = mode switch
{
EaseMode.In => easeIn(tt),
EaseMode.Out => T.One - easeIn(T.One - tt),
EaseMode.InOut => tt < half
? easeIn(tt * two) / two
: T.One - (easeIn((T.One - tt) * two) / two),
_ => easeIn(tt),
};
return double.CreateChecked(tResult);
}
public static T EaseInSine<T>(T t) where T : IFloatingPointIeee754<T>
{
return T.One - T.Cos((t * T.Pi) / T.CreateChecked(2));
}
public static T EaseInQuad<T>(T t) where T : INumber<T>
{
return t * t;
}
public static T EaseInCubic<T>(T t) where T : INumber<T>
{
return t * t * t;
}
public static T EaseInQuart<T>(T t) where T : INumber<T>
{
return t * t * t * t;
}
public static T EaseInQuint<T>(T t) where T : INumber<T>
{
return t * t * t * t * t;
}
public static T EaseInExpo<T>(T t) where T : IFloatingPointIeee754<T>
{
if (t == T.Zero)
{
return T.Zero;
return (121 * t * t) / 16f;
}
return T.Pow(T.CreateChecked(2), (T.CreateChecked(10) * t) - T.CreateChecked(10));
}
public static T EaseInCircle<T>(T t) where T : IFloatingPointIeee754<T>
{
return T.One - T.Sqrt(T.One - (t * t));
}
public static T EaseInBack<T>(T t) where T : IFloatingPointIeee754<T>
{
T c1 = T.CreateChecked(1.70158);
T c3 = c1 + T.One;
return (c3 * t * t * t) - (c1 * t * t);
}
public static T EaseInElastic<T>(T t) where T : IFloatingPointIeee754<T>
{
if (t == T.Zero || t == T.One)
else if (t < 8 / 11f)
{
return t;
return (363 / 40f * t * t) - (99 / 10f * t) + 17 / 5f;
}
const double springiness = 6;
const double oscillations = 1;
double td = double.CreateChecked(t);
double expo = (Math.Exp(springiness * td) - 1.0) / (Math.Exp(springiness) - 1.0);
double result = 0.7 * expo * Math.Sin((Math.PI * 2.0 * oscillations + (Math.PI * 0.5)) * td);
return T.CreateChecked(result);
}
private static T EaseOutBounce<T>(T t) where T : IFloatingPointIeee754<T>
{
if (t < T.CreateChecked(4.0 / 11.0))
else if (t < 9 / 10f)
{
return (T.CreateChecked(121) * t * t) / T.CreateChecked(16);
}
else if (t < T.CreateChecked(8.0 / 11.0))
{
return ((T.CreateChecked(363.0 / 40.0) * t * t) - (T.CreateChecked(99.0 / 10.0) * t)) + T.CreateChecked(17.0 / 5.0);
}
else if (t < T.CreateChecked(9.0 / 10.0))
{
return ((T.CreateChecked(4356.0 / 361.0) * t * t) - (T.CreateChecked(35442.0 / 1805.0) * t)) + T.CreateChecked(16061.0 / 1805.0);
return (4356 / 361f * t * t) - (35442 / 1805f * t) + 16061 / 1805f;
}
else
{
return ((T.CreateChecked(54.0 / 5.0) * t * t) - (T.CreateChecked(513.0 / 25.0) * t)) + T.CreateChecked(268.0 / 25.0);
return (54 / 5f * t * t) - (513 / 25f * t) + 268 / 25f;
}
}
public static T EaseInBounce<T>(T t) where T : IFloatingPointIeee754<T>
public static double EaseInOutBounce(double t)
{
return T.One - EaseOutBounce(T.One - t);
if (t < 0.5f)
{
return (1 - EaseOutBounce(1 - 2 * t)) / 2;
}
else
{
return (1 + EaseOutBounce(2 * t - 1)) / 2;
}
}
public static T SmoothStep<T>(T t) where T : IFloatingPointIeee754<T>
public static double SmoothStep(double t)
{
return t * t * (T.CreateChecked(3) - (T.CreateChecked(2) * t));
return t * t * (3f - 2f * t);
}
public static T CubicBezier<T>(T t, T p0, T p1, T p2, T p3) where T : IFloatingPointIeee754<T>
public static double CubicBezier(double t, double p0, double p1, double p2, double p3)
{
T u = T.One - t;
return (u * u * u * p0)
+ (T.CreateChecked(3) * u * u * t * p1)
+ (T.CreateChecked(3) * u * t * t * p2)
+ (t * t * t * p3);
double u = 1 - t;
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
}
public static T Linear<T>(T t) where T : INumber<T> => t;
public static double Linear(double t) => t;
}
}
}

View File

@@ -3,12 +3,10 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper
@@ -29,18 +27,6 @@ namespace BetterLyrics.WinUI3.Helper
return Encoding.GetEncoding(encoding);
}
public static async Task CopyFileAsync(string sourcePath, string destinationPath)
{
var dir = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await sourceStream.CopyToAsync(destinationStream);
}
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
@@ -52,6 +38,22 @@ namespace BetterLyrics.WinUI3.Helper
return sb.ToString();
}
public static LyricsSearchResult? ReadLyricsCache(SongInfo songInfo, LyricsSearchProvider lyricsSearchProvider)
{
var cacheFilePath = Path.Combine(
lyricsSearchProvider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
if (File.Exists(cacheFilePath))
{
var json = File.ReadAllText(cacheFilePath);
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
data?.SelfPath = cacheFilePath;
return data;
}
return null;
}
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
@@ -62,9 +64,19 @@ namespace BetterLyrics.WinUI3.Helper
return null;
}
public static void WriteLyricsCache(SongInfo songInfo, LyricsSearchResult lyricsSearchResult)
{
var cacheFilePath = Path.Combine(
lyricsSearchResult.Provider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
lyricsSearchResult.SelfPath = cacheFilePath;
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
File.WriteAllText(cacheFilePath, json);
}
public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.Artist} - {songInfo.Album}{format}"));
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.DisplayArtists} - {songInfo.Album}{format}"));
File.WriteAllBytes(cacheFilePath, img);
}
@@ -74,15 +86,5 @@ namespace BetterLyrics.WinUI3.Helper
".wav", ".aiff", ".aif", ".pcm", ".cda", ".dsf", ".dff", ".au", ".snd",
".mid", ".midi", ".mod", ".xm", ".it", ".s3m"
};
public static readonly string[] LyricExtensions =
Enum.GetValues(typeof(LyricsSearchProvider)).Cast<LyricsSearchProvider>()
.Where(x => x.IsLocal())
.Select(x => x.GetLyricsFormat())
.Where(x => x != LyricsFormat.NotSpecified)
.Select(x => x.ToFileExtension())
.ToArray();
public static readonly HashSet<string> AllSupportedExtensions = new(MusicExtensions.Union(LyricExtensions));
}
}

View File

@@ -1,90 +0,0 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
public static class FolderTreeBuilder
{
public static ObservableCollection<FolderNode> Build(List<ExtendedTrack> tracks, List<MediaFolder> folderConfigs)
{
var rootNodes = new ObservableCollection<FolderNode>();
// 按 MediaFolderId 分组
var folderGroups = tracks.GroupBy(t => t.MediaFolderId);
foreach (var group in folderGroups)
{
var config = folderConfigs.FirstOrDefault(f => f.Id == group.Key);
if (config == null) continue;
string baseUri = config.GetStandardUri().AbsoluteUri.TrimEnd('/');
var rootNode = new FolderNode
{
SourceType = config.SourceType,
FolderName = config.Name ?? config.ConnectionSummary, // 显示用户自定义的名字
MediaFolderId = group.Key,
FolderPath = baseUri,
IsExpanded = true
};
foreach (var track in group)
{
try
{
if (!track.Uri.StartsWith(baseUri)) continue; // 防御性编程
string relativePart = track.Uri.Substring(baseUri.Length);
var segments = relativePart
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(s => System.Net.WebUtility.UrlDecode(s))
.ToArray();
if (segments.Length > 1) // 长度大于1说明在子文件夹里
{
var folderSegments = segments.Take(segments.Length - 1).ToArray();
CreateFolderStructure(rootNode, folderSegments, baseUri);
}
}
catch { }
}
rootNodes.Add(rootNode);
}
return rootNodes;
}
private static void CreateFolderStructure(FolderNode parent, string[] segments, string rootBaseUri)
{
var current = parent;
string currentFullPath = parent.FolderPath;
foreach (var segmentName in segments)
{
var existingChild = current.SubFolders.FirstOrDefault(f => f.FolderName == segmentName);
currentFullPath += "/" + System.Net.WebUtility.UrlEncode(segmentName);
if (existingChild == null)
{
var newFolder = new FolderNode
{
FolderName = segmentName,
FolderPath = currentFullPath, // 存完整的 URI
MediaFolderId = parent.MediaFolderId
};
current.SubFolders.Add(newFolder);
current = newFolder;
}
else
{
current = existingChild;
currentFullPath = existingChild.FolderPath;
}
}
}
}

View File

@@ -3,6 +3,7 @@ using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection;
using NTextCat;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization;
using System.Linq;
using Windows.Globalization;

View File

@@ -1,5 +1,4 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Entities;
using F23.StringSimilarity;
using System;
using System.IO;
@@ -10,39 +9,29 @@ namespace BetterLyrics.WinUI3.Helper
{
public static partial class MetadataComparer
{
private const double WeightTitle = 0.30;
private const double WeightArtist = 0.30;
private const double WeightTitle = 0.40;
private const double WeightArtist = 0.40;
private const double WeightAlbum = 0.10;
private const double WeightDuration = 0.30;
private const double WeightDuration = 0.10;
// JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new();
public static int CalculateScore(SongInfo songInfo, LyricsCacheItem remote)
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
{
return CalculateScore(songInfo, remote.Title, remote.Artist, remote.Album, remote.Duration);
}
if (local == null || remote == null) return 0;
public static int CalculateScore(SongInfo songInfo, FilesIndexItem local)
{
return CalculateScore(songInfo, local.Title, local.Artist, local.Album, local.Duration, local.FileName);
}
public static int CalculateScore(
SongInfo songInfo,
string? compareTitle, string? compareArtist, string? compareAlbum, double? compareDuration, string? compareFileName = null)
{
double totalScore = 0;
bool localHasMetadata = !string.IsNullOrWhiteSpace(songInfo.Title);
bool remoteHasMetadata = !string.IsNullOrWhiteSpace(compareTitle);
bool localHasMetadata = !string.IsNullOrWhiteSpace(local.Title);
bool remoteHasMetadata = !string.IsNullOrWhiteSpace(remote.Title);
if (localHasMetadata && remoteHasMetadata)
{
double titleScore = GetStringSimilarity(songInfo.Title, compareTitle);
double artistScore = GetStringSimilarity(songInfo.Artist, compareArtist);
double albumScore = GetStringSimilarity(songInfo.Album, compareAlbum);
double durationScore = GetDurationSimilarity(songInfo.Duration, compareDuration);
double titleScore = GetStringSimilarity(local.Title, remote.Title);
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
double albumScore = GetStringSimilarity(local.Album, remote.Album);
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration);
totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) +
@@ -52,12 +41,12 @@ namespace BetterLyrics.WinUI3.Helper
else
{
string? localQuery = localHasMetadata
? $"{songInfo.Title} {songInfo.Artist}"
: Path.GetFileNameWithoutExtension(songInfo.LinkedFileName);
? $"{local.Title} {string.Join(" ", local.Artists ?? [])}"
: Path.GetFileNameWithoutExtension(local.LinkedFileName);
string? remoteQuery = remoteHasMetadata
? $"{compareTitle} {compareArtist}"
: Path.GetFileNameWithoutExtension(compareFileName);
string remoteQuery = remoteHasMetadata
? $"{remote.Title} {string.Join(" ", remote.Artists ?? [])}"
: Path.GetFileNameWithoutExtension(remote.Reference);
string fp1 = CreateSortedFingerprint(localQuery);
string fp2 = CreateSortedFingerprint(remoteQuery);
@@ -94,18 +83,19 @@ namespace BetterLyrics.WinUI3.Helper
return _algo.Similarity(s1, s2);
}
private static double GetDurationSimilarity(double localSeconds, double? remoteSeconds)
private static double GetDurationSimilarity(double localMs, double? remoteSeconds)
{
if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配
double localSeconds = localMs / 1000.0;
double diff = Math.Abs(localSeconds - remoteSeconds.Value);
// 差距 <= 1 100 % 相似
// 差距 >= 10 0 % 相似
// 差距 <= 3100% 相似
// 差距 >= 200% 相似
// 中间线性插值
const double PerfectTolerance = 1.0;
const double MaxTolerance = 10.0;
const double PerfectTolerance = 3.0;
const double MaxTolerance = 20.0;
if (diff <= PerfectTolerance) return 1.0;
if (diff >= MaxTolerance) return 0.0;

View File

@@ -1,5 +1,4 @@
using ColorThiefDotNet;
using CommunityToolkit.WinUI.Helpers;
using Impressionist.Abstractions;
using Impressionist.Implementations;
using System;
@@ -51,29 +50,7 @@ namespace BetterLyrics.WinUI3.Helper
return paletteResult;
}
public static List<Windows.UI.Color> GenerateChartColors(Windows.UI.Color baseColor, int count)
{
List<Windows.UI.Color> results = [];
var baseHsl = baseColor.ToHsl();
double baseHue = baseHsl.H;
double baseSaturation = baseHsl.S;
double baseBrightness = baseHsl.L;
double step = 360.0 / count;
for (int i = 0; i < count; i++)
{
double newHue = (baseHue + (step * i)) % 360;
Windows.UI.Color newColor = CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(newHue, baseSaturation, baseBrightness);
results.Add(newColor);
}
return results;
}
private static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();

View File

@@ -33,34 +33,48 @@ namespace BetterLyrics.WinUI3.Helper
public static string SaltPlayerForWindowsLogoPath => Path.Combine(AssetsFolder, "SaltPlayerForWindows.png");
public static string MoeKoeMusicLogoPath => Path.Combine(AssetsFolder, "MoeKoeMusic.png");
public static string Listen1LogoPath => Path.Combine(AssetsFolder, "Listen1.png");
public static string OriginalSoundHQPlayerLogoPath => Path.Combine(AssetsFolder, "OriginalSoundHQPlayer.png");
public static string UnknownPlayerLogoPath => Path.Combine(AssetsFolder, "Question.png");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics");
public static string LrcLibLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "lrclib");
public static string NeteaseLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "netease");
public static string QQLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "qq");
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
public static string AppleMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "apple-music");
public static string LocalMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-music");
public static string LocalLrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-lrc");
public static string LocalEslrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-eslrc");
public static string LocalTtmlCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-ttml");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
public static string LocalAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "local");
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.db");
public static string SongSearchMapPath => Path.Combine(LocalFolder, "song-search-map.db");
public static string LyricsCachePath => Path.Combine(LyricsCacheDirectory, "lyrics-cache.db");
public static string PlayQueuePath => Path.Combine(CacheFolder, "play-queue.m3u");
public static void EnsureDirectories()
{
Directory.CreateDirectory(SettingsDirectory);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LyricsCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
}
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(AppleMusicCacheDirectory);
Directory.CreateDirectory(LocalMusicCacheDirectory);
Directory.CreateDirectory(LocalLrcCacheDirectory);
Directory.CreateDirectory(LocalEslrcCacheDirectory);
Directory.CreateDirectory(LocalTtmlCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}
}
}

View File

@@ -42,23 +42,19 @@ namespace BetterLyrics.WinUI3.Helper
return file;
}
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices)
{
var window = WindowHook.GetWindow<T>();
return await PickSaveFileAsync(window, fileTypeChoices, suggestedFileName);
return await PickSaveFileAsync(window, fileTypeChoices);
}
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices)
{
if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileSavePicker();
picker.FileTypeChoices.AddRange(fileTypeChoices);
if (suggestedFileName != null)
{
picker.SuggestedFileName = suggestedFileName;
}
var hwnd = WindowNative.GetWindowHandle(window);
InitializeWithWindow.Initialize(picker, hwnd);

View File

@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class PlayerIdHelper
public static class PlayerIDHelper
{
private static readonly List<string> neteaseFamilyRegex =
[
@@ -25,66 +25,64 @@ namespace BetterLyrics.WinUI3.Helper
return false;
}
public static bool IsLXMusic(string? id) => id is PlayerId.LXMusic or PlayerId.LXMusicPortable;
public static bool IsLXMusic(string? id) => id is PlayerID.LXMusic or PlayerID.LXMusicPortable;
public static bool IsAppleMusic(string? id) => id is PlayerId.AppleMusic or PlayerId.AppleMusicAlternative;
public static bool IsAppleMusic(string? id) => id is PlayerID.AppleMusic or PlayerID.AppleMusicAlternative;
public static bool IsBetterLyrics(string? id) => id is PlayerId.BetterLyrics or PlayerId.BetterLyricsDebug;
public static bool IsBetterLyrics(string? id) => id is PlayerID.BetterLyrics or PlayerID.BetterLyricsDebug;
public static string? GetDisplayName(string? id) => id switch
{
PlayerId.Spotify => PlayerName.Spotify,
PlayerId.AppleMusic => PlayerName.AppleMusic,
PlayerId.iTunes => PlayerName.iTunes,
PlayerId.KugouMusic => PlayerName.KugouMusic,
PlayerId.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerId.QQMusic => PlayerName.QQMusic,
PlayerId.LXMusic => PlayerName.LXMusic,
PlayerId.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerId.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerId.AIMP => PlayerName.AIMP,
PlayerId.Foobar2000 => PlayerName.Foobar2000,
PlayerId.MusicBee => PlayerName.MusicBee,
PlayerId.PotPlayer => PlayerName.PotPlayer,
PlayerId.Chrome => PlayerName.Chrome,
PlayerId.Edge => PlayerName.Edge,
PlayerId.BetterLyrics => PlayerName.BetterLyrics,
PlayerId.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerId.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerId.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerId.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerId.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerId.Listen1 => PlayerName.Listen1,
PlayerId.OriginalSoundHQPlayer => PlayerName.OriginalSoundHQPlayer,
PlayerID.Spotify => PlayerName.Spotify,
PlayerID.AppleMusic => PlayerName.AppleMusic,
PlayerID.iTunes => PlayerName.iTunes,
PlayerID.KugouMusic => PlayerName.KugouMusic,
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerID.QQMusic => PlayerName.QQMusic,
PlayerID.LXMusic => PlayerName.LXMusic,
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerID.AIMP => PlayerName.AIMP,
PlayerID.Foobar2000 => PlayerName.Foobar2000,
PlayerID.MusicBee => PlayerName.MusicBee,
PlayerID.PotPlayer => PlayerName.PotPlayer,
PlayerID.Chrome => PlayerName.Chrome,
PlayerID.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerID.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1,
_ => id,
};
public static string GetLogoPath(string? id) => id switch
{
PlayerId.Spotify => PathHelper.SpotifyLogoPath,
PlayerId.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerId.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerId.iTunes => PathHelper.iTunesLogoPath,
PlayerId.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerId.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerId.QQMusic => PathHelper.QQMusicLogoPath,
PlayerId.LXMusic => PathHelper.LXMusicLogoPath,
PlayerId.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerId.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerId.AIMP => PathHelper.AIMPLogoPath,
PlayerId.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerId.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerId.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerId.Chrome => PathHelper.ChromeLogoPath,
PlayerId.Edge => PathHelper.EdgeLogoPath,
PlayerId.BetterLyrics => PathHelper.LogoPath,
PlayerId.BetterLyricsDebug => PathHelper.LogoPath,
PlayerId.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerId.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerId.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerId.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerId.Listen1 => PathHelper.Listen1LogoPath,
PlayerId.OriginalSoundHQPlayer => PathHelper.OriginalSoundHQPlayerLogoPath,
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerID.QQMusic => PathHelper.QQMusicLogoPath,
PlayerID.LXMusic => PathHelper.LXMusicLogoPath,
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerID.AIMP => PathHelper.AIMPLogoPath,
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerID.Chrome => PathHelper.ChromeLogoPath,
PlayerID.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath,
_ => PathHelper.UnknownPlayerLogoPath,
};
}

View File

@@ -1,232 +1,153 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System;
using System.Collections.Generic;
using static BetterLyrics.WinUI3.Helper.EasingHelper;
namespace BetterLyrics.WinUI3.Helper
{
public class ValueTransition<T> where T : struct
public class ValueTransition<T>
where T : struct
{
// 状态变量
private T _currentValue;
private double _durationSeconds;
private double _delaySeconds;
private double _delayRemaining;
private EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress;
private T _startValue;
private T _targetValue;
// 核心队列
private readonly Queue<Keyframe<T>> _keyframeQueue = new Queue<Keyframe<T>>();
public double DurationSeconds => _durationSeconds;
public double DelaySeconds => _delaySeconds;
// 时间控制
private double _stepDuration; // 当前这一段的时长 (动态变化)
private double _totalDurationForAutoSplit; // 自动均分模式的总时长
private double _configuredDelaySeconds; // 配置的延迟时长
// 动画状态
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress; // 当前段的进度 (0.0 ~ 1.0)
// 公开属性
public T Value => _currentValue;
public bool IsTransitioning => _isTransitioning;
public T TargetValue => _targetValue; // 获取当前段的目标值
public double DurationSeconds => _totalDurationForAutoSplit;
public T Value => _currentValue;
public T StartValue => _startValue;
public T TargetValue => _targetValue;
public EasingType? EasingType => _easingType;
public double Progress => _progress;
public Func<T, T, double, T> Interpolator => _interpolator;
public ValueTransition(T initialValue, Func<T, T, double, T>? interpolator, double defaultTotalDuration = 0.3)
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null, double delaySeconds = 0)
{
_currentValue = initialValue;
_startValue = initialValue;
_targetValue = initialValue;
_totalDurationForAutoSplit = defaultTotalDuration;
_durationSeconds = durationSeconds;
_delaySeconds = delaySeconds;
_delayRemaining = 0;
_progress = 1f;
_isTransitioning = false;
if (interpolator != null)
{
_interpolator = interpolator;
_easingType = null;
}
}
#region Configuration
public void SetDuration(double seconds)
{
if (seconds < 0) throw new ArgumentOutOfRangeException(nameof(seconds));
_totalDurationForAutoSplit = seconds;
}
public void SetDurationMs(double millionSeconds) => SetDuration(millionSeconds / 1000.0);
/// <summary>
/// 设置启动延迟。
/// 原理:在动画队列最前方插入一个“数值不变”的关键帧。
/// </summary>
public void SetDelay(double seconds)
{
_configuredDelaySeconds = seconds;
}
public void SetInterpolator(Func<T, T, double, T> interpolator)
{
_interpolator = interpolator;
}
#endregion
#region Control Methods
/// <summary>
/// 立即跳转到指定值(停止动画)
/// </summary>
public void JumpTo(T value)
{
_keyframeQueue.Clear();
_currentValue = value;
_startValue = value;
_targetValue = value;
_isTransitioning = false;
_progress = 0;
}
/// <summary>
/// 模式 A: 精确控制模式
/// 显式指定每一段的目标值和时长。
/// </summary>
public void Start(params Keyframe<T>[] keyframes)
{
if (keyframes == null || keyframes.Length == 0) return;
PrepareStart();
// 1. 处理延迟 (插入静止帧)
if (_configuredDelaySeconds > 0)
else if (easingType.HasValue)
{
_keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
}
// 2. 入队用户帧
foreach (var kf in keyframes)
{
_keyframeQueue.Enqueue(kf);
}
MoveToNextSegment(firstStart: true);
}
/// <summary>
/// 模式 B: 自动均分模式 (兼容旧写法)
/// 指定一串目标值,系统根据 SetDuration 的总时长平均分配。
/// </summary>
public void Start(params T[] values)
{
if (values == null || values.Length == 0) return;
// 如果目标就是当前值且只有1帧直接跳过以省性能
if (values.Length == 1 && values[0].Equals(_currentValue) && _configuredDelaySeconds <= 0) return;
PrepareStart();
// 1. 处理延迟
if (_configuredDelaySeconds > 0)
{
_keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
}
// 2. 计算均分时长
double autoStepDuration = _totalDurationForAutoSplit / values.Length;
// 3. 入队生成帧
foreach (var val in values)
{
_keyframeQueue.Enqueue(new Keyframe<T>(val, autoStepDuration));
}
MoveToNextSegment(firstStart: true);
}
#endregion
#region Core Logic
private void PrepareStart()
{
_keyframeQueue.Clear();
_isTransitioning = true;
}
private void MoveToNextSegment(bool firstStart = false)
{
if (_keyframeQueue.Count > 0)
{
var kf = _keyframeQueue.Dequeue();
// 起点逻辑:如果是刚开始,起点是当前值;如果是中间切换,起点是上一段的终点
_startValue = firstStart ? _currentValue : _targetValue;
_targetValue = kf.Value;
_stepDuration = kf.Duration;
if (firstStart) _progress = 0f;
// 注意:非 firstStart 时不重置 _progress保留溢出值以平滑过渡
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
else
{
// 队列耗尽,动画结束
_currentValue = _targetValue;
_isTransitioning = false;
_progress = 1f;
_easingType = Enums.EasingType.EaseInOutQuad;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
}
public void SetDuration(double seconds)
{
if (seconds < 0)
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive.");
_durationSeconds = seconds;
}
public void SetDelay(double seconds)
{
_delaySeconds = seconds;
}
private void JumpTo(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 1f;
_delayRemaining = 0;
_isTransitioning = false;
}
public void Reset(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 0f;
_delayRemaining = 0;
_isTransitioning = false;
}
public void StartTransition(T targetValue, bool jumpTo = false)
{
if (jumpTo)
{
JumpTo(targetValue);
return;
}
if (!targetValue.Equals(_currentValue))
{
_startValue = _currentValue;
_targetValue = targetValue;
_progress = 0f;
_delayRemaining = _delaySeconds;
_isTransitioning = true;
}
}
public static bool Equals(double x, double y, double tolerance)
{
var diff = Math.Abs(x - y);
return diff <= tolerance || diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
}
public void Update(TimeSpan elapsedTime)
{
if (!_isTransitioning) return;
double timeStep = elapsedTime.TotalSeconds;
// 使用 while 处理单帧时间过长跨越多段的情况
while (timeStep > 0 && _isTransitioning)
if (_delayRemaining > 0)
{
// 计算当前帧的步进比例
// 极小值保护防止除以0
double progressDelta = (_stepDuration > 0.000001) ? (timeStep / _stepDuration) : 1.0;
double consume = Math.Min(_delayRemaining, elapsedTime.TotalSeconds);
_delayRemaining -= consume;
if (_delayRemaining > 0)
return;
elapsedTime = TimeSpan.FromSeconds(elapsedTime.TotalSeconds - consume);
}
if (_progress + progressDelta >= 1.0)
{
// === 当前段结束 ===
if (_durationSeconds <= 0)
{
_progress = 1f;
}
else
{
_progress += elapsedTime.TotalSeconds / _durationSeconds;
}
// 1. 计算这一段实际消耗的时间
double timeConsumed = (1.0 - _progress) * _stepDuration;
// 2. 剩余时间留给下一段
timeStep -= timeConsumed;
// 3. 修正当前值到目标值
_progress = 1.0;
_currentValue = _targetValue;
// 4. 切换到下一段
MoveToNextSegment();
// 5. 如果还有下一段,进度归零
if (_isTransitioning) _progress = 0f;
}
else
{
// === 当前段进行中 ===
_progress += progressDelta;
timeStep = 0; // 时间耗尽
// 插值计算
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
if (_progress >= 1f)
{
_progress = 1f;
_currentValue = _targetValue;
_isTransitioning = false;
}
else
{
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
}
#endregion
#region Interpolators
public static Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type, EaseMode easingMode)
private Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type)
{
if (typeof(T) == typeof(double))
{
@@ -234,32 +155,59 @@ namespace BetterLyrics.WinUI3.Helper
{
double s = (double)(object)start;
double e = (double)(object)end;
Func<double, double> easeInFunc = type switch
double t = progress;
switch (type)
{
Enums.EasingType.Sine => EaseInSine,
Enums.EasingType.Quad => EaseInQuad,
Enums.EasingType.Cubic => EaseInCubic,
Enums.EasingType.Quart => EaseInQuart,
Enums.EasingType.Quint => EaseInQuint,
Enums.EasingType.Expo => EaseInExpo,
Enums.EasingType.Circle => EaseInCircle,
Enums.EasingType.Back => EaseInBack,
Enums.EasingType.Elastic => EaseInElastic,
Enums.EasingType.Bounce => EaseInBounce,
Enums.EasingType.SmoothStep => SmoothStep,
Enums.EasingType.Linear => Linear,
_ => EaseInQuad,
};
double t = Ease(progress, easingMode, easeInFunc);
case Enums.EasingType.EaseInOutSine:
t = EasingHelper.EaseInOutSine(t);
break;
case Enums.EasingType.EaseInOutQuad:
t = EasingHelper.EaseInOutQuad(t);
break;
case Enums.EasingType.EaseInOutCubic:
t = EasingHelper.EaseInOutCubic(t);
break;
case Enums.EasingType.EaseInOutQuart:
t = EasingHelper.EaseInOutQuart(t);
break;
case Enums.EasingType.EaseInOutQuint:
t = EasingHelper.EaseInOutQuint(t);
break;
case Enums.EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case Enums.EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case Enums.EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case Enums.EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case Enums.EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case Enums.EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case Enums.EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
t = EasingHelper.EaseInOutQuad(t);
break;
}
return (T)(object)(s + (e - s) * t);
};
}
throw new NotSupportedException($"Type {typeof(T)} is not supported.");
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
}
#endregion
public void SetEasingType(EasingType? easingType)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
}
}

View File

@@ -1,70 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class WebDavProbeHelper
{
/// <summary>
/// 自动检测目标主机是 HTTP 还是 HTTPS
/// </summary>
/// <returns>返回 "https" 或 "http",如果都连不上返回 null</returns>
public static async Task<string?> DetectSchemeAsync(string host, int port, string? path, string? user, string? pwd)
{
if (port == 443) return "https";
if (port == 80) return "http";
// 忽略 SSL 证书错误,因为很多 NAS 是自签名的
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,
UseProxy = false
};
// 设置认证
if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(pwd))
{
handler.Credentials = new NetworkCredential(user, pwd);
handler.PreAuthenticate = true;
}
using var client = new HttpClient(handler);
client.Timeout = TimeSpan.FromSeconds(3);
if (await ProbeUrlAsync(client, "https", host, port, path))
{
return "https";
}
if (await ProbeUrlAsync(client, "http", host, port, path))
{
return "http";
}
// 都失败
return null;
}
private static async Task<bool> ProbeUrlAsync(HttpClient client, string scheme, string host, int port, string? path)
{
try
{
var uriBuilder = new UriBuilder(scheme, host, port, path);
// 使用 PROPFIND 方法,且 Depth 为 0只检测根节点是否存在不拉取列表
var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), uriBuilder.Uri);
request.Headers.Add("Depth", "0");
var response = await client.SendAsync(request);
return response.StatusCode != HttpStatusCode.BadRequest;
}
catch
{
return false;
}
}
}
}

View File

@@ -20,7 +20,7 @@ namespace BetterLyrics.WinUI3.Hooks
/// <param name="id"></param>
/// <param name="keys"></param>
/// <param name="action"></param>
private static void RegisterHotKey(Window window, ShortcutId id, List<string> keys, Action action)
private static void RegisterHotKey(Window window, ShortcutID id, List<string> keys, Action action)
{
if (keys.Count == 0) return;
@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Hooks
}
}
private static void UnregisterHotKey(Window window, ShortcutId id)
private static void UnregisterHotKey(Window window, ShortcutID id)
{
HWND hwnd = WindowNative.GetWindowHandle(window);
User32.UnregisterHotKey(hwnd, (int)id);
@@ -66,13 +66,13 @@ namespace BetterLyrics.WinUI3.Hooks
_keys.Remove((int)id);
}
public static void UpdateHotKey(Window window, ShortcutId id, List<string> keys, Action action)
public static void UpdateHotKey(Window window, ShortcutID id, List<string> keys, Action action)
{
UnregisterHotKey(window, id);
RegisterHotKey(window, id, keys, action);
}
public static bool IsHotKeyRegistered(ShortcutId id)
public static bool IsHotKeyRegistered(ShortcutID id)
{
return _actions.ContainsKey((int)id);
}
@@ -82,7 +82,7 @@ namespace BetterLyrics.WinUI3.Hooks
return _keys.ContainsValue(keys);
}
public static bool TryInvokeAction(ShortcutId id)
public static bool TryInvokeAction(ShortcutID id)
{
return TryInvokeAction((int)id);
}

View File

@@ -17,20 +17,12 @@ namespace BetterLyrics.WinUI3.Hooks
static SystemVolumeHook()
{
try
{
_deviceEnumerator = new MMDeviceEnumerator();
// 找不到设备会抛出异常,在这里截获它
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
_deviceEnumerator = new MMDeviceEnumerator();
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (_defaultDevice != null)
{
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
}
}
catch (Exception ex)
if (_defaultDevice != null)
{
_defaultDevice = null;
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
}
}

View File

@@ -186,8 +186,7 @@ namespace BetterLyrics.WinUI3.Hooks
if (width < 20) return Rectangle.Empty;
var finalRect = new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height);
return finalRect;
return new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height);
}
catch (Exception ex)
{

View File

@@ -1,7 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;

View File

@@ -1,8 +1,6 @@
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using DevWinUI;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -19,7 +17,7 @@ namespace BetterLyrics.WinUI3.Logic
IList<RenderLyricsLine>? lines,
int startIndex,
int endIndex,
int primaryPlayingLineIndex,
int playingLineIndex,
double canvasHeight,
double targetYScrollOffset,
double playingLineTopOffsetFactor,
@@ -31,84 +29,38 @@ namespace BetterLyrics.WinUI3.Logic
TimeSpan elapsedTime,
bool isMouseScrolling,
bool isLayoutChanged,
bool isPrimaryPlayingLineChanged,
bool isMouseScrollingChanged,
double currentPositionMs
bool isPlayingLineChanged,
bool isMouseScrollingChanged
)
{
if (lines == null || lines.Count == 0) return;
if (lines == null) return;
if (primaryPlayingLineIndex < 0 || primaryPlayingLineIndex >= lines.Count) return;
var primaryPlayingLine = lines[primaryPlayingLineIndex];
var currentPlayingLine = lines.ElementAtOrDefault(playingLineIndex);
if (currentPlayingLine == null) return;
var phoneticOpacity = lyricsStyle.PhoneticLyricsOpacity / 100.0;
var originalOpacity = lyricsStyle.OriginalLyricsOpacity / 100.0;
var translatedOpacity = lyricsStyle.TranslatedLyricsOpacity / 100.0;
double topHeightFactor = canvasHeight * playingLineTopOffsetFactor;
double bottomHeightFactor = canvasHeight * (1 - playingLineTopOffsetFactor);
double scrollTopDurationSec = lyricsEffect.LyricsScrollTopDuration / 1000.0;
double scrollTopDelaySec = lyricsEffect.LyricsScrollTopDelay / 1000.0;
double scrollBottomDurationSec = lyricsEffect.LyricsScrollBottomDuration / 1000.0;
double scrollBottomDelaySec = lyricsEffect.LyricsScrollBottomDelay / 1000.0;
double canvasTransDuration = canvasYScrollTransition.DurationSeconds;
bool isBlurEnabled = lyricsEffect.IsLyricsBlurEffectEnabled;
bool isOutOfSightEnabled = lyricsEffect.IsLyricsOutOfSightEffectEnabled;
bool isFanEnabled = lyricsEffect.IsFanLyricsEnabled;
double fanAngleRad = Math.PI * (lyricsEffect.FanLyricsAngle / 180.0);
bool isGlowEnabled = lyricsEffect.IsLyricsGlowEffectEnabled;
bool isFloatEnabled = lyricsEffect.IsLyricsFloatAnimationEnabled;
bool isScaleEnabled = lyricsEffect.IsLyricsScaleEffectEnabled;
int safeStart = Math.Max(0, startIndex);
int safeEnd = Math.Min(lines.Count - 1, endIndex + 1);
for (int i = safeStart; i <= safeEnd; i++)
for (int i = startIndex; i <= endIndex + 1; i++)
{
var line = lines[i];
var lineHeight = line.PrimaryLineHeight;
if (lineHeight == null || lineHeight <= 0) continue;
var line = lines.ElementAtOrDefault(i);
if (line == null) continue;
bool isWordAnimationEnabled = lyricsEffect.WordByWordEffectMode switch
if (isLayoutChanged || isPlayingLineChanged || isMouseScrollingChanged)
{
Enums.WordByWordEffectMode.Auto => line.IsPrimaryHasRealSyllableInfo,
Enums.WordByWordEffectMode.Always => true,
Enums.WordByWordEffectMode.Never => false,
_ => line.IsPrimaryHasRealSyllableInfo
};
int lineCountDelta = i - playingLineIndex;
int absLineCountDelta = Math.Abs(lineCountDelta);
double distanceFromPlayingLine = Math.Abs(line.OriginalPosition.Y - currentPlayingLine.OriginalPosition.Y);
double targetCharFloat = lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust
? lineHeight.Value * 0.1
: lyricsEffect.LyricsFloatAnimationAmount;
double targetCharGlow = lyricsEffect.IsLyricsGlowEffectAmountAutoAdjust
? lineHeight.Value * 0.2
: lyricsEffect.LyricsGlowEffectAmount;
double targetCharScale = lyricsEffect.IsLyricsScaleEffectAmountAutoAdjust
? 1.15
: lyricsEffect.LyricsScaleEffectAmount / 100.0;
var maxAnimationDurationMs = Math.Max(line.EndMs ?? 0 - currentPositionMs, 0);
bool isSecondaryLinePlaying = line.GetIsPlaying(currentPositionMs);
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
line.IsPlayingLastFrame = isSecondaryLinePlaying;
// 行动画
if (isLayoutChanged || isPrimaryPlayingLineChanged || isMouseScrollingChanged)
{
int lineCountDelta = i - primaryPlayingLineIndex;
double distanceFromPlayingLine = Math.Abs(line.PrimaryPosition.Y - primaryPlayingLine.PrimaryPosition.Y);
double distanceFactor;
double distanceFactor = 0;
if (lineCountDelta < 0)
{
distanceFactor = Math.Clamp(distanceFromPlayingLine / topHeightFactor, 0, 1);
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * playingLineTopOffsetFactor), 0, 1);
}
else
{
distanceFactor = Math.Clamp(distanceFromPlayingLine / bottomHeightFactor, 0, 1);
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * (1 - playingLineTopOffsetFactor)), 0, 1);
}
double yScrollDuration;
@@ -117,179 +69,83 @@ namespace BetterLyrics.WinUI3.Logic
if (lineCountDelta < 0)
{
yScrollDuration =
canvasTransDuration +
distanceFactor * (scrollTopDurationSec - canvasTransDuration);
yScrollDelay = distanceFactor * scrollTopDelaySec;
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollTopDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollTopDelay / 1000.0;
}
else if (lineCountDelta == 0)
{
yScrollDuration = canvasTransDuration;
yScrollDuration = canvasYScrollTransition.DurationSeconds;
yScrollDelay = 0;
}
else
{
yScrollDuration =
canvasTransDuration +
distanceFactor * (scrollBottomDurationSec - canvasTransDuration);
yScrollDelay = distanceFactor * scrollBottomDelaySec;
canvasYScrollTransition.DurationSeconds +
distanceFactor * (lyricsEffect.LyricsScrollBottomDuration / 1000.0 - canvasYScrollTransition.DurationSeconds);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollBottomDelay / 1000.0;
}
line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.Start(
(isMouseScrolling || isSecondaryLinePlaying) ? 0 :
(isBlurEnabled ? (5 * distanceFactor) : 0));
line.BlurAmountTransition.StartTransition(isMouseScrolling ? 0 : (lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.Start(
isSecondaryLinePlaying ? _highlightedScale :
(isOutOfSightEnabled ?
line.ScaleTransition.StartTransition(
lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
_highlightedScale));
_highlightedScale);
line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
line.PhoneticOpacityTransition.Start(
isSecondaryLinePlaying ? phoneticOpacity :
line.PhoneticOpacityTransition.StartTransition(
CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(已播放)
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.PlayedOriginalOpacityTransition.Start(
isSecondaryLinePlaying ? 1.0 :
line.PlayedOriginalOpacityTransition.StartTransition(
CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(未播放)
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.UnplayedOriginalOpacityTransition.Start(
isSecondaryLinePlaying ? originalOpacity :
line.UnplayedOriginalOpacityTransition.StartTransition(
CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
line.TranslatedOpacityTransition.Start(
isSecondaryLinePlaying ? translatedOpacity :
line.TranslatedOpacityTransition.StartTransition(
CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.ColorTransition.SetDuration(yScrollDuration);
line.ColorTransition.SetDelay(yScrollDelay);
line.ColorTransition.Start(isSecondaryLinePlaying ? fgColor : bgColor);
line.ColorTransition.StartTransition(absLineCountDelta == 0 ? fgColor : bgColor);
line.AngleTransition.SetInterpolator(canvasYScrollTransition.Interpolator);
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.Start(
(isFanEnabled && !isMouseScrolling) ?
fanAngleRad * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
line.AngleTransition.StartTransition(
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) :
0);
line.YOffsetTransition.SetInterpolator(canvasYScrollTransition.Interpolator);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay);
// 设计之初是当 isLayoutChanged 为真时 jumpTo
// 但考虑到动画视觉,强制使用动画
line.YOffsetTransition.Start(targetYScrollOffset);
line.YOffsetTransition.StartTransition(targetYScrollOffset);
}
if (isWordAnimationEnabled)
{
if (isSecondaryLinePlayingChanged)
{
// 辉光动画
if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
&& isSecondaryLinePlaying)
{
foreach (var renderChar in line.PrimaryRenderChars)
{
var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0;
var stepLastingDuration = Math.Max(maxAnimationDurationMs / 1000.0 - stepInOutDuration * 2, 0);
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepInOutDuration),
new Models.Keyframe<double>(targetCharGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
);
}
}
// 浮动动画
if (isFloatEnabled)
{
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetCharFloat : 0);
}
}
}
// 字符动画
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.ProgressPlayed = renderChar.GetPlayProgress(currentPositionMs);
bool isCharPlaying = renderChar.GetIsPlaying(currentPositionMs);
bool isCharPlayingChanged = renderChar.IsPlayingLastFrame != isCharPlaying;
if (isCharPlayingChanged)
{
if (isFloatEnabled)
{
renderChar.FloatTransition.SetDurationMs(Math.Min(lyricsEffect.LyricsFloatAnimationDuration, maxAnimationDurationMs));
renderChar.FloatTransition.Start(0);
}
renderChar.IsPlayingLastFrame = isCharPlaying;
}
}
// 音节动画
foreach (var syllable in line.PrimaryRenderSyllables)
{
bool isSyllablePlaying = syllable.GetIsPlaying(currentPositionMs);
bool isSyllablePlayingChanged = syllable.IsPlayingLastFrame != isSyllablePlaying;
if (isSyllablePlayingChanged)
{
if (isScaleEnabled && isSyllablePlaying)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.ScaleTransition.Start(
new Models.Keyframe<double>(targetCharScale, stepDuration),
new Models.Keyframe<double>(1.0, stepDuration)
);
}
}
}
if (isGlowEnabled && isSyllablePlaying && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LongDurationSyllable
&& syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepDuration),
new Models.Keyframe<double>(0, stepDuration)
);
}
}
syllable.IsPlayingLastFrame = isSyllablePlaying;
}
}
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.Update(elapsedTime);
}
}
line.Update(elapsedTime);
line.AngleTransition.Update(elapsedTime);
line.ScaleTransition.Update(elapsedTime);
line.BlurAmountTransition.Update(elapsedTime);
line.PhoneticOpacityTransition.Update(elapsedTime);
line.PlayedOriginalOpacityTransition.Update(elapsedTime);
line.UnplayedOriginalOpacityTransition.Update(elapsedTime);
line.TranslatedOpacityTransition.Update(elapsedTime);
line.YOffsetTransition.Update(elapsedTime);
line.ColorTransition.Update(elapsedTime);
}
}

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas.UI.Xaml;
using System;
@@ -79,52 +79,50 @@ namespace BetterLyrics.WinUI3.Logic
// 左上角坐标
line.TopLeftPosition = new Vector2(0, (float)currentY);
// 注音层
line.TertiaryPosition = line.TopLeftPosition;
if (line.TertiaryTextLayout != null)
line.PhoneticPosition = line.TopLeftPosition;
if (line.PhoneticCanvasTextLayout != null)
{
currentY += line.TertiaryTextLayout.LayoutBounds.Height;
currentY += line.PhoneticCanvasTextLayout.LayoutBounds.Height;
// 间距
currentY += (line.TertiaryTextLayout.LayoutBounds.Height / line.TertiaryTextLayout.LineCount) * 0.1;
currentY += (line.PhoneticCanvasTextLayout.LayoutBounds.Height / line.PhoneticCanvasTextLayout.LineCount) * 0.1;
actualWidth = Math.Max(actualWidth, line.TertiaryTextLayout.LayoutBounds.Width);
actualWidth = Math.Max(actualWidth, line.PhoneticCanvasTextLayout.LayoutBounds.Width);
}
// 原文层
line.PrimaryPosition = new Vector2(0, (float)currentY);
if (line.PrimaryTextLayout != null)
line.OriginalPosition = new Vector2(0, (float)currentY);
if (line.OriginalCanvasTextLayout != null)
{
currentY += line.PrimaryTextLayout.LayoutBounds.Height;
currentY += line.OriginalCanvasTextLayout.LayoutBounds.Height;
actualWidth = Math.Max(actualWidth, line.PrimaryTextLayout.LayoutBounds.Width);
actualWidth = Math.Max(actualWidth, line.OriginalCanvasTextLayout.LayoutBounds.Width);
}
// 翻译层
if (line.SecondaryTextLayout != null)
if (line.TranslatedCanvasTextLayout != null)
{
// 间距
currentY += (line.SecondaryTextLayout.LayoutBounds.Height / line.SecondaryTextLayout.LineCount) * 0.1;
currentY += (line.TranslatedCanvasTextLayout.LayoutBounds.Height / line.TranslatedCanvasTextLayout.LineCount) * 0.1;
}
line.SecondaryPosition = new Vector2(0, (float)currentY);
if (line.SecondaryTextLayout != null)
line.TranslatedPosition = new Vector2(0, (float)currentY);
if (line.TranslatedCanvasTextLayout != null)
{
currentY += line.SecondaryTextLayout.LayoutBounds.Height;
currentY += line.TranslatedCanvasTextLayout.LayoutBounds.Height;
actualWidth = Math.Max(actualWidth, line.SecondaryTextLayout.LayoutBounds.Width);
actualWidth = Math.Max(actualWidth, line.TranslatedCanvasTextLayout.LayoutBounds.Width);
}
// 右下角坐标
line.BottomRightPosition = new Vector2(0 + (float)actualWidth, (float)currentY);
// 行间距
if (line.PrimaryTextLayout != null)
if (line.OriginalCanvasTextLayout != null)
{
currentY += (line.PrimaryTextLayout.LayoutBounds.Height / line.PrimaryTextLayout.LineCount) * style.LyricsLineSpacingFactor;
currentY += (line.OriginalCanvasTextLayout.LayoutBounds.Height / line.OriginalCanvasTextLayout.LineCount) * style.LyricsLineSpacingFactor;
}
// 更新中心点
line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType);
line.RecreateRenderChars();
}
}
@@ -140,9 +138,9 @@ namespace BetterLyrics.WinUI3.Logic
var currentLine = lines.ElementAtOrDefault(playingLineIndex);
var firstLine = lines.FirstOrDefault();
if (currentLine?.PrimaryTextLayout == null || firstLine == null) return null;
if (currentLine?.OriginalCanvasTextLayout == null || firstLine == null) return null;
return -currentLine.PrimaryPosition.Y + firstLine.PrimaryPosition.Y
return -currentLine.OriginalPosition.Y + firstLine.OriginalPosition.Y
- (currentLine.BottomRightPosition.Y - currentLine.TopLeftPosition.Y) / 2.0;
}
@@ -189,37 +187,6 @@ namespace BetterLyrics.WinUI3.Logic
return lines.Last().BottomRightPosition.Y;
}
public static void CalculateLanes(IList<RenderLyricsLine>? lines, int toleranceMs = 50)
{
if (lines == null) return;
var lanesEndMs = new List<int> { 0 };
foreach (var line in lines)
{
var start = line.StartMs;
var end = line.EndMs;
int assignedLane = -1;
for (int i = 0; i < lanesEndMs.Count; i++)
{
if (lanesEndMs[i] <= start + toleranceMs)
{
assignedLane = i;
break;
}
}
if (assignedLane == -1)
{
assignedLane = lanesEndMs.Count;
lanesEndMs.Add(0);
}
lanesEndMs[assignedLane] = end ?? 0;
line.LaneIndex = assignedLane;
}
}
public static int FindMouseHoverLineIndex(
IList<RenderLyricsLine>? lines,
bool isMouseInLyricsArea,
@@ -241,7 +208,7 @@ namespace BetterLyrics.WinUI3.Logic
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.PrimaryTextLayout == null) break;
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y;
if (value >= mousePosition.Y) { result = mid; right = mid - 1; }
else { left = mid + 1; }
@@ -267,7 +234,7 @@ namespace BetterLyrics.WinUI3.Logic
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.PrimaryTextLayout == null) break;
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y;
// 理论上说应该使用下面这一行来精确计算视野内的首个可见行,但是考虑到动画视觉效果,还是注释掉了
//if (value >= lyricsY) { result = mid; right = mid - 1; }
@@ -284,7 +251,7 @@ namespace BetterLyrics.WinUI3.Logic
{
int mid = (left + right) / 2;
var line = lines[mid];
if (line.PrimaryTextLayout == null) break;
if (line.OriginalCanvasTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y;
// 同理
//if (value >= lyricsY + lyricsHeight) { result = mid; right = mid - 1; }

View File

@@ -1,6 +1,4 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
@@ -15,66 +13,48 @@ namespace BetterLyrics.WinUI3.Logic
_lastFoundIndex = 0;
}
public int GetCurrentLineIndex(double currentTimeMs, IList<RenderLyricsLine>? lines)
public int GetCurrentLineIndex(double currentTimeMs, LyricsData? lyricsData)
{
if (lines == null || lines.Count == 0) return 0;
if (lyricsData == null || lyricsData.LyricsLines.Count == 0) return 0;
var lines = lyricsData.LyricsLines;
if (_lastFoundIndex >= 0 && _lastFoundIndex < lines.Count)
// Cache hit
if (IsTimeInLine(currentTimeMs, lines, _lastFoundIndex)) return _lastFoundIndex;
if (_lastFoundIndex + 1 < lines.Count && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex + 1))
{
var lastLine = lines[_lastFoundIndex];
if (lastLine.LaneIndex == 0 && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex))
{
return _lastFoundIndex;
}
_lastFoundIndex++;
return _lastFoundIndex;
}
int bestCandidateIndex = -1;
int bestCandidateLane = int.MaxValue;
// Cache miss
for (int i = 0; i < lines.Count; i++)
{
if (IsTimeInLine(currentTimeMs, lines, i))
{
var currentLine = lines[i];
int currentLane = currentLine.LaneIndex;
if (currentLane == 0)
{
_lastFoundIndex = i;
return i;
}
if (currentLane < bestCandidateLane)
{
bestCandidateIndex = i;
bestCandidateLane = currentLane;
}
}
else if (lines[i].StartMs > currentTimeMs + 1000)
{
break;
_lastFoundIndex = i;
return i;
}
}
if (bestCandidateIndex != -1)
{
_lastFoundIndex = bestCandidateIndex;
return bestCandidateIndex;
}
// Default
return Math.Min(_lastFoundIndex, lines.Count - 1);
}
public LinePlaybackState GetLinePlayingProgress(
double currentTimeMs,
RenderLyricsLine line,
WordByWordEffectMode wordByWordEffectMode)
LyricsLine line,
LyricsLine? nextLine,
double songDurationMs,
bool isForceWordByWord)
{
var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 };
if (line == null) return state;
double lineEndMs = line.EndMs ?? 0;
double lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = songDurationMs;
// 还没到
if (currentTimeMs < line.StartMs) return state;
@@ -83,53 +63,42 @@ namespace BetterLyrics.WinUI3.Logic
if (currentTimeMs > lineEndMs)
{
state.SyllableProgress = 1f;
state.SyllableStartIndex = Math.Max(0, line.PrimaryText.Length - 1);
state.SyllableStartIndex = Math.Max(0, line.OriginalText.Length - 1);
state.SyllableLength = 1;
return state;
}
switch (wordByWordEffectMode)
// 逐字
if (line.LyricsSyllables != null && line.LyricsSyllables.Count > 1)
{
case WordByWordEffectMode.Auto:
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
else
{
state.SyllableStartIndex = line.PrimaryText.Length;
state.SyllableProgress = 1f;
return state;
}
case WordByWordEffectMode.Never:
state.SyllableStartIndex = line.PrimaryText.Length;
state.SyllableProgress = 1f;
return state;
case WordByWordEffectMode.Always:
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
else
{
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs);
}
default:
return state;
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
// 强制逐字
if (isForceWordByWord && line.OriginalText.Length > 0)
{
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs);
}
else
{
// 普通行
state.SyllableStartIndex = line.OriginalText.Length;
state.SyllableProgress = 1f;
return state;
}
}
private LinePlaybackState CalculateSyllableProgress(double time, RenderLyricsLine line, double lineEndMs)
private LinePlaybackState CalculateSyllableProgress(double time, LyricsLine line, double lineEndMs)
{
var state = new LinePlaybackState();
int count = line.PrimaryRenderSyllables.Count;
int count = line.LyricsSyllables.Count;
for (int i = 0; i < count; i++)
{
var timing = line.PrimaryRenderSyllables[i];
var nextTiming = (i + 1 < count) ? line.PrimaryRenderSyllables[i + 1] : null;
var timing = line.LyricsSyllables[i];
var nextTiming = (i + 1 < count) ? line.LyricsSyllables[i + 1] : null;
double timingEndMs = timing.EndMs ?? 0;
double timingEndMs = timing.EndMs ?? nextTiming?.StartMs ?? lineEndMs;
// 在当前字范围内
if (time >= timing.StartMs && time <= timingEndMs)
@@ -153,10 +122,10 @@ namespace BetterLyrics.WinUI3.Logic
return state;
}
private LinePlaybackState CalculateSimulatedProgress(double time, RenderLyricsLine line, double lineEndMs)
private LinePlaybackState CalculateSimulatedProgress(double time, LyricsLine line, double lineEndMs)
{
var state = new LinePlaybackState();
int textLength = line.PrimaryText.Length;
int textLength = line.OriginalText.Length;
double progress = (time - line.StartMs) / (lineEndMs - line.StartMs);
progress = Math.Clamp(progress, 0, 1);
@@ -171,7 +140,7 @@ namespace BetterLyrics.WinUI3.Logic
return state;
}
private bool IsTimeInLine(double time, IList<RenderLyricsLine> lines, int index)
private bool IsTimeInLine(double time, IList<LyricsLine> lines, int index)
{
if (index < 0 || index >= lines.Count) return false;
var line = lines[index];

View File

@@ -3,7 +3,7 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models.Settings
namespace BetterLyrics.WinUI3.Models
{
public partial class AlbumArtSearchProviderInfo : ObservableRecipient
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models.Http
namespace BetterLyrics.WinUI3.Models
{
public class CutletDockerRequest
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models.Http
namespace BetterLyrics.WinUI3.Models
{
public class CutletDockerResponse
{

View File

@@ -1,12 +0,0 @@
using BetterLyrics.WinUI3.Models.Entities;
using Microsoft.EntityFrameworkCore;
namespace BetterLyrics.WinUI3.Models.DbContext
{
public partial class FilesIndexDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public FilesIndexDbContext(DbContextOptions<FilesIndexDbContext> options) : base(options) { }
public DbSet<FilesIndexItem> FilesIndex { get; set; }
}
}

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