Compare commits

...

79 Commits

Author SHA1 Message Date
Zhe Fang
0a3dadbd82 Merge pull request #55 from jayfunc/dev
v1.0.26.0
2025-07-22 22:29:55 -04:00
Zhe Fang
da377838e8 fix 2025-07-22 22:29:34 -04:00
Zhe Fang
67cf6e47c8 fix 2025-07-22 21:15:32 -04:00
Zhe Fang
abf4c3498f Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-07-22 20:36:55 -04:00
Zhe Fang
ff8c85b2d0 fix playbackservice 2025-07-22 20:36:52 -04:00
Zhe Fang
8cbdb32931 Merge pull request #54 from jayfunc/l10n_dev
New Crowdin updates
2025-07-21 22:22:16 -04:00
Zhe Fang
757f9f4156 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-07-21 21:41:08 -04:00
Zhe Fang
c632f4b01a update readme 2025-07-21 21:41:07 -04:00
Zhe Fang
a843d0a0e3 New translations resources.resw (English) 2025-07-21 20:38:58 -04:00
Zhe Fang
905df92b05 New translations resources.resw (Chinese Traditional) 2025-07-21 19:38:04 -04:00
Zhe Fang
7445299537 New translations resources.resw (Chinese Simplified) 2025-07-21 19:38:03 -04:00
Zhe Fang
ba4958f837 New translations resources.resw (Korean) 2025-07-21 19:38:02 -04:00
Zhe Fang
8ca5245bf5 New translations resources.resw (Japanese) 2025-07-21 19:38:00 -04:00
Zhe Fang
89aa97552a Update Crowdin configuration file 2025-07-21 19:36:41 -04:00
Zhe Fang
fa6da81988 Merge pull request #53 from jayfunc/dev
fix
2025-07-21 17:18:37 -04:00
Zhe Fang
aa692e2735 fix auto transparent issue after restart and lock when hover on non-transparent window 2025-07-21 17:17:57 -04:00
Zhe Fang
c7ee26f284 fix timeline update strategy 2025-07-21 11:29:52 -04:00
Zhe Fang
b103e6efd1 update readme 2025-07-21 10:28:44 -04:00
Zhe Fang
09e8cce69a Merge pull request #52 from jayfunc/dev
fix
2025-07-21 09:51:41 -04:00
Zhe Fang
16cd12e22b fix 2025-07-21 09:51:14 -04:00
Zhe Fang
e0121bee64 Merge pull request #51 from jayfunc/dev
v1.0.20.0
2025-07-20 23:28:59 -04:00
Zhe Fang
34bdbc89bc fix #50 2025-07-20 23:28:37 -04:00
Zhe Fang
b649e9761d update readme 2025-07-19 14:50:47 -04:00
Zhe Fang
f5638c6880 Merge pull request #49 from jayfunc/dev
v1.0.18.0
2025-07-19 14:34:34 -04:00
Zhe Fang
a9807f4f09 fix #41 2025-07-19 14:33:45 -04:00
Zhe Fang
def287715d fix 2025-07-19 12:54:20 -04:00
Zhe Fang
966f926112 fix #47 fix #44 2025-07-19 08:30:29 -04:00
Zhe Fang
4568293b51 update readme 2025-07-18 11:05:51 -04:00
Zhe Fang
10115ab0a8 update readme 2025-07-18 11:00:42 -04:00
Zhe Fang
ecefaedcb9 update readme 2025-07-17 21:06:15 -04:00
Zhe Fang
b9aae0866d update readme 2025-07-17 21:05:25 -04:00
Zhe Fang
afbfcc921e update readme 2025-07-17 20:55:53 -04:00
Zhe Fang
0606711023 Merge pull request #43 from jayfunc/dev
v1.0.17.0
2025-07-17 14:54:12 -04:00
Zhe Fang
8f997ca3d9 fix #42 2025-07-17 14:45:00 -04:00
Zhe Fang
042d557eb1 fix #40 fix #14 2025-07-17 14:29:47 -04:00
Zhe Fang
153679228d fix playbackservice 2025-07-17 13:04:50 -04:00
Zhe Fang
5a4fe54ac2 update webhook username 2025-07-17 10:42:12 -04:00
Zhe Fang
14e8d88cd1 update webhook logo 2025-07-17 10:39:32 -04:00
Zhe Fang
79e726db33 fix closing issue 2025-07-17 10:37:59 -04:00
Zhe Fang
d4f1949833 update doc 2025-07-17 09:02:26 -04:00
Zhe Fang
6135f996bf update readme 2025-07-17 08:49:08 -04:00
Zhe Fang
5b97621af4 fix for dock mode (color sampling issue for bottom position) 2025-07-17 08:48:02 -04:00
Zhe Fang
0f084d04f8 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-07-17 07:52:14 -04:00
Zhe Fang
bcb114a171 fix #39 2025-07-17 07:52:13 -04:00
Zhe Fang
7bbe2a3045 Update releases-to-discord.yml 2025-07-16 21:37:45 -04:00
Zhe Fang
fab4e8fe22 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-07-16 21:21:40 -04:00
Zhe Fang
bc4a06577e fix 2025-07-16 21:21:38 -04:00
Zhe Fang
22a790fff0 Update FUNDING.yml 2025-07-16 21:19:39 -04:00
Zhe Fang
5e3d0cb78f Create FUNDING.yml 2025-07-16 21:15:20 -04:00
Zhe Fang
59ee2a6fc3 fix #25 2025-07-16 20:23:01 -04:00
Zhe Fang
fd5e752b43 fix #33 2025-07-16 19:22:47 -04:00
Zhe Fang
df31d03da7 fix #34 fix #28 update faq 2025-07-16 16:22:34 -04:00
Zhe Fang
a7ebba4a62 fix #26 fix #27 fix #29 2025-07-16 07:33:43 -04:00
Zhe Fang
6ca1a84a54 update doc 2025-07-14 20:26:58 -04:00
Zhe Fang
bab5a827f6 update readme 2025-07-14 20:10:17 -04:00
Zhe Fang
3b1e0389aa fix #13 , fix #20 , fix #21 , fix #24 2025-07-13 20:32:10 -04:00
Zhe Fang
ae05a55d31 update doc 2025-07-11 21:55:25 -04:00
Zhe Fang
4b6d417db3 update docs 2025-07-11 21:48:16 -04:00
Zhe Fang
6ad79180e4 Merge pull request #19 from jayfunc/dev
github actions
2025-07-11 19:49:17 -04:00
Zhe Fang
86118bac02 Rename github-releases-to-discord.yml to releases-to-discord.yml 2025-07-11 19:47:19 -04:00
Zhe Fang
7bfbec4b01 Update and rename notify-discord.yml to github-releases-to-discord.yml 2025-07-11 19:41:07 -04:00
Zhe Fang
a5d6dd1305 Create notify-discord.yml 2025-07-11 19:20:52 -04:00
Zhe Fang
68f690e1a7 update doc 2025-07-11 19:00:43 -04:00
Zhe Fang
42af22a7e3 Merge pull request #18 from jayfunc/dev
v1.0.11.0
2025-07-11 18:08:49 -04:00
Zhe Fang
34d7f3f319 fix 2025-07-11 18:08:05 -04:00
Zhe Fang
07b82191d0 change language detection model 2025-07-10 23:08:29 -04:00
Zhe Fang
f8c6060d32 fix #17 2025-07-09 20:11:01 -04:00
Zhe Fang
bfdb36ff95 init docs 2025-07-09 13:13:23 -04:00
Zhe Fang
ce83777c1d Delete docs directory 2025-07-09 12:56:56 -04:00
Zhe Fang
d709e70fa2 Create jekyll-gh-pages.yml 2025-07-09 12:53:33 -04:00
Zhe Fang
8fe4f8fd58 add docs 2025-07-09 12:46:06 -04:00
Zhe Fang
b6319e522a fix #16 2025-07-09 10:29:28 -04:00
Zhe Fang
58d74c1515 fix #15 2025-07-09 09:37:38 -04:00
Zhe Fang
806f3fdd63 fix #11 #12 update readme 2025-07-09 09:19:48 -04:00
Zhe Fang
90d2055dff update readme 2025-07-07 20:32:48 -04:00
Zhe Fang
509079e8c7 Merge pull request #10 from jayfunc/dev
1.0.9.0
2025-07-07 17:32:48 -04:00
Zhe Fang
a29e5c98f8 fix 2025-07-07 17:30:44 -04:00
Zhe Fang
78a6ba8e1f add local machine translation func 2025-07-05 15:16:34 -04:00
Zhe Fang
352ceca81d fix 2025-07-04 07:25:38 -04:00
146 changed files with 65889 additions and 3843 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: [jayfunc]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: founchoo
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -0,0 +1,20 @@
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: GitHub Releases to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "2105893"
username: "GitHub"
avatar_url: "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png"
content: "||@everyone||"
footer_title: "Changelog"
reduce_headings: true

View File

@@ -5,52 +5,62 @@
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:uap18="http://schemas.microsoft.com/appx/manifest/uap/windows10/18"
IgnorableNamespaces="uap rescap uap18">
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.7.0" />
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.26.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Resources>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="BetterLyrics"
Description="BetterLyrics.WinUI3 (Package)"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="BetterLyrics"
Description="BetterLyrics.WinUI3 (Package)"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<uap5:Extension
Category="windows.startupTask">
<uap5:StartupTask
TaskId="AutoStartup"
Enabled="false"
DisplayName="BetterLyrics" />
</uap5:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -12,6 +12,7 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
@@ -47,13 +48,15 @@
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style (inc. the correct spacing) of a section header -->
<!-- Style -->
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
@@ -63,14 +66,16 @@
</Style.Setters>
</Style>
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Margin" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Padding" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
@@ -79,20 +84,399 @@
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="CardGridStyle" TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TransparentSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
<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="8"
Fill="Transparent" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="Transparent" />
<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="8"
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="8"
Height="8"
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="8"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />
<!-- Dimensions -->
<!-- Fonts -->
<FontFamily x:Key="IconFontFamily">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
<FontFamily x:Key="IconFontFamily">ms-appx:///Assets/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,10 +1,5 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
@@ -17,6 +12,12 @@ using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Serilog;
using ShadowViewer.Controls;
using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3
{
@@ -30,6 +31,9 @@ namespace BetterLyrics.WinUI3
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
public static ResourceLoader? ResourceLoader { get; private set; }
public NotificationPanel? LyricsWindowNotificationPanel { get; set; }
public NotificationPanel? SettingsWindowNotificationPanel { get; set; }
public App()
{
this.InitializeComponent();
@@ -39,10 +43,10 @@ namespace BetterLyrics.WinUI3
ResourceLoader = new ResourceLoader();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
AppInfo.EnsureDirectories();
PathHelper.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetService<ILogger<App>>()!;
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
@@ -52,26 +56,19 @@ namespace BetterLyrics.WinUI3
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
WindowHelper.OpenWindow<LyricsWindow>();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow == null) return;
string[] commandLineArguments = Environment.GetCommandLineArgs();
if (commandLineArguments.Length > 1)
{
commandLineArguments = commandLineArguments.Skip(1).ToArray();
if (commandLineArguments.First() == AppInfo.UnlockWindowTag)
{
lyricsWindow.AutoSelectLyricsMode(AutoStartWindowType.DesktopMode, false);
return;
}
}
lyricsWindow.ViewModel.InitLockHotKey();
lyricsWindow.AutoSelectLyricsMode();
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
@@ -85,16 +82,18 @@ namespace BetterLyrics.WinUI3
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IMusicSearchService, MusicSearchService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ITranslateService, TranslateService>()
// ViewModels
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsPageViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<MusicGalleryViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<LyricsSettingsControlViewModel>()
.BuildServiceProvider()
);
}
@@ -107,7 +106,7 @@ namespace BetterLyrics.WinUI3
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
//_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
@@ -117,7 +116,7 @@ namespace BetterLyrics.WinUI3
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
//_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -1,114 +1,142 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="ViewModels\Lyrics\**" />
<Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\DesktopLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\SystemTray.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants)</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShouldCreateLogs>True</ShouldCreateLogs>
<AdvancedSettingsExpanded>True</AdvancedSettingsExpanded>
<UpdateAssemblyVersion>False</UpdateAssemblyVersion>
<UpdateAssemblyFileVersion>False</UpdateAssemblyFileVersion>
<UpdateAssemblyInfoVersion>False</UpdateAssemblyInfoVersion>
<UpdatePackageVersion>True</UpdatePackageVersion>
<AssemblyInfoVersionType>SettingsVersion</AssemblyInfoVersionType>
<InheritWinAppVersionFrom>AssemblyVersion</InheritWinAppVersionFrom>
<PackageVersionSettings>AssemblyVersion.None.None</PackageVersionSettings>
<Version>2025.6.0</Version>
<AssemblyVersion>2025.6.18.0110</AssemblyVersion>
<FileVersion>2025.6.18.0110</FileVersion>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="ViewModels\Lyrics\**" />
<Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Core14.profile.xml" />
<None Remove="Assets\Segoe Fluent Icons.ttf" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
<None Remove="Views\MusicGalleryWindow.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="3v.EvtSource" Version="2.0.0" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="NTextCat" Version="0.3.65" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.7" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\DesktopLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\SystemTray.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants)</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShouldCreateLogs>True</ShouldCreateLogs>
<AdvancedSettingsExpanded>True</AdvancedSettingsExpanded>
<UpdateAssemblyVersion>False</UpdateAssemblyVersion>
<UpdateAssemblyFileVersion>False</UpdateAssemblyFileVersion>
<UpdateAssemblyInfoVersion>False</UpdateAssemblyInfoVersion>
<UpdatePackageVersion>True</UpdatePackageVersion>
<AssemblyInfoVersionType>SettingsVersion</AssemblyInfoVersionType>
<InheritWinAppVersionFrom>AssemblyVersion</InheritWinAppVersionFrom>
<PackageVersionSettings>AssemblyVersion.None.None</PackageVersionSettings>
<Version>2025.6.0</Version>
<AssemblyVersion>2025.6.18.0110</AssemblyVersion>
<FileVersion>2025.6.18.0110</FileVersion>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,33 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class AlbumArtSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is AlbumArtSearchProvider provider)
{
return provider switch
{
AlbumArtSearchProvider.Local => App.ResourceLoader!.GetString("AlbumArtSearchLocalProvider"),
AlbumArtSearchProvider.SMTC => App.ResourceLoader!.GetString("AlbumArtSearchSMTCProvider"),
AlbumArtSearchProvider.iTunes => "iTunes",
_ => throw new Exception($"Unknown AlbumArtSearchProvider: {provider}"),
};
}
throw new ArgumentException("Value must be of type AlbumArtSearchProvider", nameof(value));
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

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

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum AlbumArtSearchProvider
{
Local,
SMTC,
iTunes,
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum DockPlacement
{
Top,
Bottom
}
public static class DockPlacementExtensions
{
public static WindowPixelSampleMode ToWindowPixelSampleMode(this DockPlacement placement)
{
return placement switch
{
DockPlacement.Top => WindowPixelSampleMode.BelowWindow,
DockPlacement.Bottom => WindowPixelSampleMode.AboveWindow,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
}
}

View File

@@ -4,12 +4,17 @@ namespace BetterLyrics.WinUI3.Enums
{
public enum EasingType
{
EaseInOutQuad,
EaseInQuad,
EaseOutQuad,
EaseInOutExpo,
Linear,
SmoothStep,
SmootherStep,
EaseInOutSine,
EaseInOutQuad,
EaseInOutCubic,
EaseInOutQuart,
EaseInOutQuint,
EaseInOutExpo,
EaseInOutCirc,
EaseInOutBack,
EaseInOutElastic,
EaseInOutBounce,
}
}

View File

@@ -4,7 +4,8 @@ namespace BetterLyrics.WinUI3.Enums
{
public enum LineRenderingType
{
UntilCurrentChar,
CurrentCharOnly,
CurrentChar,
LineStartToCurrentChar,
CurrentLine
}
}

View File

@@ -1,11 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsAlignmentType
{
Left,
Center,
Right,
}
}

View File

@@ -7,6 +7,5 @@ namespace BetterLyrics.WinUI3.Enums
AlbumArtOnly,
LyricsOnly,
SplitView,
PlaceholderOnly,
}
}

View File

@@ -16,13 +16,36 @@ namespace BetterLyrics.WinUI3.Enums
{
public static LyricsFormat? DetectFormat(this string content)
{
if (content.StartsWith("<?xml") && System.Text.RegularExpressions.Regex.IsMatch(content, @"<tt(:\w+)?\b"))
if (string.IsNullOrWhiteSpace(content))
return null;
// TTML: 检查 <tt ... xmlns="http://www.w3.org/ns/ttml"
if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"<tt\b[^>]*\bxmlns\s*=\s*[""']http://www\.w3\.org/ns/ttml[""']",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
return LyricsFormat.Ttml;
}
// 检测标准LRC和增强型LRC
else if (System.Text.RegularExpressions.Regex.IsMatch(content, @"\[\d{1,2}:\d{2}")
|| System.Text.RegularExpressions.Regex.IsMatch(content, @"<\d{1,2}:\d{2}\.\d{2,3}>"))
// KRC: 检测主内容格式 [start,duration]<offset,duration,0>字...
else if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"^\[\d+,\d+\](<\d+,\d+,0>.+)+",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Krc;
}
// QRC: 检测主内容格式 [start,duration]字(offset,duration)
else if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"^\[\d+,\d+\].*?\(\d+,\d+\)",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Qrc;
}
// 标准LRC和增强型LRC
else if (System.Text.RegularExpressions.Regex.IsMatch(content, @"\[\d{1,2}:\d{2}") ||
System.Text.RegularExpressions.Regex.IsMatch(content, @"<\d{1,2}:\d{2}\.\d{2,3}>"))
{
return LyricsFormat.Lrc;
}

View File

@@ -23,11 +23,11 @@ namespace BetterLyrics.WinUI3.Enums
{
return provider switch
{
LyricsSearchProvider.LrcLib => AppInfo.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => AppInfo.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => AppInfo.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => AppInfo.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => AppInfo.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
_ => throw new System.ArgumentOutOfRangeException(nameof(provider)),
};
}

View File

@@ -1,10 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum MusicSearchMatchMode
{
TitleAndArtist,
TitleArtistAlbumAndDuration,
}
}

View File

@@ -0,0 +1,28 @@
// 2025/6/23 by Zhe Fang
using Microsoft.Graphics.Canvas.Text;
using System;
namespace BetterLyrics.WinUI3.Enums
{
public enum TextAlignmentType
{
Left,
Center,
Right,
}
public static class LyricsAlignmentTypeExtensions
{
public static CanvasHorizontalAlignment ToCanvasHorizontalAlignment(this TextAlignmentType alignmentType)
{
return alignmentType switch
{
TextAlignmentType.Left => CanvasHorizontalAlignment.Left,
TextAlignmentType.Center => CanvasHorizontalAlignment.Center,
TextAlignmentType.Right => CanvasHorizontalAlignment.Right,
_ => throw new ArgumentOutOfRangeException(nameof(alignmentType), alignmentType, null),
};
}
}
}

View File

@@ -1,8 +1,9 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum WindowColorSampleMode
public enum WindowPixelSampleMode
{
BelowWindow,
AboveWindow,
WindowArea,
WindowEdge,
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, Color? albumArtAccentColor) : EventArgs
{
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public Color? AlbumArtAccentColor { get; set; } = albumArtAccentColor;
}
}

View File

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

View File

@@ -1,70 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Helper
{
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.FileProperties;
public static class AppInfo
{
public const string AppAuthor = "Zhe Fang";
public const string AppDisplayName = "Better Lyrics";
public const string AppName = "BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string UnlockWindowTag = "UnlockWindow";
public static string AmllTtmlDbIndexPath => Path.Combine(CacheFolder, "amll-ttml-db-index.json");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(CacheFolder, "amll-ttml-db-lyrics");
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
public static string KugouLyricsCacheDirectory => Path.Combine(CacheFolder, "kugou-lyrics");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LrcLibLyricsCacheDirectory => Path.Combine(CacheFolder, "lrclib-lyrics");
public static string NeteaseLyricsCacheDirectory => Path.Combine(CacheFolder, "netease-lyrics");
public static string QQLyricsCacheDirectory => Path.Combine(CacheFolder, "qq-lyrics");
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static void EnsureDirectories()
{
Directory.CreateDirectory(LocalFolder);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
}
public static async Task<DateTime> GetBuildDate()
{
var assembly = Assembly.GetExecutingAssembly();
var filePath = assembly.Location;
if (!File.Exists(filePath))
return DateTime.MinValue;
StorageFile file = await StorageFile.GetFileFromPathAsync(filePath);
// 获取文件基本属性
BasicProperties props = await file.GetBasicPropertiesAsync();
// 返回修改日期
return props.DateModified.DateTime;
}
}
}

View File

@@ -1,15 +1,43 @@
// 2025/6/23 by Zhe Fang
using ATL;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class CollectionHelper
{
public static T? SafeGet<T>(this IList<T> list, int index)
public static ObservableCollection<GroupInfoList> GetGroupedByTitleAsync(this ICollection<Track> tracks)
{
if (list == null || index < 0 || index >= list.Count) return default;
return list[index];
// Grab Contact objects from pre-existing list (list is returned from function GetContactsAsync())
var query = from item in tracks
// Group the items returned from the query, sort and select the ones you want to keep
group item by item.Title.Substring(0, 1).ToUpper() into g
orderby g.Key
// GroupInfoList is a simple custom class that has an IEnumerable type attribute, and
// a key attribute. The IGrouping-typed variable g now holds the Contact objects,
// and these objects will be used to create a new GroupInfoList object.
select new GroupInfoList(g) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (items == null) throw new ArgumentNullException(nameof(items));
foreach (var item in items)
{
collection.Add(item);
}
}
}
}

View File

@@ -1,10 +1,17 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using Vanara.PInvoke;
using Windows.UI;
using Color = Windows.UI.Color;
namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
@@ -81,5 +88,164 @@ namespace BetterLyrics.WinUI3.Helper
{
return Color.FromArgb(color.A, color.R, color.G, color.B);
}
public static Color WithAlpha(this Color color, byte alpha)
{
return Color.FromArgb(alpha, color.R, color.G, color.B);
}
public static Color WithBrightness(this Color color, double brightness)
{
// 确保亮度因子在合理范围内
brightness = Math.Max(0, Math.Min(1, brightness));
var hsl = CommunityToolkit.WinUI.Helpers.ColorHelper.ToHsl(color);
double h = hsl.H;
double s = hsl.S;
return CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, s, brightness);
}
public static System.Drawing.Color GetAccentColor(IntPtr myHwnd, WindowPixelSampleMode mode)
{
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return System.Drawing.Color.Transparent;
switch (mode)
{
case WindowPixelSampleMode.BelowWindow:
{
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
case WindowPixelSampleMode.AboveWindow:
{
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Top - 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
case WindowPixelSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
case WindowPixelSampleMode.WindowEdge:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
var edgeThickness = new Thickness(36, 0, 36, 0);
List<System.Drawing.Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// 合并四边平均色
if (edgeColors.Count == 0)
return System.Drawing.Color.Transparent;
long r = 0,
g = 0,
b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return System.Drawing.Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
);
}
default:
return System.Drawing.Color.Transparent;
}
}
private static System.Drawing.Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, Gdi32.RasterOperationMode.SRCCOPY);
gDest.ReleaseHdc(hdcDest);
User32.ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static System.Drawing.Color ComputeAverageColor(Bitmap bmp)
{
long r = 0, g = 0, b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
System.Drawing.Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0) return System.Drawing.Color.Transparent;
return System.Drawing.Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
}
}

View File

@@ -1,9 +1,12 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using Vanara.PInvoke;
using WinRT.Interop;
@@ -15,7 +18,6 @@ namespace BetterLyrics.WinUI3.Helper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private static readonly Dictionary<IntPtr, bool> _clickThroughStates = [];
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<IntPtr, (double X, double Y, double Width, double Height)> _originalWindowBounds = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyles = [];
@@ -34,10 +36,9 @@ namespace BetterLyrics.WinUI3.Helper
}
// <20>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
var windowManager = WindowManager.Get(window);
if (_originalWindowBounds.TryGetValue(hwnd, out var bounds))
{
windowManager.AppWindow.MoveAndResize(
window.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(
(int)bounds.X,
(int)bounds.Y,
@@ -48,13 +49,6 @@ namespace BetterLyrics.WinUI3.Helper
_originalWindowBounds.Remove(hwnd);
}
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
_originalWindowStyles.Remove(hwnd);
}
window.SetIsShownInSwitchers(true);
}
@@ -63,14 +57,13 @@ namespace BetterLyrics.WinUI3.Helper
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20><>¼ԭʼ<D4AD><CABC><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
var windowManager = WindowManager.Get(window);
if (!_originalWindowBounds.ContainsKey(hwnd))
{
_originalWindowBounds[hwnd] = (
windowManager.AppWindow.Position.X,
windowManager.AppWindow.Position.Y,
windowManager.Width,
windowManager.Height
window.AppWindow.Position.X,
window.AppWindow.Position.Y,
window.AppWindow.Size.Width,
window.AppWindow.Size.Height
);
}
@@ -80,15 +73,19 @@ namespace BetterLyrics.WinUI3.Helper
int targetX = _settingsService.DesktopWindowLeft;
int targetY = _settingsService.DesktopWindowTop;
if (targetWidth <= 0 || targetHeight <= 0 || targetX < 0 || targetY < 0)
{
targetWidth = 1200;
targetHeight = 600;
targetX = 200;
targetY = 200;
}
// <20><><EFBFBD>ô<EFBFBD><C3B4>ڴ<EFBFBD>С<EFBFBD><D0A1>λ<EFBFBD><CEBB>
windowManager.AppWindow.MoveAndResize(
window.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(targetX, targetY, targetWidth, targetHeight)
);
// <20><><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD>ʽ
if (!_originalWindowStyles.ContainsKey(hwnd))
_originalWindowStyles[hwnd] = window.GetWindowStyle();
// <20><><EFBFBD><EFBFBD>ԭTopMost״̬
if (!_originalTopmostStates.ContainsKey(hwnd))
_originalTopmostStates[hwnd] = window.GetIsAlwaysOnTop();
@@ -99,48 +96,30 @@ namespace BetterLyrics.WinUI3.Helper
window.SetIsShownInSwitchers(false);
}
public static void Lock(Window window)
{
window.SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
// <20><><EFBFBD><EFBFBD><EFBFBD>ޱ߿<DEB1><DFBF><EFBFBD>͸<EFBFBD><CDB8>
window.ToggleWindowStyle(true, WindowStyle.Popup | WindowStyle.Visible);
window.ExtendsContentIntoTitleBar = false;
SetClickThrough(window, true);
}
public static void SetClickThrough(Window window, bool enable)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
int exStyle = User32.GetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE);
if (enable)
{
// <20><><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD>ʽ
if (!_originalWindowStyles.ContainsKey(hwnd))
_originalWindowStyles[hwnd] = window.GetWindowStyle();
window.ToggleWindowStyle(true, WindowStyle.Popup | WindowStyle.Visible);
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT | (int)User32.WindowStylesEx.WS_EX_LAYERED);
_clickThroughStates[hwnd] = true;
}
else
{
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle & ~(int)User32.WindowStylesEx.WS_EX_TRANSPARENT);
_clickThroughStates[hwnd] = false;
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
_originalWindowStyles.Remove(hwnd);
}
}
}
public static void Unlock(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƴ<EFBFBD><C6B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD> Disable ʱ<><CAB1><EFBFBD>Ƴ<EFBFBD><C6B3><EFBFBD>
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
}
window.ExtendsContentIntoTitleBar = true;
SetClickThrough(window, false);
// To recover the system backdrop, we need to reopen the window
WindowHelper.RestartApp(AppInfo.UnlockWindowTag);
}
}
}

View File

@@ -1,4 +1,6 @@
using Microsoft.UI.Xaml;
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
@@ -20,15 +22,21 @@ namespace BetterLyrics.WinUI3.Helper
public static void Disable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_registered.Contains(hwnd)) return;
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
window.SetIsAlwaysOnTop(false);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
UnregisterAppBar(hwnd);
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
window.ExtendsContentIntoTitleBar = true;
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
User32.SetWindowPos(
@@ -42,14 +50,12 @@ namespace BetterLyrics.WinUI3.Helper
);
_originalPositions.Remove(hwnd);
}
UnregisterAppBar(hwnd);
}
public static void Enable(Window window, int appBarHeight)
public static void Enable(Window window, int appBarHeight, DockPlacement dockPlacement)
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
//window.ExtendsContentIntoTitleBar = false;
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
@@ -58,7 +64,6 @@ namespace BetterLyrics.WinUI3.Helper
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
if (!_originalPositions.ContainsKey(hwnd))
{
@@ -68,40 +73,52 @@ namespace BetterLyrics.WinUI3.Helper
}
}
RegisterAppBar(hwnd, appBarHeight);
RegisterAppBar(hwnd, appBarHeight, dockPlacement);
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int screenHeight = User32.GetSystemMetrics(User32.SystemMetric.SM_CYSCREEN);
int y = dockPlacement == DockPlacement.Top ? 0 : screenHeight - appBarHeight;
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
y,
screenWidth,
appBarHeight,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
User32.SetWindowPosFlags.SWP_HIDEWINDOW
);
window.ExtendsContentIntoTitleBar = false;
window.ToggleWindowStyle(true, WindowStyle.Popup);
window.Show();
}
private static void RegisterAppBar(IntPtr hwnd, int height)
private static void RegisterAppBar(IntPtr hwnd, int height, DockPlacement dockPlacement)
{
if (_registered.Contains(hwnd)) return;
var uEdge = dockPlacement == DockPlacement.Top ? Shell32.ABE.ABE_TOP : Shell32.ABE.ABE_BOTTOM;
int screenHeight = User32.GetSystemMetrics(User32.SystemMetric.SM_CYSCREEN);
int top = dockPlacement == DockPlacement.Top ? 0 : screenHeight - height;
int bottom = dockPlacement == DockPlacement.Top ? height : screenHeight;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = Shell32.ABE.ABE_TOP,
uEdge = uEdge,
rc = new RECT
{
Left = 0,
Top = 0,
Top = top,
Right = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
Bottom = height,
Bottom = bottom,
},
};
// Ref: https://github.com/TwilightLemon/AppBarTest/blob/master/AppBarCreator.cs
Shell32.SHAppBarMessage(Shell32.ABM.ABM_NEW, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
_registered.Add(hwnd);
@@ -119,40 +136,56 @@ namespace BetterLyrics.WinUI3.Helper
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_REMOVE, ref abd);
_registered.Remove(hwnd);
}
public static void UpdateAppBarHeight(IntPtr hwnd, int newHeight)
private static void RefreshWorkArea()
{
if (!_registered.Contains(hwnd))
return;
User32.SendMessage(HWND.HWND_BROADCAST, User32.WindowMessage.WM_SETTINGCHANGE, IntPtr.Zero, IntPtr.Zero);
}
Shell32.APPBARDATA abd = new()
public static void UpdateAppBarHeight(IntPtr hwnd, int newHeight, DockPlacement dockPlacement)
{
App.DispatcherQueueTimer?.Debounce(() =>
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = Shell32.ABE.ABE_TOP,
rc = new RECT
if (!_registered.Contains(hwnd))
return;
var uEdge = dockPlacement == DockPlacement.Top ? Shell32.ABE.ABE_TOP : Shell32.ABE.ABE_BOTTOM;
int screenHeight = User32.GetSystemMetrics(User32.SystemMetric.SM_CYSCREEN);
int top = dockPlacement == DockPlacement.Top ? 0 : screenHeight - newHeight;
int bottom = dockPlacement == DockPlacement.Top ? newHeight : screenHeight;
Shell32.APPBARDATA abd = new()
{
Left = 0,
Top = 0,
Right = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
Bottom = newHeight,
},
};
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = uEdge,
rc = new RECT
{
Left = 0,
Top = top,
Right = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
Bottom = bottom,
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
// 同步窗口实际高度
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
newHeight,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
// 同步窗口实际高度和位置
int y = dockPlacement == DockPlacement.Top ? 0 : screenHeight - newHeight;
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
0,
y,
User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN),
newHeight,
newHeight == 0 ? User32.SetWindowPosFlags.SWP_HIDEWINDOW : User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
}, TimeSpan.FromMilliseconds(100));
}
}
}

View File

@@ -10,6 +10,29 @@ namespace BetterLyrics.WinUI3.Helper
{
public class EasingHelper
{
public static float EaseInOutSine(float t)
{
return -(MathF.Cos(MathF.PI * t) - 1f) / 2f;
}
public static float EaseInOutQuad(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
public static float EaseInOutCubic(float t)
{
return t < 0.5f ? 4 * t * t * t : 1 - MathF.Pow(-2 * t + 2, 3) / 2;
}
public static float EaseInOutQuart(float t)
{
return t < 0.5f ? 8 * t * t * t * t : 1 - MathF.Pow(-2 * t + 2, 4) / 2;
}
public static float EaseInOutQuint(float t)
{
return t < 0.5f ? 16 * t * t * t * t * t : 1 - MathF.Pow(-2 * t + 2, 5) / 2;
}
public static float EaseInOutExpo(float t)
{
return t == 0
@@ -20,25 +43,70 @@ namespace BetterLyrics.WinUI3.Helper
: (2 - MathF.Pow(2, -20 * t + 10)) / 2;
}
public static float EaseInOutQuad(float t)
public static float EaseInOutCirc(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
return t < 0.5f
? (1 - MathF.Sqrt(1 - MathF.Pow(2 * t, 2))) / 2
: (MathF.Sqrt(1 - MathF.Pow(-2 * t + 2, 2)) + 1) / 2;
}
public static float EaseInQuad(float t) => t * t;
public static float EaseOutQuad(float t) => t * (2 - t);
public static float Linear(float t) => t;
public static float SmootherStep(float t)
public static float EaseInOutBack(float t)
{
return t * t * t * (t * (6 * t - 15) + 10);
float c1 = 1.70158f;
float c2 = c1 * 1.525f;
return t < 0.5
? (MathF.Pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (MathF.Pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
public static float EaseInOutElastic(float t)
{
if (t == 0 || t == 1) return t;
float p = 0.3f;
float s = p / 4;
return t < 0.5f
? -(MathF.Pow(2, 20 * t - 10) * MathF.Sin((20 * t - 11.125f) * (2 * MathF.PI) / p)) / 2
: (MathF.Pow(2, -20 * t + 10) * MathF.Sin((20 * t - 11.125f) * (2 * MathF.PI) / p)) / 2 + 1;
}
private static float EaseOutBounce(float t)
{
if (t < 4 / 11f)
{
return (121 * t * t) / 16f;
}
else if (t < 8 / 11f)
{
return (363 / 40f * t * t) - (99 / 10f * t) + 17 / 5f;
}
else if (t < 9 / 10f)
{
return (4356 / 361f * t * t) - (35442 / 1805f * t) + 16061 / 1805f;
}
else
{
return (54 / 5f * t * t) - (513 / 25f * t) + 268 / 25f;
}
}
public static float EaseInOutBounce(float t)
{
if (t < 0.5f)
{
return (1 - EaseOutBounce(1 - 2 * t)) / 2;
}
else
{
return (1 + EaseOutBounce(2 * t - 1)) / 2;
}
}
public static float SmoothStep(float t)
{
return t * t * (3 - 2 * t);
return t * t * (3f - 2f * t);
}
public static float Linear(float t) => t;
}
}

View File

@@ -1,5 +1,7 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System;
using System.IO;
using System.Text;
using Ude;
@@ -21,5 +23,60 @@ namespace BetterLyrics.WinUI3.Helper
}
return Encoding.GetEncoding(encoding);
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
public static string? ReadLyricsCache(string title, string artist, LyricsFormat format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title}{format.ToFileExtension()}"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllBytes(cacheFilePath);
}
return null;
}
public static void WriteLyricsCache(string title, string artist, string lyrics, LyricsFormat format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title}{format.ToFileExtension()}"));
File.WriteAllText(cacheFilePath, lyrics);
}
public static void WriteAlbumArtCache(string album, string artist, byte[] img, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
File.WriteAllBytes(cacheFilePath, img);
}
public static bool IsSwitchableNormalizedMatch(string fileName, string q1, string q2)
{
var normFileName = StringHelper.Normalize(fileName.Normalize());
var normQ1 = StringHelper.Normalize(q1);
var normQ2 = StringHelper.Normalize(q2);
// 常见两种顺序
return normFileName == normQ1 + normQ2
|| normFileName == normQ2 + normQ1;
}
}
}

View File

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

View File

@@ -4,37 +4,35 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
using Windows.System;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcherHelper
public class ForegroundWindowWatcher
{
private readonly User32.WinEventProc _winEventDelegate;
private readonly List<User32.HWINEVENTHOOK> _hooks = new();
private HWND _currentForeground = HWND.NULL;
private readonly IntPtr _selfHwnd;
private readonly DispatcherTimer _pollingTimer;
private DateTime _lastEventTime = DateTime.MinValue;
private const int ThrottleIntervalMs = 1000;
private readonly ThrottleHelper _winEventProcThrottle = new(TimeSpan.FromSeconds(1));
public delegate void WindowChangedHandler(HWND hwnd);
private readonly WindowChangedHandler _onWindowChanged;
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
private readonly DispatcherTimer _timer;
public ForegroundWindowWatcher(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new User32.WinEventProc(WinEventProc);
_pollingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_pollingTimer.Tick += (_, _) =>
{
if (_currentForeground != IntPtr.Zero && _currentForeground != _selfHwnd)
_onWindowChanged?.Invoke(_currentForeground);
};
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += Timer_Tick;
}
public void Start()
@@ -65,7 +63,7 @@ namespace BetterLyrics.WinUI3.Helper
)
);
_pollingTimer.Start();
_timer.Start();
}
public void Stop()
@@ -74,7 +72,16 @@ namespace BetterLyrics.WinUI3.Helper
User32.UnhookWinEvent(hook);
_hooks.Clear();
_pollingTimer.Stop();
_timer.Stop();
}
private void Timer_Tick(object? sender, object e)
{
if (_currentForeground != HWND.NULL)
{
_onWindowChanged?.Invoke(_currentForeground);
}
}
private void WinEventProc(
@@ -87,15 +94,9 @@ namespace BetterLyrics.WinUI3.Helper
uint dwmsEventTime
)
{
if (hwnd == IntPtr.Zero || hwnd == _selfHwnd)
if (hwnd == IntPtr.Zero)
return;
var now = DateTime.Now;
if ((now - _lastEventTime).TotalMilliseconds < ThrottleIntervalMs)
return;
_lastEventTime = now;
if (eventType == User32.EventConstants.EVENT_SYSTEM_FOREGROUND)
{
_currentForeground = hwnd;

View File

@@ -0,0 +1,46 @@
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.Helper
{
public class GlobalHotKeyHelper
{
private static Dictionary<int, Action> _hotKeyActions = [];
private static int _nextId = 0;
public static void RegisterHotKey(Window window, User32.HotKeyModifiers modifiers, uint key, Action action)
{
HWND hwnd = WindowNative.GetWindowHandle(window);
int id = _nextId++;
User32.RegisterHotKey(hwnd, id, modifiers, key);
_hotKeyActions[id] = action;
}
public static void UnregisterAllHotKeys(Window window)
{
HWND hwnd = WindowNative.GetWindowHandle(window);
foreach (var id in _hotKeyActions.Keys.ToList())
{
User32.UnregisterHotKey(hwnd, id);
_hotKeyActions.Remove(id);
}
}
public static bool TryInvokeAction(int id)
{
if (_hotKeyActions.TryGetValue(id, out var action))
{
action?.Invoke();
return true;
}
return false;
}
}
}

View File

@@ -1,5 +1,14 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.WinUI.Helpers;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Generic;
using System.IO;
@@ -7,10 +16,6 @@ using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
@@ -19,7 +24,7 @@ namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
public const int AccentColorCount = 3;
private const int _accentColorCount = 1;
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
@@ -30,62 +35,44 @@ namespace BetterLyrics.WinUI3.Helper
return stream;
}
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(string text, int width, int height)
public static RandomAccessStreamReference ByteArrayToRandomAccessStreamReference(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
var writer = new DataWriter(stream);
writer.WriteBytes(bytes);
writer.StoreAsync().GetAwaiter().GetResult();
writer.FlushAsync().GetAwaiter().GetResult();
writer.DetachStream();
return RandomAccessStreamReference.CreateFromStream(stream);
}
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(int width, int height)
{
var device = CanvasDevice.GetSharedDevice();
var renderTarget = new CanvasRenderTarget(device, width, height, 96);
// 居中绘制文字
// 随机生成渐变色
Windows.UI.Color RandomColor()
{
var rand = new Random(Guid.NewGuid().GetHashCode());
double h = rand.NextDouble() * 360;
double s = 0.35 + rand.NextDouble() * 0.3; // 0.35~0.65,适中饱和度
double l = 0.5 + rand.NextDouble() * 0.3; // 0.5~0.8,明亮
return HslToColor(h, s, l);
}
Windows.UI.Color color1 = RandomColor();
Windows.UI.Color color2 = RandomColor();
using (var ds = renderTarget.CreateDrawingSession())
{
// 背景
ds.Clear(Colors.LightGray);
// 文字格式
var format = new CanvasTextFormat
// 绘制线性渐变背景
var gradientBrush = new Microsoft.Graphics.Canvas.Brushes.CanvasLinearGradientBrush(ds, color1, color2)
{
FontSize = Math.Min(width, height) / 6f,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
HorizontalAlignment = CanvasHorizontalAlignment.Center,
VerticalAlignment = CanvasVerticalAlignment.Center,
WordWrapping = CanvasWordWrapping.Wrap,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
Options = CanvasDrawTextOptions.Default,
StartPoint = new System.Numerics.Vector2(0, 0),
EndPoint = new System.Numerics.Vector2(width, height)
};
// 设定边距
float margin = Math.Min(width, height) / 12f;
float availableWidth = width - 2 * margin;
float availableHeight = height - 2 * margin;
// 计算合适的字体大小以适应内容区域
float fontSize = format.FontSize;
float minFontSize = 8f;
float maxFontSize = format.FontSize;
CanvasTextLayout layout;
do
{
format.FontSize = fontSize;
layout = new CanvasTextLayout(
ds,
text,
format,
availableWidth,
availableHeight
);
if (
layout.LayoutBounds.Width <= availableWidth
&& layout.LayoutBounds.Height <= availableHeight
)
break;
fontSize -= 1f;
} while (fontSize >= minFontSize);
// 居中绘制文字(在内容区域内居中)
var bounds = layout.LayoutBounds;
var x = margin + (availableWidth - (float)bounds.Width) / 2f - (float)bounds.X;
var y = margin + (availableHeight - (float)bounds.Height) / 2f - (float)bounds.Y;
ds.DrawTextLayout(layout, new Vector2(x, y), Colors.DarkGray);
ds.FillRectangle(0, 0, width, height, gradientBrush);
}
// 保存为 PNG 并转为 byte[]
@@ -100,6 +87,31 @@ namespace BetterLyrics.WinUI3.Helper
}
return buffer;
}
// HSL转Color
static Windows.UI.Color HslToColor(double h, double s, double l)
{
h = h / 360.0;
double r = l, g = l, b = l;
if (s != 0)
{
double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
double p = 2 * l - q;
r = HueToRgb(p, q, h + 1.0 / 3.0);
g = HueToRgb(p, q, h);
b = HueToRgb(p, q, h - 1.0 / 3.0);
}
return Windows.UI.Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255));
}
static double HueToRgb(double p, double q, double t)
{
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1.0 / 6.0) return p + (q - p) * 6 * t;
if (t < 1.0 / 2.0) return q;
if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6;
return p;
}
}
public static List<Windows.UI.Color> GetAccentColorsFromByte(byte[] bytes)
@@ -127,7 +139,7 @@ namespace BetterLyrics.WinUI3.Helper
// 按出现次数排序,取前 AccentColorCount 个
var topColors = colorCount
.OrderByDescending(kv => kv.Value)
.Take(AccentColorCount)
.Take(_accentColorCount)
.Select(kv => kv.Key)
.ToList();
@@ -137,39 +149,87 @@ namespace BetterLyrics.WinUI3.Helper
.ToList();
}
//public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
//{
// var stream = new InMemoryRandomAccessStream();
// await stream.WriteAsync(imageBytes.AsBuffer());
// stream.Seek(0);
public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
stream.Seek(0);
// var bitmapImage = new BitmapImage();
// await bitmapImage.SetSourceAsync(stream);
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(stream);
// return bitmapImage;
//}
return bitmapImage;
}
//public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
// await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
//public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
//{
// if (imageBytes == null || imageBytes.Length == 0)
// return null;
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
{
if (imageBytes == null || imageBytes.Length == 0)
return null;
// InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
// await stream.WriteAsync(imageBytes.AsBuffer());
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
return stream;
}
// return stream;
//}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
using var memoryStream = new MemoryStream();
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
using var reader = new DataReader(stream);
await reader.LoadAsync((uint)stream.Size);
byte[] buffer = new byte[stream.Size];
reader.ReadBytes(buffer);
return buffer;
}
public static float GetAverageLuminance(CanvasBitmap bitmap)
{
var pixels = bitmap.GetPixelBytes();
double sum = 0;
for (int i = 0; i < pixels.Length; i += 4)
{
// BGRA
byte b = pixels[i];
byte g = pixels[i + 1];
byte r = pixels[i + 2];
// 忽略A
double y = 0.299 * r + 0.587 * g + 0.114 * b;
sum += y / 255.0;
}
return (float)(sum / (pixels.Length / 4));
}
public static byte[] MakeSquareWithThemeColor(byte[] imageBytes)
{
using var image = Image.Load<Rgba32>(imageBytes);
if (image.Width == image.Height)
{
// 已经是正方形,直接返回
return imageBytes;
}
int size = Math.Max(image.Width, image.Height);
var themeColor = Rgba32.ParseHex(GetAccentColorsFromByte(imageBytes).FirstOrDefault().ToHex());
// 新建正方形画布
using var square = new Image<Rgba32>(size, size, themeColor);
// 计算居中位置
int offsetX = (size - image.Width) / 2;
int offsetY = (size - image.Height) / 2;
// 绘制原图到正方形画布
square.Mutate(ctx => ctx.DrawImage(image, new Point(offsetX, offsetY), 1f));
// 保存为 PNG 字节流
using var ms = new MemoryStream();
square.Save(ms, new PngEncoder());
return ms.ToArray();
}
}
}

View File

@@ -0,0 +1,128 @@
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Helpers.General;
using NTextCat;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Windows.Globalization;
namespace BetterLyrics.WinUI3.Services
{
public class LanguageHelper
{
private static readonly RankedLanguageIdentifierFactory _factory = new();
private static readonly RankedLanguageIdentifier _identifier;
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public static List<Models.LanguageInfo> SupportedTargetLanguages =>
[
new Models.LanguageInfo("ar", "العربية"),
new Models.LanguageInfo("az", "Azərbaycan dili"),
new Models.LanguageInfo("zh-Hans", "简体中文"),
new Models.LanguageInfo("zh-Hant", "繁體中文"),
new Models.LanguageInfo("cs", "Čeština"),
new Models.LanguageInfo("da", "Dansk"),
new Models.LanguageInfo("nl", "Nederlands"),
new Models.LanguageInfo("en", "English"),
new Models.LanguageInfo("eo", "Esperanto"),
new Models.LanguageInfo("fi", "Suomi"),
new Models.LanguageInfo("fr", "Français"),
new Models.LanguageInfo("de", "Deutsch"),
new Models.LanguageInfo("el", "Ελληνικά"),
new Models.LanguageInfo("he", "עברית"),
new Models.LanguageInfo("hi", "हिन्दी"),
new Models.LanguageInfo("hu", "Magyar"),
new Models.LanguageInfo("id", "Bahasa Indonesia"),
new Models.LanguageInfo("ga", "Gaeilge"),
new Models.LanguageInfo("it", "Italiano"),
new Models.LanguageInfo("ja", "日本語"),
new Models.LanguageInfo("ko", "한국어"),
new Models.LanguageInfo("fa", "فارسی"),
new Models.LanguageInfo("pl", "Polski"),
new Models.LanguageInfo("pt", "Português"),
new Models.LanguageInfo("ru", "Русский"),
new Models.LanguageInfo("sk", "Slovenčina"),
new Models.LanguageInfo("es", "Español"),
new Models.LanguageInfo("sv", "Svenska"),
new Models.LanguageInfo("tr", "Türkçe"),
new Models.LanguageInfo("uk", "Українська"),
new Models.LanguageInfo("vi", "Tiếng Việt"),
];
static LanguageHelper()
{
_identifier = _factory.Load(PathHelper.LanguageProfilePath);
}
private static string? ThreeLetterToTwoLetter(string? threeLetterCode)
{
if (threeLetterCode == null) return null;
foreach (var ci in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
if (string.Equals(ci.ThreeLetterISOLanguageName, threeLetterCode, StringComparison.OrdinalIgnoreCase))
{
return ci.TwoLetterISOLanguageName;
}
}
return null;
}
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
string? code = ThreeLetterToTwoLetter(_identifier.Identify(text).FirstOrDefault()?.Item1.Iso639_2T);
if (code != null && code == "zh")
{
if (ChineseConverter.ConvertToTraditionalChinese(text) == text)
{
return "zh-Hant";
}
else
{
return "zh-Hans";
}
}
return code;
}
public static bool IsCJK(string text)
{
return DetectLanguageCode(text)?.Substring(0, 2) switch
{
"zh" or "ja" or "ko" => true,
_ => false
};
}
public static string ConvertToCountryCode(string? languageCode)
{
if (languageCode == null) return "us";
return languageCode switch
{
"zh" => "cn",
"zh-Hans" => "cn",
"zh-Hant" => "tw",
"ja" => "jp",
"ko" => "kr",
_ => "us"
};
}
public static string GetUserTargetLanguageCode()
{
return SupportedTargetLanguages[_settingsService.SelectedTargetLanguageIndex].Code;
}
public static int GetDefaultTargetLanguageIndex()
{
int found = SupportedTargetLanguages.FindIndex(x => ApplicationLanguages.Languages.FirstOrDefault()?.Contains(x.Code) == true);
if (found == -1) found = 7; // 默认使用英语
return found;
}
}
}

View File

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

View File

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using Lyricify.Lyrics.Models;
using System;
using System.Collections.Generic;
@@ -9,43 +10,35 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Windows.Globalization.Fonts;
using LyricsData = BetterLyrics.WinUI3.Models.LyricsData;
namespace BetterLyrics.WinUI3.Helper
{
public class LyricsParser
{
private List<List<LyricsLine>> _multiLangLyricsLines = [];
private List<LyricsData> _lyricsDataArr = [];
public List<List<LyricsLine>> Parse(string? raw, LyricsFormat? lyricsFormat = null, string? title = null, string? artist = null, int durationMs = 0)
public List<LyricsData> Parse(string? raw, int? durationMs)
{
_multiLangLyricsLines = [];
durationMs ??= (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
_lyricsDataArr = [];
if (raw == null)
{
_multiLangLyricsLines.Add(
[
new LyricsLine
{
StartMs = 0,
EndMs = durationMs,
Text = App.ResourceLoader!.GetString("LyricsNotFound"),
CharTimings = [],
},
]
);
_lyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder(durationMs.Value));
}
else
{
switch (lyricsFormat)
switch (raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw);
break;
case LyricsFormat.Qrc:
ParseUsingLyricify(Lyricify.Lyrics.Parsers.QrcParser.Parse(raw).Lines);
ParseQQNeteaseKugou(Lyricify.Lyrics.Parsers.QrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Krc:
ParseUsingLyricify(Lyricify.Lyrics.Parsers.KrcParser.Parse(raw).Lines);
ParseQQNeteaseKugou(Lyricify.Lyrics.Parsers.KrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(raw);
@@ -54,8 +47,8 @@ namespace BetterLyrics.WinUI3.Helper
break;
}
}
PostProcessLyricsLines(durationMs);
return _multiLangLyricsLines;
_lyricsDataArr.Add(new LyricsData()); // 为机翻预留
return _lyricsDataArr;
}
private void ParseLrc(string raw)
@@ -119,9 +112,8 @@ namespace BetterLyrics.WinUI3.Helper
int languageCount = grouped.Max(g => g.Count());
// 初始化每种语言的歌词列表
_multiLangLyricsLines.Clear();
for (int i = 0; i < languageCount; i++)
_multiLangLyricsLines.Add(new List<LyricsLine>());
_lyricsDataArr.Clear();
for (int i = 0; i < languageCount; i++) _lyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
foreach (var group in grouped)
@@ -135,9 +127,8 @@ namespace BetterLyrics.WinUI3.Helper
var line = new LyricsLine
{
StartMs = start,
EndMs = 0, // 稍后统一修正
Text = text,
CharTimings = [],
OriginalText = text,
LyricsChars = [],
};
if (syllables != null && syllables.Count > 0)
{
@@ -146,11 +137,10 @@ namespace BetterLyrics.WinUI3.Helper
{
var (charStart, charText) = syllables[j];
int startIndex = currentIndex;
line.CharTimings.Add(
new CharTiming
line.LyricsChars.Add(
new LyricsChar
{
StartMs = charStart,
EndMs = 0, // Fixed later
Text = charText ?? "",
StartIndex = startIndex,
}
@@ -158,7 +148,7 @@ namespace BetterLyrics.WinUI3.Helper
currentIndex += charText?.Length ?? 0;
}
}
_multiLangLyricsLines[langIdx].Add(line);
_lyricsDataArr[langIdx].LyricsLines.Add(line);
}
}
}
@@ -167,7 +157,8 @@ namespace BetterLyrics.WinUI3.Helper
{
try
{
List<LyricsLine> singleLangLyricsLine = [];
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
var xdoc = XDocument.Parse(raw);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
@@ -180,49 +171,92 @@ namespace BetterLyrics.WinUI3.Helper
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 处理分词分时
var spans = p.Elements().Where(s => s.Name.LocalName == "span").ToList();
// 只获取一级span且排除ttm:role="x-bg"的span
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-bg")
.ToList();
string text = string.Concat(spans.Select(s => s.Value));
var charTimings = new List<CharTiming>();
// 原文和翻译分离
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-translation")
.ToList();
var translationTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
int startIndex = 0;
for (int i = 0; i < spans.Count; i++)
// 原文(非 CJK 语言添加空格)
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
if (!LanguageHelper.IsCJK(originalText))
{
foreach (var span in originalTextSpans)
{
span.Value += " ";
}
originalText = string.Concat(originalTextSpans.Select(s => s.Value));
}
var originalCharTimings = new List<LyricsChar>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
var span = spans[i];
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
if (sStartMs == 0 && sEndMs == 0)
continue;
if (sEndMs == 0)
sEndMs =
(i + 1 < spans.Count)
? ParseTtmlTime(spans[i + 1].Attribute("begin")?.Value)
: pEndMs;
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = 0, StartIndex = startIndex, Text = span.Value });
startIndex += span.Value.Length;
originalCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
originalText = p.Value;
if (spans.Count == 0)
text = p.Value;
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = originalText,
LyricsChars = originalCharTimings,
});
singleLangLyricsLine.Add(
new LyricsLine
// 翻译
string translationText = string.Concat(translationTextSpans.Select(s => s.Value));
var translationCharTimings = new List<LyricsChar>();
int translationStartIndex = 0;
foreach (var span in translationTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
translationCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = translationStartIndex,
Text = span.Value
});
translationStartIndex += span.Value.Length;
}
if (translationTextSpans.Count > 0)
{
translationLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = 0,
Text = text,
CharTimings = charTimings,
}
);
EndMs = pEndMs,
OriginalText = translationText,
LyricsChars = translationCharTimings,
});
}
}
_multiLangLyricsLines.Add(singleLangLyricsLine);
_lyricsDataArr.Add(new LyricsData(originalLines));
if (translationLines.Count > 0)
_lyricsDataArr.Add(new LyricsData(translationLines));
}
catch
{
@@ -230,7 +264,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
private int ParseTtmlTime(string? t)
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
@@ -291,7 +325,7 @@ namespace BetterLyrics.WinUI3.Helper
return 0;
}
private void ParseUsingLyricify(List<ILineInfo>? lines)
private void ParseQQNeteaseKugou(List<ILineInfo>? lines)
{
lines = lines?.Where(x => x.Text != string.Empty).ToList();
List<LyricsLine> lyricsLines = [];
@@ -305,9 +339,9 @@ namespace BetterLyrics.WinUI3.Helper
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
EndMs = 0,
Text = lineRead.Text,
CharTimings = [],
EndMs = lineRead.EndTime ?? 0,
OriginalText = lineRead.Text,
LyricsChars = [],
};
var syllables = (lineRead as SyllableLineInfo)?.Syllables;
@@ -321,22 +355,14 @@ namespace BetterLyrics.WinUI3.Helper
)
{
var syllable = syllables[syllableIndex];
var charTiming = new CharTiming
var charTiming = new LyricsChar
{
StartMs = syllable.StartTime,
EndMs = 0,
EndMs = syllable.EndTime,
Text = syllable.Text,
StartIndex = startIndex,
};
if (syllableIndex + 1 < syllables.Count)
{
charTiming.EndMs = syllables[syllableIndex + 1].StartTime;
}
else
{
charTiming.EndMs = lineWrite.EndMs;
}
lineWrite.CharTimings.Add(charTiming);
lineWrite.LyricsChars.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
@@ -345,56 +371,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
_multiLangLyricsLines.Add(lyricsLines);
}
private void PostProcessLyricsLines(int durationMs)
{
for (int langIdx = 0; langIdx < _multiLangLyricsLines.Count; langIdx++)
{
var linesInSingleLang = _multiLangLyricsLines[langIdx];
for (int i = 0; i < linesInSingleLang.Count; i++)
{
if (i + 1 < linesInSingleLang.Count)
{
linesInSingleLang[i].EndMs = linesInSingleLang[i + 1].StartMs;
}
else
{
linesInSingleLang[i].EndMs = durationMs;
}
// 修正 CharTimings 的 EndMs
var timings = linesInSingleLang[i].CharTimings;
if (timings.Count > 0)
{
for (int j = 0; j < timings.Count; j++)
{
if (j + 1 < timings.Count)
{
timings[j].EndMs = timings[j + 1].StartMs;
}
else
{
timings[j].EndMs = linesInSingleLang[i].EndMs;
}
}
}
}
if (linesInSingleLang.Count > 0 && linesInSingleLang[0].StartMs > 0)
{
linesInSingleLang.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = linesInSingleLang[0].StartMs,
Text = "● ● ●",
CharTimings = [],
}
);
}
}
_lyricsDataArr.Add(new LyricsData(lyricsLines));
}
}
}

View File

@@ -0,0 +1,47 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Helper
{
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.FileProperties;
public static class MetadataHelper
{
public const string AppAuthor = "Zhe Fang";
public const string AppDisplayName = "Better Lyrics";
public const string AppName = "BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string QQGroupUrl = "https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info";
public const string DiscordUrl = "https://discord.gg/5yAQPnyCKv";
public static async Task<DateTime> GetBuildDate()
{
var assembly = Assembly.GetExecutingAssembly();
var filePath = assembly.Location;
if (!File.Exists(filePath))
return DateTime.MinValue;
StorageFile file = await StorageFile.GetFileFromPathAsync(filePath);
// 获取文件基本属性
BasicProperties props = await file.GetBasicPropertiesAsync();
// 返回修改日期
return props.DateModified.DateTime;
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class NetHelper
{
public static async Task<bool> CheckConnectivity(string url)
{
try
{
using var client = new System.Net.Http.HttpClient();
// Try to reach a reliable endpoint
var res = await client.GetAsync(url);
return res.IsSuccessStatusCode;
}
catch
{
return false; // If any exception occurs, assume no connectivity
}
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Helper
{
public class PathHelper
{
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
public static string LanguageProfilePath => Path.Combine(AssetsFolder, "Core14.profile.xml");
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 AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.json");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
public static string TranslationCacheDirectory => Path.Combine(CacheFolder, "translations");
public static string QQTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "qq");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(QQTranslationCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class StringHelper
{
// 去除空格、括号、下划线、横杠、点、大小写等
public static string Normalize(string s) =>
new string(s
.Where(c => char.IsLetterOrDigit(c))
.ToArray())
.ToLowerInvariant();
public static string NewLine = "\n";
}
}

View File

@@ -0,0 +1,74 @@
using Microsoft.UI.Dispatching;
using System;
using Vanara.Extensions;
using Vanara.PInvoke;
using static Vanara.PInvoke.CoreAudio;
namespace BetterLyrics.WinUI3.Helper
{
public static class SystemVolumeHelper
{
private readonly static IMMDeviceEnumerator _deviceEnumerator = new();
private static IAudioEndpointVolume? _endpointVolume = null;
private static VolumeCallbackImpl? _callbackImpl;
private static int _masterVolume = 0;
private static DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
public static event Action<int>? VolumeChanged;
static SystemVolumeHelper()
{
var device = _deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);
if (device != null)
{
device.Activate(typeof(IAudioEndpointVolume).GUID, 0, null, out var obj);
if (obj is IAudioEndpointVolume endpointVolume)
{
_endpointVolume = endpointVolume;
_callbackImpl = new VolumeCallbackImpl();
_endpointVolume.RegisterControlChangeNotify(_callbackImpl);
}
}
}
/// <summary>
/// 获取当前系统主音量0~100
/// </summary>
public static int GetMasterVolume()
{
if (_endpointVolume != null)
{
float level = _endpointVolume.GetMasterVolumeLevelScalar();
_masterVolume = (int)(level * 100);
}
return _masterVolume;
}
/// <summary>
/// 设置当前系统主音量0~100
/// </summary>
public static void SetMasterVolume(int volume)
{
if (_masterVolume == volume) return;
_masterVolume = volume;
_endpointVolume?.SetMasterVolumeLevelScalar(_masterVolume / 100f, Guid.Empty);
}
// 内部回调实现
private class VolumeCallbackImpl : IAudioEndpointVolumeCallback
{
HRESULT IAudioEndpointVolumeCallback.OnNotify(nint pNotify)
{
var data = pNotify.ToStructure<AUDIO_VOLUME_NOTIFICATION_DATA>();
_masterVolume = (int)(data.fMasterVolume * 100);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
VolumeChanged?.Invoke(_masterVolume);
});
return HRESULT.S_OK;
}
}
}
}

View File

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

View File

@@ -1,31 +1,28 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Diagnostics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public class AnimationHelper
{
public const int DebounceDefaultDuration = 200;
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
}
public class ValueTransition<T>
where T : struct
{
private T _currentValue;
private float _durationSeconds;
private readonly EasingType? _easingType;
private EasingType? _easingType;
private Func<T, T, float, T> _interpolator;
private bool _isTransitioning;
private float _progress;
private T _startValue;
private T _targetValue;
public float DurationSeconds => _durationSeconds;
public bool IsTransitioning => _isTransitioning;
public T Value => _currentValue;
public T TargetValue => _targetValue;
public ValueTransition(T initialValue, float durationSeconds, Func<T, T, float, T>? interpolator = null, EasingType? easingType = null)
{
@@ -44,16 +41,23 @@ namespace BetterLyrics.WinUI3.Helper
else if (easingType.HasValue)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType.Value);
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
else
{
_interpolator = GetInterpolatorByEasingType(EasingType.Linear);
_easingType = EasingType.Linear;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
}
public void JumpTo(T value)
public void SetDuration(float seconds)
{
if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive.");
_durationSeconds = seconds;
}
private void JumpTo(T value)
{
_currentValue = value;
_startValue = value;
@@ -71,8 +75,14 @@ namespace BetterLyrics.WinUI3.Helper
_isTransitioning = false;
}
public void StartTransition(T targetValue)
public void StartTransition(T targetValue, bool jumpTo = false)
{
if (jumpTo)
{
JumpTo(targetValue);
return;
}
if (!targetValue.Equals(_currentValue))
{
_startValue = _currentValue;
@@ -82,6 +92,12 @@ namespace BetterLyrics.WinUI3.Helper
}
}
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;
@@ -110,26 +126,41 @@ namespace BetterLyrics.WinUI3.Helper
float t = progress;
switch (type)
{
case EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
case EasingType.EaseInOutSine:
t = EasingHelper.EaseInOutSine(t);
break;
case EasingType.EaseInOutQuad:
t = EasingHelper.EaseInOutQuad(t);
break;
case EasingType.EaseInQuad:
t = EasingHelper.EaseInQuad(t);
case EasingType.EaseInOutCubic:
t = EasingHelper.EaseInOutCubic(t);
break;
case EasingType.EaseOutQuad:
t = EasingHelper.EaseOutQuad(t);
case EasingType.EaseInOutQuart:
t = EasingHelper.EaseInOutQuart(t);
break;
case EasingType.Linear:
t = EasingHelper.Linear(t);
case EasingType.EaseInOutQuint:
t = EasingHelper.EaseInOutQuint(t);
break;
case EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case EasingType.SmootherStep:
t = EasingHelper.SmootherStep(t);
case EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
break;
@@ -139,5 +170,11 @@ namespace BetterLyrics.WinUI3.Helper
}
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
}
public void SetEasingType(EasingType easingType)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
}
}

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowColorHelper
{
public static Color GetDominantColor(IntPtr myHwnd, WindowColorSampleMode mode)
{
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return Color.Transparent;
switch (mode)
{
case WindowColorSampleMode.BelowWindow:
{
int screenWidth = User32.GetSystemMetrics(User32.SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
case WindowColorSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return Color.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
case WindowColorSampleMode.WindowEdge:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return Color.Transparent;
var edgeThickness = new Thickness(36, 0, 36, 0);
List<Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// 合并四边平均色
if (edgeColors.Count == 0)
return Color.Transparent;
long r = 0,
g = 0,
b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
);
}
default:
return Color.Transparent;
}
}
private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, Gdi32.RasterOperationMode.SRCCOPY);
gDest.ReleaseHdc(hdcDest);
User32.ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static Color ComputeAverageColor(Bitmap bmp)
{
long r = 0, g = 0, b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0) return Color.Transparent;
return Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
}
}

View File

@@ -1,10 +1,11 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using Windows.ApplicationModel.Core;
using WinRT.Interop;
using WinUIEx;
@@ -25,18 +26,7 @@ namespace BetterLyrics.WinUI3.Helper
}
}
public static void ExitAllWindows()
{
while (_activeWindows.Count > 0)
{
var window = _activeWindows[0];
((Window)window).Close();
_activeWindows.Remove(window);
}
App.Current.Exit();
}
public static T GetWindowByWindowType<T>()
public static T? GetWindowByWindowType<T>()
{
foreach (var window in _activeWindows)
{
@@ -45,34 +35,34 @@ namespace BetterLyrics.WinUI3.Helper
return castedWindow;
}
}
throw new InvalidOperationException($"No window of type {typeof(T).Name} found.");
return default;
}
public static void OpenOrShowWindow<T>()
public static void OpenWindow<T>()
{
var window = _activeWindows.Find(w => w is T);
if (window != null)
if (window == null)
{
var castedWindow = (Window)window;
castedWindow.Restore();
}
else
{
object newWindow;
if (typeof(T) == typeof(LyricsWindow))
{
newWindow = new LyricsWindow();
window = new LyricsWindow();
((LyricsWindow)window).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
}
else if (typeof(T) == typeof(SettingsWindow))
{
newWindow = new SettingsWindow();
window = new SettingsWindow();
}
else if (typeof(T) == typeof(MusicGalleryWindow))
{
window = new MusicGalleryWindow();
}
else
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
((Window)newWindow).Activate();
TrackWindow(newWindow);
TrackWindow(window);
}
var castedWindow = (Window)window;
castedWindow.Restore();
}
public static void RestartApp(string args = "")
@@ -98,7 +88,19 @@ namespace BetterLyrics.WinUI3.Helper
private static void TrackWindow(object window)
{
if (!_activeWindows.Contains(window))
{
_activeWindows.Add(window);
var castedWindow = (Window)window;
castedWindow.Closed += WindowHelper_Closed;
}
}
private static void WindowHelper_Closed(object sender, WindowEventArgs args)
{
if (_activeWindows.Contains(sender))
{
_activeWindows.Remove(sender);
}
}
}
}

View File

@@ -1,9 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging.Messages;
namespace BetterLyrics.WinUI3.Messages
{
public class ShowNotificatonMessage(Notification value) : ValueChangedMessage<Notification>(value) { }
}

View File

@@ -0,0 +1,24 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class AlbumArtSearchProviderInfo : ObservableObject
{
[ObservableProperty]
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial AlbumArtSearchProvider Provider { get; set; }
public AlbumArtSearchProviderInfo() { }
public AlbumArtSearchProviderInfo(AlbumArtSearchProvider provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class GroupInfoList(IEnumerable<object> items) : List<object>(items)
{
public required object Key { get; set; }
public override string ToString()
{
return "Group " + Key.ToString();
}
}
}

View File

@@ -0,0 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class LanguageInfo : ObservableObject
{
[ObservableProperty]
public partial string Code { get; set; }
[ObservableProperty]
public partial string Name { get; set; }
public LanguageInfo(string code, string name)
{
Code = code;
Name = name;
}
}
}

View File

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

View File

@@ -1,10 +1,12 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Models
{
public class CharTiming
public class LyricsChar
{
public int EndMs { get; set; }
public int? EndMs { get; set; }
public int StartIndex { get; set; }
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty;

View File

@@ -1,15 +1,137 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using Lyricify.Lyrics.Helpers.General;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StringHelper = BetterLyrics.WinUI3.Helper.StringHelper;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsData
{
public int LanguageIndex { get; set; } = 0;
public List<LyricsLine> LyricsLines { get; set; }
public string? LanguageCode => LanguageHelper.DetectLanguageCode(WrappedOriginalText);
public string WrappedOriginalText => string.Join(StringHelper.NewLine, LyricsLines.Select(line => line.OriginalText));
public List<LyricsLine> LyricsLines => MultiLangLyricsLines[LanguageIndex];
public LyricsData()
{
LyricsLines = [];
}
public List<List<LyricsLine>> MultiLangLyricsLines { get; set; } = [];
public LyricsData(List<LyricsLine> lyricsLines)
{
LyricsLines = lyricsLines;
}
public void SetDisplayedTextAlongWith(LyricsData translationData)
{
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= translationData.LyricsLines.Count)
{
line.DisplayedText = line.OriginalText; // No translation available, keep original text
}
else
{
if (translationData.LanguageCode?.Substring(0, 2) == "zh")
{
string tmp = "";
if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hant")
{
tmp = ChineseConverter.ConvertToTraditionalChinese(translationData.LyricsLines[i].OriginalText);
}
else if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hans")
{
tmp = ChineseConverter.ConvertToSimplifiedChinese(translationData.LyricsLines[i].OriginalText);
}
line.DisplayedText = $"{line.OriginalText}\n{tmp}";
}
else
{
line.DisplayedText = $"{line.OriginalText}\n{translationData.LyricsLines[i].OriginalText}";
}
}
i++;
}
}
public void SetDisplayedTextAlongWith(string translation)
{
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= translationArr.Count)
{
line.DisplayedText = line.OriginalText; // No translation available, keep original text
}
else
{
line.DisplayedText = $"{line.OriginalText}{StringHelper.NewLine}{translationArr[i]}";
}
i++;
}
}
public void SetDisplayedTextInOriginalText()
{
foreach (var line in LyricsLines)
{
line.DisplayedText = line.OriginalText;
}
}
public LyricsData CreateLyricsDataFrom(string translation)
{
var result = new LyricsData(LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
}).ToList());
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in result.LyricsLines)
{
if (i >= translationArr.Count)
{
break;
}
else
{
line.OriginalText = translationArr[i];
}
i++;
}
return result;
}
public static LyricsData GetNotfoundPlaceholder(int durationMs)
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = durationMs,
OriginalText = App.ResourceLoader!.GetString("LyricsNotFound"),
LyricsChars = [],
}]);
}
public static LyricsData GetLoadingPlaceholder()
{
return new LyricsData([
new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = "● ● ●",
DisplayedText = "● ● ●",
LyricsChars = [],
},
]);
}
}
}

View File

@@ -9,25 +9,25 @@ namespace BetterLyrics.WinUI3.Models
{
public class LyricsLine
{
public ValueTransition<float> AngleTransition { get; set; } = new(initialValue: 0f, durationSeconds: 0.3f);
public ValueTransition<float> BlurAmountTransition { get; set; } = new(initialValue: 0f, durationSeconds: 0.3f);
public ValueTransition<float> HighlightOpacityTransition { get; set; } = new(initialValue: 0f, durationSeconds: 0.3f);
public ValueTransition<float> OpacityTransition { get; set; } = new(initialValue: 0f, durationSeconds: 0.3f);
public ValueTransition<float> ScaleTransition { get; set; } = new(initialValue: 0.95f, durationSeconds: 0.3f);
private const float _animationDuration = 0.3f;
public ValueTransition<float> AngleTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> BlurAmountTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> HighlightOpacityTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> OpacityTransition { get; set; } = new(initialValue: 0f, durationSeconds: _animationDuration);
public ValueTransition<float> ScaleTransition { get; set; } = new(initialValue: 0.95f, durationSeconds: _animationDuration);
public CanvasTextLayout? CanvasTextLayout { get; set; }
public Vector2 CenterPosition { get; set; }
public Vector2 Position { get; set; }
public List<CharTiming> CharTimings { get; set; } = [];
public int DurationMs => EndMs - StartMs;
public int EndMs { get; set; }
public List<LyricsChar> LyricsChars { get; set; } = [];
public int? DurationMs => EndMs - StartMs;
public int? EndMs { get; set; }
public int StartMs { get; set; }
public string Text { get; set; } = "";
public string DisplayedText { get; set; } = "";
public string OriginalText { get; set; } = "";
}
}

View File

@@ -0,0 +1,25 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class MediaSourceProviderInfo : ObservableObject
{
[ObservableProperty]
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial string Provider { get; set; }
public MediaSourceProviderInfo() { }
public MediaSourceProviderInfo(string provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -1,35 +0,0 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Models
{
public partial class Notification : ObservableObject
{
[ObservableProperty]
public partial bool IsForeverDismissable { get; set; }
[ObservableProperty]
public partial string? Message { get; set; }
[ObservableProperty]
public partial string? RelatedSettingsKeyName { get; set; }
[ObservableProperty]
public partial InfoBarSeverity Severity { get; set; }
[ObservableProperty]
public partial Visibility Visibility { get; set; }
public Notification(string? message = null, InfoBarSeverity severity = InfoBarSeverity.Informational, bool isForeverDismissable = false, string? relatedSettingsKeyName = null)
{
Message = message;
Severity = severity;
IsForeverDismissable = isForeverDismissable;
Visibility = IsForeverDismissable ? Visibility.Visible : Visibility.Collapsed;
RelatedSettingsKeyName = relatedSettingsKeyName;
}
}
}

View File

@@ -1,6 +1,8 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Models
{
@@ -9,11 +11,12 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string? Album { get; set; }
public byte[]? AlbumArt { get; set; } = null;
[ObservableProperty]
public partial string Artist { get; set; }
[ObservableProperty]
public partial int? Duration { get; set; }
[ObservableProperty]
public partial double? DurationMs { get; set; }

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class TranslateResponse
{
[JsonPropertyName("translatedText")]
public string TranslatedText { get; set; }
}
}

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
namespace BetterLyrics.WinUI3.Renderer
{

View File

@@ -8,9 +8,12 @@ using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Serialization
{
[JsonSerializable(typeof(List<AlbumArtSearchProviderInfo>))]
[JsonSerializable(typeof(List<LyricsSearchProviderInfo>))]
[JsonSerializable(typeof(List<LocalLyricsFolder>))]
[JsonSerializable(typeof(List<MediaSourceProviderInfo>))]
[JsonSerializable(typeof(List<LocalMediaFolder>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(TranslateResponse))]
[JsonSerializable(typeof(JsonElement))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class SourceGenerationContext : JsonSerializerContext { }

View File

@@ -0,0 +1,141 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class AlbumArtSearchService : IAlbumArtSearchService
{
private readonly HttpClient _iTunesHttpClinet;
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public AlbumArtSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_logger = Ioc.Default.GetRequiredService<ILogger<AlbumArtSearchService>>();
_iTunesHttpClinet = new();
}
public async Task<byte[]?> SearchAsync(string title, string artist, string album, byte[]? bytesFromSMTC = null)
{
byte[]? result = null;
foreach (var provider in _settingsService.AlbumArtSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
switch (provider.Provider)
{
case AlbumArtSearchProvider.Local:
result = SearchFile(artist, album);
break;
case AlbumArtSearchProvider.SMTC:
result = bytesFromSMTC;
break;
case AlbumArtSearchProvider.iTunes:
foreach (string countryCode in new List<string>() { "us", "cn", "jp", "kr" })
{
result = await SearchiTunesAsync(artist, album, title, countryCode);
if (result != null) break;
}
break;
default:
break;
}
if (result != null) return result;
}
return null;
}
private byte[]? SearchFile(string artist, string album)
{
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), album, artist))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
}
}
}
}
return null;
}
private async Task<byte[]?> SearchiTunesAsync(string artist, string album, string title, string countryCode)
{
// Source: https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce
try
{
string format = ".jpg";
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(artist, album, format, PathHelper.iTunesAlbumArtCacheDirectory);
if (cachedAlbumArt != null)
{
return cachedAlbumArt;
}
// Build the iTunes API URL
string url = $"https://itunes.apple.com/search?term=" + WebUtility.UrlEncode($"{artist} {album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
// Make a request to the API
HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
// Parse the JSON response
var data = JsonSerializer.Deserialize(responseBody, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array && results.GetArrayLength() > 0)
{
// Get the first result
var result = results[0];
if (result.TryGetProperty("artworkUrl100", out var artworkUrlProp))
{
string artworkUrl = artworkUrlProp.GetString()?.Replace("100x100bb.jpg", "1200x1200bb.jpg") ?? string.Empty;
var fetched = await _iTunesHttpClinet.GetByteArrayAsync(artworkUrl);
if (fetched != null && fetched.Length > 0)
{
// Write to cache
FileHelper.WriteAlbumArtCache(artist, album, fetched, format, PathHelper.iTunesAlbumArtCacheDirectory);
return fetched;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error searching iTunes album art for {Artist} - {Album}", artist, album);
}
return null;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Services
{
public interface IMusicSearchService
{
byte[]? SearchAlbumArtAsync(string title, string artist);
Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title,
string artist,
string album = "",
double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleAndArtist
);
}
}

View File

@@ -1,6 +1,7 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Models;
@@ -9,15 +10,18 @@ namespace BetterLyrics.WinUI3.Services
public interface IPlaybackService
{
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<PositionChangedEventArgs>? PositionChanged;
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
Task PlayAsync();
Task PauseAsync();
Task PreviousAsync();
Task NextAsync();
Task ChangePosition(double seconds);
bool IsPlaying { get; }
TimeSpan Position { get; }
SongInfo? SongInfo { get; }
}
}

View File

@@ -3,10 +3,8 @@
using System.Collections.Generic;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Windows.UI;
using Windows.UI.Text;
namespace BetterLyrics.WinUI3.Services
{
@@ -29,33 +27,79 @@ namespace BetterLyrics.WinUI3.Services
int DesktopWindowTop { get; set; }
int DesktopWindowWidth { get; set; }
int DesktopWindowHeight { get; set; }
int StandardWindowWidth { get; set; }
int StandardWindowHeight { get; set; }
int StandardWindowLeft { get; set; }
int StandardWindowTop { get; set; }
bool AutoLockOnDesktopMode { get; set; }
string LibreTranslateServer { get; set; }
int SelectedTargetLanguageIndex { get; set; }
bool ResetPositionOffsetOnSongChanged { get; set; }
int PositionOffset { get; set; }
// Lyrics lib
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
List<LocalMediaFolder> LocalMediaFolders { get; set; }
// Lyrics style and effetc
LyricsAlignmentType LyricsAlignmentType { get; set; }
TextAlignmentType LyricsAlignmentType { get; set; }
TextAlignmentType SongInfoAlignmentType { get; set; }
int LyricsBlurAmount { get; set; }
Color LyricsCustomFontColor { get; set; }
Color LyricsCustomBgFontColor { get; set; }
Color LyricsCustomFgFontColor { get; set; }
Color LyricsCustomStrokeFontColor { get; set; }
LyricsFontColorType LyricsFontColorType { get; set; }
int LyricsBgFontOpacity { get; set; }
LyricsFontColorType LyricsBgFontColorType { get; set; }
LyricsFontColorType LyricsFgFontColorType { get; set; }
LyricsFontColorType LyricsStrokeFontColorType { get; set; }
int LyricsFontSize { get; set; }
ElementTheme LyricsBackgroundTheme { get; set; }
int LyricsFontStrokeWidth { get; set; }
LyricsFontWeight LyricsFontWeight { get; set; }
LineRenderingType LyricsGlowEffectScope { get; set; }
LineRenderingType LyricsHighlightScope { get; set; }
bool IsLyricsFloatAnimationEnabled { get; set; }
float LyricsLineSpacingFactor { get; set; }
List<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
List<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; }
List<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; }
EasingType LyricsScrollEasingType { get; set; }
int LyricsScrollDuration { get; set; }
int LyricsVerticalEdgeOpacity { get; set; }
bool IgnoreFullscreenWindow { get; set; }
bool IsTranslationEnabled { get; set; }
bool ShowTranslationOnly { get; set; }
LyricsDisplayType DisplayType { get; set; }
int TimelineSyncThreshold { get; set; }
int LockHotKeyIndex { get; set; }
bool IsImmersiveMode { get; set; }
string LXMusicServer { get; set; }
DockPlacement DockPlacement { get; set; }
bool HideWindowWhenNotPlaying { get; set; }
int DockWindowHeight { get; set; }
int SelectedFontFamilyIndex { get; set; }
string LyricsFontFamily { get; set; }
bool IsDragEverywhereEnabled { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public interface ITranslateService
{
Task<string> TranslateTextAsync(string text, string targetLangCode, CancellationToken? token);
int SearchTranslatedLyricsItself(List<LyricsData> lyricsDataArr);
}
}

View File

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

View File

@@ -1,37 +1,60 @@
// 2025/6/23 by Zhe Fang
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Providers.Web.Kugou;
using Lyricify.Lyrics.Searchers;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Lyricify.Lyrics.Providers.Web.Kugou;
using Lyricify.Lyrics.Searchers;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services
{
public class MusicSearchService : IMusicSearchService
public class LyricsSearchService : ILyricsSearchService
{
private readonly HttpClient _amllTtmlDbHttpClient;
private readonly HttpClient _lrcLibHttpClient;
private readonly ISettingsService _settingsService;
private readonly ILogger _logger;
public MusicSearchService(ISettingsService settingsService)
public LyricsSearchService(ISettingsService settingsService)
{
_settingsService = settingsService;
_lrcLibHttpClient = new HttpClient();
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsSearchService>>();
_lrcLibHttpClient = new();
_lrcLibHttpClient.DefaultRequestHeaders.Add(
"User-Agent",
$"{AppInfo.AppName} {AppInfo.AppVersion} ({AppInfo.GithubUrl})"
$"{MetadataHelper.AppName} {MetadataHelper.AppVersion} ({MetadataHelper.GithubUrl})"
);
_amllTtmlDbHttpClient = new HttpClient();
_amllTtmlDbHttpClient = new();
}
private static bool IsAmllTtmlDbIndexInvalid()
{
bool existed = File.Exists(PathHelper.AmllTtmlDbIndexPath);
if (!existed)
{
return true;
}
else
{
long currentTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string lastUpdatedStr = File.ReadAllText(PathHelper.AmllTtmlDbLastUpdatedPath);
long lastUpdated = Convert.ToInt64(lastUpdatedStr);
return currentTs - lastUpdated > 1 * 24 * 60 * 60;
}
}
public async Task<bool> DownloadAmllTtmlDbIndexAsync()
@@ -44,13 +67,16 @@ namespace BetterLyrics.WinUI3.Services
await using var stream = await response.Content.ReadAsStreamAsync();
await using var fs = new FileStream(
AppInfo.AmllTtmlDbIndexPath,
PathHelper.AmllTtmlDbIndexPath,
FileMode.Create,
FileAccess.Write,
FileShare.None
);
await stream.CopyToAsync(fs);
long currentTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
File.WriteAllText(PathHelper.AmllTtmlDbLastUpdatedPath, currentTs.ToString());
return true;
}
catch
@@ -59,131 +85,96 @@ namespace BetterLyrics.WinUI3.Services
}
}
public byte[]? SearchAlbumArtAsync(string title, string artist)
public async Task<(string?, LyricsSearchProvider?)> SearchAsync(string title, string artist, string album, double durationMs, CancellationToken token)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
try
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
if (!provider.IsEnabled)
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
continue;
}
string? cachedLyrics;
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
// Check cache first
if (provider.Provider.IsRemote())
{
cachedLyrics = FileHelper.ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
Track track = new(file);
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null)
{
return bytes;
}
return (cachedLyrics, provider.Provider);
}
}
}
}
return null;
}
string? searchedLyrics = null;
public async Task<(string?, LyricsFormat?)> SearchLyricsAsync(
string title, string artist, string album = "", double durationMs = 0.0,
MusicSearchMatchMode matchMode = MusicSearchMatchMode.TitleArtistAlbumAndDuration
)
{
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
if (!provider.IsEnabled)
{
continue;
}
string? cachedLyrics;
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
// Check cache first
if (provider.Provider.IsRemote())
{
cachedLyrics = ReadCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
if (provider.Provider.IsLocal())
{
return (cachedLyrics, lyricsFormat);
}
}
string? searchedLyrics = null;
if (provider.Provider.IsLocal())
{
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = LocalLyricsSearchInMusicFiles(title, artist);
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = SearchEmbedded(title, artist);
}
else
{
searchedLyrics = await SearchFile(title, artist, lyricsFormat);
}
}
else
{
searchedLyrics = await LocalLyricsSearchInLyricsFiles(title, artist, lyricsFormat);
}
}
else
{
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000), matchMode);
break;
case LyricsSearchProvider.QQ:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, matchMode, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, matchMode, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
searchedLyrics = await SearchUsingLyricifyAsync(title, artist, album, (int)durationMs, matchMode, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
break;
default:
break;
}
}
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
if (provider.Provider.IsRemote())
{
WriteCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
switch (provider.Provider)
{
case LyricsSearchProvider.LrcLib:
searchedLyrics = await SearchLrcLibAsync(title, artist, album, (int)(durationMs / 1000));
break;
case LyricsSearchProvider.QQ:
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.QQMusic);
break;
case LyricsSearchProvider.Kugou:
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Kugou);
break;
case LyricsSearchProvider.Netease:
searchedLyrics = await SearchQQNeteaseKugouAsync(title, artist, album, (int)durationMs, Searchers.Netease);
break;
case LyricsSearchProvider.AmllTtmlDb:
searchedLyrics = await SearchAmllTtmlDbAsync(title, artist);
break;
default:
break;
}
}
return (searchedLyrics, lyricsFormat == LyricsFormat.NotSpecified ? searchedLyrics.DetectFormat() : lyricsFormat);
token.ThrowIfCancellationRequested();
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
if (provider.Provider.IsRemote())
{
FileHelper.WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
}
return (searchedLyrics, provider.Provider);
}
}
}
catch (Exception) { }
return (null, null);
}
private static bool MusicMatch(string fileName, string title, string artist)
private async Task<string?> SearchFile(string title, string artist, LyricsFormat format)
{
return fileName.Contains(title) && fileName.Contains(artist);
}
private static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
private async Task<string?> LocalLyricsSearchInLyricsFiles(string title, string artist, LyricsFormat format)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*{format.ToFileExtension()}", SearchOption.AllDirectories))
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
string? raw = await File.ReadAllTextAsync(file, FileHelper.GetEncoding(file));
if (raw != null)
@@ -197,15 +188,15 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private string? LocalLyricsSearchInMusicFiles(string title, string artist)
private string? SearchEmbedded(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
foreach (var file in Directory.GetFiles(folder.Path, $"*.*", SearchOption.AllDirectories))
{
if (MusicMatch(Path.GetFileNameWithoutExtension(file), title, artist))
if (FileHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), title, artist))
{
try
{
@@ -224,30 +215,17 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private string? ReadCache(string title, string artist, LyricsFormat format, string cacheFolderPath)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(cacheFolderPath, $"{safeArtist} - {safeTitle}{format.ToFileExtension()}");
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
private async Task<string?> SearchAmllTtmlDbAsync(string title, string artist)
{
// 检索本地 JSONL 索引文件,查找 rawLyricFile
if (!File.Exists(AppInfo.AmllTtmlDbIndexPath))
if (IsAmllTtmlDbIndexInvalid())
{
var downloadOk = await DownloadAmllTtmlDbIndexAsync();
if (!downloadOk || !File.Exists(AppInfo.AmllTtmlDbIndexPath))
if (!downloadOk)
return null;
}
string? rawLyricFile = null;
await foreach (var line in File.ReadLinesAsync(AppInfo.AmllTtmlDbIndexPath))
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
{
if (string.IsNullOrWhiteSpace(line))
continue;
@@ -273,7 +251,7 @@ namespace BetterLyrics.WinUI3.Services
if (musicName == null || artists == null)
continue;
if (MusicMatch($"{artists} - {musicName}", title, artist))
if (FileHelper.IsSwitchableNormalizedMatch($"{artists} - {musicName}", title, artist))
{
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
{
@@ -303,20 +281,15 @@ namespace BetterLyrics.WinUI3.Services
}
}
private async Task<string?> SearchLrcLibAsync(string title, string artist, string album, int duration, MusicSearchMatchMode matchMode)
private async Task<string?> SearchLrcLibAsync(string title, string artist, string album, int duration)
{
// Build API query URL
var url =
$"https://lrclib.net/api/search?"
+ $"track_name={Uri.EscapeDataString(title)}&"
+ $"artist_name={Uri.EscapeDataString(artist)}";
if (matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration)
{
url +=
$"&album_name={Uri.EscapeDataString(album)}"
+ $"&durationMs={Uri.EscapeDataString(duration.ToString())}";
}
$"https://lrclib.net/api/search?" +
$"track_name={Uri.EscapeDataString(title)}&" +
$"artist_name={Uri.EscapeDataString(artist)}&" +
$"&album_name={Uri.EscapeDataString(album)}" +
$"&durationMs={Uri.EscapeDataString(duration.ToString())}";
var response = await _lrcLibHttpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
@@ -342,21 +315,13 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private async Task<string?> SearchUsingLyricifyAsync(
string title,
string artist,
string album,
int durationMs,
MusicSearchMatchMode matchMode,
Searchers searchers
)
private static async Task<string?> SearchQQNeteaseKugouAsync(string title, string artist, string album, int durationMs, Searchers searchers)
{
var result = await SearchersHelper.GetSearcher(searchers).SearchForResult(
new Lyricify.Lyrics.Models.TrackMultiArtistMetadata()
{
DurationMs = matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration ? durationMs : null,
Album = matchMode == MusicSearchMatchMode.TitleArtistAlbumAndDuration ? album : null,
AlbumArtists = [artist],
DurationMs = durationMs,
Album = album,
Artists = [artist],
Title = title,
}
@@ -364,8 +329,19 @@ namespace BetterLyrics.WinUI3.Services
if (result is QQMusicSearchResult qqResult)
{
var response = await Lyricify.Lyrics.Decrypter.Qrc.Helper.GetLyricsAsync(qqResult.Id);
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.QQMusicApi.GetLyricsAsync(qqResult.Id);
var original = response?.Lyrics;
var translated = response?.Trans;
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(
title,
artist,
translated,
LyricsFormat.Lrc,
PathHelper.QQTranslationCacheDirectory
);
}
return original;
}
else if (result is NeteaseSearchResult neteaseResult)
@@ -375,7 +351,7 @@ namespace BetterLyrics.WinUI3.Services
}
else if (result is KugouSearchResult kugouResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(kugouResult.Hash);
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.KugouApi.GetSearchLyrics(hash: kugouResult.Hash);
if (response?.Candidates.FirstOrDefault() is SearchLyricsResponse.Candidate candidate)
{
return Lyricify.Lyrics.Decrypter.Krc.Helper.GetLyrics(
@@ -387,22 +363,5 @@ namespace BetterLyrics.WinUI3.Services
return null;
}
private void WriteCache(
string title,
string artist,
string lyrics,
LyricsFormat format,
string cacheFolderPath
)
{
var safeArtist = SanitizeFileName(artist);
var safeTitle = SanitizeFileName(title);
var cacheFilePath = Path.Combine(
cacheFolderPath,
$"{safeArtist} - {safeTitle}{format.ToFileExtension()}"
);
File.WriteAllText(cacheFilePath, lyrics);
}
}
}

View File

@@ -1,208 +1,401 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.WinUI;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using EvtSource;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Windows.ApplicationModel;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using Windows.UI.Shell;
using WindowsMediaController;
using static WindowsMediaController.MediaManager;
namespace BetterLyrics.WinUI3.Services
{
public partial class PlaybackService : IPlaybackService
public partial class PlaybackService : BaseViewModel, IPlaybackService,
IRecipient<PropertyChangedMessage<ObservableCollection<MediaSourceProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<AlbumArtSearchProviderInfo>>>
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly IAlbumArtSearchService _albumArtSearchService;
private readonly ILogger<PlaybackService> _logger;
private readonly IMusicSearchService _musicSearchService;
private readonly string _lxMusicId = "cn.toside.music.desktop";
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
private bool _cachedIsPlaying = false;
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private EventSourceReader? _sse = null;
public PlaybackService(ISettingsService settingsService, IMusicSearchService musicSearchService)
{
_musicSearchService = musicSearchService;
InitMediaManager().ConfigureAwait(true);
}
private readonly MediaManager _mediaManager = new();
private MediaManager.MediaSession? _focusedSession = null;
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();
private readonly LatestOnlyTaskRunner _onAnyMediaPropertyChangedRunner = new();
private SongInfo? _cachedSongInfo;
private List<MediaSourceProviderInfo> _mediaSourceProvidersInfo;
private byte[]? _SMTCAlbumArtBytes = null;
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<PositionChangedEventArgs>? PositionChanged;
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
public event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
public bool IsPlaying { get; private set; }
public TimeSpan Position { get; private set; }
public SongInfo? SongInfo { get; private set; }
private void CurrentSession_MediaPropertiesChanged(GlobalSystemMediaTransportControlsSession? sender, MediaPropertiesChangedEventArgs? args)
public PlaybackService(ISettingsService settingsService, IAlbumArtSearchService albumArtSearchService) : base(settingsService)
{
App.DispatcherQueueTimer!.Debounce(
async () =>
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
if (sender == null)
{
SongInfo = null;
}
else
{
try
{
mediaProps = await sender.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
_albumArtSearchService = albumArtSearchService;
_logger = Ioc.Default.GetRequiredService<ILogger<PlaybackService>>();
if (mediaProps == null)
{
SongInfo = null;
}
else
{
SongInfo = new SongInfo
{
Title = mediaProps.Title,
Artist = mediaProps.Artist,
Album = mediaProps?.AlbumTitle ?? string.Empty,
DurationMs = _currentSession
?.GetTimelineProperties()
.EndTime.TotalMilliseconds,
SourceAppUserModelId = _currentSession?.SourceAppUserModelId,
};
if (mediaProps?.Thumbnail is IRandomAccessStreamReference streamReference)
{
SongInfo.AlbumArt = await ImageHelper.ToByteArrayAsync(
streamReference
);
}
else
{
SongInfo.AlbumArt = _musicSearchService.SearchAlbumArtAsync(
SongInfo.Title,
SongInfo.Artist
);
if (SongInfo.AlbumArt == null)
{
SongInfo.AlbumArt =
await ImageHelper.CreateTextPlaceholderBytesAsync(
$"{SongInfo.Artist} - {SongInfo.Title}",
400,
400
);
}
}
}
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(SongInfo));
}
);
},
TimeSpan.FromMilliseconds(1000)
);
_mediaSourceProvidersInfo = _settingsService.MediaSourceProvidersInfo;
InitMediaManager();
}
private void CurrentSession_PlaybackInfoChanged(GlobalSystemMediaTransportControlsSession? sender, PlaybackInfoChangedEventArgs? args)
public bool IsPlaying => _cachedIsPlaying;
public SongInfo? SongInfo => _cachedSongInfo;
private bool IsMediaSourceEnabled(string id)
{
if (sender == null)
return _mediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id)?.IsEnabled ?? true;
}
private void InitMediaManager()
{
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
_mediaManager.OnAnySessionClosed += MediaManager_OnAnySessionClosed;
_mediaManager.OnFocusedSessionChanged += MediaManager_OnFocusedSessionChanged;
_mediaManager.OnAnyMediaPropertyChanged += MediaManager_OnAnyMediaPropertyChanged;
_mediaManager.OnAnyPlaybackStateChanged += MediaManager_OnAnyPlaybackStateChanged;
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
_mediaManager.Start();
Task.Run(() =>
{
IsPlaying = false;
MediaManager_OnFocusedSessionChanged(null);
});
}
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
{
if (!_mediaManager.IsStarted) return;
_focusedSession = mediaSession ?? _mediaManager.GetFocusedSession();
if (_focusedSession == null || !IsMediaSourceEnabled(_focusedSession.Id))
{
SendNullMessages();
}
else
{
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
// _logger.LogDebug(playbackState.ToString());
switch (playbackState)
{
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
IsPlaying = false;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
IsPlaying = true;
break;
default:
break;
}
SendFocusedMessagesAsync().ConfigureAwait(false);
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties timelineProperties)
{
if (!_mediaManager.IsStarted) return;
if (!IsMediaSourceEnabled(mediaSession.Id) || mediaSession != _focusedSession) return;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(timelineProperties.Position));
});
}
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo playbackInfo)
{
if (!_mediaManager.IsStarted) return;
RecordMediaSourceProviderInfo(mediaSession);
if (!IsMediaSourceEnabled(mediaSession.Id) || mediaSession != _focusedSession) return;
_cachedIsPlaying = playbackInfo.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(_cachedIsPlaying));
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties mediaProperties)
{
if (!_mediaManager.IsStarted) return;
string id = mediaSession.Id;
RecordMediaSourceProviderInfo(mediaSession);
if (!IsMediaSourceEnabled(id) || mediaSession != _focusedSession) return;
_cachedSongInfo = new SongInfo
{
Title = mediaProperties.Title,
Artist = mediaProperties.Artist,
Album = mediaProperties.AlbumTitle,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
SourceAppUserModelId = id,
};
_cachedSongInfo.Duration = (int)(_cachedSongInfo.DurationMs / 1000f);
_onAnyMediaPropertyChangedRunner.RunAsync(async token =>
{
_logger.LogInformation("Media properties changed: Title: {Title}, Artist: {Artist}, Album: {Album}",
mediaProperties.Title, mediaProperties.Artist, mediaProperties.AlbumTitle);
if (id == _lxMusicId)
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(IsPlaying));
StartSSE();
}
else
{
StopSSE();
}
if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBytes = await ImageHelper.ToByteArrayAsync(streamReference);
}
else
{
_SMTCAlbumArtBytes = null;
}
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
}
}).ConfigureAwait(false);
}
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (_mediaManager.CurrentMediaSessions.Count == 0)
{
SendNullMessages();
}
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
RecordMediaSourceProviderInfo(mediaSession);
_focusedSession = _mediaManager.GetFocusedSession();
SendFocusedMessagesAsync().ConfigureAwait(false);
}
private void RecordMediaSourceProviderInfo(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
var id = mediaSession?.Id;
if (string.IsNullOrEmpty(id)) return;
var found = _mediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
if (found == null)
{
_mediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, true));
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
MediaSourceProvidersInfoChanged?.Invoke(this, new MediaSourceProvidersInfoEventArgs(_mediaSourceProvidersInfo));
});
}
}
private void SendNullMessages()
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
_cachedSongInfo = null;
_cachedIsPlaying = false;
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(_cachedIsPlaying));
PositionChanged?.Invoke(this, new PositionChangedEventArgs(TimeSpan.Zero));
});
}
private async Task SendFocusedMessagesAsync()
{
if (_focusedSession == null) return;
var mediaProps = await _focusedSession.ControlSession.TryGetMediaPropertiesAsync();
MediaManager_OnAnyMediaPropertyChanged(_focusedSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(_focusedSession, _focusedSession.ControlSession.GetPlaybackInfo());
MediaManager_OnAnyTimelinePropertyChanged(_focusedSession, _focusedSession.ControlSession.GetTimelineProperties());
}
private async Task UpdateAlbumArtRelated(CancellationToken token)
{
if (_cachedSongInfo == null)
{
_logger.LogWarning("Cached song info is null, cannot update album art.");
return;
}
byte[]? bytes = await _albumArtSearchService.SearchAsync(
_cachedSongInfo.Title,
_cachedSongInfo.Artist,
_cachedSongInfo?.Album ?? string.Empty,
_SMTCAlbumArtBytes
);
token.ThrowIfCancellationRequested();
if (bytes == null)
{
bytes = await ImageHelper.CreateTextPlaceholderBytesAsync(400, 400);
token.ThrowIfCancellationRequested();
}
bytes = ImageHelper.MakeSquareWithThemeColor(bytes);
using var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
token.ThrowIfCancellationRequested();
var decoder = await BitmapDecoder.CreateAsync(stream);
token.ThrowIfCancellationRequested();
var _albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
token.ThrowIfCancellationRequested();
var _albumArtAccentColor = ImageHelper.GetAccentColorsFromByte(bytes).FirstOrDefault();
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
AlbumArtChangedChanged?.Invoke(this, new AlbumArtChangedEventArgs(_albumArtSwBitmap, _albumArtAccentColor));
});
}
private void CurrentSession_TimelinePropertiesChanged(GlobalSystemMediaTransportControlsSession? sender, TimelinePropertiesChangedEventArgs? args)
private void StartSSE()
{
if (sender == null)
try
{
Position = TimeSpan.Zero;
_sse = new EventSourceReader(new Uri($"{_settingsService.LXMusicServer}/subscribe-player-status?filter=progress")).Start();
_sse.MessageReceived += Sse_MessageReceived;
_sse.Disconnected += Sse_Disconnected;
}
else
catch (Exception)
{
Position = sender.GetTimelineProperties().Position;
}
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
_logger.LogError("Failed to start SSE connection for LX Music.");
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(Position));
App.Current.LyricsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("FailToStartLXMusicServer"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
});
StopSSE();
}
}
private void StopSSE()
{
if (_sse != null)
{
_sse.MessageReceived -= Sse_MessageReceived;
_sse.Disconnected -= Sse_Disconnected;
_sse.Dispose();
_sse = null;
}
}
private void Sse_Disconnected(object sender, DisconnectEventArgs e)
{
Task.Run(async () =>
{
await Task.Delay(e.ReconnectDelay);
if (_sse != null && !_sse.IsDisposed) _sse.Start();
});
}
private void Sse_MessageReceived(object sender, EventSourceMessageEventArgs e)
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.TryGetDouble(out double positionSeconds))
{
if (_cachedSongInfo?.SourceAppUserModelId == _lxMusicId)
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(TimeSpan.FromSeconds(positionSeconds)));
}
);
}
}
private async Task InitMediaManager()
public async Task PlayAsync()
{
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
await _focusedSession?.ControlSession.TryPlayAsync();
}
private void SessionManager_CurrentSessionChanged(
GlobalSystemMediaTransportControlsSessionManager sender,
CurrentSessionChangedEventArgs? args
)
public async Task PauseAsync()
{
// _logger.LogDebug("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null)
await _focusedSession?.ControlSession.TryPauseAsync();
}
public async Task PreviousAsync()
{
await _focusedSession?.ControlSession.TrySkipPreviousAsync();
}
public async Task NextAsync()
{
await _focusedSession?.ControlSession.TrySkipNextAsync();
}
public async Task ChangePosition(double seconds)
{
await _focusedSession?.ControlSession.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
public void Receive(PropertyChangedMessage<ObservableCollection<MediaSourceProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)
{
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged -=
CurrentSession_TimelinePropertiesChanged;
if (message.PropertyName == nameof(SettingsPageViewModel.MediaSourceProvidersInfo))
{
_mediaSourceProvidersInfo = [.. message.NewValue];
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
MediaManager_OnFocusedSessionChanged(_mediaManager.GetFocusedSession());
}
}
}
// Record and register events for current session
_currentSession = sender.GetCurrentSession();
if (_currentSession != null)
public async void Receive(PropertyChangedMessage<ObservableCollection<AlbumArtSearchProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)
{
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged +=
CurrentSession_TimelinePropertiesChanged;
if (message.PropertyName == nameof(SettingsPageViewModel.AlbumArtSearchProvidersInfo))
{
// Album art search providers info changed, re-fetch album art
_logger.LogInformation("Album art search providers info changed, refreshing album art.");
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
}
}
CurrentSession_MediaPropertiesChanged(_currentSession, null);
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
}
}
}

View File

@@ -1,8 +1,5 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
@@ -10,6 +7,9 @@ using BetterLyrics.WinUI3.Serialization;
using CommunityToolkit.WinUI.Helpers;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Storage;
using Windows.UI;
@@ -17,13 +17,15 @@ namespace BetterLyrics.WinUI3.Services
{
public class SettingsService : ISettingsService
{
public const string LyricsCustomFontColorKey = "LyricsCustomFontColor";
public const string LyricsCustomBgFontColorKey = "LyricsCustomBgFontColor";
public const string LyricsCustomFgFontColorKey = "LyricsCustomFgFontColor";
public const string LyricsCustomStrokeFontColorKey = "LyricsCustomStrokeFontColor";
// App behavior
private const string AutoStartWindowTypeKey = "AutoStartWindowType";
private const string CoverImageRadiusKey = "CoverImageRadius";
private const string CoverImageRadiusKey = "AlbumArtCornerRadius";
private const string CoverOverlayBlurAmountKey = "CoverOverlayBlurAmount";
private const string CoverOverlayOpacityKey = "CoverOverlayOpacity";
private const string IsCoverOverlayEnabledKey = "IsCoverOverlayEnabled";
@@ -32,7 +34,14 @@ namespace BetterLyrics.WinUI3.Services
private const string DesktopWindowTopKey = "DesktopWindowTop";
private const string DesktopWindowWidthKey = "DesktopWindowWidth";
private const string DesktopWindowHeightKey = "DesktopWindowHeight";
private const string StandardWindowLeftKey = "StandardWindowLeft";
private const string StandardWindowTopKey = "StandardWindowTop";
private const string StandardWindowWidthKey = "StandardWindowWidth";
private const string StandardWindowHeightKey = "StandardWindowHeight";
private const string AutoLockOnDesktopModeKey = "AutoLockOnDesktopMode";
private const string IsImmersiveModeKey = "IsImmersiveMode";
private const string IsDynamicCoverOverlayEnabledKey = "IsDynamicCoverOverlayEnabled";
private const string IsFanLyricsEnabledKey = "IsFanLyricsEnabled";
@@ -41,16 +50,59 @@ namespace BetterLyrics.WinUI3.Services
private const string LanguageKey = "Language";
private const string LocalLyricsFoldersKey = "LocalLyricsFolders";
private const string LyricsAlignmentTypeKey = "LyricsAlignmentType";
private const string LyricsAlignmentTypeKey = "TextAlignmentType";
private const string SongInfoAlignmentTypeKey = "SongInfoAlignmentType";
private const string LyricsBlurAmountKey = "LyricsBlurAmount";
private const string LyricsFontColorTypeKey = "LyricsFontColorType";
private const string LyricsBgFontColorTypeKey = "_lyricsBgFontColorType";
private const string LyricsFgFontColorTypeKey = "LyricsFgFontColorType";
private const string LyricsStrokeFontColorTypeKey = "LyricsStrokeFontColorType";
private const string LyricsFontStrokeWidthKey = "LyricsFontStrokeWidth";
private const string LyricsFontSizeKey = "LyricsFontSize";
private const string LyricsFontWeightKey = "LyricsFontWeightKey";
private const string LyricsGlowEffectScopeKey = "LyricsGlowEffectScope";
private const string LyricsHighlightSopeKey = "LyricsHighlightSope";
private const string LyricsLineSpacingFactorKey = "LyricsLineSpacingFactor";
private const string LyricsSearchProvidersInfoKey = "LyricsSearchProvidersInfo";
private const string AlbumArtSearchProvidersInfoKey = "AlbumArtSearchProvidersInfo";
private const string LyricsVerticalEdgeOpacityKey = "LyricsVerticalEdgeOpacity";
private const string MediaSourceProvidersInfoKey = "MediaSourceProvidersInfo";
private const string IsTranslationEnabledKey = "IsTranslationEnabled";
private const string ShowTranslationOnlyKey = "ShowTranslationOnly";
private const string LibreTranslateServerKey = "LibreTranslateServer";
private const string SelectedTargetLanguageIndexKey = "SelectedTargetLanguageIndex";
private const string LXMusicServerKey = "LXMusicServer";
private const string LyricsBackgroundThemeKey = "LyricsBackgroundTheme";
private const string IgnoreFullscreenWindowKey = "IgnoreFullscreenWindow";
private const string PreferredDisplayTypeKey = "PreferredDisplayTypeKey";
private const string LyricsScrollEasingTypeKey = "LyricsScrollEasingType";
private const string LyricsScrollDurationKey = "LyricsScrollDuration";
public const string TimelineSyncThresholdKey = "TimelineSyncThreshold";
private const string IsLyricsFloatAnimationEnabledKey = "IsLyricsFloatAnimationEnabled";
private const string ResetPositionOffsetOnSongChangedKey = "ResetPositionOffsetOnSongChanged";
private const string PositionOffsetKey = "PositionOffset";
private const string LockHotKeyIndexKey = "LockHotKeyIndex";
private const string DockPlacementKey = "DockPlacement";
private const string LyricsBgFontOpacityKey = "LyricsBgFontOpacity";
private const string HideWindowWhenNotPlayingKey = "HideWindowWhenNotPlaying";
private const string DockWindowHeightKey = "DockWindowHeight";
private const string SelectedFontFamilyIndexKey = "SelectedFontFamilyIndex";
private const string LyricsFontFamilyKey = "LyricsFontFamily";
private const string IsDragEverywhereEnabledKey = "IsDragEverywhereEnabled";
private readonly ApplicationDataContainer _localSettings;
public SettingsService()
@@ -81,33 +133,183 @@ namespace BetterLyrics.WinUI3.Services
))
.ToList();
}
SetDefault(
AlbumArtSearchProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
Enum.GetValues<AlbumArtSearchProvider>()
.Select(p => new AlbumArtSearchProviderInfo(p, true))
.ToList(),
SourceGenerationContext.Default.ListAlbumArtSearchProviderInfo
)
);
if (AlbumArtSearchProvidersInfo.Count != Enum.GetValues<AlbumArtSearchProvider>().Length)
{
AlbumArtSearchProvidersInfo = Enum.GetValues<AlbumArtSearchProvider>()
.Select(p => new AlbumArtSearchProviderInfo(
p,
AlbumArtSearchProvidersInfo
.Where(x => x.Provider == p)
.FirstOrDefault()
?.IsEnabled ?? true
))
.ToList();
}
SetDefault(MediaSourceProvidersInfoKey, "[]");
// App appearance
SetDefault(LanguageKey, (int)Language.FollowSystem);
SetDefault(DesktopWindowHeightKey, 400);
SetDefault(DesktopWindowLeftKey, 0);
SetDefault(DesktopWindowTopKey, 0);
SetDefault(DesktopWindowWidthKey, 600);
SetDefault(DesktopWindowHeightKey, 600);
SetDefault(DesktopWindowLeftKey, 200);
SetDefault(DesktopWindowTopKey, 200);
SetDefault(DesktopWindowWidthKey, 1200);
SetDefault(StandardWindowHeightKey, 800);
SetDefault(StandardWindowLeftKey, 200);
SetDefault(StandardWindowTopKey, 200);
SetDefault(StandardWindowWidthKey, 1600);
SetDefault(AutoLockOnDesktopModeKey, false);
SetDefault(IsImmersiveModeKey, false);
// App behavior
SetDefault(AutoStartWindowTypeKey, (int)AutoStartWindowType.StandardMode);
// Album art
SetDefault(IsCoverOverlayEnabledKey, true);
SetDefault(IsDynamicCoverOverlayEnabledKey, true);
SetDefault(CoverOverlayOpacityKey, 75); // 100 % = 1.0
SetDefault(CoverOverlayBlurAmountKey, 200);
SetDefault(CoverImageRadiusKey, 24); // 24 %
SetDefault(CoverOverlayOpacityKey, 100); // 100 % = 1.0
SetDefault(CoverOverlayBlurAmountKey, 100);
SetDefault(CoverImageRadiusKey, 12); // 12 %
// Lyrics
SetDefault(LyricsAlignmentTypeKey, (int)LyricsAlignmentType.Center);
SetDefault(LyricsAlignmentTypeKey, (int)TextAlignmentType.Center);
SetDefault(SongInfoAlignmentTypeKey, (int)TextAlignmentType.Left);
SetDefault(LyricsFontWeightKey, (int)LyricsFontWeight.Bold);
SetDefault(LyricsBlurAmountKey, 5);
SetDefault(LyricsFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsCustomFontColorKey, Colors.White.ToInt());
SetDefault(LyricsBackgroundThemeKey, (int)ElementTheme.Default);
SetDefault(LyricsBgFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsFgFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsStrokeFontColorTypeKey, (int)LyricsFontColorType.AdaptiveGrayed);
SetDefault(LyricsCustomBgFontColorKey, Colors.White.ToInt());
SetDefault(LyricsCustomFgFontColorKey, Colors.White.ToInt());
SetDefault(LyricsCustomStrokeFontColorKey, Colors.White.ToInt());
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
SetDefault(LyricsGlowEffectScopeKey, (int)LineRenderingType.CurrentCharOnly);
SetDefault(LyricsGlowEffectScopeKey, (int)LineRenderingType.CurrentChar);
SetDefault(LyricsHighlightSopeKey, (int)LineRenderingType.LineStartToCurrentChar);
SetDefault(IsFanLyricsEnabledKey, false);
SetDefault(LibreTranslateServerKey, "");
SetDefault(IsTranslationEnabledKey, true);
SetDefault(ShowTranslationOnlyKey, false);
SetDefault(SelectedTargetLanguageIndexKey, LanguageHelper.GetDefaultTargetLanguageIndex());
SetDefault(LXMusicServerKey, "");
SetDefault(LyricsFontStrokeWidthKey, 3);
SetDefault(IgnoreFullscreenWindowKey, false);
SetDefault(PreferredDisplayTypeKey, (int)LyricsDisplayType.SplitView);
SetDefault(LyricsScrollEasingTypeKey, (int)EasingType.EaseInOutQuad);
SetDefault(LyricsScrollDurationKey, 500); // 500ms
SetDefault(TimelineSyncThresholdKey, 0); // 0ms
SetDefault(IsLyricsFloatAnimationEnabledKey, true);
SetDefault(ResetPositionOffsetOnSongChangedKey, false);
SetDefault(PositionOffsetKey, 0);
SetDefault(LockHotKeyIndexKey, 'U' - 'A');
SetDefault(DockPlacementKey, (int)DockPlacement.Top);
SetDefault(LyricsBgFontOpacityKey, 30); // 30%
SetDefault(HideWindowWhenNotPlayingKey, false);
SetDefault(DockWindowHeightKey, 64); // 64px
SetDefault(SelectedFontFamilyIndexKey, 0);
SetDefault(LyricsFontFamilyKey, FontHelper.SystemFontFamilies.ElementAtOrDefault(0));
SetDefault(IsDragEverywhereEnabledKey, false);
}
public bool IsDragEverywhereEnabled
{
get => GetValue<bool>(IsDragEverywhereEnabledKey);
set => SetValue(IsDragEverywhereEnabledKey, value);
}
public string LyricsFontFamily
{
get => GetValue<string>(LyricsFontFamilyKey)!;
set => SetValue(LyricsFontFamilyKey, value);
}
public int SelectedFontFamilyIndex
{
get => GetValue<int>(SelectedFontFamilyIndexKey);
set => SetValue(SelectedFontFamilyIndexKey, value);
}
public bool HideWindowWhenNotPlaying
{
get => GetValue<bool>(HideWindowWhenNotPlayingKey);
set => SetValue(HideWindowWhenNotPlayingKey, value);
}
public int DockWindowHeight
{
get => GetValue<int>(DockWindowHeightKey);
set => SetValue(DockWindowHeightKey, value);
}
public int LyricsBgFontOpacity
{
get => GetValue<int>(LyricsBgFontOpacityKey);
set => SetValue(LyricsBgFontOpacityKey, value);
}
public bool ShowTranslationOnly
{
get => GetValue<bool>(ShowTranslationOnlyKey);
set => SetValue(ShowTranslationOnlyKey, value);
}
public DockPlacement DockPlacement
{
get => (DockPlacement)GetValue<int>(DockPlacementKey);
set => SetValue(DockPlacementKey, (int)value);
}
public int LockHotKeyIndex
{
get => GetValue<int>(LockHotKeyIndexKey);
set => SetValue(LockHotKeyIndexKey, value);
}
public EasingType LyricsScrollEasingType
{
get => (EasingType)GetValue<int>(LyricsScrollEasingTypeKey);
set => SetValue(LyricsScrollEasingTypeKey, (int)value);
}
public int LyricsScrollDuration
{
get => GetValue<int>(LyricsScrollDurationKey);
set => SetValue(LyricsScrollDurationKey, value);
}
public LyricsDisplayType DisplayType
{
get => (LyricsDisplayType)GetValue<int>(PreferredDisplayTypeKey);
set => SetValue(PreferredDisplayTypeKey, (int)value);
}
public ElementTheme LyricsBackgroundTheme
{
get => (ElementTheme)GetValue<int>(LyricsBackgroundThemeKey);
set => SetValue(LyricsBackgroundThemeKey, (int)value);
}
public AutoStartWindowType AutoStartWindowType
@@ -139,6 +341,31 @@ namespace BetterLyrics.WinUI3.Services
get => GetValue<int>(DesktopWindowHeightKey);
set => SetValue(DesktopWindowHeightKey, value);
}
public int StandardWindowLeft
{
get => GetValue<int>(StandardWindowLeftKey);
set => SetValue(StandardWindowLeftKey, value);
}
public int StandardWindowTop
{
get => GetValue<int>(StandardWindowTopKey);
set => SetValue(StandardWindowTopKey, value);
}
public int StandardWindowWidth
{
get => GetValue<int>(StandardWindowWidthKey);
set => SetValue(StandardWindowWidthKey, value);
}
public int StandardWindowHeight
{
get => GetValue<int>(StandardWindowHeightKey);
set => SetValue(StandardWindowHeightKey, value);
}
public bool AutoLockOnDesktopMode
{
get => GetValue<bool>(AutoLockOnDesktopModeKey);
@@ -193,45 +420,81 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(LanguageKey, (int)value);
}
public List<LocalLyricsFolder> LocalLyricsFolders
public List<LocalMediaFolder> LocalMediaFolders
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LocalLyricsFoldersKey) ?? "[]",
SourceGenerationContext.Default.ListLocalLyricsFolder
SourceGenerationContext.Default.ListLocalMediaFolder
)!;
set =>
SetValue(
LocalLyricsFoldersKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLocalLyricsFolder
SourceGenerationContext.Default.ListLocalMediaFolder
)
);
}
public LyricsAlignmentType LyricsAlignmentType
public TextAlignmentType LyricsAlignmentType
{
get => (LyricsAlignmentType)GetValue<int>(LyricsAlignmentTypeKey);
get => (TextAlignmentType)GetValue<int>(LyricsAlignmentTypeKey);
set => SetValue(LyricsAlignmentTypeKey, (int)value);
}
public TextAlignmentType SongInfoAlignmentType
{
get => (TextAlignmentType)GetValue<int>(SongInfoAlignmentTypeKey);
set => SetValue(SongInfoAlignmentTypeKey, (int)value);
}
public int LyricsBlurAmount
{
get => GetValue<int>(LyricsBlurAmountKey);
set => SetValue(LyricsBlurAmountKey, value);
}
public Color LyricsCustomFontColor
public Color LyricsCustomBgFontColor
{
get => GetValue<int>(LyricsCustomFontColorKey)!.ToColor();
set => SetValue(LyricsCustomFontColorKey, value.ToInt());
get => GetValue<int>(LyricsCustomBgFontColorKey)!.ToColor();
set => SetValue(LyricsCustomBgFontColorKey, value.ToInt());
}
public LyricsFontColorType LyricsFontColorType
public Color LyricsCustomFgFontColor
{
get => (LyricsFontColorType)GetValue<int>(LyricsFontColorTypeKey);
set => SetValue(LyricsFontColorTypeKey, (int)value);
get => GetValue<int>(LyricsCustomFgFontColorKey)!.ToColor();
set => SetValue(LyricsCustomFgFontColorKey, value.ToInt());
}
public Color LyricsCustomStrokeFontColor
{
get => GetValue<int>(LyricsCustomStrokeFontColorKey)!.ToColor();
set => SetValue(LyricsCustomStrokeFontColorKey, value.ToInt());
}
public LyricsFontColorType LyricsBgFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsBgFontColorTypeKey);
set => SetValue(LyricsBgFontColorTypeKey, (int)value);
}
public LyricsFontColorType LyricsFgFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsFgFontColorTypeKey);
set => SetValue(LyricsFgFontColorTypeKey, (int)value);
}
public LyricsFontColorType LyricsStrokeFontColorType
{
get => (LyricsFontColorType)GetValue<int>(LyricsStrokeFontColorTypeKey);
set => SetValue(LyricsStrokeFontColorTypeKey, (int)value);
}
public int LyricsFontStrokeWidth
{
get => GetValue<int>(LyricsFontStrokeWidthKey);
set => SetValue(LyricsFontStrokeWidthKey, value);
}
public int LyricsFontSize
@@ -252,6 +515,12 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(LyricsGlowEffectScopeKey, (int)value);
}
public LineRenderingType LyricsHighlightScope
{
get => (LineRenderingType)GetValue<int>(LyricsHighlightSopeKey);
set => SetValue(LyricsHighlightSopeKey, (int)value);
}
public float LyricsLineSpacingFactor
{
get => GetValue<float>(LyricsLineSpacingFactorKey);
@@ -275,12 +544,106 @@ namespace BetterLyrics.WinUI3.Services
);
}
public List<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(AlbumArtSearchProvidersInfoKey) ?? "[]",
SourceGenerationContext.Default.ListAlbumArtSearchProviderInfo
)!;
set =>
SetValue(
AlbumArtSearchProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListAlbumArtSearchProviderInfo
)
);
}
public List<MediaSourceProviderInfo> MediaSourceProvidersInfo
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(MediaSourceProvidersInfoKey) ?? "[]",
SourceGenerationContext.Default.ListMediaSourceProviderInfo
)!;
set =>
SetValue(
MediaSourceProvidersInfoKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListMediaSourceProviderInfo
)
);
}
public int LyricsVerticalEdgeOpacity
{
get => GetValue<int>(LyricsVerticalEdgeOpacityKey);
set => SetValue(LyricsVerticalEdgeOpacityKey, value);
}
public string LibreTranslateServer
{
get => GetValue<string>(LibreTranslateServerKey)!;
set => SetValue(LibreTranslateServerKey, value);
}
public bool IsTranslationEnabled
{
get => GetValue<bool>(IsTranslationEnabledKey);
set => SetValue(IsTranslationEnabledKey, value);
}
public int SelectedTargetLanguageIndex
{
get => GetValue<int>(SelectedTargetLanguageIndexKey);
set => SetValue(SelectedTargetLanguageIndexKey, value);
}
public string LXMusicServer
{
get => GetValue<string>(LXMusicServerKey)!;
set => SetValue(LXMusicServerKey, value);
}
public bool IgnoreFullscreenWindow
{
get => GetValue<bool>(IgnoreFullscreenWindowKey);
set => SetValue(IgnoreFullscreenWindowKey, value);
}
public int TimelineSyncThreshold
{
get => GetValue<int>(TimelineSyncThresholdKey);
set => SetValue(TimelineSyncThresholdKey, value);
}
public bool IsLyricsFloatAnimationEnabled
{
get => GetValue<bool>(IsLyricsFloatAnimationEnabledKey);
set => SetValue(IsLyricsFloatAnimationEnabledKey, value);
}
public bool ResetPositionOffsetOnSongChanged
{
get => GetValue<bool>(ResetPositionOffsetOnSongChangedKey);
set => SetValue(ResetPositionOffsetOnSongChangedKey, value);
}
public int PositionOffset
{
get => GetValue<int>(PositionOffsetKey);
set => SetValue(PositionOffsetKey, value);
}
public bool IsImmersiveMode
{
get => GetValue<bool>(IsImmersiveModeKey);
set => SetValue(IsImmersiveModeKey, value);
}
private T? GetValue<T>(string key)
{
if (_localSettings.Values.TryGetValue(key, out object? value))

View File

@@ -0,0 +1,86 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.ViewModels;
using Lyricify.Lyrics.Helpers.General;
using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class TranslateService : BaseViewModel, ITranslateService
{
private readonly HttpClient _httpClient;
public TranslateService(ISettingsService settingsService) : base(settingsService)
{
_httpClient = new HttpClient();
}
public async Task<string> TranslateTextAsync(string text, string targetLangCode, CancellationToken? token)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new Exception(text + " is empty or null.");
}
string? originalLangCode = LanguageHelper.DetectLanguageCode(text);
if (string.IsNullOrWhiteSpace(originalLangCode) || originalLangCode == targetLangCode)
{
return text; // No translation needed
}
else if (originalLangCode == "zh-Hant" && targetLangCode == "zh-Hans")
{
return ChineseConverter.ConvertToSimplifiedChinese(text);
}
else if (originalLangCode == "zh-Hans" && targetLangCode == "zh-Hant")
{
return ChineseConverter.ConvertToTraditionalChinese(text);
}
if (string.IsNullOrEmpty(_settingsService.LibreTranslateServer))
{
throw new Exception("LibreTranslate server URL is not set in settings.");
}
var url = $"{_settingsService.LibreTranslateServer}/translate";
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(
[
new("q", text),
new("source", originalLangCode),
new("target", targetLangCode),
]));
token?.ThrowIfCancellationRequested();
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
token?.ThrowIfCancellationRequested();
var result = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.TranslateResponse);
return result?.TranslatedText ?? string.Empty;
}
public int SearchTranslatedLyricsItself(List<LyricsData> lyricsDataArr)
{
string targetLangCode = LanguageHelper.GetUserTargetLanguageCode().Substring(0, 2);
if (lyricsDataArr.Count > 1)
{
for (int i = 1; i < lyricsDataArr.Count; i++)
{
if (lyricsDataArr[i].LanguageCode?.Substring(0, 2) == targetLangCode)
{
return i; // Translation lyrics data found
}
}
}
return -1; // No translation lyrics data found
}
}
}

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -118,40 +118,43 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Local music libraries</value>
<value>本地媒体库</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>Add folders storing music or lyrics</value>
<value>添加存放音乐或歌词的文件夹</value>
</data>
<data name="SettingsPageOpenLogFolderButton.Content" xml:space="preserve">
<value>Open in file explorer</value>
<value>在文件资源管理器中打开</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>Remove from app</value>
<value>从应用中移除</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>You are safe to remove the following items</value>
<value>您可以安全删除以下项目</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>Original files and folders in this path will not be deleted when removing it from this app</value>
<value>路径中的原始文件和文件夹不会被删除</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>Add a folder</value>
<value>添加文件夹</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>Lyrics window theme</value>
<value>歌词背景主题</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>Language</value>
<value>语言</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adaptive to lyrics background (Colored)</value>
<value>适应歌词背景(彩色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>适应歌词背景(彩色)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Light</value>
<value>浅色</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Dark</value>
<value>深色</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
@@ -163,313 +166,320 @@
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>This is an open source app</value>
<value>此应用已开源</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>Open in new window</value>
<value>在新窗口中打开</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>See source code on GitHub</value>
<value>在 GitHub 上查看源代码</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>Version</value>
<value>版本号</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>None</value>
<value></value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>Mica</value>
<value>云母</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>Mica Alt</value>
<value>云母(替代样式)</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>Desktop Acrylic</value>
<value>亚克力(桌面)</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>Acrylic Base</value>
<value>亚克力(基础)</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>Acrylic Thin</value>
<value>亚克力(薄层)</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>Transparent</value>
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Lyrics backdrop</value>
<value>歌词背景材质</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>Default</value>
<value>默认</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>Restart app to apply change</value>
<value>重启应用以应用更改</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>The path cannot be found on your computer</value>
<value>无法在您的计算机中找到该路径</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>The folder has been added. Please do not add it again.</value>
<value>已添加过该文件夹,请勿重复添加</value>
</data>
<data name="SettingsPageLyricsBackground.Header" xml:space="preserve">
<value>Lyrics background</value>
<value>歌词背景</value>
</data>
<data name="SettingsPageDynamicLyricsBackground.Header" xml:space="preserve">
<value>Dynamic lyrics background</value>
<value>动态歌词背景</value>
</data>
<data name="SettingsPageLyricsBackgroundOpacity.Header" xml:space="preserve">
<value>Lyrics background opacity</value>
<value>歌词背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>Settings - BetterLyrics</value>
<value>设置 - BetterLyrics</value>
</data>
<data name="LyricsPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>Alignment</value>
<value>对齐方式</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>Center</value>
<value>居中</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Left</value>
<value>靠左</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>Right</value>
<value>靠右</value>
</data>
<data name="SettingsPageLyricsBackgroundBlurAmount.Header" xml:space="preserve">
<value>Lyrics background blur amount</value>
<value>歌词背景模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>Blur amount</value>
<value>模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>Adjusting this value will also increase the background blur intensity of the album image.</value>
<value>调整该数值将同步提高专辑图片背景模糊强度</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Current value: </value>
<value>当前值:</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>Significantly higher GPU usage when blur is enabled (&gt; 0)</value>
<value>启用模糊(&gt; 0时将显著提升 GPU 占用率</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Top and bottom edge opacity</value>
<value>上下边缘不透明度</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Line spacing</value>
<value>行间距</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>x line height</value>
<value> 倍行高</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>Font size</value>
<value>字体大小</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Lyrics only</value>
<value>仅显示歌词</value>
</data>
<data name="MainWindowImmersiveMode.ToolTipService.ToolTip" xml:space="preserve">
<value>Immersive mode</value>
<value>沉浸模式</value>
</data>
<data name="SettingsPageBackgroundOverlay.Content" xml:space="preserve">
<value>Lyrics background</value>
<value>歌词背景</value>
</data>
<data name="SettingsPageAbout.Content" xml:space="preserve">
<value>About</value>
<value>关于</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>Lyrics library</value>
<value>歌词源</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>App appearance</value>
<value>应用外观</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Glow effect</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>Glow effect scope</value>
<value>辉光效果</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>Configure lyrics search providers</value>
<value>配置歌词源</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>Drag to sort, the lyrics search order will be in the following order</value>
<value>拖动排序,歌词搜索顺序将按以下顺序</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Add</value>
<value>添加</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Welcome to BetterLyrics</value>
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's setup lyrics database now</value>
<value>单击左上按钮以启用沉浸式模式。
如果遇到任何问题,请转到“设置”页面,关于标签,查看常见问题解答或联系作者以获取反馈</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>No music playing now</value>
<value>当前没有正在播放的音乐</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>Developer options</value>
<value>高级选项</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>Play test music</value>
<value>播放测试音乐</value>
</data>
<data name="SettingsPagePlayingMockMusicButton.Content" xml:space="preserve">
<value>Play "Cut To The Feeling" on "soundcloud.com"</value>
<value>在 “soundcloud.com” 上播放 “Cut to the Feeling</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
<value>缓存</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Including log files, network lyrics cache</value>
<value>包括日志文件,网络歌词缓存</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>Font color</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>字体颜色(非当前播放区域)</value>
</data>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>字体颜色(当前播放区域)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adaptive to lyrics background (Grayed)</value>
<value>适应歌词背景(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>适应歌词背景(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>Album art style</value>
<value>专辑区域样式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Corner radius</value>
<value>圆角半径</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Title bar size</value>
<value>标题栏大小</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Compact</value>
<value>紧凑</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Extended</value>
<value>扩展</value>
</data>
<data name="BaseWindowAOTFlyoutItem.Text" xml:space="preserve">
<value>Always on top</value>
<value>将应用置于顶层</value>
</data>
<data name="BaseWindowFullScreenFlyoutItem.Text" xml:space="preserve">
<value>Full screen</value>
<value>全屏</value>
</data>
<data name="BaseWindowEnterFullScreenHint" xml:space="preserve">
<value>Press Esc to exit full screen mode</value>
<value>按 Esc 退出全屏模式</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Hover back again to show the toggle button</value>
<value>再次悬停以显示切换按钮</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
<value>不再显示此消息</value>
</data>
<data name="MainPageNoLocalFilesMatched.Text" xml:space="preserve">
<value>No local files matched</value>
<value>找不到匹配的本地文件</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Album art only</value>
<value>仅显示专辑封面</value>
</data>
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Split view</value>
<value>分屏视图</value>
</data>
<data name="MainPageDisplayTypeSwitcher.ToolTipService.ToolTip" xml:space="preserve">
<value>Change display type</value>
<value>切换显示模式</value>
</data>
<data name="MainPageDesktopLyricsToggler.ToolTipService.ToolTip" xml:space="preserve">
<value>Switch to desktop lyrics mode</value>
<value>切换到桌面歌词模式</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-picture mode</value>
<value>画中画模式</value>
</data>
<data name="BaseWindowUnMiniFlyoutItem.Text" xml:space="preserve">
<value>Exit picture-in-picture mode</value>
<value>退出画中画模式</value>
</data>
<data name="LyricsNotFound" xml:space="preserve">
<value>Lyrics not found</value>
<value>未找到歌词</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
<value>歌词动效</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Lyrics style</value>
<value>歌词样式</value>
</data>
<data name="SettingsPagePathBeIncludedInfo" xml:space="preserve">
<value>This folder is already included in the existing folder and does not need to be added again</value>
<value>该文件夹已包含在已有文件夹中,无需再次添加</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>App behavior</value>
<value>应用行为</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>When starting the app</value>
<value>启动应用时</value>
</data>
<data name="SettingsPageAutoStartInAppLyrics.Content" xml:space="preserve">
<value>Activate standard mode</value>
<value>启动标准模式</value>
</data>
<data name="SettingsPageAutoStartDesktopLyrics.Content" xml:space="preserve">
<value>Activate desktop mode</value>
<value>启动桌面模式</value>
</data>
<data name="SettingsPageAutoStartDockLyrics.Content" xml:space="preserve">
<value>Activate dock mode</value>
<value>启动停靠模式</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>System tray - BetterLyrics</value>
<value>系统托盘 - BetterLyrics</value>
</data>
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>Dock mode</value>
<value>停靠模式</value>
</data>
<data name="SettingsPageAppDock.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>Desktop mode</value>
<value>桌面模式</value>
</data>
<data name="SettingsPageAppDesktop.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>Font weight</value>
<value>字体粗细</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>Thin</value>
<value>极细</value>
</data>
<data name="SettingsPageLyricsExtraLight.Content" xml:space="preserve">
<value>Extra Light</value>
<value>超细</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Light</value>
<value>细体</value>
</data>
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Semi Light</value>
<value>次细</value>
</data>
<data name="SettingsPageLyricsNormal.Content" xml:space="preserve">
<value>Normal</value>
<value>常规</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Medium</value>
<value>中等</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>Semi Bold</value>
<value>次粗</value>
</data>
<data name="SettingsPageLyricsBold.Content" xml:space="preserve">
<value>Bold</value>
<value>粗体</value>
</data>
<data name="SettingsPageLyricsExtraBold.Content" xml:space="preserve">
<value>Extra Bold</value>
<value>特粗</value>
</data>
<data name="SettingsPageLyricsBlack.Content" xml:space="preserve">
<value>Black</value>
<value>黑体</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>Extra Black</value>
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>Whole lyrics</value>
<data name="SettingsPageLyricsRendingScopeCurrentLine.Content" xml:space="preserve">
<value>当前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<value>Current line</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<value>Current char</value>
<data name="SettingsPageLyricsRendingScopeCurrentChar.Content" xml:space="preserve">
<value>当前字符</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
<value>Settings</value>
<value>设置</value>
</data>
<data name="LyricsLoading" xml:space="preserve">
<value>Loading lyrics...</value>
<value>加载歌词中...</value>
</data>
<data name="LyricsSearchProviderLocalLrcFile" xml:space="preserve">
<value>Local .LRC files</value>
<value>本地 .LRC 文件</value>
</data>
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>Local music files</value>
<value>本地音乐文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
@@ -481,13 +491,13 @@
<value>한국어</value>
</data>
<data name="LyricsSearchProviderEslrcFile" xml:space="preserve">
<value>Local .ESLRC files</value>
<value>本地 .ESLRC 文件</value>
</data>
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Local .TTML files</value>
<value>本地 .TTML 文件</value>
</data>
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains added folders, please delete these folders to add the folder</value>
<value>该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
@@ -502,42 +512,288 @@
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Show debug overlay</value>
<value>显示调试覆盖层</value>
</data>
<data name="DependenciesSettingsExpander.Header" xml:space="preserve">
<value>Dependencies</value>
<value>依赖</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Lock</value>
<value>锁定</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>Settings</value>
<value>打开设置</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Exit</value>
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>Unlock the window (Restart needed)</value>
<value>解锁窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Lock</value>
<value>锁定</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>To unlock after locking, go to the system tray to unlock</value>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>锁定后解锁,请转到系统托盘解锁或按下</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Fan lyrics</value>
<value>扇形歌词</value>
</data>
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>Custom</value>
<value>自定义</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>Lyrics style and effect</value>
<value>歌词样式与动效</value>
</data>
<data name="SettingsPageApp.Content" xml:space="preserve">
<value>App appearance and behavior</value>
<value>应用外观与行为</value>
</data>
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>Auto-lock when activating desktop mode</value>
<value>启动桌面模式时随即锁定窗口</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>对齐方式</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>专辑</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>歌曲标题和艺术家</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>缓动动画类型</value>
</data>
<data name="SettingsPagePlaybackLib.Content" xml:space="preserve">
<value>播放源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>播放源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>为指定媒体源启用或禁用歌词显示</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日志记录</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌词翻译</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌词时间轴偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻译服务</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服务器地址</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>测试服务器</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>目标语言</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>翻译服务由 LibreTranslate 驱动</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>访问 https://github.com/LibreTranslate/LibreTranslate 获取安装教程及更多信息(本软件与该翻译服务无任何联系)</value>
</data>
<data name="SettingsPageServerTestSuccessInfo" xml:space="preserve">
<value>服务器测试成功</value>
</data>
<data name="SettingsPageServerTestFailedInfo" xml:space="preserve">
<value>服务器测试失败</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌词描边宽度(仅桌面模式)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自动启动</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌词描边颜色(仅桌面模式)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>始终显示在全屏应用上方</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>当启用停靠模式或桌面模式时强制将本应用显示在全屏应用的上方</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>更多</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌词时间偏移</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻译</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>显示类型</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>设置</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未设置翻译服务器,请先在设置中进行配置</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>切换歌曲时重置为 0</value>
</data>
<data name="SettingsPageTargetLanguage.Description" xml:space="preserve">
<value>将优先读取歌词内翻译,若无匹配则向 LibreTranslate 服务器请求机器翻译</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Header" xml:space="preserve">
<value>配置专辑封面源</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Description" xml:space="preserve">
<value>拖动排序,专辑封面搜索顺序将按以下顺序</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>专辑封面源</value>
</data>
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
<value>本地音乐文件</value>
</data>
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>音乐播放器</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒体库</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌词滚动动画类型</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌词滚动动画持续时间</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>线性</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>平滑步进</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>正弦缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>二次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>弹性缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>回弹缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>弹跳缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>圆形缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>指数缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>五次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>四次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>三次缓入缓出</value>
</data>
<data name="SettingsPageLyricsRendingScopeLineStartToCurrentChar.Content" xml:space="preserve">
<value>当前歌词开始到当前字符</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>高亮显示范围</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌词时间轴同步阈值</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>当歌词进度抖动时,请尝试增加该阈值;更改此值会导致歌词同步有偏差</value>
</data>
<data name="SettingsPageQQGroup.Header" xml:space="preserve">
<value>QQ 反馈交流群</value>
</data>
<data name="SettingsPageDiscord.Header" xml:space="preserve">
<value>Discord</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>立即加入</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>浮动动画</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>范围</value>
</data>
<data name="SettingsPageLockHotKey.Header" xml:space="preserve">
<value>解锁和锁定快捷键</value>
</data>
<data name="SettingsPageLXMusicServer.Header" xml:space="preserve">
<value>LX 音乐服务器</value>
</data>
<data name="SettingsPageDockPlacement.Header" xml:space="preserve">
<value>停靠模式位置</value>
</data>
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>顶部</value>
</data>
<data name="SettingsPageDockPlacementBottom.Content" xml:space="preserve">
<value>底部</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>启用翻译</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>仅显示翻译</value>
</data>
<data name="SettingsPageLyricsBgFontOpacity.Header" xml:space="preserve">
<value>字体不透明度(非当前播放区域)</value>
</data>
<data name="SettingsPageFAQ.Header" xml:space="preserve">
<value>常见问题与解答</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>无法连接到 LX 音乐服务器,请转到设置 - 高级选项以检查是否正确输入链接</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自动隐藏窗口</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>停靠模式或桌面模式下,无歌曲正在播放时,自动隐藏窗口</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窗口高度</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌词字体</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>全局拖拽</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>将标题栏扩展至整个页面使得在任意非交互区域均可拖拽窗口</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音乐库</value>
</data>
</root>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>地元の音楽図書館</value>
<value>地元のメディア図書館</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>音楽や歌詞を保存するフォルダーを追加します</value>
@@ -139,7 +139,7 @@
<value>フォルダーを追加します</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>歌詞ウィンドウのテーマ</value>
<value>歌詞の背景テーマ</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>言語</value>
@@ -147,6 +147,9 @@
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>歌詞の背景に適応する(色付き)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>歌詞の背景に適応する(色付き)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>ライト</value>
</data>
@@ -277,7 +280,7 @@
<value>について</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞ライブラリ</value>
<value>歌詞</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>アプリの外観</value>
@@ -285,11 +288,8 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>グロー効果</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>グローエフェクトスコープ</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>歌詞検索プロバイダーを構成します</value>
<value>歌詞ソースを構成します</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>ドラッグしてソートすると、歌詞の検索注文は次の順序で行われます</value>
@@ -301,13 +301,14 @@
<value>BetterLyrics へようこそ</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>今すぐ歌詞データベースをセットアップしましょう</value>
<value>左上ボタンをクリックして、没入型モードを有効にします。
問題が発生した場合は、[設定]ページ、[タブについて]にアクセスして、FAQを表示するか、フィードバックについて著者に連絡してください</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>今は音楽が再生されていません</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>開発者オプション</value>
<value>高度なオプション</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>テスト音楽を再生します</value>
@@ -321,14 +322,20 @@
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>ログファイル、ネットワーク歌詞キャッシュを含む</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>フォントカラー</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>フォントカラー(非電流再生エリア)</value>
</data>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>フォントカラー(現在の再生エリア)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>歌詞の背景に適応する(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>歌詞の背景に適応する(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>アルバムアートスタイル</value>
<value>アルバムエリアスタイル</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>コーナー半径</value>
@@ -411,9 +418,15 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>ドックモード</value>
</data>
<data name="SettingsPageAppDock.Text" xml:space="preserve">
<value>ドックモード</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>デスクトップモード</value>
</data>
<data name="SettingsPageAppDesktop.Text" xml:space="preserve">
<value>デスクトップモード</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>フォント重量</value>
</data>
@@ -450,13 +463,10 @@
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>余分な黒</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>歌詞全体</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentLine.Content" xml:space="preserve">
<value>現在の行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentChar.Content" xml:space="preserve">
<value>現在の文字</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
@@ -517,13 +527,13 @@
<value>プログラムを終了します</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>ウィンドウのロックを解除する(再起動が必要)</value>
<value>ウィンドウのロックを解除します</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>ロック</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>ロック後にロックを解除するには、システムトレイに移動してロックを解除します</value>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>ロック後にロックを解除するには、システムトレイに移動してロックを解除または押します</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>ファンの歌詞</value>
@@ -531,6 +541,9 @@
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>カスタマイズ</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>カスタマイズ</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>歌詞のスタイルと効果</value>
</data>
@@ -540,4 +553,247 @@
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>デスクトップモードをアクティブにするときの自動ロック</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>アライメント</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>中心</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>アルバムアート</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>曲のタイトル&アーティスト</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>アニメーションタイプを緩和します</value>
</data>
<data name="SettingsPagePlaybackLib.Content" xml:space="preserve">
<value>再生ソース</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>再生ソース</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>指定されたメディアソースの歌詞ディスプレイを有効または無効にする</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>ログレコード</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌詞翻訳</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌詞タイムラインオフセット(ミリ秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>翻訳サービスを構成します</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>サーバーアドレス</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>テストサーバー</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>ターゲット言語</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>LibreTranslate を搭載した翻訳サービス</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>https://github.com/LibreTranslate/LibreTranslate にアクセスしてください。</value>
</data>
<data name="SettingsPageServerTestSuccessInfo" xml:space="preserve">
<value>サーバーテストが成功しました</value>
</data>
<data name="SettingsPageServerTestFailedInfo" xml:space="preserve">
<value>サーバーテストに失敗しました</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌詞ストローク幅(デスクトップモードのみ)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>システムをフォローします</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動起動</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌詞ストロークカラー(デスクトップモードのみ)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>常にフルスクリーンアプリケーションを常に把握してください</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>このアプリは、ドッキングまたはデスクトップモードが有効になっているときにフルスクリーンアプリの上に表示されます</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>もっと</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌詞タイムラインオフセット</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻訳する</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>表示タイプ</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>設定</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>翻訳サーバーは設定されていません。最初に設定で構成してください</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>曲を切り替えるときに0にリセットします</value>
</data>
<data name="SettingsPageTargetLanguage.Description" xml:space="preserve">
<value>歌詞の翻訳は最初に読まれます。一致していない場合、機械の翻訳はLibretranslate Serverから要求されます</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Header" xml:space="preserve">
<value>アルバムカバーソースを構成します</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Description" xml:space="preserve">
<value>ドラッグしてソートすると、アルバムアートサーチオーダーは次の順序で行われます</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>アルバムアートソース</value>
</data>
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
<value>ローカル音楽ファイル</value>
</data>
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>音楽プレーヤー</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞スクロールアニメーションタイプ</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌詞スクロールアニメーションの期間</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>リニア</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>スムーズなステップ</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>サインがゆっくりと出入りします</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>セカンダリスローインとアウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>弾力性は内外に遅くなります</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>リバウンドはスローアウトで遅くなります</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>ゆっくりと出入りします</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>丸い、ゆっくりと出入り</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>インデックスは内外に遅くなります</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>5つの遅いインとアウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>4つの遅いインとアウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>3つの遅いインとアウト</value>
</data>
<data name="SettingsPageLyricsRendingScopeLineStartToCurrentChar.Content" xml:space="preserve">
<value>現在のラインが現在の文字から始まります</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>ハイライトスコープ</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌詞タイムライン同期しきい値</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>歌詞の進行が不安定な場合は、このしきい値を増やしてみてください。この値を変更すると、歌詞の同期が逸脱する可能性があります</value>
</data>
<data name="SettingsPageQQGroup.Header" xml:space="preserve">
<value>QQフィードバックチャットグループ</value>
</data>
<data name="SettingsPageDiscord.Header" xml:space="preserve">
<value>Discord</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>今すぐ参加してください</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>フローティングアニメーション</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>範囲</value>
</data>
<data name="SettingsPageLockHotKey.Header" xml:space="preserve">
<value>ショートカットキーのロックを解除およびロックします</value>
</data>
<data name="SettingsPageLXMusicServer.Header" xml:space="preserve">
<value>LX Music Server</value>
</data>
<data name="SettingsPageDockPlacement.Header" xml:space="preserve">
<value>ドックモード配置</value>
</data>
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>トップ</value>
</data>
<data name="SettingsPageDockPlacementBottom.Content" xml:space="preserve">
<value>底</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>翻訳を有効にします</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>翻訳のみを表示します</value>
</data>
<data name="SettingsPageLyricsBgFontOpacity.Header" xml:space="preserve">
<value>フォントの不透明度(非電流再生エリア)</value>
</data>
<data name="SettingsPageFAQ.Header" xml:space="preserve">
<value>よくある質問</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>LX Music Serverに接続できない場合は、設定に移動してください。リンクが正しく入力されているかどうかを確認するための高度なオプションに移動してください</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自動ハイドウィンドウ</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>ドックモードやデスクトップモードで再生されている曲がないときにウィンドウを自動的に非表示にする</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窓の高さ</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌詞フォントファミリー</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>グローバルドラッグ</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>タイトルバーをページ全体に拡張して、ウィンドウを非対話領域でドラッグできるようにします</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音楽ギャラリー</value>
</data>
</root>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>로컬 음악 도서관</value>
<value>로컬 미디어 라이브러리</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>음악이나 가사를 저장하는 폴더 추가</value>
@@ -139,7 +139,7 @@
<value>폴더를 추가하십시오</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>가사 테마</value>
<value>가사 배경 테마</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>언어</value>
@@ -147,6 +147,9 @@
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>가사 배경 (색상)에 적응</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>가사 배경 (색상)에 적응</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>빛</value>
</data>
@@ -277,7 +280,7 @@
<value>에 대한</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>가사 도서관</value>
<value>가사</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>앱 모양</value>
@@ -285,11 +288,8 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>글로우 효과</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>글로우 효과 범위</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>가사 검색 제공 업체를 구성하십시오</value>
<value>가사 소스를 구성하십시오</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>정렬하기 위해 드래그하면 가사 검색 순서는 다음 순서로됩니다.</value>
@@ -301,13 +301,14 @@
<value>Betterlyrics에 오신 것을 환영합니다</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>지금 가사 데이터베이스를 설정합시다</value>
<value>몰입 형 모드를 활성화하려면 왼쪽 상단 버튼을 클릭하십시오.
문제가 발생하면 설정 페이지, 탭 정보로 이동하여 FAQ를보고 저자에게 연락하여 피드백을 받으십시오.</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>지금 음악이 재생되지 않습니다</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>개발자 옵션</value>
<value>고급 옵션</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>테스트 음악을 재생하십시오</value>
@@ -321,14 +322,20 @@
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>로그 파일, 네트워크 가사 캐시 포함</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>글꼴 색상</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>글꼴 색상 (비 전류 재생 영역)</value>
</data>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>글꼴 색상 (현재 재생 영역)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>가사 배경 (회색)에 적응</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>가사 배경 (회색)에 적응</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>앨범 아트 스타일</value>
<value>앨범 영역 스타일</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>코너 반경</value>
@@ -411,9 +418,15 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>도크 모드</value>
</data>
<data name="SettingsPageAppDock.Text" xml:space="preserve">
<value>도크 모드</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>데스크탑 모드</value>
</data>
<data name="SettingsPageAppDesktop.Text" xml:space="preserve">
<value>데스크탑 모드</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>글꼴 무게</value>
</data>
@@ -450,13 +463,10 @@
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>여분의 검은 색</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>전체 가사</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentLine.Content" xml:space="preserve">
<value>현재 라인</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentChar.Content" xml:space="preserve">
<value>현재 숯</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
@@ -517,13 +527,13 @@
<value>프로그램을 종료하십시오</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>창 잠금 해제 (다시 시작)</value>
<value>창 잠금 해제하십시오</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>잠금</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>잠금 잠금을 해제하려면 시스템 트레이로 이동하여 잠금을 해제하십시오.</value>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>잠금 잠금을 해제하려면 시스템 트레이로 이동하여 잠금을 해제하거나 누릅니다.</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>팬 가사</value>
@@ -531,6 +541,9 @@
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>사용자 정의하십시오</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>사용자 정의하십시오</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>가사 스타일과 효과</value>
</data>
@@ -540,4 +553,247 @@
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>데스크탑 모드를 활성화 할 때 자동 잠금</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>조정</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>센터</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>왼쪽</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>오른쪽</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>앨범 아트</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>노래 제목 및 아티스트</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>애니메이션 유형 완화</value>
</data>
<data name="SettingsPagePlaybackLib.Content" xml:space="preserve">
<value>재생 소스</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>재생 소스</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>지정된 미디어 소스의 가사 디스플레이 활성화 또는 비활성화</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>로그 레코드</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>가사 번역</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>가사 타임 라인 오프셋 (밀리초)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>번역 서비스를 구성하십시오</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>서버 주소</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>테스트 서버</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>대상 언어</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>LibreTranslate 가 구동하는 번역 서비스</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>설치 지침 및 자세한 정보는 https://github.com/LibreTranslate/LibreTranslate 를 방문하십시오 (이 소프트웨어는이 번역 서비스와 제휴하지 않습니다).</value>
</data>
<data name="SettingsPageServerTestSuccessInfo" xml:space="preserve">
<value>서버 테스트 성공</value>
</data>
<data name="SettingsPageServerTestFailedInfo" xml:space="preserve">
<value>서버 테스트가 실패했습니다</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>가사 뇌졸중 너비 (데스크탑 모드 만)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>시스템을 따르십시오</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>자동 시작</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>가사 스트로크 컬러 (데스크탑 모드 만)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>항상 전체 화면 응용 프로그램 위에 머물러 있습니다</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>도킹 또는 데스크탑 모드가 활성화 될 때이 앱이 전체 화면 앱 위에 나타나도록 강요</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>더</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>가사 타임 라인 오프셋</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>번역하다</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>디스플레이 유형</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>설정</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>번역 서버가 설정되지 않았습니다. 먼저 설정으로 구성하십시오.</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>노래를 전환 할 때 0 으로 재설정하십시오</value>
</data>
<data name="SettingsPageTargetLanguage.Description" xml:space="preserve">
<value>가사의 번역은 먼저 읽습니다. 일치하지 않으면 LibreTranslate 서버에서 기계 번역이 요청됩니다.</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Header" xml:space="preserve">
<value>앨범 표지 소스를 구성합니다</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Description" xml:space="preserve">
<value>분류하기 위해 드래그하면 앨범 아트 검색 순서는 다음 순서로됩니다.</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>앨범 아트 소스</value>
</data>
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
<value>로컬 음악 파일</value>
</data>
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>음악 플레이어</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>미디어 라이브러리</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>가사 스크롤링 애니메이션 유형</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>가사 스크롤링 애니메이션 지속 시간</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>선의</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>부드러운 단계</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>천천히 입력하십시오</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>이차 느린 속도가 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>탄력성이 속도가 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>리바운드는 느리게 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>천천히 안팎으로 튀어 나옵니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>둥글고 느리게 안팎으로</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>인덱스 속도가 느려집니다</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>5 번 느리게 안팎으로</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>4 개의 느린 안팎</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>세 번 느리게 안팎으로</value>
</data>
<data name="SettingsPageLyricsRendingScopeLineStartToCurrentChar.Content" xml:space="preserve">
<value>현재 라인은 현재 숯으로 시작합니다</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>하이라이트 범위</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>가사 타임 라인 동기화 임계 값</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>가사 진행 상황이 불안하다면이 임계 값을 높이십시오. 이 값을 변경하면 가사가 동기화 될 수 있습니다</value>
</data>
<data name="SettingsPageQQGroup.Header" xml:space="preserve">
<value>QQ 피드백 및 채팅 그룹</value>
</data>
<data name="SettingsPageDiscord.Header" xml:space="preserve">
<value>Discord</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>지금 가입하십시오</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>떠 다니는 애니메이션</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>범위</value>
</data>
<data name="SettingsPageLockHotKey.Header" xml:space="preserve">
<value>바로 가기 키를 잠금 해제하고 잠그십시오</value>
</data>
<data name="SettingsPageLXMusicServer.Header" xml:space="preserve">
<value>LX 음악 서버</value>
</data>
<data name="SettingsPageDockPlacement.Header" xml:space="preserve">
<value>도크 모드 배치</value>
</data>
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>맨 위</value>
</data>
<data name="SettingsPageDockPlacementBottom.Content" xml:space="preserve">
<value>맨 아래</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>번역 활성화</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>번역 만 표시하십시오</value>
</data>
<data name="SettingsPageLyricsBgFontOpacity.Header" xml:space="preserve">
<value>글꼴 불투명 (비 전류 재생 영역)</value>
</data>
<data name="SettingsPageFAQ.Header" xml:space="preserve">
<value>자주 묻는 질문</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>LX Music Server에 연결할 수 없습니다. 설정으로 이동하십시오 - 고급 옵션이 링크가 올바르게 입력되었는지 확인하십시오.</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>자동 가죽 창</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>도크 모드 또는 데스크탑 모드에서 재생되지 않을 때는 창을 자동으로 숨기고 있습니다.</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>창 높이</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>가사 글꼴 가족</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>글로벌 드래그</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>비 중과 영역에서 창을 드래그 할 수 있도록 제목 표시 줄을 전체 페이지로 확장하십시오.</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>음악 갤러리</value>
</data>
</root>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>本地音乐媒体库</value>
<value>本地媒体库</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>添加存放音乐或歌词的文件夹</value>
@@ -139,7 +139,7 @@
<value>添加文件夹</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>歌词窗口主题</value>
<value>歌词背景主题</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>语言</value>
@@ -147,6 +147,9 @@
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>适应歌词背景(彩色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>适应歌词背景(彩色)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>浅色</value>
</data>
@@ -277,7 +280,7 @@
<value>关于</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌词</value>
<value>歌词</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>应用外观</value>
@@ -285,11 +288,8 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>辉光效果</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>辉光效果作用范围</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌词搜索服务</value>
<value>配置歌词</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>拖动排序,歌词搜索顺序将按以下顺序</value>
@@ -301,13 +301,14 @@
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>现在就来初始化歌词数据库吧</value>
<value>单击左上按钮以启用沉浸式模式。
如果遇到任何问题,请转到“设置”页面,关于标签,查看常见问题解答或联系作者以获取反馈</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>当前没有正在播放的音乐</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>开发者选项</value>
<value>高级选项</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>播放测试音乐</value>
@@ -321,14 +322,20 @@
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>包括日志文件,网络歌词缓存</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>字体颜色</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>字体颜色(非当前播放区域)</value>
</data>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>字体颜色(当前播放区域)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>适应歌词背景(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>适应歌词背景(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>专辑封面样式</value>
<value>专辑区域样式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圆角半径</value>
@@ -411,9 +418,15 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="SettingsPageAppDock.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageAppDesktop.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字体粗细</value>
</data>
@@ -450,13 +463,10 @@
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>全部歌词</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentLine.Content" xml:space="preserve">
<value>当前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentChar.Content" xml:space="preserve">
<value>当前字符</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
@@ -517,13 +527,13 @@
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>解锁窗口(需要重新启动)</value>
<value>解锁窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>锁定</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>锁定后解锁,请转到系统托盘解锁</value>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>锁定后解锁,请转到系统托盘解锁或按下</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>扇形歌词</value>
@@ -531,6 +541,9 @@
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>歌词样式与动效</value>
</data>
@@ -540,4 +553,247 @@
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>启动桌面模式时随即锁定窗口</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>对齐方式</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>专辑</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>歌曲标题和艺术家</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>缓动动画类型</value>
</data>
<data name="SettingsPagePlaybackLib.Content" xml:space="preserve">
<value>播放源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>播放源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>为指定媒体源启用或禁用歌词显示</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日志记录</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌词翻译</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌词时间轴偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻译服务</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服务器地址</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>测试服务器</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>目标语言</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>翻译服务由 LibreTranslate 驱动</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>访问 https://github.com/LibreTranslate/LibreTranslate 获取安装教程及更多信息(本软件与该翻译服务无任何联系)</value>
</data>
<data name="SettingsPageServerTestSuccessInfo" xml:space="preserve">
<value>服务器测试成功</value>
</data>
<data name="SettingsPageServerTestFailedInfo" xml:space="preserve">
<value>服务器测试失败</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌词描边宽度(仅桌面模式)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自动启动</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌词描边颜色(仅桌面模式)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>始终显示在全屏应用上方</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>当启用停靠模式或桌面模式时强制将本应用显示在全屏应用的上方</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>更多</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌词时间偏移</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻译</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>显示类型</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>设置</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未设置翻译服务器,请先在设置中进行配置</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>切换歌曲时重置为 0</value>
</data>
<data name="SettingsPageTargetLanguage.Description" xml:space="preserve">
<value>将优先读取歌词内翻译,若无匹配则向 LibreTranslate 服务器请求机器翻译</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Header" xml:space="preserve">
<value>配置专辑封面源</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Description" xml:space="preserve">
<value>拖动排序,专辑封面搜索顺序将按以下顺序</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>专辑封面源</value>
</data>
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
<value>本地音乐文件</value>
</data>
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>音乐播放器</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒体库</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌词滚动动画类型</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌词滚动动画持续时间</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>线性</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>平滑步进</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>正弦缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>二次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>弹性缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>回弹缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>弹跳缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>圆形缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>指数缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>五次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>四次缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>三次缓入缓出</value>
</data>
<data name="SettingsPageLyricsRendingScopeLineStartToCurrentChar.Content" xml:space="preserve">
<value>当前歌词开始到当前字符</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>高亮显示范围</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌词时间轴同步阈值</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>当歌词进度抖动时,请尝试增加该阈值;更改此值会导致歌词同步有偏差</value>
</data>
<data name="SettingsPageQQGroup.Header" xml:space="preserve">
<value>QQ 反馈交流群</value>
</data>
<data name="SettingsPageDiscord.Header" xml:space="preserve">
<value>Discord</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>立即加入</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>浮动动画</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>范围</value>
</data>
<data name="SettingsPageLockHotKey.Header" xml:space="preserve">
<value>解锁和锁定快捷键</value>
</data>
<data name="SettingsPageLXMusicServer.Header" xml:space="preserve">
<value>LX 音乐服务器</value>
</data>
<data name="SettingsPageDockPlacement.Header" xml:space="preserve">
<value>停靠模式位置</value>
</data>
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>顶部</value>
</data>
<data name="SettingsPageDockPlacementBottom.Content" xml:space="preserve">
<value>底部</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>启用翻译</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>仅显示翻译</value>
</data>
<data name="SettingsPageLyricsBgFontOpacity.Header" xml:space="preserve">
<value>字体不透明度(非当前播放区域)</value>
</data>
<data name="SettingsPageFAQ.Header" xml:space="preserve">
<value>常见问题与解答</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>无法连接到 LX 音乐服务器,请转到设置 - 高级选项以检查是否正确输入链接</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自动隐藏窗口</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>停靠模式或桌面模式下,无歌曲正在播放时,自动隐藏窗口</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窗口高度</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌词字体</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>全局拖拽</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>将标题栏扩展至整个页面使得在任意非交互区域均可拖拽窗口</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音乐库</value>
</data>
</root>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>本地音樂媒體庫</value>
<value>本地媒體庫</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>新增存放音樂或歌詞的資料夾</value>
@@ -139,7 +139,7 @@
<value>新增資料夾</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>歌詞窗口主題</value>
<value>歌詞背景主題</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>語言</value>
@@ -147,6 +147,9 @@
<data name="SettingsPageLyricsFontColorAdaptiveColored.Content" xml:space="preserve">
<value>適應歌詞背景(彩色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>適應歌詞背景(彩色)</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>淺色</value>
</data>
@@ -277,7 +280,7 @@
<value>關於</value>
</data>
<data name="SettingsPageLyricsLib.Content" xml:space="preserve">
<value>歌詞</value>
<value>歌詞</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>應用外觀</value>
@@ -285,11 +288,8 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>輝光效果</value>
</data>
<data name="SettingsPageLyricsGlowEffectScope.Header" xml:space="preserve">
<value>輝光效果作用範圍</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Header" xml:space="preserve">
<value>配置歌詞搜尋服務</value>
<value>配置歌詞</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Description" xml:space="preserve">
<value>拖動排序,歌詞搜索順序將按以下順序</value>
@@ -301,13 +301,14 @@
<value>歡迎使用 BetterLyrics</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>現在就來初始化歌詞資料庫吧</value>
<value>單擊左上按鈕以啟用沉浸式模式。
如果遇到任何問題,請轉到“設置”頁面,關於標籤,查看常見問題解答或聯繫作者以獲取反饋</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>目前沒有正在播放的音樂</value>
</data>
<data name="SettingsPageDev.Content" xml:space="preserve">
<value>開發者選項</value>
<value>高級選項</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>播放測試音樂</value>
@@ -321,14 +322,20 @@
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>包括日誌文件,網絡歌詞緩存</value>
</data>
<data name="SettingsPageLyricsFontColor.Header" xml:space="preserve">
<value>字體顏色</value>
<data name="SettingsPageLyricsBgFontColor.Header" xml:space="preserve">
<value>字體顏色(非當前播放區域)</value>
</data>
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>字體顏色(當前播放區域)</value>
</data>
<data name="SettingsPageLyricsFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>適應歌詞背景(灰色)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>適應歌詞背景(灰色)</value>
</data>
<data name="SettingsPageAlbumStyle.Content" xml:space="preserve">
<value>專輯封面樣式</value>
<value>專輯區域樣式</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>圓角半徑</value>
@@ -411,9 +418,15 @@
<data name="HostWindowDockFlyoutItem.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="SettingsPageAppDock.Text" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="HostWindowDesktopFlyoutItem.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageAppDesktop.Text" xml:space="preserve">
<value>桌面模式</value>
</data>
<data name="SettingsPageLyricsFontWeight.Header" xml:space="preserve">
<value>字體粗細</value>
</data>
@@ -450,13 +463,10 @@
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超黑</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeWholeLyrics.Content" xml:space="preserve">
<value>全部歌詞</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentLine.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentLine.Content" xml:space="preserve">
<value>目前行</value>
</data>
<data name="SettingsPageLyricsGlowEffectScopeCurrentChar.Content" xml:space="preserve">
<data name="SettingsPageLyricsRendingScopeCurrentChar.Content" xml:space="preserve">
<value>目前字元</value>
</data>
<data name="HostWindowSettingsFlyoutItem.Text" xml:space="preserve">
@@ -517,13 +527,13 @@
<value>退出程序</value>
</data>
<data name="SystemTrayUnlock.Text" xml:space="preserve">
<value>解鎖窗口(需要重新啟動)</value>
<value>解鎖窗口</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>鎖定</value>
</data>
<data name="HostWindowLockToolTip.Content" xml:space="preserve">
<value>鎖定後解鎖,請轉到系統托盤解鎖</value>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>鎖定後解鎖,請轉到系統托盤解鎖或按下</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>扇形歌詞</value>
@@ -531,6 +541,9 @@
<data name="SettingsPageLyricsFontColorCustom.Content" xml:space="preserve">
<value>自定義</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>自定義</value>
</data>
<data name="SettingsPageLyrics.Content" xml:space="preserve">
<value>歌詞樣式與動效</value>
</data>
@@ -540,4 +553,247 @@
<data name="SettingsPageAutoLock.Header" xml:space="preserve">
<value>啟動桌面模式時隨即鎖定窗口</value>
</data>
<data name="SettingsPageSongInfoAlignment.Header" xml:space="preserve">
<value>對齊方式</value>
</data>
<data name="SettingsPageSongInfoCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageSongInfoLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageSongInfoRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageAlbumArt.Text" xml:space="preserve">
<value>專輯</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>歌曲標題和藝術家</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>缓动动画类型</value>
</data>
<data name="SettingsPagePlaybackLib.Content" xml:space="preserve">
<value>播放來源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Header" xml:space="preserve">
<value>播放來源</value>
</data>
<data name="SettingsPageMediaSourceProvidersConfig.Description" xml:space="preserve">
<value>為指定媒體源啟用或禁用歌詞顯示</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>日誌記錄</value>
</data>
<data name="SettingsPageTranslation.Content" xml:space="preserve">
<value>歌詞翻譯</value>
</data>
<data name="MainPagePositionOffsetSlider.Header" xml:space="preserve">
<value>歌詞時間軸偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻譯服務</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服務器地址</value>
</data>
<data name="SettingsPageServerTestButton.Content" xml:space="preserve">
<value>測試服務器</value>
</data>
<data name="SettingsPageTargetLanguage.Header" xml:space="preserve">
<value>目標語言</value>
</data>
<data name="SettingsPageTranslationInfo.Header" xml:space="preserve">
<value>翻译服务由 LibreTranslate 驱动</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>造訪 https://github.com/LibreTranslate/LibreTranslate 以取得安裝教學及更多資訊(本軟體與此翻譯服務無任何關聯)</value>
</data>
<data name="SettingsPageServerTestSuccessInfo" xml:space="preserve">
<value>服務器測試成功</value>
</data>
<data name="SettingsPageServerTestFailedInfo" xml:space="preserve">
<value>服務器測試失敗</value>
</data>
<data name="SettingsPageLyricsFontStrokeWidth.Header" xml:space="preserve">
<value>歌詞描邊寬度(僅桌面模式)</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動啟動</value>
</data>
<data name="SettingsPageLyricsStrokeFontColor.Header" xml:space="preserve">
<value>歌词描边颜色(仅桌面模式)</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Header" xml:space="preserve">
<value>始終顯示在全螢幕應用程式上方</value>
</data>
<data name="SettingsPageIgnoreFullscreenWindow.Description" xml:space="preserve">
<value>啟用停靠模式或桌面模式時強制將本應用程式顯示在全螢幕應用程式的上方</value>
</data>
<data name="HostWindowMoreButtonToolTip.Content" xml:space="preserve">
<value>更多</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>歌詞時間偏移</value>
</data>
<data name="LyricsPageTranslationButtonToolTip.Content" xml:space="preserve">
<value>翻譯</value>
</data>
<data name="LyricsPageDisplayTypeButtonToolTip.Content" xml:space="preserve">
<value>顯示類型</value>
</data>
<data name="LyricsPageSettingsButtonToolTip.Content" xml:space="preserve">
<value>設定</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未設定翻譯伺服器,請先在設定中進行配置</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>切換歌曲時重置為 0</value>
</data>
<data name="SettingsPageTargetLanguage.Description" xml:space="preserve">
<value>將優先讀取歌詞內翻譯,若無匹配則向 LibreTranslate 伺服器請求機器翻譯</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Header" xml:space="preserve">
<value>配置專輯封面源</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Description" xml:space="preserve">
<value>拖曳排序,專輯封面搜尋順序將按以下順序</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>專輯封面來源</value>
</data>
<data name="AlbumArtSearchLocalProvider" xml:space="preserve">
<value>本地音樂文件</value>
</data>
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>音樂播放器</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>媒體庫</value>
</data>
<data name="SettingsPageScrollEasing.Header" xml:space="preserve">
<value>歌詞滾動動畫類型</value>
</data>
<data name="SettingsPageScrollDuration.Header" xml:space="preserve">
<value>歌詞滾動動畫持續時間</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>線性</value>
</data>
<data name="SettingsPageEasingTypeSmoothStep.Content" xml:space="preserve">
<value>平滑步進</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>正弦緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>二次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>彈性緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>回彈緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>彈跳緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>圓形緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>指數緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>五次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>四次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>三次緩入緩出</value>
</data>
<data name="SettingsPageLyricsRendingScopeLineStartToCurrentChar.Content" xml:space="preserve">
<value>當前歌詞開始到當前字符</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>高亮顯示範圍</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>歌詞時間軸同步閾值</value>
</data>
<data name="SettingsPageLyricsTimelineThreshold.Description" xml:space="preserve">
<value>當歌詞進度抖動時,請嘗試增加該閾值;更改此值會導致歌詞同步偏差</value>
</data>
<data name="SettingsPageQQGroup.Header" xml:space="preserve">
<value>QQ 回饋交流群</value>
</data>
<data name="SettingsPageDiscord.Header" xml:space="preserve">
<value>Discord</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>立即加入</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>浮動動畫</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>範圍</value>
</data>
<data name="SettingsPageLockHotKey.Header" xml:space="preserve">
<value>解鎖和鎖定快捷鍵</value>
</data>
<data name="SettingsPageLXMusicServer.Header" xml:space="preserve">
<value>LX 音樂服務器</value>
</data>
<data name="SettingsPageDockPlacement.Header" xml:space="preserve">
<value>停靠模式位置</value>
</data>
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>頂部</value>
</data>
<data name="SettingsPageDockPlacementBottom.Content" xml:space="preserve">
<value>底部</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>啟用翻譯</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>僅顯示翻譯</value>
</data>
<data name="SettingsPageLyricsBgFontOpacity.Header" xml:space="preserve">
<value>字體不透明度(非目前播放區域)</value>
</data>
<data name="SettingsPageFAQ.Header" xml:space="preserve">
<value>常見問題與解答</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>無法連接到 LX 音樂服務器,請轉到設置 - 高級選項以檢查是否正確輸入鏈接</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>自動隱藏窗口</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>停靠模式或桌面模式下,無歌曲正在播放時,自動隱藏窗口</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>窗口高度</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>歌詞字體</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>全域拖曳</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>將標題列擴展至整個頁面使得在任意非互動區域均可拖曳窗口</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音樂庫</value>
</data>
</root>

View File

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

View File

@@ -11,8 +11,7 @@ using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class BaseWindowViewModel : BaseViewModel
public partial class BaseWindowViewModel(ISettingsService settingsService) : BaseViewModel(settingsService)
{
public BaseWindowViewModel(ISettingsService settingsService) : base(settingsService) { }
}
}

View File

@@ -1,9 +1,5 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
@@ -13,46 +9,92 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using WinUIEx.Messaging;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsPageViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<int>>, IRecipient<PropertyChangedMessage<bool>>
public partial class LyricsPageViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<TimeSpan>>
{
private readonly IPlaybackService _playbackService;
private LyricsDisplayType? _preferredDisplayTypeBeforeSwitchToNonStandardMode;
private readonly ThrottleHelper _timelineThrottle = new(TimeSpan.FromSeconds(1));
public LyricsPageViewModel(ISettingsService settingsService, IPlaybackService playbackService) : base(settingsService)
{
IsFirstRun = _settingsService.IsFirstRun;
IsTranslationEnabled = _settingsService.IsTranslationEnabled;
DisplayType = _settingsService.DisplayType;
ResetPositionOffsetOnSongChanged = _settingsService.ResetPositionOffsetOnSongChanged;
PositionOffset = _settingsService.PositionOffset;
IsImmersiveMode = _settingsService.IsImmersiveMode;
ShowTranslationOnly = _settingsService.ShowTranslationOnly;
LyricsFontSize = _settingsService.LyricsFontSize;
CoverImageRadius = _settingsService.CoverImageRadius;
LyricsFontFamily = _settingsService.LyricsFontFamily;
OnIsImmersiveModeChanged(IsImmersiveMode);
//Volume = SystemVolumeHelper.GetMasterVolume();
//SystemVolumeHelper.VolumeChanged += SystemVolumeHelper_VolumeChanged;
_playbackService = playbackService;
_playbackService.SongInfoChanged += async (_, args) =>
await UpdateSongInfoUI(args.SongInfo).ConfigureAwait(true);
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
IsFirstRun = _settingsService.IsFirstRun;
IsSongPlaying = _playbackService.IsPlaying;
}
UpdateSongInfoUI(_playbackService.SongInfo).ConfigureAwait(true);
//private void SystemVolumeHelper_VolumeChanged(int volume)
//{
// Volume = volume;
//}
private void PlaybackService_IsPlayingChanged(object? sender, Events.IsPlayingChangedEventArgs e)
{
IsSongPlaying = e.IsPlaying;
}
private void PlaybackService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
SongDurationSeconds = SongInfo?.Duration ?? 0;
if (ResetPositionOffsetOnSongChanged)
{
PositionOffset = 0;
}
}
[ObservableProperty]
public partial bool AboutToUpdateUI { get; set; }
public partial double TimelinePositionSeconds { get; set; }
[ObservableProperty]
public partial BitmapImage? CoverImage { get; set; }
public partial int SongDurationSeconds { get; set; }
[ObservableProperty]
public partial double CoverImageGridActualHeight { get; set; }
public partial int Volume { get; set; }
[ObservableProperty]
public partial CornerRadius CoverImageGridCornerRadius { get; set; }
public partial string LyricsFontFamily { get; set; }
[ObservableProperty]
public partial int CoverImageRadius { get; set; }
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial bool IsImmersiveMode { get; set; }
[ObservableProperty]
public partial float BottomCommandGridOpacity { get; set; }
[ObservableProperty]
public partial float BottomCommandFlyoutTriggerOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
@@ -61,36 +103,30 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial bool IsFirstRun { get; set; }
[ObservableProperty]
public partial bool IsNotDockMode { get; set; } = true;
[ObservableProperty]
public partial bool IsWelcomeTeachingTipOpen { get; set; }
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial double MaxLyricsWidth { get; set; } = 0.0;
[ObservableProperty]
public partial LyricsDisplayType? PreferredDisplayType { get; set; } = LyricsDisplayType.SplitView;
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; } = null;
public void OpenMatchedFileFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"/select,\"{path}\"",
UseShellExecute = true,
}
);
}
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int PositionOffset { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsTranslationEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool ShowTranslationOnly { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool ResetPositionOffsetOnSongChanged { get; set; }
[ObservableProperty]
public partial bool IsSongPlaying { get; set; }
public void Receive(PropertyChangedMessage<bool> message)
{
@@ -98,109 +134,61 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(LyricsWindowViewModel.IsDockMode))
{
IsNotDockMode = !message.NewValue;
SetNonStandardModePreferredDisplayType(message.NewValue);
TrySwitchToPreferredDisplayType(SongInfo);
if (message.NewValue)
{
DisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
DisplayType = _settingsService.DisplayType;
}
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
SetNonStandardModePreferredDisplayType(message.NewValue);
TrySwitchToPreferredDisplayType(SongInfo);
if (message.NewValue)
{
DisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
DisplayType = _settingsService.DisplayType;
}
}
}
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.CoverImageRadius))
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsImmersiveMode))
{
CoverImageRadius = message.NewValue;
IsImmersiveMode = message.NewValue;
}
}
if (message.Sender is LyricsSettingsControlViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
public async Task UpdateSongInfoUI(SongInfo? songInfo)
{
AboutToUpdateUI = true;
await Task.Delay(AnimationHelper.StoryboardDefaultDuration);
SongInfo = songInfo;
CoverImage =
(songInfo?.AlbumArt == null)
? null
: await ImageHelper.GetBitmapImageFromBytesAsync(songInfo.AlbumArt);
TrySwitchToPreferredDisplayType(songInfo);
AboutToUpdateUI = false;
}
[RelayCommand]
private void OpenSettingsWindow()
private static void OpenSettingsWindow()
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
private void SetNonStandardModePreferredDisplayType(bool isEnabled)
[RelayCommand]
private async Task PlaySongAsync()
{
if (isEnabled)
{
_preferredDisplayTypeBeforeSwitchToNonStandardMode = PreferredDisplayType;
PreferredDisplayType = LyricsDisplayType.LyricsOnly;
}
else
{
PreferredDisplayType = _preferredDisplayTypeBeforeSwitchToNonStandardMode;
}
await _playbackService.PlayAsync();
}
private void TrySwitchToPreferredDisplayType(SongInfo? songInfo)
[RelayCommand]
private async Task PauseSongAsync()
{
LyricsDisplayType displayType;
if (songInfo == null)
{
displayType = LyricsDisplayType.PlaceholderOnly;
}
else if (PreferredDisplayType is LyricsDisplayType preferredDisplayType)
{
displayType = preferredDisplayType;
}
else
{
displayType = LyricsDisplayType.SplitView;
}
DisplayType = displayType;
await _playbackService.PauseAsync();
}
partial void OnCoverImageGridActualHeightChanged(double value)
[RelayCommand]
private async Task PreviousSongAsync()
{
if (double.IsNaN(value))
return;
CoverImageGridCornerRadius = new CornerRadius(CoverImageRadius / 100f * value / 2);
await _playbackService.PreviousAsync();
}
partial void OnCoverImageRadiusChanged(int value)
[RelayCommand]
private async Task NextSongAsync()
{
if (double.IsNaN(CoverImageGridActualHeight))
return;
CoverImageGridCornerRadius = new CornerRadius(
value / 100f * CoverImageGridActualHeight / 2
);
await _playbackService.NextAsync();
}
partial void OnIsFirstRunChanged(bool value)
@@ -208,5 +196,78 @@ namespace BetterLyrics.WinUI3.ViewModels
IsWelcomeTeachingTipOpen = value;
_settingsService.IsFirstRun = false;
}
partial void OnIsTranslationEnabledChanged(bool value)
{
_settingsService.IsTranslationEnabled = value;
}
partial void OnPositionOffsetChanged(int value)
{
_settingsService.PositionOffset = value;
}
partial void OnIsImmersiveModeChanged(bool value)
{
if (value)
{
BottomCommandGridOpacity = 0f;
BottomCommandFlyoutTriggerOpacity = 0f;
}
else
{
BottomCommandGridOpacity = 1f;
BottomCommandFlyoutTriggerOpacity = 1f;
}
}
partial void OnShowTranslationOnlyChanged(bool value)
{
_settingsService.ShowTranslationOnly = value;
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
LyricsFontSize = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontFamily))
{
LyricsFontFamily = message.NewValue;
}
}
}
//partial void OnVolumeChanged(int value)
//{
// SystemVolumeHelper.SetMasterVolume(value);
//}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.TotalTime))
{
if (_timelineThrottle.CanTrigger())
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelinePositionSeconds = message.NewValue.TotalSeconds;
});
}
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
public LyricsRendererViewModel(ISettingsService settingsService, IPlaybackService playbackService, ILyricsSearchService musicSearchService, ILibWatcherService libWatcherService, ITranslateService libreTranslateService) : base(settingsService)
{
_lyrcsSearchService = musicSearchService;
_playbackService = playbackService;
_libWatcherService = libWatcherService;
_translateService = libreTranslateService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
_albumArtCornerRadius = _settingsService.CoverImageRadius;
_isDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
_albumArtBgOpacity = _settingsService.CoverOverlayOpacity;
_albumArtBgBlurAmount = _settingsService.CoverOverlayBlurAmount;
_lyricsBgFontColorType = _settingsService.LyricsBgFontColorType;
_lyricsFgFontColorType = _settingsService.LyricsFgFontColorType;
_lyricsTextFormat.FontWeight = _settingsService.LyricsFontWeight.ToFontWeight();
_lyricsTextFormat.FontFamily = _artistTextFormat.FontFamily = _titleTextFormat.FontFamily = _settingsService.LyricsFontFamily;
_lyricsAlignmentType = _settingsService.LyricsAlignmentType;
_lyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
_lyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
_lyricsFontSize = _settingsService.LyricsFontSize;
_lyricsBlurAmount = _settingsService.LyricsBlurAmount;
_isLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
_lyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
_lyricsHighlightScope = _settingsService.LyricsHighlightScope;
_customBgFontColor = _settingsService.LyricsCustomBgFontColor;
_customFgFontColor = _settingsService.LyricsCustomFgFontColor;
_lyricsBgTheme = _settingsService.LyricsBackgroundTheme;
_isFanLyricsEnabled = _settingsService.IsFanLyricsEnabled;
_lyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
_isTranslationEnabled = _settingsService.IsTranslationEnabled;
_showTranslationOnly = _settingsService.ShowTranslationOnly;
_targetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
_timelineSyncThreshold = _settingsService.TimelineSyncThreshold;
_canvasYScrollTransition.SetDuration(_settingsService.LyricsScrollDuration / 1000f);
_canvasYScrollTransition.SetEasingType(_settingsService.LyricsScrollEasingType);
_defaultOpacity = _settingsService.LyricsBgFontOpacity / 100f;
_isLyricsFloatAnimationEnabled = _settingsService.IsLyricsFloatAnimationEnabled;
_displayType = _displayTypeReceived = _settingsService.DisplayType;
_libWatcherService.MusicLibraryFilesChanged +=
LibWatcherService_MusicLibraryFilesChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.AlbumArtChangedChanged += PlaybackService_AlbumArtChangedChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
_isPlaying = _playbackService.IsPlaying;
UpdateColorConfig();
}
}
}

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
@@ -11,8 +7,11 @@ using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
@@ -25,100 +24,150 @@ namespace BetterLyrics.WinUI3.ViewModels
using var blurredLyrics = new CanvasCommandList(control);
using (var blurredLyricsDs = blurredLyrics.CreateDrawingSession())
{
switch (DisplayType)
{
case LyricsDisplayType.AlbumArtOnly:
case LyricsDisplayType.PlaceholderOnly:
break;
case LyricsDisplayType.LyricsOnly:
case LyricsDisplayType.SplitView:
DrawBlurredLyrics(control, blurredLyricsDs);
break;
default:
break;
}
DrawBlurredLyrics(control, blurredLyricsDs);
}
if (_lastAlbumArtSwBitmap != null && _lastAlbumArtCanvasBitmap == null)
{
_lastAlbumArtCanvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, _lastAlbumArtSwBitmap);
}
if (_albumArtSwBitmap != null && _albumArtCanvasBitmap == null)
{
_albumArtCanvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, _albumArtSwBitmap);
}
using var combined = new CanvasCommandList(control);
using var combinedDs = combined.CreateDrawingSession();
DrawAlbumArtBackground(control, combinedDs);
if (_isDockMode)
{
DrawImmersiveBackground(control, combinedDs);
DrawImmersiveBackground(control, combinedDs, 0f);
}
else if (_isDesktopMode)
{
DrawImmersiveBackground(control, combinedDs, 0f);
}
else
{
DrawAlbumArtBackground(control, combinedDs);
}
combinedDs.DrawImage(blurredLyrics);
if (_isDesktopMode)
{
ds.DrawImage(blurredLyrics);
}
else
{
ds.DrawImage(combined);
}
ds.DrawImage(combined);
DrawAlbumArt(control, ds);
DrawTitleAndArtist(control, ds);
if (_isDebugOverlayEnabled)
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
var currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(currentPlayingLineIndex);
var currentPlayingLine = _lyricsDataArr
.ElementAtOrDefault(_langIndex)
?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine != null)
{
GetLinePlayingProgress(
currentPlayingLine,
_playingLineIndex,
out int charStartIndex,
out int charLength,
out float charProgress
);
ds.DrawText(
$"DEBUG: "
+ $"Cur playing {currentPlayingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n"
+ $"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n"
+ $"Cur time {TotalTime}\n" +
$"Lang size: {_multiLangLyrics.Count}\n" +
$"{_lyricsOpacityTransition.Value}",
$"[DEBUG]\n" +
$"Cur playing {_playingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n" +
$"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
$"Cur time {TotalTime + _positionOffset}\n" +
$"Lang size {_lyricsDataArr.Count}\n" +
$"Song duration {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}",
new Vector2(10, 10),
ThemeTypeSent == Microsoft.UI.Xaml.ElementTheme.Light ? Colors.Black : Colors.White
);
//for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
//{
// LyricsLine? line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
// if (line != null)
// {
// ds.DrawText(
// $"[{i}] {line.OriginalText} {line.HighlightOpacityTransition.Value}",
// new Vector2(10, 30 + (i - _startVisibleLineIndex) * 20),
// ThemeTypeSent == ElementTheme.Light ? Colors.Black : Colors.White
// );
// }
//}
}
}
}
private static void DrawImgae(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
SoftwareBitmap softwareBitmap,
float opacity
)
private void DrawBackgroundImgae(ICanvasAnimatedControl control, CanvasDrawingSession ds, CanvasBitmap canvasBitmap, float opacity)
{
using var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(control, softwareBitmap);
float imageWidth = (float)canvasBitmap.Size.Width;
float imageHeight = (float)canvasBitmap.Size.Height;
var scaleFactor =
(float)Math.Sqrt(Math.Pow(control.Size.Width, 2) + Math.Pow(control.Size.Height, 2))
/ Math.Min(imageWidth, imageHeight);
float targetSize = MathF.Sqrt(MathF.Pow(_canvasWidth, 2) + MathF.Pow(_canvasHeight, 2)) * 1.4f;
float scaleFactor = targetSize / MathF.Min(imageWidth, imageHeight);
ds.DrawImage(
new OpacityEffect
float x = _canvasWidth / 2 - imageWidth * scaleFactor / 2;
float y = _canvasHeight / 2 - imageHeight * scaleFactor / 2;
// Original source: https://zhuanlan.zhihu.com/p/37178216
float gain = _lyricsBgBrightnessTransition.Value;
float whiteX = 1 - 0.5f * gain;
float whiteY = 0.5f + 0.5f * gain;
float blackX = 0.5f - 0.5f * gain;
float blackY = 0 + 0.5f * gain;
ds.DrawImage(new OpacityEffect
{
Source = new BrightnessEffect
{
Source = new ScaleEffect
{
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
BorderMode = EffectBorderMode.Hard,
Scale = new Vector2(scaleFactor),
Source = canvasBitmap,
},
Opacity = opacity,
WhitePoint = new Vector2(whiteX, whiteY),
BlackPoint = new Vector2(blackX, blackY),
},
(float)control.Size.Width / 2 - imageWidth * scaleFactor / 2,
(float)control.Size.Height / 2 - imageHeight * scaleFactor / 2
Opacity = opacity,
}, new Vector2(x, y));
}
private void DrawForegroundImgae(ICanvasAnimatedControl control, CanvasDrawingSession ds, CanvasBitmap canvasBitmap, float opacity)
{
if (opacity == 0) return;
float imageWidth = (float)canvasBitmap.Size.Width;
float imageHeight = (float)canvasBitmap.Size.Height;
float scaleFactor = _albumArtSize / Math.Min(imageWidth, imageHeight);
if (scaleFactor < 0.01f) return;
float cornerRadius = _albumArtCornerRadius / 100f * _albumArtSize / 2;
using var cornerRadiusMask = new CanvasCommandList(control.Device);
using var cornerRadiusMaskDs = cornerRadiusMask.CreateDrawingSession();
cornerRadiusMaskDs.FillRoundedRectangle(
new Rect(0, 0, imageWidth * scaleFactor, imageHeight * scaleFactor),
cornerRadius, cornerRadius, Colors.White
);
ds.DrawImage(new OpacityEffect
{
Source = new AlphaMaskEffect
{
Source = new ScaleEffect
{
Scale = new Vector2(scaleFactor),
Source = canvasBitmap,
},
AlphaMask = cornerRadiusMask,
},
Opacity = opacity,
}, new Vector2(_albumArtXTransition.Value, _albumArtY)
);
}
@@ -126,35 +175,27 @@ namespace BetterLyrics.WinUI3.ViewModels
{
ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
var overlappedCovers = new CanvasCommandList(control.Device);
using var overlappedCovers = new CanvasCommandList(control.Device);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
if (_lastAlbumArtBitmap != null)
if (_lastAlbumArtCanvasBitmap != null)
{
DrawImgae(
control,
overlappedCoversDs,
_lastAlbumArtBitmap,
1 - _albumArtBgTransition.Value
);
DrawBackgroundImgae(control, overlappedCoversDs, _lastAlbumArtCanvasBitmap, 1 - _albumArtBgTransition.Value);
}
if (_albumArtBitmap != null)
if (_albumArtCanvasBitmap != null)
{
DrawImgae(
control,
overlappedCoversDs,
_albumArtBitmap,
_albumArtBgTransition.Value
);
DrawBackgroundImgae(control, overlappedCoversDs, _albumArtCanvasBitmap, _albumArtBgTransition.Value);
}
using var coverOverlayEffect = new OpacityEffect
{
Opacity = CoverOverlayOpacity / 100f,
Opacity = _albumArtBgOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = CoverOverlayBlurAmount,
BlurAmount = _albumArtBgBlurAmount,
Source = overlappedCovers,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Speed,
},
};
ds.DrawImage(coverOverlayEffect);
@@ -162,13 +203,73 @@ namespace BetterLyrics.WinUI3.ViewModels
ds.Transform = Matrix3x2.Identity;
}
private void DrawAlbumArt(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
using var albumArt = new CanvasCommandList(control.Device);
using var albumArtDs = albumArt.CreateDrawingSession();
if (_albumArtCanvasBitmap != null)
{
DrawForegroundImgae(control, albumArtDs, _albumArtCanvasBitmap, _albumArtBgTransition.Value);
}
if (_lastAlbumArtCanvasBitmap != null)
{
DrawForegroundImgae(control, albumArtDs, _lastAlbumArtCanvasBitmap, 1 - _albumArtBgTransition.Value);
}
using var opacity = new CanvasCommandList(control.Device);
using var opacityDs = opacity.CreateDrawingSession();
opacityDs.DrawImage(new GaussianBlurEffect
{
Source = albumArt,
BlurAmount = 12f,
Optimization = EffectOptimization.Quality,
});
opacityDs.DrawImage(albumArt);
ds.DrawImage(new OpacityEffect
{
Source = opacity,
Opacity = _albumArtOpacityTransition.Value
});
}
private void DrawTitleAndArtist(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
if (_lastSongTitle != null || _lastSongArtist != null)
{
DrawSingleTitleAndArtist(control, ds, _lastSongTitle, _lastSongArtist, 1 - _songInfoOpacityTransition.Value);
}
if (_songTitle != null || _songArtist != null)
{
DrawSingleTitleAndArtist(control, ds, _songTitle, _songArtist, _songInfoOpacityTransition.Value);
}
}
private void DrawSingleTitleAndArtist(ICanvasAnimatedControl control, CanvasDrawingSession ds, string? title, string? artist, float opacity)
{
CanvasTextLayout titleLayout = new(
control, title ?? string.Empty,
_titleTextFormat, _albumArtSize, _canvasHeight
);
CanvasTextLayout artistLayout = new(
control, artist ?? string.Empty,
_artistTextFormat, _albumArtSize, _canvasHeight
);
ds.DrawTextLayout(
titleLayout,
new Vector2(_albumArtXTransition.Value, _titleY),
_bgFontColor.WithAlpha((byte)(_albumArtOpacityTransition.Value * 255 * opacity)));
ds.DrawTextLayout(
artistLayout,
new Vector2(_albumArtXTransition.Value, _titleY + (float)titleLayout.LayoutBounds.Height),
_bgFontColor.WithAlpha((byte)(_albumArtOpacityTransition.Value * 128 * opacity)));
}
private void DrawBlurredLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
var currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(currentPlayingLineIndex);
var currentPlayingLine = _lyricsDataArr
.ElementAtOrDefault(_langIndex)
?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null)
{
@@ -177,99 +278,96 @@ namespace BetterLyrics.WinUI3.ViewModels
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null)
{
continue;
}
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var textLayout = line.CanvasTextLayout;
if (textLayout == null)
{
continue;
}
if (textLayout == null) continue;
var position = new Vector2(line.Position.X, line.Position.Y);
float layoutWidth = (float)textLayout.LayoutBounds.Width;
float layoutHeight = (float)textLayout.LayoutBounds.Height;
if (layoutWidth <= 0 || layoutHeight <= 0)
{
continue;
}
if (layoutWidth <= 0 || layoutHeight <= 0) continue;
float centerX = position.X;
float centerY = position.Y + layoutHeight / 2;
switch (LyricsAlignmentType)
switch (_lyricsAlignmentType)
{
case LyricsAlignmentType.Left:
case TextAlignmentType.Left:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case LyricsAlignmentType.Center:
case TextAlignmentType.Center:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += (float)_maxLyricsWidthTransition.Value / 2;
centerX += _maxLyricsWidth / 2;
break;
case LyricsAlignmentType.Right:
case TextAlignmentType.Right:
textLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += (float)_maxLyricsWidthTransition.Value;
centerX += _maxLyricsWidth;
break;
default:
break;
}
float offsetToLeft =
(float)control.Size.Width - _rightMargin - _maxLyricsWidthTransition.Value;
float xOffset = _lyricsXTransition.Value;
float yOffset = _canvasYScrollTransition.Value + _canvasHeight / 2;
// 组合变换:缩放 -> 旋转 -> 平移
ds.Transform =
Matrix3x2.CreateScale(line.ScaleTransition.Value, new Vector2(centerX, centerY))
* Matrix3x2.CreateRotation(
line.AngleTransition.Value,
currentPlayingLine.Position
)
* Matrix3x2.CreateTranslation(
offsetToLeft,
_canvasYScrollTransition.Value + (float)(control.Size.Height / 2)
);
* Matrix3x2.CreateRotation(line.AngleTransition.Value, currentPlayingLine.Position)
* Matrix3x2.CreateTranslation(xOffset, yOffset);
// Create the original lyrics line
using var lyrics = new CanvasCommandList(control.Device);
using var lyricsDs = lyrics.CreateDrawingSession();
lyricsDs.DrawTextLayout(textLayout, position, _fontColor);
// Create the background lyrics line with stroke and fill
using var bgLyrics = new CanvasCommandList(control.Device);
using var bgLyricsDs = bgLyrics.CreateDrawingSession();
// Create the foreground lyrics line with stroke and fill
using var fgLyrics = new CanvasCommandList(control.Device);
using var fgLyricsDs = fgLyrics.CreateDrawingSession();
// 创建文字几何体
using var textGeometry = CanvasGeometry.CreateText(textLayout);
if (_isDesktopMode)
{
bgLyricsDs.DrawGeometry(textGeometry, position, _strokeFontColor, _lyricsFontStrokeWidth); // 背景描边
fgLyricsDs.DrawGeometry(textGeometry, position, _strokeFontColor, _lyricsFontStrokeWidth); // 前景描边
}
bgLyricsDs.FillGeometry(textGeometry, position, _bgFontColor); // 背景填充
fgLyricsDs.FillGeometry(textGeometry, position, _fgFontColor); // 前景填充
using var combined = new CanvasCommandList(control.Device);
using var combinedDs = combined.CreateDrawingSession();
// Mock gradient blurred lyrics layer
// 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层
// 先铺一层带默认透明度的已经加了模糊效果的歌词作为最底层(背景歌词层次)
// Current line will not be blurred
ds.DrawImage(
combinedDs.DrawImage(
new GaussianBlurEffect
{
Source = new OpacityEffect { Source = lyrics, Opacity = line.OpacityTransition.Value * _lyricsOpacityTransition.Value },
Source = new OpacityEffect { Source = bgLyrics, Opacity = line.OpacityTransition.Value * _lyricsOpacityTransition.Value },
BlurAmount = line.BlurAmountTransition.Value,
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
}
);
// 再叠加当前行歌词层
// Only draw the current line and the two lines around it
// This layer is to highlight the current line
// and for fade-in and fade-out effects, two lines around it is also drawn
if (Math.Abs(i - currentPlayingLineIndex) <= 1)
if (line.HighlightOpacityTransition.Value != 0)
{
// 再叠加高亮行歌词层(前景歌词层)
using var mask = new CanvasCommandList(control.Device);
using var maskDs = mask.CreateDrawingSession();
using var highlightMask = new CanvasCommandList(control.Device);
using var highlightMaskDs = highlightMask.CreateDrawingSession();
if (i == currentPlayingLineIndex)
if (i == _playingLineIndex)
{
GetLinePlayingProgress(
line,
i,
out int charStartIndex,
out int charLength,
out float charProgress
@@ -290,7 +388,7 @@ namespace BetterLyrics.WinUI3.ViewModels
region.LayoutBounds.Width,
region.LayoutBounds.Height
);
maskDs.FillRectangle(rect, Colors.Black);
maskDs.FillRectangle(rect, Color.FromArgb(255, 128, 128, 128));
}
}
@@ -335,7 +433,7 @@ namespace BetterLyrics.WinUI3.ViewModels
fadingWidth
);
maskDs.FillRectangle(highlightRect, Colors.White);
maskDs.FillRectangle(highlightRect, Color.FromArgb(255, 128, 128, 128));
maskDs.FillRectangle(fadeOutRect, fadeOutBrush);
highlightMaskDs.FillRectangle(fadeInRect, fadeInBrush);
@@ -343,32 +441,40 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
float height = 0f;
//var regions = textLayout.GetCharacterRegions(0, string.Join("", line.LyricsChars.Select(x => x.Text)).Length);
var regions = textLayout.GetCharacterRegions(0, line.OriginalText.Length);
if (regions.Length > 0)
{
height = (float)regions[^1].LayoutBounds.Bottom - (float)regions[0].LayoutBounds.Top;
}
maskDs.FillRectangle(
new Rect(
textLayout.LayoutBounds.X,
position.Y,
textLayout.LayoutBounds.Width,
textLayout.LayoutBounds.Height
height
),
Colors.White
);
}
ds.DrawImage(
new OpacityEffect
using var opacityEffect = new OpacityEffect
{
Source = new BlendEffect
{
Source = new BlendEffect
{
Background = IsLyricsGlowEffectEnabled
Background = _isLyricsGlowEffectEnabled
? new GaussianBlurEffect
{
Source = new AlphaMaskEffect
{
Source = lyrics,
AlphaMask = LyricsGlowEffectScope switch
Source = fgLyrics,
AlphaMask = _lyricsGlowEffectScope switch
{
LineRenderingType.UntilCurrentChar => mask,
LineRenderingType.CurrentCharOnly => highlightMask,
LineRenderingType.CurrentChar => highlightMask,
LineRenderingType.LineStartToCurrentChar => mask,
LineRenderingType.CurrentLine => fgLyrics,
_ => mask,
},
},
@@ -376,15 +482,49 @@ namespace BetterLyrics.WinUI3.ViewModels
Optimization = EffectOptimization.Quality,
}
: new CanvasCommandList(control.Device),
Foreground = new AlphaMaskEffect
Foreground = new AlphaMaskEffect
{
Source = fgLyrics,
AlphaMask = _lyricsHighlightScope switch
{
Source = lyrics,
AlphaMask = mask,
LineRenderingType.CurrentChar => highlightMask,
LineRenderingType.LineStartToCurrentChar => mask,
LineRenderingType.CurrentLine => fgLyrics,
_ => mask,
},
},
Opacity = line.HighlightOpacityTransition.Value * _lyricsOpacityTransition.Value,
},
Opacity = line.HighlightOpacityTransition.Value * _lyricsOpacityTransition.Value,
};
combinedDs.DrawImage(opacityEffect);
if (i == _playingLineIndex)
{
if (_isLyricsFloatAnimationEnabled)
{
ds.DrawImage(new DisplacementMapEffect
{
Source = combined,
Displacement = mask,
XChannelSelect = EffectChannelSelect.Red,
YChannelSelect = EffectChannelSelect.Alpha,
Amount = 1f
});
}
);
else
{
ds.DrawImage(combined);
}
}
else
{
ds.DrawImage(combined);
}
}
else
{
ds.DrawImage(combined);
}
// Reset scale
@@ -392,41 +532,21 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
private void DrawImmersiveBackground(
ICanvasAnimatedControl control,
CanvasDrawingSession ds,
bool withGradient = true
)
private void DrawImmersiveBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds, float radius)
{
ds.FillRectangle(
new Rect(0, 0, control.Size.Width, control.Size.Height),
new CanvasLinearGradientBrush(
control,
[
new CanvasGradientStop
{
Position = 0f,
Color = withGradient
? Color.FromArgb(
211,
_immersiveBgTransition.Value.R,
_immersiveBgTransition.Value.G,
_immersiveBgTransition.Value.B
)
: _immersiveBgTransition.Value,
},
new CanvasGradientStop
{
Position = 1,
Color = _immersiveBgTransition.Value,
},
]
)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, (float)control.Size.Height),
}
CanvasCommandList list = new(control.Device);
using var listDs = list.CreateDrawingSession();
listDs.FillRoundedRectangle(
new Rect(0, 0, _canvasWidth, _canvasHeight),
radius,
radius,
_immersiveBgTransition.Value
);
ds.DrawImage(new OpacityEffect
{
Source = list,
Opacity = _immersiveBgOpacityTransition.Value
});
}
private CanvasLinearGradientBrush GetHorizontalFillBrush(
@@ -442,7 +562,7 @@ namespace BetterLyrics.WinUI3.ViewModels
.Select(stops => new CanvasGradientStop
{
Position = stops.position,
Color = Color.FromArgb((byte)(stops.opacity * 255), 0, 0, 0),
Color = Color.FromArgb((byte)(stops.opacity * 255), 128, 128, 128),
})
.ToArray()
)
@@ -451,87 +571,5 @@ namespace BetterLyrics.WinUI3.ViewModels
EndPoint = new Vector2(startX + width, 0),
};
}
void DrawShenGuang(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
float w = (float)control.Size.Width;
float h = (float)control.Size.Height;
float beamLength = h; // 光束长度等于画布高度
float beamAngle = (float)(Math.PI / 6); // 30°
float centerX = w / 2;
float centerY = h;
float angle = _shenGuangAngleTransition.Value;
var p0 = new Vector2(centerX, centerY);
var p1 = new Vector2(
centerX + beamLength * (float)Math.Cos(angle - beamAngle / 2),
centerY + beamLength * (float)Math.Sin(angle - beamAngle / 2)
);
var p2 = new Vector2(
centerX + beamLength * (float)Math.Cos(angle + beamAngle / 2),
centerY + beamLength * (float)Math.Sin(angle + beamAngle / 2)
);
using var path = new CanvasPathBuilder(control);
path.BeginFigure(p0);
path.AddLine(p1);
path.AddArc(
p2,
beamLength,
beamLength,
0,
CanvasSweepDirection.Clockwise,
CanvasArcSize.Small
);
path.EndFigure(CanvasFigureLoop.Closed);
using var geometry = CanvasGeometry.CreatePath(path);
// 渐变为白色,透明度递减
using var brush = new CanvasRadialGradientBrush(
control,
new[]
{
new CanvasGradientStop
{
Position = 0f,
Color = Color.FromArgb(180, 255, 255, 255),
},
new CanvasGradientStop
{
Position = 0.5f,
Color = Color.FromArgb(60, 255, 255, 255),
},
new CanvasGradientStop
{
Position = 1f,
Color = Color.FromArgb(0, 255, 255, 255),
},
}
)
{
Center = p0,
OriginOffset = new Vector2(0, 0),
RadiusX = beamLength * 0.8f,
RadiusY = beamLength * 0.8f,
};
using var beamCmd = new CanvasCommandList(control);
using (var beamDs = beamCmd.CreateDrawingSession())
{
beamDs.FillGeometry(geometry, brush);
}
var blur = new GaussianBlurEffect
{
Source = beamCmd,
BlurAmount = 36f,
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
};
ds.DrawImage(blur);
}
}
}

View File

@@ -1,96 +1,88 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using Windows.Graphics.Imaging;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
: IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<float>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<LyricsFontColorType>>,
IRecipient<PropertyChangedMessage<LyricsAlignmentType>>,
IRecipient<PropertyChangedMessage<TextAlignmentType>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<LineRenderingType>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<EasingType>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
IRecipient<PropertyChangedMessage<ObservableCollection<LocalMediaFolder>>>
{
public async void Receive(
PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>> message
)
public void Receive(PropertyChangedMessage<ObservableCollection<LocalMediaFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalLyricsFolders))
if (message.PropertyName == nameof(SettingsPageViewModel.LocalMediaFolders))
{
// Music lib changed, re-fetch lyrics
await RefreshLyricsAsync();
_logger.LogInformation("Local lyrics folders changed, refreshing lyrics.");
_ = _refreshLyricsRunner.RunAsync(async tokne =>
{
await RefreshLyricsAsync(tokne);
});
}
}
}
public async void Receive(
PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>> message
)
public void Receive(PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsSearchProvidersInfo))
{
// Lyrics search providers info changed, re-fetch lyrics
await RefreshLyricsAsync();
_logger.LogInformation("Lyrics search providers info changed, refreshing lyrics.");
_ = _refreshLyricsRunner.RunAsync(async token =>
{
await RefreshLyricsAsync(token);
});
}
}
}
// Receive methods for handling messages from other view models
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (
message.PropertyName
== nameof(SettingsPageViewModel.IsDynamicCoverOverlayEnabled)
)
if (message.PropertyName == nameof(SettingsPageViewModel.IsDynamicCoverOverlayEnabled))
{
IsDynamicCoverOverlayEnabled = message.NewValue;
_isDynamicCoverOverlayEnabled = message.NewValue;
}
else if (
message.PropertyName == nameof(SettingsPageViewModel.IsDebugOverlayEnabled)
)
else if (message.PropertyName == nameof(SettingsPageViewModel.IsDebugOverlayEnabled))
{
_isDebugOverlayEnabled = message.NewValue;
}
}
else if (message.Sender is LyricsSettingsControlViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.IsLyricsGlowEffectEnabled)
)
else if (message.PropertyName == nameof(SettingsPageViewModel.IsLyricsGlowEffectEnabled))
{
IsLyricsGlowEffectEnabled = message.NewValue;
_isLyricsGlowEffectEnabled = message.NewValue;
}
else if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.IsFanLyricsEnabled)
)
else if (message.PropertyName == nameof(SettingsPageViewModel.IsFanLyricsEnabled))
{
_isFanLyricsEnabled = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsLyricsFloatAnimationEnabled))
{
_isLyricsFloatAnimationEnabled = message.NewValue;
}
}
else if (message.Sender is LyricsWindowViewModel)
@@ -98,10 +90,38 @@ namespace BetterLyrics.WinUI3.ViewModels
if (message.PropertyName == nameof(LyricsWindowViewModel.IsDockMode))
{
_isDockMode = message.NewValue;
UpdateColorConfig();
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
_isDesktopMode = message.NewValue;
UpdateColorConfig();
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsLyricsWindowLocked))
{
_isLyricsWindowLocked = message.NewValue;
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsMouseWithinWindow))
{
_isMouseWithinWindow = message.NewValue;
UpdateImmersiveBackgroundOpacity();
}
}
else if (message.Sender is LyricsPageViewModel)
{
if (message.PropertyName == nameof(LyricsPageViewModel.IsTranslationEnabled))
{
_isTranslationEnabled = message.NewValue;
_logger.LogInformation("Translation enabled state changed: {IsEnabled}", _isTranslationEnabled);
UpdateTranslations();
}
else if (message.PropertyName == nameof(LyricsPageViewModel.ShowTranslationOnly))
{
_showTranslationOnly = message.NewValue;
UpdateTranslations();
}
}
}
@@ -113,42 +133,38 @@ namespace BetterLyrics.WinUI3.ViewModels
if (message.PropertyName == nameof(LyricsWindowViewModel.ActivatedWindowAccentColor))
{
_immersiveBgTransition.StartTransition(message.NewValue);
_lyricsWindowBgColor = message.NewValue;
_adaptiveFontColor = Helper.ColorHelper.GetForegroundColor(_lyricsWindowBgColor);
UpdateFontColor();
_environmentalColor = message.NewValue;
UpdateColorConfig();
}
}
else if (message.Sender is LyricsSettingsControlViewModel)
else if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsCustomFontColor))
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsCustomBgFontColor))
{
_customFontColor = message.NewValue;
UpdateFontColor();
_customBgFontColor = message.NewValue;
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<double> message)
{
if (message.Sender is LyricsPageViewModel)
{
if (message.PropertyName == nameof(LyricsPageViewModel.MaxLyricsWidth))
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsCustomFgFontColor))
{
_maxLyricsWidthTransition.StartTransition((float)message.NewValue);
_customFgFontColor = message.NewValue;
UpdateColorConfig();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsCustomStrokeFontColor))
{
_customStrokeFontColor = message.NewValue;
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<float> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
if (message.Sender is SettingsPageViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsLineSpacingFactor)
)
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsLineSpacingFactor))
{
LyricsLineSpacingFactor = message.NewValue;
_lyricsLineSpacingFactor = message.NewValue;
_isLayoutChanged = true;
}
}
}
@@ -159,155 +175,167 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (message.PropertyName == nameof(SettingsPageViewModel.CoverImageRadius))
{
CoverImageRadius = message.NewValue;
_albumArtCornerRadius = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayOpacity))
{
CoverOverlayOpacity = message.NewValue;
_albumArtBgOpacity = message.NewValue;
}
else if (
message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayBlurAmount)
)
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverOverlayBlurAmount))
{
CoverOverlayBlurAmount = message.NewValue;
_albumArtBgBlurAmount = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsVerticalEdgeOpacity))
{
_lyricsVerticalEdgeOpacity = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBlurAmount))
{
_lyricsBlurAmount = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
{
_lyricsFontSize = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SelectedTargetLanguageIndex))
{
_targetLanguageIndex = message.NewValue;
_logger.LogInformation("Target language index changed: {Index}", _targetLanguageIndex);
UpdateTranslations();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontStrokeWidth))
{
_lyricsFontStrokeWidth = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsScrollDuration))
{
_canvasYScrollTransition.SetDuration(message.NewValue / 1000f);
}
else if (message.PropertyName == nameof(SettingsPageViewModel.TimelineSyncThreshold))
{
_timelineSyncThreshold = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBgFontOpacity))
{
_defaultOpacity = message.NewValue / 100f;
_isLayoutChanged = true;
}
}
else if (message.Sender is LyricsSettingsControlViewModel)
else if (message.Sender is LyricsPageViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsVerticalEdgeOpacity)
)
if (message.PropertyName == nameof(LyricsPageViewModel.PositionOffset))
{
LyricsVerticalEdgeOpacity = message.NewValue;
}
else if (
message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsBlurAmount)
)
{
LyricsBlurAmount = message.NewValue;
}
else if (
message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize)
)
{
LyricsFontSize = message.NewValue;
_positionOffset = TimeSpan.FromMilliseconds(message.NewValue);
}
}
}
public void Receive(PropertyChangedMessage<LineRenderingType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
if (message.Sender is SettingsPageViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsGlowEffectScope)
)
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsGlowEffectScope))
{
LyricsGlowEffectScope = message.NewValue;
_lyricsGlowEffectScope = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsHighlightScope))
{
_lyricsHighlightScope = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<LyricsAlignmentType> message)
public void Receive(PropertyChangedMessage<TextAlignmentType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
if (message.Sender is SettingsPageViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsAlignmentType)
)
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsAlignmentType))
{
LyricsAlignmentType = message.NewValue;
_lyricsAlignmentType = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SongInfoAlignmentType))
{
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment =
message.NewValue.ToCanvasHorizontalAlignment();
}
}
}
public void Receive(PropertyChangedMessage<LyricsDisplayType> message)
{
DisplayType = message.NewValue;
_displayTypeReceived = message.NewValue;
}
public void Receive(PropertyChangedMessage<LyricsFontColorType> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
if (message.Sender is SettingsPageViewModel)
{
if (
message.PropertyName
== nameof(LyricsSettingsControlViewModel.LyricsFontColorType)
)
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBgFontColorType))
{
LyricsFontColorType = message.NewValue;
_lyricsBgFontColorType = message.NewValue;
UpdateColorConfig();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFgFontColorType))
{
_lyricsFgFontColorType = message.NewValue;
UpdateColorConfig();
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsStrokeFontColorType))
{
_lyricsStrokeFontColorType = message.NewValue;
UpdateColorConfig();
}
}
}
public void Receive(PropertyChangedMessage<LyricsFontWeight> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontWeight))
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontWeight))
{
LyricsFontWeight = message.NewValue;
_lyricsTextFormat.FontWeight = message.NewValue.ToFontWeight();
_isLayoutChanged = true;
}
}
}
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
public void Receive(PropertyChangedMessage<ElementTheme> message)
{
UpdateFontColor();
}
partial void OnLyricsFontSizeChanged(int value)
{
_isRelayoutNeeded = true;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_textFormat.FontWeight = value.ToFontWeight();
_isRelayoutNeeded = true;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_isRelayoutNeeded = true;
}
async partial void OnSongInfoChanged(SongInfo? oldValue, SongInfo? newValue)
{
TotalTime = TimeSpan.Zero;
SoftwareBitmap? newalbumArtBitmap;
Color? newAlbumArtAccentColor;
if (newValue?.AlbumArt is byte[] bytes)
if (message.Sender is SettingsPageViewModel)
{
var decoder = await ImageHelper.GetDecoderFromByte(bytes);
newalbumArtBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
newAlbumArtAccentColor = (ImageHelper.GetAccentColorsFromByte(bytes)).SafeGet(0);
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsBackgroundTheme))
{
_lyricsBgTheme = message.NewValue;
UpdateColorConfig();
}
}
else
}
public void Receive(PropertyChangedMessage<EasingType> message)
{
if (message.Sender is SettingsPageViewModel)
{
newalbumArtBitmap = null;
newAlbumArtAccentColor = null;
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsScrollEasingType))
{
_canvasYScrollTransition.SetEasingType(message.NewValue);
}
}
}
_lastAlbumArtBitmap = _albumArtBitmap;
_albumArtBitmap = newalbumArtBitmap;
_albumArtBgTransition.Reset(0f);
_albumArtBgTransition.StartTransition(1f);
_albumArtAccentColor = newAlbumArtAccentColor;
_lyricsWindowBgColor = _albumArtAccentColor ?? Colors.Gray;
if (!_isDesktopMode && !_isDockMode) _adaptiveFontColor = Helper.ColorHelper.GetForegroundColor(_lyricsWindowBgColor);
UpdateFontColor();
await RefreshLyricsAsync();
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontFamily))
{
_lyricsTextFormat.FontFamily = _artistTextFormat.FontFamily = _titleTextFormat.FontFamily = message.NewValue;
_isLayoutChanged = true;
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
private readonly ValueTransition<float> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.5f,
easingType: EasingType.EaseInOutCubic
);
private readonly ValueTransition<Color> _immersiveBgTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly ValueTransition<float> _immersiveBgOpacityTransition = new(
initialValue: 1f,
durationSeconds: 0.2f
);
private readonly ValueTransition<float> _lyricsXTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<float> _lyricsOpacityTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<float> _albumArtBgTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<float> _albumArtOpacityTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<float> _albumArtXTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private readonly ValueTransition<float> _songInfoOpacityTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
private readonly ValueTransition<float> _lyricsBgBrightnessTransition = new(
initialValue: 0f,
durationSeconds: 1f
);
}
}

View File

@@ -1,91 +1,129 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
{
private bool _isCanvasWidthChanged = false;
private bool _isCanvasHeightChanged = false;
private bool _isDisplayTypeChanged = false;
private bool _isPlayingLineChanged = false;
private bool _isVisibleLinesBoundaryChanged = false;
public void Update(ICanvasAnimatedControl control, CanvasAnimatedUpdateEventArgs args)
{
_elapsedTime = args.Timing.ElapsedTime;
if (_isPlaying)
{
TotalTime += args.Timing.ElapsedTime;
TotalTime += _elapsedTime;
}
ElapsedTime = args.Timing.ElapsedTime;
var playingLineIndex = GetCurrentPlayingLineIndex();
if (_immersiveBgTransition.IsTransitioning)
{
_immersiveBgTransition.Update(ElapsedTime);
}
_isCanvasWidthChanged = _canvasWidth != control.Size.Width;
_isCanvasHeightChanged = _canvasHeight != control.Size.Height;
_isDisplayTypeChanged = _displayType != _displayTypeReceived;
_isPlayingLineChanged = _playingLineIndex != playingLineIndex;
if (_albumArtBgTransition.IsTransitioning)
{
_albumArtBgTransition.Update(ElapsedTime);
}
_canvasWidth = (float)control.Size.Width;
_canvasHeight = (float)control.Size.Height;
_displayType = _displayTypeReceived;
_playingLineIndex = playingLineIndex;
if (IsDynamicCoverOverlayEnabled)
_immersiveBgOpacityTransition.Update(_elapsedTime);
_immersiveBgTransition.Update(_elapsedTime);
_albumArtBgTransition.Update(_elapsedTime);
_lyricsBgBrightnessTransition.Update(_elapsedTime);
_songInfoOpacityTransition.Update(_elapsedTime);
if (_isDynamicCoverOverlayEnabled)
{
_rotateAngle += _coverRotateSpeed;
_rotateAngle %= MathF.PI * 2;
}
if (_maxLyricsWidthTransition.IsTransitioning)
if (_isCanvasHeightChanged)
{
_maxLyricsWidthTransition.Update(ElapsedTime);
_isRelayoutNeeded = true;
_albumArtY = 36 + (_canvasHeight - 36 * 2) * 3 / 16f;
}
switch (DisplayType)
if (_isCanvasWidthChanged || _isCanvasHeightChanged)
{
case Enums.LyricsDisplayType.AlbumArtOnly:
_lyricsOpacityTransition.StartTransition(0f);
break;
case Enums.LyricsDisplayType.LyricsOnly:
case Enums.LyricsDisplayType.SplitView:
_lyricsOpacityTransition.StartTransition(1f);
break;
case Enums.LyricsDisplayType.PlaceholderOnly:
break;
default:
break;
_albumArtSize = MathF.Min(
(_canvasHeight - _topMargin - _bottomMargin) * 8.5f / 16,
(_canvasWidth - _leftMargin - _middleMargin - _rightMargin) / 2);
_albumArtSize = MathF.Max(0, _albumArtSize);
_titleY = _albumArtY + _albumArtSize * 1.05f;
}
if (_lyricsOpacityTransition.IsTransitioning)
if (_isDisplayTypeChanged || _isCanvasWidthChanged)
{
_lyricsOpacityTransition.Update(ElapsedTime);
bool jumpTo = !_isDisplayTypeChanged && _isCanvasWidthChanged;
switch (_displayType)
{
case LyricsDisplayType.AlbumArtOnly:
_lyricsOpacityTransition.StartTransition(0f, jumpTo);
_albumArtOpacityTransition.StartTransition(1f, jumpTo);
_albumArtXTransition.StartTransition(_canvasWidth / 2 - _albumArtSize / 2, jumpTo);
break;
case LyricsDisplayType.LyricsOnly:
_lyricsOpacityTransition.StartTransition(1f, jumpTo);
_albumArtOpacityTransition.StartTransition(0f, jumpTo);
_lyricsXTransition.StartTransition(_leftMargin, jumpTo);
break;
case LyricsDisplayType.SplitView:
_lyricsOpacityTransition.StartTransition(1f, jumpTo);
_albumArtOpacityTransition.StartTransition(1f, jumpTo);
_lyricsXTransition.StartTransition((_canvasWidth - _leftMargin - _middleMargin - _rightMargin) / 2 + _leftMargin + _middleMargin, jumpTo);
_albumArtXTransition.StartTransition(_leftMargin + ((_canvasWidth - _leftMargin - _middleMargin - _rightMargin) / 2 - _albumArtSize) / 2, jumpTo);
break;
default:
break;
}
}
// 神光角度目标值左右±15度摆动周期约4秒
double t = DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1000.0;
float targetAngle = (float)(-Math.PI / 2 + Math.Sin(t * Math.PI / 2) * (Math.PI / 12)); // -90°为正上±15°摆动
_shenGuangAngleTransition.StartTransition(targetAngle);
_lyricsXTransition.Update(_elapsedTime);
_albumArtXTransition.Update(_elapsedTime);
_lyricsOpacityTransition.Update(_elapsedTime);
_albumArtOpacityTransition.Update(_elapsedTime);
if (_shenGuangAngleTransition.IsTransitioning)
if (_isCanvasWidthChanged || _lyricsXTransition.IsTransitioning)
{
_shenGuangAngleTransition.Update(ElapsedTime);
_maxLyricsWidth = _canvasWidth - _lyricsXTransition.Value - _rightMargin;
_maxLyricsWidth = Math.Max(_maxLyricsWidth, 0);
_isLayoutChanged = true;
}
if (_isRelayoutNeeded)
if (_isLayoutChanged)
{
ReLayout(control);
_isRelayoutNeeded = false;
UpdateCanvasYScrollOffset(control, false);
UpdateCanvasYScrollOffset(control, true, false);
}
else
{
UpdateCanvasYScrollOffset(control, true);
UpdateCanvasYScrollOffset(control, false, true);
}
UpdateLinesProps();
_isLayoutChanged = false;
}
private void ReLayout(ICanvasAnimatedControl control)
@@ -93,14 +131,14 @@ namespace BetterLyrics.WinUI3.ViewModels
if (control == null)
return;
_textFormat.FontSize = LyricsFontSize;
_lyricsTextFormat.FontSize = _lyricsFontSize;
float y = _topMargin;
float y = 0;
// Init Positions
for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
for (int i = 0; i < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.Count; i++)
{
var line = _multiLangLyrics[_langIndex].SafeGet(i);
var line = _lyricsDataArr[_langIndex].LyricsLines.ElementAtOrDefault(i);
if (line == null)
{
@@ -116,10 +154,10 @@ namespace BetterLyrics.WinUI3.ViewModels
// Calculate layout bounds
line.CanvasTextLayout = new CanvasTextLayout(
control,
line.Text,
_textFormat,
(float)_maxLyricsWidthTransition.Value,
(float)control.Size.Height
line.DisplayedText,
_lyricsTextFormat,
_maxLyricsWidth,
_canvasHeight
);
line.Position = new Vector2(0, y);
@@ -127,130 +165,206 @@ namespace BetterLyrics.WinUI3.ViewModels
y +=
(float)line.CanvasTextLayout.LayoutBounds.Height
/ line.CanvasTextLayout.LineCount
* (line.CanvasTextLayout.LineCount + LyricsLineSpacingFactor);
* (line.CanvasTextLayout.LineCount + _lyricsLineSpacingFactor);
}
}
private void UpdateCanvasYScrollOffset(ICanvasAnimatedControl control, bool withAnimation)
private void UpdateCanvasYScrollOffset(ICanvasAnimatedControl control, bool forceScroll, bool withAnimation)
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0)
{
return;
}
if (startLineIndex < 0 || endLineIndex < 0) return;
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = _multiLangLyrics
.SafeGet(_langIndex)
?.SafeGet(currentPlayingLineIndex);
var playingTextLayout = currentPlayingLine?.CanvasTextLayout;
if (currentPlayingLine == null || playingTextLayout == null)
if ((!_isPlayingLineChanged && forceScroll) || _isPlayingLineChanged)
{
return;
LyricsLine? currentPlayingLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
if (currentPlayingLine == null) return;
var playingTextLayout = currentPlayingLine?.CanvasTextLayout;
if (playingTextLayout == null) return;
float? targetYScrollOffset = (float?)(-currentPlayingLine!.Position.Y + _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines[0].Position.Y - playingTextLayout.LayoutBounds.Height / 2);
if (!targetYScrollOffset.HasValue) return;
_canvasYScrollTransition.StartTransition(targetYScrollOffset.Value, !withAnimation);
}
float targetYScrollOffset =
(float?)(
-currentPlayingLine.Position.Y
+ _multiLangLyrics.SafeGet(_langIndex)?[0].Position.Y
- playingTextLayout.LayoutBounds.Height / 2
) ?? 0f;
if (withAnimation && !_canvasYScrollTransition.IsTransitioning)
{
_canvasYScrollTransition.StartTransition(targetYScrollOffset);
}
else if (!withAnimation)
{
_canvasYScrollTransition.JumpTo(targetYScrollOffset);
}
if (_canvasYScrollTransition.IsTransitioning)
{
_canvasYScrollTransition.Update(ElapsedTime);
}
_startVisibleLineIndex = _endVisibleLineIndex = -1;
_canvasYScrollTransition.Update(_elapsedTime);
// Update visible line indices
for (int i = startLineIndex; i <= endLineIndex; i++)
var lines = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines;
if (lines == null || lines.Count == 0) return;
float offset = _canvasYScrollTransition.Value + _canvasHeight / 2;
int startVisibleLineIndex = FindFirstVisibleLine(lines, offset);
int endVisibleLineIndex = FindLastVisibleLine(lines, offset, _canvasHeight);
if (startVisibleLineIndex != -1 && endVisibleLineIndex == -1)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
if (line == null || line.CanvasTextLayout == null)
{
continue;
}
var textLayout = line.CanvasTextLayout;
if (
_canvasYScrollTransition.Value
+ (float)(control.Size.Height / 2)
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= 0
)
{
if (_startVisibleLineIndex == -1)
{
_startVisibleLineIndex = i;
}
}
if (
_canvasYScrollTransition.Value
+ (float)(control.Size.Height / 2)
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (_endVisibleLineIndex == -1)
{
_endVisibleLineIndex = i;
}
}
endVisibleLineIndex = endLineIndex;
}
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1)
{
_endVisibleLineIndex = endLineIndex;
}
_isVisibleLinesBoundaryChanged = _startVisibleLineIndex != startVisibleLineIndex || _endVisibleLineIndex != endVisibleLineIndex;
_startVisibleLineIndex = startVisibleLineIndex;
_endVisibleLineIndex = endVisibleLineIndex;
}
private protected void UpdateFontColor()
private int FindFirstVisibleLine(IList<LyricsLine> lines, float offset)
{
ThemeTypeSent =
Helper.ColorHelper.GetElementThemeFromBackgroundColor(_lyricsWindowBgColor);
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
var layout = line.CanvasTextLayout;
if (layout == null) break;
float value = offset + line.Position.Y + (float)layout.LayoutBounds.Height;
if (value >= 0)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private int FindLastVisibleLine(IList<LyricsLine> lines, float offset, float canvasHeight)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
var layout = line.CanvasTextLayout;
if (layout == null) break;
float value = offset + line.Position.Y + (float)layout.LayoutBounds.Height;
if (value >= canvasHeight)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private void UpdateColorConfig()
{
if (_isDesktopMode || _isDockMode)
{
ThemeTypeSent = Helper.ColorHelper.GetElementThemeFromBackgroundColor(_environmentalColor);
}
else
{
ThemeTypeSent = _lyricsBgTheme;
}
float brightness = 0f;
Color grayedEnvironmentalColor = Colors.Transparent;
Color fallbackFg = Colors.Transparent;
switch (ThemeTypeSent)
{
case ElementTheme.Default:
switch (Application.Current.RequestedTheme)
{
case ApplicationTheme.Light:
_adaptiveGrayedFontColor = _darkColor;
brightness = 0.7f;
break;
case ApplicationTheme.Dark:
_adaptiveGrayedFontColor = _lightColor;
brightness = 0.3f;
break;
default:
break;
}
break;
case ElementTheme.Light:
fallbackFg = _darkFontColor;
_adaptiveGrayedFontColor = _darkColor;
brightness = 0.7f;
break;
case ElementTheme.Dark:
fallbackFg = _lightFontColor;
_adaptiveGrayedFontColor = _lightColor;
brightness = 0.3f;
break;
default:
break;
}
switch (LyricsFontColorType)
if (_adaptiveGrayedFontColor == _lightColor)
{
case Enums.LyricsFontColorType.AdaptiveGrayed:
_fontColor = fallbackFg;
grayedEnvironmentalColor = _darkColor;
}
else if (_adaptiveGrayedFontColor == _darkColor)
{
grayedEnvironmentalColor = _lightColor;
}
_lyricsBgBrightnessTransition.StartTransition(brightness);
if (_isDesktopMode || _isDockMode)
{
_adaptiveColoredFontColor = Helper.ColorHelper.GetForegroundColor(_environmentalColor);
}
else
{
_adaptiveColoredFontColor = Helper.ColorHelper.GetForegroundColor(_albumArtAccentColor?.WithBrightness(brightness) ?? Colors.Transparent);
}
switch (_lyricsBgFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
_bgFontColor = _adaptiveGrayedFontColor;
break;
case Enums.LyricsFontColorType.AdaptiveColored:
_fontColor = _adaptiveFontColor ?? fallbackFg;
case LyricsFontColorType.AdaptiveColored:
_bgFontColor = _adaptiveColoredFontColor ?? _adaptiveGrayedFontColor;
break;
case Enums.LyricsFontColorType.Custom:
_fontColor = _customFontColor ?? fallbackFg;
case LyricsFontColorType.Custom:
_bgFontColor = _customBgFontColor ?? _adaptiveGrayedFontColor;
break;
default:
break;
}
switch (_lyricsFgFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
_fgFontColor = _adaptiveGrayedFontColor;
break;
case LyricsFontColorType.AdaptiveColored:
_fgFontColor = _adaptiveColoredFontColor ?? _adaptiveGrayedFontColor;
break;
case LyricsFontColorType.Custom:
_fgFontColor = _customFgFontColor ?? _adaptiveGrayedFontColor;
break;
default:
break;
}
switch (_lyricsStrokeFontColorType)
{
case LyricsFontColorType.AdaptiveGrayed:
_strokeFontColor = grayedEnvironmentalColor.WithBrightness(0.7);
break;
case LyricsFontColorType.AdaptiveColored:
_strokeFontColor = _environmentalColor.WithBrightness(0.7);
break;
case LyricsFontColorType.Custom:
_strokeFontColor = _customStrokeFontColor ?? _environmentalColor;
break;
default:
break;
@@ -259,84 +373,71 @@ namespace BetterLyrics.WinUI3.ViewModels
private void UpdateLinesProps()
{
var currentPlayingLineIndex = GetCurrentPlayingLineIndex();
var currentPlayingLine = _lyricsDataArr
.ElementAtOrDefault(_langIndex)
?.LyricsLines.ElementAtOrDefault(_playingLineIndex);
int halfVisibleLineCount =
Math.Max(1, Math.Max(
currentPlayingLineIndex - _startVisibleLineIndex,
_endVisibleLineIndex - currentPlayingLineIndex
));
if (halfVisibleLineCount < 1)
{
return;
}
if (currentPlayingLine == null) return;
for (int i = _startVisibleLineIndex; i <= _endVisibleLineIndex; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?.SafeGet(i);
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
if (line == null)
if (line == null) continue;
if (_isLayoutChanged || _isVisibleLinesBoundaryChanged || _isPlayingLineChanged)
{
continue;
}
float distanceFromPlayingLine = Math.Abs(line.Position.Y - currentPlayingLine.Position.Y);
float distanceFactor = Math.Clamp(distanceFromPlayingLine / (_canvasHeight / 2), 0, 1);
int distanceFromPlayingLine = Math.Abs(i - currentPlayingLineIndex);
if (distanceFromPlayingLine > halfVisibleLineCount)
{
continue;
}
float distanceFactor = distanceFromPlayingLine / (float)halfVisibleLineCount;
line.AngleTransition.StartTransition(
_isFanLyricsEnabled
? (float)Math.PI
* (30f / 180f)
* distanceFactor
* (i - currentPlayingLineIndex > 0 ? 1 : -1)
: 0
);
line.BlurAmountTransition.StartTransition(LyricsBlurAmount * distanceFactor);
line.ScaleTransition.StartTransition(
_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)
);
line.OpacityTransition.StartTransition(_defaultOpacity - distanceFactor * _defaultOpacity * (1 - LyricsVerticalEdgeOpacity / 100f));
// Only calculate highlight opacity for the current line and the two lines around it
// to avoid unnecessary calculations
if (distanceFromPlayingLine <= 1)
{
line.HighlightOpacityTransition.StartTransition(
distanceFromPlayingLine == 0 ? 1 : 0
line.AngleTransition.StartTransition(_isFanLyricsEnabled
? (float)Math.PI
* (30f / 180f)
* distanceFactor
* (i > _playingLineIndex ? 1 : -1)
: 0
);
line.BlurAmountTransition.StartTransition(_lyricsBlurAmount * distanceFactor);
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale));
line.OpacityTransition.StartTransition(_defaultOpacity - distanceFactor * _defaultOpacity * (1 - _lyricsVerticalEdgeOpacity / 100f));
line.HighlightOpacityTransition.StartTransition(i == _playingLineIndex ? 1f : 0f);
}
if (line.AngleTransition.IsTransitioning)
line.AngleTransition.Update(_elapsedTime);
line.ScaleTransition.Update(_elapsedTime);
line.BlurAmountTransition.Update(_elapsedTime);
line.OpacityTransition.Update(_elapsedTime);
line.HighlightOpacityTransition.Update(_elapsedTime);
}
}
private void UpdateImmersiveBackgroundOpacity()
{
float targetOpacity;
if (_isDesktopMode)
{
if (_isLyricsWindowLocked)
{
line.AngleTransition.Update(ElapsedTime);
targetOpacity = 0;
}
if (line.ScaleTransition.IsTransitioning)
else
{
line.ScaleTransition.Update(ElapsedTime);
}
if (line.BlurAmountTransition.IsTransitioning)
{
line.BlurAmountTransition.Update(ElapsedTime);
}
if (line.OpacityTransition.IsTransitioning)
{
line.OpacityTransition.Update(ElapsedTime);
}
// Only update highlight opacity for the current line and the two lines around it
if (distanceFromPlayingLine <= 1)
{
if (line.HighlightOpacityTransition.IsTransitioning)
if (_isMouseWithinWindow)
{
line.HighlightOpacityTransition.Update(ElapsedTime);
targetOpacity = 1f;
}
else
{
targetOpacity = 0f;
}
}
}
else
{
targetOpacity = 1f;
}
_immersiveBgOpacityTransition.StartTransition(targetOpacity);
}
}
}

View File

@@ -1,17 +1,28 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ABI.Microsoft.UI.Xaml;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Helpers.General;
using Lyricify.Lyrics.Providers;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI;
@@ -19,175 +30,163 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel : BaseViewModel
{
private readonly ValueTransition<float> _albumArtBgTransition = new(
initialValue: 0f,
durationSeconds: 1.0f
);
private TimeSpan _elapsedTime = TimeSpan.Zero;
private readonly ValueTransition<float> _canvasYScrollTransition = new(
initialValue: 0f,
durationSeconds: 0.8f,
easingType: EasingType.SmootherStep
);
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
private readonly float _coverRotateSpeed = 0.003f;
private TimeSpan _positionOffset = TimeSpan.Zero;
private readonly float _defaultOpacity = 0.3f;
private int _songDurationMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
private readonly float _defaultScale = 0.75f;
private SoftwareBitmap? _lastAlbumArtSwBitmap = null;
private SoftwareBitmap? _albumArtSwBitmap = null;
private CanvasBitmap? _lastAlbumArtCanvasBitmap = null;
private CanvasBitmap? _albumArtCanvasBitmap = null;
private float _albumArtSize = 0f;
private int _albumArtCornerRadius = 0;
private float _albumArtY = 0f;
private string? _lastSongTitle;
private string? _songTitle;
private float _titleY = 0f;
private string? _lastSongArtist;
private string? _songArtist;
private float _canvasWidth = 0f;
private float _canvasHeight = 0f;
private float _defaultOpacity;
private readonly float _highlightedOpacity = 1.0f;
private readonly float _defaultScale = 0.75f;
private readonly float _highlightedScale = 1.0f;
private readonly ValueTransition<Color> _immersiveBgTransition = new(
initialValue: Colors.Transparent,
durationSeconds: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
private readonly float _coverRotateSpeed = 0.003f;
private float _rotateAngle = 0f;
private readonly ILibWatcherService _libWatcherService;
private TextAlignmentType _lyricsAlignmentType;
private readonly float _lyricsGlowEffectAmount = 8f;
private int _lyricsBlurAmount;
private int _lyricsVerticalEdgeOpacity;
private readonly ValueTransition<float> _maxLyricsWidthTransition = new(
initialValue: 0f,
durationSeconds: 0.3f,
easingType: EasingType.SmoothStep
);
private ElementTheme _lyricsBgTheme;
private LineRenderingType _lyricsGlowEffectScope;
private LineRenderingType _lyricsHighlightScope;
private readonly ValueTransition<float> _lyricsOpacityTransition = new(
initialValue: 0f,
durationSeconds: 0.3f
);
private int _lyricsFontStrokeWidth;
private int _lyricsFontSize;
private float _lyricsLineSpacingFactor;
private protected readonly IMusicSearchService _musicSearchService;
private LyricsFontColorType _lyricsBgFontColorType;
private LyricsFontColorType _lyricsFgFontColorType;
private LyricsFontColorType _lyricsStrokeFontColorType;
private protected readonly IPlaybackService _playbackService;
private float _maxLyricsWidth = 0f;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ILibWatcherService _libWatcherService;
private readonly IPlaybackService _playbackService;
private readonly ITranslateService _translateService;
private readonly ILogger _logger;
private readonly float _leftMargin = 36f;
private readonly float _middleMargin = 36f;
private readonly float _rightMargin = 36f;
private readonly float _topMargin = 36f;
private readonly float _bottomMargin = 36f;
private readonly ValueTransition<float> _shenGuangAngleTransition = new(0f, 0.2f);
private readonly float _topMargin = 0f;
private Color? _adaptiveFontColor = null;
private Color _adaptiveGrayedFontColor = Colors.Transparent;
private Color? _adaptiveColoredFontColor = null;
private Color? _albumArtAccentColor = null;
private Color _environmentalColor = Colors.Transparent;
private SoftwareBitmap? _albumArtBitmap = null;
private Color _lightColor = Colors.White;
private Color _darkColor = Colors.Black;
private Color? _customFontColor;
private Color _bgFontColor;
private Color _fgFontColor;
private Color _strokeFontColor;
private Color _darkFontColor = Colors.Black;
private Color? _customBgFontColor;
private Color? _customFgFontColor;
private Color? _customStrokeFontColor;
private int _playingLineIndex = -1;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private protected Color _fontColor;
private bool _isDebugOverlayEnabled = false;
private bool _isDesktopMode = false;
private bool _isDockMode = false;
private bool _isFanLyricsEnabled = false;
private bool _isPlaying = true;
private protected bool _isRelayoutNeeded = true;
private bool _isLyricsWindowLocked = false;
private bool _isMouseWithinWindow = false;
private bool _isDynamicCoverOverlayEnabled;
private bool _isLyricsGlowEffectEnabled;
private bool _isLyricsFloatAnimationEnabled;
private bool _isLayoutChanged = true;
private int _langIndex = 0;
private SoftwareBitmap? _lastAlbumArtBitmap = null;
private List<LyricsData> _lyricsDataArr = [];
private List<string> _translationList = [];
private bool _isTranslationEnabled;
private bool _showTranslationOnly;
private int _targetLanguageIndex;
private Color _lightFontColor = Colors.White;
private int _timelineSyncThreshold;
private Color _lyricsWindowBgColor = Colors.Transparent;
private List<List<LyricsLine>> _multiLangLyrics = [];
private float _rotateAngle = 0f;
private int _startVisibleLineIndex = -1;
private protected CanvasTextFormat _textFormat = new()
private CanvasTextFormat _lyricsTextFormat = new()
{
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
};
public LyricsRendererViewModel(
ISettingsService settingsService, IPlaybackService playbackService,
IMusicSearchService musicSearchService, ILibWatcherService libWatcherService) : base(settingsService)
private CanvasTextFormat _titleTextFormat = new()
{
_musicSearchService = musicSearchService;
_playbackService = playbackService;
_libWatcherService = libWatcherService;
FontSize = 18,
FontWeight = FontWeights.Bold,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
WordWrapping = CanvasWordWrapping.NoWrap,
TrimmingSign = CanvasTrimmingSign.Ellipsis,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
};
private CanvasTextFormat _artistTextFormat = new()
{
FontSize = 16,
FontWeight = FontWeights.Bold,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
WordWrapping = CanvasWordWrapping.NoWrap,
TrimmingSign = CanvasTrimmingSign.Ellipsis,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
};
CoverImageRadius = _settingsService.CoverImageRadius;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
private LatestOnlyTaskRunner _refreshLyricsRunner = new();
private LatestOnlyTaskRunner _showTranslationsRunner = new();
LyricsFontColorType = _settingsService.LyricsFontColorType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
_customFontColor = _settingsService.LyricsCustomFontColor;
private LyricsDisplayType _displayTypeReceived;
private LyricsDisplayType _displayType;
_libWatcherService.MusicLibraryFilesChanged +=
LibWatcherService_MusicLibraryFilesChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
_isPlaying = _playbackService.IsPlaying;
SongInfo = _playbackService.SongInfo;
TotalTime = _playbackService.Position;
UpdateFontColor();
}
public int CoverImageRadius { get; set; }
public int CoverOverlayBlurAmount { get; set; }
public int CoverOverlayOpacity { get; set; }
public LyricsDisplayType DisplayType { get; set; }
public TimeSpan ElapsedTime { get; set; } = TimeSpan.Zero;
public bool IsDynamicCoverOverlayEnabled { get; set; }
public bool IsLyricsGlowEffectEnabled { get; set; }
public LyricsAlignmentType LyricsAlignmentType { get; set; }
public int LyricsBlurAmount { get; set; }
private int _albumArtBgBlurAmount;
private int _albumArtBgOpacity;
[ObservableProperty]
public partial LyricsFontColorType LyricsFontColorType { get; set; }
[ObservableProperty]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
public LineRenderingType LyricsGlowEffectScope { get; set; }
[ObservableProperty]
public partial float LyricsLineSpacingFactor { get; set; }
public int LyricsVerticalEdgeOpacity { get; set; }
public partial bool IsTranslating { get; set; } = false;
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; }
@@ -196,36 +195,45 @@ namespace BetterLyrics.WinUI3.ViewModels
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeTypeSent { get; set; }
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
private int GetCurrentPlayingLineIndex()
{
for (int i = 0; i < _multiLangLyrics.SafeGet(_langIndex)?.Count; i++)
var totalMs = TotalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (totalMs < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
for (int i = 0; i < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.Count; i++)
{
var line = _multiLangLyrics.SafeGet(_langIndex)?[i];
if (line == null)
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var nextLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i + 1);
if (nextLine != null && line.StartMs <= totalMs && totalMs < nextLine.StartMs)
{
continue;
return i;
}
if (
line.StartMs <= TotalTime.TotalMilliseconds
&& TotalTime.TotalMilliseconds <= line.EndMs
)
else if (nextLine == null && line.StartMs <= totalMs)
{
return i;
}
}
return -1;
return GetMaxLyricsLineIndexBoundaries().Item2;
}
private void GetLinePlayingProgress(LyricsLine line, out int charStartIndex, out int charLength, out float charProgress)
private void GetLinePlayingProgress(int lineIndex, out int charStartIndex, out int charLength, out float charProgress)
{
charStartIndex = 0;
charLength = 0;
charProgress = 0f;
float now = (float)TotalTime.TotalMilliseconds;
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(lineIndex);
if (line == null) return;
var nextLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(lineIndex + 1);
int lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = _songDurationMs;
float now = (float)TotalTime.TotalMilliseconds + (float)_positionOffset.TotalMilliseconds;
// 1. 还没到本句
if (now < line.StartMs)
@@ -234,27 +242,37 @@ namespace BetterLyrics.WinUI3.ViewModels
}
// 2. 已经超过本句
if (now > line.EndMs)
if (now > lineEndMs)
{
charProgress = 1f;
charStartIndex = line.OriginalText.Length - 1;
charLength = 1;
return;
}
// 3. 有逐字时间轴
if (line.CharTimings != null && line.CharTimings.Count > 0)
if (line.LyricsChars != null && line.LyricsChars.Count > 1)
{
int charTimingsCount = line.CharTimings.Count;
int charTimingsCount = line.LyricsChars.Count;
for (int i = 0; i < charTimingsCount; i++)
{
var timing = line.CharTimings[i];
var timing = line.LyricsChars[i];
var nextTiming = line.LyricsChars.ElementAtOrDefault(i + 1);
int timingEndMs;
if (timing.EndMs != null) timingEndMs = timing.EndMs.Value;
else if (nextTiming != null) timingEndMs = nextTiming.StartMs;
else timingEndMs = lineEndMs;
charStartIndex = timing.StartIndex;
charLength = timing.Text.Length;
// 当前时间在某个字的高亮区间
if (now >= timing.StartMs && now <= timing.EndMs)
if (now >= timing.StartMs && now <= timingEndMs)
{
charStartIndex = timing.StartIndex;
charLength = timing.Text.Length;
if (timing.EndMs != timing.StartMs)
if (timingEndMs != timing.StartMs)
{
charProgress = (now - timing.StartMs) / (timing.EndMs - timing.StartMs);
charProgress = (now - timing.StartMs) / (timingEndMs - timing.StartMs);
}
else
{
@@ -262,15 +280,30 @@ namespace BetterLyrics.WinUI3.ViewModels
}
return;
}
else if (now > timingEndMs && (nextTiming == null || now < nextTiming?.StartMs))
{
charProgress = 1f;
return;
}
}
}
else
{
// 没有逐字时间轴,直接线性
charProgress = (now - line.StartMs) / line.DurationMs;
charProgress = Math.Clamp(charProgress, 0f, 1f);
charStartIndex = 0;
charLength = line.Text.Length;
// 没有逐字时间轴,均匀分配每个字的高亮时间
int textLength = line.OriginalText.Length;
if (textLength == 0) return;
float lineProgress = (now - line.StartMs) / (lineEndMs - line.StartMs);
lineProgress = Math.Clamp(lineProgress, 0f, 1f);
// 计算当前高亮到第几个字
float charFloatIndex = lineProgress * textLength;
int charIndex = (int)charFloatIndex;
charStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
charLength = 1;
// 当前字的进度0~1
charProgress = charFloatIndex - charIndex;
}
}
@@ -278,19 +311,23 @@ namespace BetterLyrics.WinUI3.ViewModels
{
if (
SongInfo == null
|| _multiLangLyrics.SafeGet(_langIndex) == null
|| _multiLangLyrics[_langIndex].Count == 0
|| _lyricsDataArr.ElementAtOrDefault(_langIndex) == null
|| _lyricsDataArr[_langIndex].LyricsLines.Count == 0
)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _multiLangLyrics[_langIndex].Count - 1);
return new Tuple<int, int>(0, _lyricsDataArr[_langIndex].LyricsLines.Count - 1);
}
private async void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
{
await RefreshLyricsAsync();
_logger.LogInformation("Music library files changed: {ChangeType} {FilePath}, refreshing lyrics...", e.ChangeType, e.FilePath);
_ = _refreshLyricsRunner.RunAsync(async token =>
{
await RefreshLyricsAsync(token);
});
}
private void PlaybackService_IsPlayingChanged(object? sender, IsPlayingChangedEventArgs e)
@@ -300,56 +337,217 @@ namespace BetterLyrics.WinUI3.ViewModels
private void PlaybackService_PositionChanged(object? sender, PositionChangedEventArgs e)
{
TotalTime = e.Position;
if (Math.Abs(TotalTime.TotalMilliseconds - e.Position.TotalMilliseconds) >= _timelineSyncThreshold)
{
TotalTime = e.Position;
}
}
private void PlaybackService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
if (SongInfo?.Title != _songTitle || SongInfo?.Artist != _songArtist)
{
_lastSongTitle = _songTitle;
_songTitle = SongInfo?.Title;
_lastSongArtist = _songArtist;
_songArtist = SongInfo?.Artist;
_songDurationMs = (int)(SongInfo?.DurationMs ?? TimeSpan.FromMinutes(99).TotalMilliseconds);
_songInfoOpacityTransition.Reset(0f);
_songInfoOpacityTransition.StartTransition(1f);
_logger.LogInformation("Song info changed: Title={Title}, Artist={Artist}, refreshing lyrics...", _songTitle, _songArtist);
Debug.WriteLine($"Song info changed: Title={_songTitle}, Artist={_songArtist}");
_ = _refreshLyricsRunner.RunAsync(async token =>
{
await RefreshLyricsAsync(token);
});
TotalTime = TimeSpan.Zero;
}
}
private async Task RefreshLyricsAsync()
private void PlaybackService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
{
SetLyricsLoadingPlaceholder();
if (e.AlbumArtSwBitmap != _albumArtSwBitmap)
{
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
_lastAlbumArtCanvasBitmap = null;
_albumArtSwBitmap = e.AlbumArtSwBitmap;
_albumArtCanvasBitmap = null;
_albumArtAccentColor = e.AlbumArtAccentColor;
_albumArtBgTransition.Reset(0f);
_albumArtBgTransition.StartTransition(1f);
UpdateColorConfig();
}
}
private void UpdateTranslations()
{
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
_isLayoutChanged = true;
IsTranslating = true;
if (_isTranslationEnabled)
{
_ = _refreshLyricsRunner.RunAsync(async token =>
{
await SetDisplayedAlongWithTranslationsAsync(token);
IsTranslating = false;
_isLayoutChanged = true;
});
}
else
{
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
_langIndex = 0;
IsTranslating = false;
_isLayoutChanged = true;
}
}
private async Task SetDisplayedAlongWithTranslationsAsync(CancellationToken token)
{
_logger.LogInformation("Showing translation for lyrics...");
string targetLangCode = LanguageHelper.GetUserTargetLanguageCode();
string? originalText = _lyricsDataArr.FirstOrDefault()?.WrappedOriginalText;
if (originalText == null) return;
string? originalLangCode = LanguageHelper.DetectLanguageCode(originalText);
if (originalLangCode == targetLangCode)
{
_logger.LogInformation("Original lyrics already in target language: {TargetLangCode}", targetLangCode);
_lyricsDataArr[0].SetDisplayedTextInOriginalText();
}
else
{
// Try get translation from itself first
int found = _translateService.SearchTranslatedLyricsItself(_lyricsDataArr);
if (found >= 0)
{
if (_showTranslationOnly)
{
_lyricsDataArr[found].SetDisplayedTextInOriginalText();
_langIndex = found;
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(_lyricsDataArr[found]);
_langIndex = 0;
}
}
else
{
string translated = string.Empty;
try
{
translated = await _translateService.TranslateTextAsync(originalText, targetLangCode, token);
if (translated == string.Empty) return;
if (_showTranslationOnly)
{
_lyricsDataArr[^1] = _lyricsDataArr[0].CreateLyricsDataFrom(translated);
_lyricsDataArr[^1].SetDisplayedTextInOriginalText();
_langIndex = _lyricsDataArr.Count - 1;
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(translated);
_langIndex = 0;
}
token.ThrowIfCancellationRequested();
}
catch (Exception) { }
}
}
}
private async Task RefreshLyricsAsync(CancellationToken token)
{
_logger.LogInformation("Refreshing lyrics...");
_lyricsDataArr = [LyricsData.GetLoadingPlaceholder()];
_isLayoutChanged = true;
string? lyricsRaw = null;
LyricsFormat? lyricsFormat = null;
LyricsSearchProvider? provider = null;
if (SongInfo != null)
{
(lyricsRaw, lyricsFormat) = await _musicSearchService.SearchLyricsAsync(
(lyricsRaw, provider) = await _lyrcsSearchService.SearchAsync(
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",
SongInfo.DurationMs ?? 0
SongInfo.DurationMs ?? 0,
token
);
_logger.LogInformation("Lyrics search result: {LyricsRaw}", lyricsRaw ?? "null");
token.ThrowIfCancellationRequested();
_lyricsDataArr = new LyricsParser().Parse(lyricsRaw, (int?)SongInfo?.DurationMs);
FillTranslationFromCache(provider);
}
else
{
_logger.LogWarning("SongInfo is null, cannot search lyrics.");
}
_multiLangLyrics = new LyricsParser().Parse(
lyricsRaw,
lyricsFormat,
SongInfo?.Title,
SongInfo?.Artist,
(int)(SongInfo?.DurationMs ?? 0)
);
_isRelayoutNeeded = true;
_logger.LogInformation("Parsed lyrics: {MultiLangLyricsCount} languages", _lyricsDataArr.Count);
// This ensures that original lyrics are always shown while waiting for translations
_lyricsDataArr[0].SetDisplayedTextInOriginalText();
_isLayoutChanged = true;
UpdateTranslations();
}
private void SetLyricsLoadingPlaceholder()
private void FillTranslationFromCache(LyricsSearchProvider? provider)
{
_multiLangLyrics = [];
_multiLangLyrics.Add(
[
new LyricsLine
string? translationRaw = null;
switch (provider)
{
case LyricsSearchProvider.QQ:
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.QQTranslationCacheDirectory);
break;
case LyricsSearchProvider.Kugou:
break;
case LyricsSearchProvider.Netease:
break;
case LyricsSearchProvider.LrcLib:
break;
case LyricsSearchProvider.AmllTtmlDb:
break;
case LyricsSearchProvider.LocalMusicFile:
break;
case LyricsSearchProvider.LocalLrcFile:
break;
case LyricsSearchProvider.LocalEslrcFile:
break;
case LyricsSearchProvider.LocalTtmlFile:
break;
default:
break;
}
if (translationRaw != null)
{
var translationData = new LyricsParser().Parse(translationRaw, (int?)SongInfo?.DurationMs);
foreach (var data in translationData)
{
data.LyricsLines = data.LyricsLines.Where(line => !string.IsNullOrWhiteSpace(line.OriginalText)).ToList();
foreach (var item in data.LyricsLines)
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
Text = App.ResourceLoader!.GetString("LyricsLoading"),
CharTimings = [],
},
]
);
_isRelayoutNeeded = true;
if (item.OriginalText == "//") item.OriginalText = "";
}
}
_lyricsDataArr = _lyricsDataArr.Concat(translationData).ToList();
}
}
}
}

View File

@@ -1,130 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using Windows.UI;
namespace BetterInAppLyrics.WinUI3.ViewModels
{
public partial class LyricsSettingsControlViewModel : BaseViewModel
{
public LyricsSettingsControlViewModel(ISettingsService settingsService)
: base(settingsService)
{
IsActive = true;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
IsFanLyricsEnabled = _settingsService.IsFanLyricsEnabled;
LyricsFontColorType = _settingsService.LyricsFontColorType;
LyricsCustomFontColor = _settingsService.LyricsCustomFontColor;
}
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsFanLyricsEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsGlowEffectEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsAlignmentType LyricsAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LineRenderingType LyricsGlowEffectScope { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial float LyricsLineSpacingFactor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsVerticalEdgeOpacity { get; set; }
partial void OnIsFanLyricsEnabledChanged(bool value)
{
_settingsService.IsFanLyricsEnabled = value;
}
partial void OnIsLyricsGlowEffectEnabledChanged(bool value)
{
_settingsService.IsLyricsGlowEffectEnabled = value;
}
partial void OnLyricsAlignmentTypeChanged(LyricsAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
partial void OnLyricsCustomFontColorChanged(Color value)
{
_settingsService.LyricsCustomFontColor = value;
}
partial void OnLyricsFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsFontColorType = value;
}
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
partial void OnLyricsGlowEffectScopeChanged(LineRenderingType value)
{
_settingsService.LyricsGlowEffectScope = value;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
}
}

View File

@@ -1,49 +1,61 @@
// 2025/6/23 by Zhe Fang
using System.Threading.Tasks;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Windows.System;
using Windows.UI;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3
{
public partial class LyricsWindowViewModel
: BaseWindowViewModel,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<bool>>
IRecipient<PropertyChangedMessage<DockPlacement>>
{
private ForegroundWindowWatcherHelper? _watcherHelper = null;
private readonly IPlaybackService _playbackService = Ioc.Default.GetRequiredService<IPlaybackService>();
private ForegroundWindowWatcher? _windowWatcher = null;
private bool _ignoreFullscreenWindow;
private bool _hideWindowWhenNotPlaying;
public LyricsWindowViewModel(ISettingsService settingsService)
: base(settingsService)
private DockPlacement _dockPlacement;
private int _dockWindowHeight;
public LyricsWindowViewModel(ISettingsService settingsService) : base(settingsService)
{
WeakReferenceMessenger.Default.Register<ShowNotificatonMessage>(
this,
async (r, m) =>
{
Notification = m.Value;
if (!Notification.IsForeverDismissable)
{
Notification.Visibility = Notification.IsForeverDismissable ? Visibility.Visible : Visibility.Collapsed;
ShowInfoBar = true;
await Task.Delay(AnimationHelper.StackedNotificationsShowingDuration);
ShowInfoBar = false;
}
}
);
_ignoreFullscreenWindow = _settingsService.IgnoreFullscreenWindow;
_hideWindowWhenNotPlaying = _settingsService.HideWindowWhenNotPlaying;
IsImmersiveMode = _settingsService.IsImmersiveMode;
_dockPlacement = _settingsService.DockPlacement;
_dockWindowHeight = _settingsService.DockWindowHeight;
OnIsImmersiveModeChanged(_settingsService.IsImmersiveMode);
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
}
private void PlaybackService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
AutoHideOrShowWindow();
}
[ObservableProperty]
@@ -63,10 +75,11 @@ namespace BetterLyrics.WinUI3
public partial bool IsLyricsWindowLocked { get; set; } = false;
[ObservableProperty]
public partial Notification Notification { get; set; } = new();
[NotifyPropertyChangedRecipients]
public partial bool IsImmersiveMode { get; set; }
[ObservableProperty]
public partial bool ShowInfoBar { get; set; } = false;
public partial float TopCommandGridOpacity { get; set; }
[ObservableProperty]
public partial ElementTheme ThemeType { get; set; } = ElementTheme.Default;
@@ -75,7 +88,62 @@ namespace BetterLyrics.WinUI3
public partial double TitleBarFontSize { get; set; } = 11;
[ObservableProperty]
public partial double TitleBarHeight { get; set; } = 36;
[NotifyPropertyChangedRecipients]
public partial bool IsMouseWithinWindow { get; set; } = false;
[ObservableProperty]
public partial string LockHotKey { get; set; } = "";
private void AutoHideOrShowWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
if (IsDockMode || IsDesktopMode)
{
if (_hideWindowWhenNotPlaying && _playbackService.SongInfo == null)
{
if (IsDockMode)
{
DockModeHelper.UpdateAppBarHeight(hwnd, 0, _dockPlacement);
}
window.Hide();
}
else
{
if (IsDockMode)
{
DockModeHelper.UpdateAppBarHeight(hwnd, _dockWindowHeight, _dockPlacement);
}
window.Show();
}
}
}
private void UpdateDockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
if (!_hideWindowWhenNotPlaying || _playbackService.SongInfo != null)
{
DockModeHelper.UpdateAppBarHeight(WindowNative.GetWindowHandle(window), _dockWindowHeight, _dockPlacement);
}
}
partial void OnIsImmersiveModeChanged(bool value)
{
if (value)
{
TopCommandGridOpacity = 0f;
}
else
{
TopCommandGridOpacity = 1f;
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
@@ -89,6 +157,18 @@ namespace BetterLyrics.WinUI3
}
}
}
else if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.IgnoreFullscreenWindow))
{
_ignoreFullscreenWindow = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.HideWindowWhenNotPlaying))
{
_hideWindowWhenNotPlaying = message.NewValue;
AutoHideOrShowWindow();
}
}
}
public void Receive(PropertyChangedMessage<ElementTheme> message)
@@ -104,73 +184,124 @@ namespace BetterLyrics.WinUI3
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is LyricsSettingsControlViewModel)
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(LyricsSettingsControlViewModel.LyricsFontSize))
if (message.PropertyName == nameof(SettingsPageViewModel.DockWindowHeight))
{
if (IsDockMode)
_dockWindowHeight = message.NewValue;
UpdateDockWindow();
}
else if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LockHotKeyIndex))
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
DockModeHelper.UpdateAppBarHeight(
WindowNative.GetWindowHandle(window),
message.NewValue * 3
);
UpdateLockHotKey(message.NewValue);
}
}
}
}
public void StartWatchWindowColorChange(WindowColorSampleMode mode)
private void UpdateLockHotKey(int hotKeyIndex)
{
var hwnd = WindowNative.GetWindowHandle(
WindowHelper.GetWindowByWindowType<LyricsWindow>()
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
GlobalHotKeyHelper.UnregisterAllHotKeys(window);
GlobalHotKeyHelper.RegisterHotKey(
window,
User32.HotKeyModifiers.MOD_CONTROL | User32.HotKeyModifiers.MOD_ALT,
(uint)(hotKeyIndex + (int)VirtualKey.A),
() =>
{
if (IsDesktopMode)
{
ToggleLockWindowCommand.Execute(null);
}
}
);
_watcherHelper = new ForegroundWindowWatcherHelper(
LockHotKey = ((VirtualKey)(hotKeyIndex + (int)VirtualKey.A)).ToString();
}
public void StartWatchWindowColorChange()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
var hwnd = WindowNative.GetWindowHandle(window);
_windowWatcher = new ForegroundWindowWatcher(
hwnd,
onWindowChanged =>
{
UpdateAccentColor(hwnd, mode);
_dispatcherQueueTimer.Debounce(() =>
{
if (_ignoreFullscreenWindow && window.AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsAlwaysOnTop = true;
}
UpdateAccentColor(hwnd);
}, TimeSpan.FromMilliseconds(300));
}
);
_watcherHelper.Start();
UpdateAccentColor(hwnd, mode);
}
public void UpdateAccentColor(nint hwnd, WindowColorSampleMode mode)
{
ActivatedWindowAccentColor = WindowColorHelper.GetDominantColor(hwnd, mode).ToColor();
}
[RelayCommand]
private void LockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
DesktopModeHelper.Lock(window);
IsLyricsWindowLocked = true;
_windowWatcher.Start();
UpdateAccentColor(hwnd);
}
private void StopWatchWindowColorChange()
{
_watcherHelper?.Stop();
_watcherHelper = null;
_windowWatcher?.Stop();
_windowWatcher = null;
}
public void UpdateAccentColor(nint hwnd)
{
WindowPixelSampleMode mode = IsDesktopMode ? WindowPixelSampleMode.WindowEdge : _dockPlacement.ToWindowPixelSampleMode();
ActivatedWindowAccentColor = Helper.ColorHelper.GetAccentColor(hwnd, mode).ToColor();
}
public void InitLockHotKey()
{
UpdateLockHotKey(_settingsService.LockHotKeyIndex);
}
[RelayCommand]
private void ToggleLockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
if (IsLyricsWindowLocked)
{
DesktopModeHelper.SetClickThrough(window, false);
IsLyricsWindowLocked = false;
IsImmersiveMode = _settingsService.IsImmersiveMode;
}
else
{
DesktopModeHelper.SetClickThrough(window, true);
IsLyricsWindowLocked = true;
IsImmersiveMode = true;
}
AutoHideOrShowWindow();
}
[RelayCommand]
private void ToggleDesktopMode()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
StopWatchWindowColorChange();
IsDesktopMode = !IsDesktopMode;
if (IsDesktopMode)
{
StartWatchWindowColorChange(WindowColorSampleMode.WindowEdge);
DesktopModeHelper.Enable(window);
StartWatchWindowColorChange();
}
else
{
DesktopModeHelper.Disable(window);
StopWatchWindowColorChange();
}
}
@@ -178,19 +309,40 @@ namespace BetterLyrics.WinUI3
private void ToggleDockMode()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (window == null) return;
StopWatchWindowColorChange();
IsDockMode = !IsDockMode;
if (IsDockMode)
{
StartWatchWindowColorChange(WindowColorSampleMode.BelowWindow);
DockModeHelper.Enable(window, _settingsService.LyricsFontSize * 3);
DockModeHelper.Enable(window, _dockWindowHeight, _dockPlacement);
StartWatchWindowColorChange();
}
else
{
StartWatchWindowColorChange(WindowColorSampleMode.WindowEdge);
DockModeHelper.Disable(window);
}
AutoHideOrShowWindow();
}
[RelayCommand]
private void OnImmersiveToggleButtonEnabledChanged()
{
_settingsService.IsImmersiveMode = IsImmersiveMode;
}
public void Receive(PropertyChangedMessage<DockPlacement> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.DockPlacement))
{
_dockPlacement = message.NewValue;
UpdateDockWindow();
}
}
}
}
}

View File

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

View File

@@ -1,40 +1,48 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Globalization;
using Windows.Media.Playback;
using Windows.System;
using Windows.UI;
using WinRT.Interop;
using MetadataHelper = BetterLyrics.WinUI3.Helper.MetadataHelper;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsPageViewModel : ObservableRecipient
public partial class SettingsPageViewModel : BaseViewModel
{
private readonly ILibWatcherService _libWatcherService;
private readonly ISettingsService _settingsService;
private readonly IPlaybackService _playbackService;
private readonly ITranslateService _libreTranslateService;
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService)
private readonly string _autoStartupTaskId = "AutoStartup";
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, IPlaybackService playbackService, ITranslateService libreTranslateService) : base(settingsService)
{
_settingsService = settingsService;
_libWatcherService = libWatcherService;
_playbackService = playbackService;
_libreTranslateService = libreTranslateService;
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LibreTranslateServer = _settingsService.LibreTranslateServer;
SelectedTargetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
LocalMediaFolders = [.. _settingsService.LocalMediaFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
AlbumArtSearchProvidersInfo = [.. _settingsService.AlbumArtSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
@@ -46,12 +54,90 @@ namespace BetterLyrics.WinUI3.ViewModels
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
SongInfoAlignmentType = _settingsService.SongInfoAlignmentType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
LyricsFontSize = _settingsService.LyricsFontSize;
IsLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
LyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
LyricsHighlightScope = _settingsService.LyricsHighlightScope;
IsFanLyricsEnabled = _settingsService.IsFanLyricsEnabled;
LyricsBgFontColorType = _settingsService.LyricsBgFontColorType;
LyricsFgFontColorType = _settingsService.LyricsFgFontColorType;
LyricsStrokeFontColorType = _settingsService.LyricsStrokeFontColorType;
LyricsCustomBgFontColor = _settingsService.LyricsCustomBgFontColor;
LyricsCustomFgFontColor = _settingsService.LyricsCustomFgFontColor;
LyricsCustomStrokeFontColor = _settingsService.LyricsCustomStrokeFontColor;
LyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
LyricsBackgroundTheme = _settingsService.LyricsBackgroundTheme;
MediaSourceProvidersInfo = [.. _settingsService.MediaSourceProvidersInfo];
IgnoreFullscreenWindow = _settingsService.IgnoreFullscreenWindow;
LyricsScrollEasingType = _settingsService.LyricsScrollEasingType;
LyricsScrollDuration = _settingsService.LyricsScrollDuration;
TimelineSyncThreshold = _settingsService.TimelineSyncThreshold;
IsLyricsFloatAnimationEnabled = _settingsService.IsLyricsFloatAnimationEnabled;
ResetPositionOffsetOnSongChanged = _settingsService.ResetPositionOffsetOnSongChanged;
LockHotKeyIndex = _settingsService.LockHotKeyIndex;
LXMusicServer = _settingsService.LXMusicServer;
DockPlacement = _settingsService.DockPlacement;
LyricsBgFontOpacity = _settingsService.LyricsBgFontOpacity;
HideWindowWhenNotPlaying = _settingsService.HideWindowWhenNotPlaying;
DockWindowHeight = _settingsService.DockWindowHeight;
SystemFontNames = [.. FontHelper.SystemFontFamilies];
SelectedFontFamilyIndex = _settingsService.SelectedFontFamilyIndex;
LyricsFontFamily = _settingsService.LyricsFontFamily;
IsDragEverywhereEnabled = _settingsService.IsDragEverywhereEnabled;
_playbackService.MediaSourceProvidersInfoChanged += PlaybackService_SessionIdsChanged;
Task.Run(async () =>
{
BuildDate = (await AppInfo.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
BuildDate = (await MetadataHelper.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
});
}
private void PlaybackService_SessionIdsChanged(object? sender, Events.MediaSourceProvidersInfoEventArgs e)
{
MediaSourceProvidersInfo = [.. e.MediaSourceProviersInfo];
}
[ObservableProperty]
public partial bool IsDragEverywhereEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial string LyricsFontFamily { get; set; }
[ObservableProperty]
public partial ObservableCollection<string> SystemFontNames { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int SelectedFontFamilyIndex { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial DockPlacement DockPlacement { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LockHotKeyIndex { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme LyricsBackgroundTheme { get; set; }
[ObservableProperty]
public partial AutoStartWindowType AutoStartWindowType { get; set; }
@@ -74,6 +160,10 @@ namespace BetterLyrics.WinUI3.ViewModels
[NotifyPropertyChangedRecipients]
public partial bool IsDebugOverlayEnabled { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLogEnabled { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsDynamicCoverOverlayEnabled { get; set; }
@@ -82,18 +172,150 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial Enums.Language Language { get; set; }
[ObservableProperty]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
public partial ObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; }
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; }
public string Version { get; set; } = Helper.AppInfo.AppVersion;
[ObservableProperty]
public partial ObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; }
public string BuildDate { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsFanLyricsEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsGlowEffectEnabled { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TextAlignmentType LyricsAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TextAlignmentType SongInfoAlignmentType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomBgFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomFgFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Color LyricsCustomStrokeFontColor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsBgFontOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsBgFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsFgFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontColorType LyricsStrokeFontColorType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsFontWeight LyricsFontWeight { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LineRenderingType LyricsGlowEffectScope { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LineRenderingType LyricsHighlightScope { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial float LyricsLineSpacingFactor { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsVerticalEdgeOpacity { get; set; }
[ObservableProperty]
public partial object NavViewSelectedItemTag { get; set; } = "App";
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool ResetPositionOffsetOnSongChanged { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsFloatAnimationEnabled { get; set; }
public string Version { get; set; } = MetadataHelper.AppVersion;
public string BuildDate { get; set; } = string.Empty;
[ObservableProperty]
public partial string LibreTranslateServer { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int SelectedTargetLanguageIndex { get; set; } = 0;
[ObservableProperty]
public partial bool IsLibreTranslateServerTesting { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsFontStrokeWidth { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IgnoreFullscreenWindow { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial EasingType LyricsScrollEasingType { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsScrollDuration { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int TimelineSyncThreshold { get; set; }
[ObservableProperty]
public partial bool IsLXMusicServerTesting { get; set; } = false;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial string LXMusicServer { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool HideWindowWhenNotPlaying { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int DockWindowHeight { get; set; }
public void OnLyricsSearchProvidersReordered()
{
@@ -105,23 +327,28 @@ namespace BetterLyrics.WinUI3.ViewModels
);
}
public void OpenMusicFolder(LocalLyricsFolder folder)
public void OnAlbumArtSearchProvidersReordered()
{
OpenFolderInFileExplorer(folder.Path);
_settingsService.AlbumArtSearchProvidersInfo = [.. AlbumArtSearchProvidersInfo];
Broadcast(
AlbumArtSearchProvidersInfo,
AlbumArtSearchProvidersInfo,
nameof(AlbumArtSearchProvidersInfo)
);
}
public void RemoveFolderAsync(LocalLyricsFolder folder)
public void RemoveFolderAsync(LocalMediaFolder folder)
{
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
LocalMediaFolders.Remove(folder);
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
_libWatcherService.UpdateWatchers([.. LocalMediaFolders]);
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
public void ToggleLocalLyricsFolder(LocalMediaFolder folder)
{
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
@@ -134,108 +361,67 @@ namespace BetterLyrics.WinUI3.ViewModels
);
}
public void ToggleAlbumArtSearchProvider(AlbumArtSearchProviderInfo providerInfo)
{
_settingsService.AlbumArtSearchProvidersInfo = [.. AlbumArtSearchProvidersInfo];
Broadcast(
AlbumArtSearchProvidersInfo,
AlbumArtSearchProvidersInfo,
nameof(AlbumArtSearchProvidersInfo)
);
}
public void ToggleMediaSourceProvider(MediaSourceProviderInfo providerInfo)
{
Broadcast(
MediaSourceProvidersInfo,
MediaSourceProvidersInfo,
nameof(MediaSourceProvidersInfo)
);
}
private void AddFolderAsync(string path)
{
var normalizedPath =
Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (
LocalLyricsFolders.Any(x =>
Path.GetFullPath(x.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.Equals(
normalizedPath.TrimEnd(Path.DirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase
)
)
)
if (LocalMediaFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
{
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathExistedInfo")
)
)
);
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"));
}
else if (
LocalLyricsFolders.Any(item =>
normalizedPath.StartsWith(
Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase
)
)
)
else if (LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
{
// 添加的文件夹是现有文件夹的子文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo")
)
)
);
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo"));
}
else if (
LocalLyricsFolders.Any(item =>
Path.GetFullPath(item.Path)
.TrimEnd(Path.DirectorySeparatorChar)
.StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase)
)
else if (LocalMediaFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
)
{
// 添加的文件夹是现有文件夹的父文件夹
WeakReferenceMessenger.Default.Send(
new ShowNotificatonMessage(
new Notification(
App.ResourceLoader!.GetString("SettingsPagePathIncludingOthersInfo")
)
)
);
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathIncludingOthersInfo"));
}
else
{
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
LocalMediaFolders.Add(new LocalMediaFolder(path, true));
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
_libWatcherService.UpdateWatchers([.. LocalMediaFolders]);
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(Helper.AppInfo.GithubUrl));
await Windows.System.Launcher.LaunchUriAsync(new Uri(MetadataHelper.GithubUrl));
}
[RelayCommand]
private void OpenCacheFolder()
private static async Task OpenCacheFolderAsync()
{
OpenFolderInFileExplorer(Helper.AppInfo.CacheFolder);
}
private void OpenFolderInFileExplorer(string path)
{
Process.Start(
new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = path,
UseShellExecute = true,
}
);
await Windows.System.Launcher.LaunchFolderPathAsync(PathHelper.CacheFolder);
}
[RelayCommand]
private void PlayTestingMusicTask()
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
}
[RelayCommand]
private void RestartApp()
private static void RestartApp()
{
WindowHelper.RestartApp();
}
@@ -243,11 +429,13 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private async Task SelectAndAddFolderAsync(UIElement sender)
{
var picker = new Windows.Storage.Pickers.FolderPicker();
var window = WindowHelper.GetWindowByWindowType<SettingsWindow>();
if (window == null) return;
var picker = new Windows.Storage.Pickers.FolderPicker();
picker.FileTypeFilter.Add("*");
var hwnd = WindowNative.GetWindowHandle(WindowHelper.GetWindowByWindowType<SettingsWindow>());
var hwnd = WindowNative.GetWindowHandle(window);
InitializeWithWindow.Initialize(picker, hwnd);
var folder = await picker.PickSingleFolderAsync();
@@ -258,36 +446,142 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
[RelayCommand]
private void LibreTranslateServerTest()
{
IsLibreTranslateServerTesting = true;
Task.Run(async () =>
{
try
{
string targetLangCode = LanguageHelper.SupportedTargetLanguages[SelectedTargetLanguageIndex].Code;
string result = await _libreTranslateService.TranslateTextAsync("Hello, world!", targetLangCode, null);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestSuccessInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Success);
IsLibreTranslateServerTesting = false;
});
}
catch (Exception)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestFailedInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
IsLibreTranslateServerTesting = false;
});
}
});
}
[RelayCommand]
private void LXMusicServerTest()
{
IsLXMusicServerTesting = true;
Task.Run(async () =>
{
bool testResult = await NetHelper.CheckConnectivity($"{LXMusicServer}/status");
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString($"SettingsPageServerTest{(testResult ? "Success" : "Failed")}Info"),
testResult ? InfoBarSeverity.Success : InfoBarSeverity.Error);
IsLXMusicServerTesting = false;
});
});
}
public async Task<bool> ToggleAutoStartupAsync(bool target)
{
StartupTask startupTask = await StartupTask.GetAsync(_autoStartupTaskId);
if (target)
{
await startupTask.RequestEnableAsync();
}
else
{
startupTask.Disable();
}
return await DetectIsAutoStartupEnabledAsync();
}
public async Task<bool> DetectIsAutoStartupEnabledAsync()
{
bool result = false;
var startupTask = await StartupTask.GetAsync(_autoStartupTaskId);
switch (startupTask.State)
{
case StartupTaskState.Disabled:
case StartupTaskState.DisabledByUser:
case StartupTaskState.DisabledByPolicy:
result = false;
break;
case StartupTaskState.Enabled:
result = true;
break;
}
return result;
}
partial void OnDockPlacementChanged(DockPlacement value)
{
_settingsService.DockPlacement = value;
}
partial void OnLyricsScrollEasingTypeChanged(EasingType value)
{
_settingsService.LyricsScrollEasingType = value;
}
partial void OnLyricsScrollDurationChanged(int value)
{
_settingsService.LyricsScrollDuration = value;
}
partial void OnLyricsBackgroundThemeChanged(ElementTheme value)
{
_settingsService.LyricsBackgroundTheme = value;
}
partial void OnLyricsFontStrokeWidthChanged(int value)
{
_settingsService.LyricsFontStrokeWidth = value;
}
partial void OnIgnoreFullscreenWindowChanged(bool value)
{
_settingsService.IgnoreFullscreenWindow = value;
}
partial void OnSelectedTargetLanguageIndexChanged(int value)
{
_settingsService.SelectedTargetLanguageIndex = value;
}
partial void OnLibreTranslateServerChanged(string value)
{
_settingsService.LibreTranslateServer = value;
}
partial void OnLXMusicServerChanged(string value)
{
_settingsService.LXMusicServer = value;
}
partial void OnAutoStartWindowTypeChanged(AutoStartWindowType value)
{
_settingsService.AutoStartWindowType = value;
}
partial void OnAutoLockOnDesktopModeChanged(bool value)
{
_settingsService.AutoLockOnDesktopMode = value;
}
partial void OnCoverImageRadiusChanged(int value)
{
_settingsService.CoverImageRadius = value;
}
partial void OnCoverOverlayBlurAmountChanged(int value)
{
_settingsService.CoverOverlayBlurAmount = value;
}
partial void OnCoverOverlayOpacityChanged(int value)
{
_settingsService.CoverOverlayOpacity = value;
}
partial void OnIsDynamicCoverOverlayEnabledChanged(bool value)
{
_settingsService.IsDynamicCoverOverlayEnabled = value;
}
partial void OnLanguageChanged(Enums.Language value)
{
switch (value)
@@ -315,5 +609,116 @@ namespace BetterLyrics.WinUI3.ViewModels
}
_settingsService.Language = Language;
}
partial void OnIsFanLyricsEnabledChanged(bool value)
{
_settingsService.IsFanLyricsEnabled = value;
}
partial void OnIsLyricsGlowEffectEnabledChanged(bool value)
{
_settingsService.IsLyricsGlowEffectEnabled = value;
}
partial void OnLyricsAlignmentTypeChanged(TextAlignmentType value)
{
_settingsService.LyricsAlignmentType = value;
}
partial void OnSongInfoAlignmentTypeChanged(TextAlignmentType value)
{
_settingsService.SongInfoAlignmentType = value;
}
partial void OnLyricsBlurAmountChanged(int value)
{
_settingsService.LyricsBlurAmount = value;
}
partial void OnLyricsCustomBgFontColorChanged(Color value)
{
_settingsService.LyricsCustomBgFontColor = value;
}
partial void OnLyricsCustomFgFontColorChanged(Color value)
{
_settingsService.LyricsCustomFgFontColor = value;
}
partial void OnLyricsCustomStrokeFontColorChanged(Color value)
{
_settingsService.LyricsCustomStrokeFontColor = value;
}
partial void OnLyricsBgFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsBgFontColorType = value;
}
partial void OnLyricsFgFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsFgFontColorType = value;
}
partial void OnLyricsStrokeFontColorTypeChanged(LyricsFontColorType value)
{
_settingsService.LyricsStrokeFontColorType = value;
}
partial void OnLyricsFontSizeChanged(int value)
{
_settingsService.LyricsFontSize = value;
}
partial void OnLyricsFontWeightChanged(LyricsFontWeight value)
{
_settingsService.LyricsFontWeight = value;
}
partial void OnLyricsGlowEffectScopeChanged(LineRenderingType value)
{
_settingsService.LyricsGlowEffectScope = value;
}
partial void OnLyricsHighlightScopeChanged(LineRenderingType value)
{
_settingsService.LyricsHighlightScope = value;
}
partial void OnLyricsLineSpacingFactorChanged(float value)
{
_settingsService.LyricsLineSpacingFactor = value;
}
partial void OnLyricsVerticalEdgeOpacityChanged(int value)
{
_settingsService.LyricsVerticalEdgeOpacity = value;
}
partial void OnTimelineSyncThresholdChanged(int value)
{
_settingsService.TimelineSyncThreshold = value;
}
partial void OnIsLyricsFloatAnimationEnabledChanged(bool value)
{
_settingsService.IsLyricsFloatAnimationEnabled = value;
}
partial void OnResetPositionOffsetOnSongChangedChanged(bool value)
{
_settingsService.ResetPositionOffsetOnSongChanged = value;
}
partial void OnLyricsBgFontOpacityChanged(int value)
{
_settingsService.LyricsBgFontOpacity = value;
}
partial void OnHideWindowWhenNotPlayingChanged(bool value)
{
_settingsService.HideWindowWhenNotPlaying = value;
}
partial void OnDockWindowHeightChanged(int value)
{
_settingsService.DockWindowHeight = value;
}
partial void OnSelectedFontFamilyIndexChanged(int value)
{
_settingsService.SelectedFontFamilyIndex = value;
LyricsFontFamily = SystemFontNames[value];
}
partial void OnLyricsFontFamilyChanged(string value)
{
_settingsService.LyricsFontFamily = value;
}
partial void OnIsDragEverywhereEnabledChanged(bool value)
{
_settingsService.IsDragEverywhereEnabled = value;
LyricsWindow? lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
lyricsWindow.UpdateTitleBarArea();
}
}
}
}

View File

@@ -2,8 +2,7 @@
namespace BetterLyrics.WinUI3.ViewModels
{
public class SettingsWindowViewModel : BaseWindowViewModel
public partial class SettingsWindowViewModel(ISettingsService settingsService) : BaseWindowViewModel(settingsService)
{
public SettingsWindowViewModel(ISettingsService settingsService) : base(settingsService) { }
}
}

View File

@@ -5,19 +5,18 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SystemTrayViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<bool>>
public partial class SystemTrayViewModel(ISettingsService settingsService) : BaseViewModel(settingsService), IRecipient<PropertyChangedMessage<bool>>
{
public SystemTrayViewModel(ISettingsService settingsService) : base(settingsService) { }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLyricsWindowLocked { get; set; } = false;
[ObservableProperty]
public partial string ToolTipText { get; set; } = AppInfo.AppName;
public partial string ToolTipText { get; set; } = MetadataHelper.AppName;
public void Receive(PropertyChangedMessage<bool> message)
{
@@ -34,23 +33,29 @@ namespace BetterLyrics.WinUI3.ViewModels
}
[RelayCommand]
private void ExitApp()
private static void ExitApp()
{
WindowHelper.ExitAllWindows();
LyricsWindow? lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
DockModeHelper.Disable(lyricsWindow);
}
Application.Current.Exit();
}
[RelayCommand]
private void OpenSettings()
private static void OpenSettings()
{
// 打开设置窗口
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
[RelayCommand]
private void UnlockWindow()
{
var window = WindowHelper.GetWindowByWindowType<LyricsWindow>();
DesktopModeHelper.Unlock(window);
if (window == null) return;
DesktopModeHelper.SetClickThrough(window, false);
IsLyricsWindowLocked = false;
}
}

View File

@@ -18,334 +18,379 @@
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<!-- Lyrics area -->
<Grid x:Name="LyricsGrid">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<renderer:LyricsRenderer />
</Grid>
<Grid Margin="36,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="36" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<!-- Lyrics placeholder -->
<Grid
x:Name="LyricsPlaceholderGrid"
Opacity=".5"
SizeChanged="LyricsPlaceholderGrid_SizeChanged">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoInnerGrid" Margin="0,36">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<!-- Cover area -->
<RowDefinition Height="9*" />
<!-- Spacer -->
<RowDefinition Height="*" />
<!-- Title and artist area -->
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<!-- Cover area -->
<Grid
x:Name="CoverArea"
Grid.Row="1"
SizeChanged="CoverArea_SizeChanged">
<Grid x:Name="CoverImageGrid" SizeChanged="CoverImageGrid_SizeChanged">
<Grid CornerRadius="{x:Bind ViewModel.CoverImageGridCornerRadius, Mode=OneWay}">
<Image
x:Name="CoverImage"
Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}"
Stretch="Uniform">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
<ui:Effects.Shadow>
<media:AttachedCardShadow
BlurRadius="32"
CornerRadius="{x:Bind ViewModel.CoverImageGridCornerRadius, Mode=OneWay, Converter={StaticResource CornerRadiusToDoubleConverter}}"
InnerContentClipMode="CompositionMaskBrush"
Opacity="0.1" />
</ui:Effects.Shadow>
</Grid>
</Grid>
<!-- Title and artist -->
<StackPanel
Grid.Row="3"
VerticalAlignment="Top"
Orientation="Vertical">
<!-- Song title -->
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
x:Name="TitleTextBlock"
Behavior="Bouncing"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="Bold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
<!-- Song artist -->
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<labs:MarqueeText
Behavior="Bouncing"
FontSize="{StaticResource SubtitleTextBlockFontSize}"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Opacity="0.5"
Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</controls:OpacityMaskView>
</StackPanel>
</Grid>
</Grid>
<renderer:LyricsRenderer />
<!-- No music playing placeholder -->
<TextBlock
x:Name="MainPageNoMusicPlayingTextBlock"
x:Uid="MainPageNoMusicPlaying"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}">
<TextBlock.OpacityTransition>
<Grid x:Name="NoMusicPlayingGrid" Background="{ThemeResource AcrylicBackgroundFillColorBaseBrush}">
<TextBlock
x:Uid="MainPageNoMusicPlaying"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{x:Bind ViewModel.LyricsFontFamily, Mode=OneWay}"
FontSize="{x:Bind ViewModel.LyricsFontSize, Mode=OneWay}" />
<Grid.OpacityTransition>
<ScalarTransition />
</TextBlock.OpacityTransition>
</TextBlock>
</Grid.OpacityTransition>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SongInfo, Mode=OneWay}"
ComparisonCondition="Equal"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SongInfo, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="{x:Null}">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Grid>
<!-- Bottom-right command area -->
<!-- Bottom command area -->
<Grid
x:Name="BottomCommandGrid"
Margin="0,0,4,4"
HorizontalAlignment="Right"
Margin="12"
VerticalAlignment="Bottom"
Background="Transparent"
Opacity="0"
Opacity="{x:Bind ViewModel.BottomCommandGridOpacity, Mode=OneWay}"
PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited"
Visibility="{x:Bind ViewModel.IsNotDockMode, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
PointerExited="BottomCommandGrid_PointerExited">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Grid x:Name="BottomCommandContent">
<Grid Padding="3" HorizontalAlignment="Left">
<StackPanel
x:Name="BottomLeftCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<Button Style="{StaticResource GhostButtonStyle}" Visibility="Collapsed">
<Grid>
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontWeight="Bold"
Glyph="&#xF83E;" />
<TextBlock
HorizontalAlignment="Center"
<StackPanel
Margin="0,0,0,2"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Bold"
Text="0.1" />
</Grid>
</Button>
Orientation="Horizontal"
Spacing="2">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding ElementName=TimelineSlider, Path=Value, Converter={StaticResource SecondsToFormattedTimeConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{Binding ElementName=TimelineSlider, Path=Maximum, Converter={StaticResource SecondsToFormattedTimeConverter}}" />
</StackPanel>
<Button Style="{StaticResource GhostButtonStyle}" Visibility="Collapsed">
<Grid>
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontWeight="Bold"
Glyph="&#xF83E;"
RenderTransformOrigin="0.5,0.5">
<FontIcon.RenderTransform>
<ScaleTransform ScaleX="-1" />
</FontIcon.RenderTransform>
</FontIcon>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="11"
FontWeight="Bold"
Text="0.1" />
</Grid>
</Button>
<!-- Position offset -->
<Button Click="TimelineOffsetButton_Click" Style="{StaticResource GhostButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xECE7;"
RenderTransformOrigin="0.5,0.5">
<FontIcon.RenderTransform>
<RotateTransform Angle="90" CenterX="0.5" CenterY="0.5" />
</FontIcon.RenderTransform>
</FontIcon>
<ToolTipService.ToolTip>
<ToolTip x:Name="TimelineOffsetToolTip" x:Uid="LyricsPageTimelineOffsetButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="TimelineOffsetFlyout" ShouldConstrainToRootBounds="False">
<StackPanel>
<Slider
x:Uid="MainPagePositionOffsetSlider"
Maximum="5000"
Minimum="-5000"
SnapsTo="Ticks"
StepFrequency="100"
TickFrequency="100"
TickPlacement="Outside"
Value="{x:Bind ViewModel.PositionOffset, Mode=TwoWay}" />
<RelativePanel>
<TextBlock
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Text="{x:Bind ViewModel.PositionOffset, Mode=OneWay}" />
<Button
Click="PositionOffsetResetButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE777;}"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource GhostButtonStyle}" />
</RelativePanel>
<CheckBox IsChecked="{x:Bind ViewModel.ResetPositionOffsetOnSongChanged, Mode=TwoWay}">
<TextBlock x:Uid="LyricsPagePositionOffsetHint" />
</CheckBox>
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>
<Button
x:Name="DisplayTypeSwitchButton"
x:Uid="MainPageDisplayTypeSwitcher"
Content="{ui:FontIcon FontWeight=Bold,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xF246;}"
Style="{StaticResource GhostButtonStyle}">
<Button.OpacityTransition>
<ScalarTransition />
</Button.OpacityTransition>
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="12,2,12,8" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</Flyout.FlyoutPresenterStyle>
<RadioButtons MaxColumns="1" SelectedIndex="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}">
<RadioButton x:Uid="MainPageAlbumArtOnly" Click="AlbumArtOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageLyriscOnly" Click="LyricsOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageSplitView" Click="SplitViewRadioButton_Click" />
</RadioButtons>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
<Button
x:Name="MusicInfoButton"
Content="{ui:FontIcon FontWeight=Bold,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE946;}"
Style="{StaticResource GhostButtonStyle}">
<Button.Flyout>
<Flyout>
<StackPanel Spacing="16">
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xED35;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.SourceAppUserModelId, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xEC4F;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Title, Mode=OneWay}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE77B;" />
<TextBlock Text="{x:Bind ViewModel.SongInfo.Artist, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Grid Padding="3" HorizontalAlignment="Center">
<StackPanel
x:Name="BottomCenterCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE622;}"
Style="{StaticResource GhostButtonStyle}" />
<Button
Command="{x:Bind ViewModel.PauseSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF8AE;}"
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button
Command="{x:Bind ViewModel.PlaySongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF5B0;}"
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSongPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<Button
Command="{x:Bind ViewModel.NextSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
</Grid>
<Button
x:Name="SettingsButton"
Command="{x:Bind ViewModel.OpenSettingsWindowCommand}"
Content="{ui:FontIcon FontWeight=Bold,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE713;}"
Style="{StaticResource GhostButtonStyle}" />
<Grid Padding="3" HorizontalAlignment="Right">
<StackPanel
x:Name="BottomRightCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
</StackPanel>
<!-- Volume -->
<!--<Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}">
<Grid>
-->
<!-- Volumn: 0 -->
<!--
<FontIcon
x:Name="VolumeLevel0"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE74F;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 1-32 -->
<!--
<FontIcon
x:Name="VolumeLevel1"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE993;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 33-65 -->
<!--
<FontIcon
x:Name="VolumeLevel2"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE994;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
-->
<!-- Volumn: 66-100 -->
<!--
<FontIcon
x:Name="VolumeLevel3"
FontFamily="{StaticResource IconFontFamily}"
Glyph="&#xE995;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
<Button.DataContext>
<Flyout x:Name="VolumeFlyout" ShouldConstrainToRootBounds="False">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.Volume, Mode=OneWay}" />
<TextBlock Margin="0,0,14,0" VerticalAlignment="Center" />
<Slider
Width="150"
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="None"
Value="{x:Bind ViewModel.Volume, Mode=TwoWay}" />
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>-->
<!-- Translation -->
<Button
Click="TranslationButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8BD;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="TranslationToolTip" x:Uid="LyricsPageTranslationButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="TranslationFlyout" ShouldConstrainToRootBounds="False">
<StackPanel>
<ToggleSwitch x:Uid="LyricsPageTranslationEnabled" IsOn="{x:Bind ViewModel.IsTranslationEnabled, Mode=TwoWay}" />
<ToggleSwitch
x:Uid="LyricsPageTranslationOnly"
IsEnabled="{x:Bind ViewModel.IsTranslationEnabled, Mode=OneWay}"
IsOn="{x:Bind ViewModel.ShowTranslationOnly, Mode=TwoWay}" />
</StackPanel>
</Flyout>
</Button.DataContext>
</Button>
<!-- Display type -->
<Button
x:Name="DisplayTypeSwitchButton"
x:Uid="MainPageDisplayTypeSwitcher"
Click="DisplayTypeSwitchButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF246;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="PresentationTypeToolTip" x:Uid="LyricsPageDisplayTypeButtonToolTip" />
</ToolTipService.ToolTip>
<Button.DataContext>
<Flyout x:Name="DisplayTypeSwitchFlyout" ShouldConstrainToRootBounds="false">
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="12,2,12,8" />
<Setter Property="CornerRadius" Value="8" />
</Style>
</Flyout.FlyoutPresenterStyle>
<RadioButtons MaxColumns="1" SelectedIndex="{x:Bind ViewModel.DisplayType, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}">
<RadioButton x:Uid="MainPageAlbumArtOnly" Click="AlbumArtOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageLyriscOnly" Click="LyricsOnlyRadioButton_Click" />
<RadioButton x:Uid="MainPageSplitView" Click="SplitViewRadioButton_Click" />
</RadioButtons>
</Flyout>
</Button.DataContext>
</Button>
<!-- Settings -->
<Button
x:Name="SettingsButton"
Command="{x:Bind ViewModel.OpenSettingsWindowCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xF8B0;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip x:Name="SettingsToolTip" x:Uid="LyricsPageSettingsButtonToolTip" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
<Slider
x:Name="TimelineSlider"
Margin="0,-32,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.SongDurationSeconds, Mode=OneWay}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.TimelinePositionSeconds, Mode=OneWay}" />
<Slider
x:Name="TimelineSliderOverlay"
Margin="0,-32,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{Binding ElementName=TimelineSlider, Path=Maximum}"
Minimum="0"
Style="{StaticResource TransparentSliderStyle}"
Tapped="TimelineSliderOverlay_Tapped"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" />
</Grid>
</Grid>
<!-- Bottom command flyout trigger -->
<Grid
x:Name="BottomCommandFlyoutTrigger"
Height="12"
VerticalAlignment="Bottom"
Background="Transparent"
CornerRadius="3,3,0,0"
Opacity="{x:Bind ViewModel.BottomCommandFlyoutTriggerOpacity, Mode=OneWay}"
PointerEntered="BottomCommandFlyoutTrigger_PointerEntered"
PointerExited="BottomCommandFlyoutTrigger_PointerExited"
Tapped="BottomCommandFlyoutTrigger_Tapped">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<Grid
x:Name="BottomCommandFlyoutTriggerHint"
Width="150"
Margin="4"
Background="{ThemeResource TextFillColorPrimaryBrush}"
CornerRadius="2"
Translation="0,0,0">
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
</Grid>
<Grid.ContextFlyout>
<Flyout x:Name="BottomCommandFlyout" ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="600" />
<Setter Property="MinHeight" Value="100" />
<Setter Property="CornerRadius" Value="12" />
</Style>
</Flyout.FlyoutPresenterStyle>
<Grid x:Name="BottomCommandFlyoutContainer" VerticalAlignment="Bottom" />
</Flyout>
</Grid.ContextFlyout>
</Grid>
<TeachingTip
@@ -353,34 +398,69 @@
x:Uid="MainPageWelcomeTeachingTip"
Closed="WelcomeTeachingTip_Closed"
IsOpen="{x:Bind ViewModel.IsWelcomeTeachingTipOpen, Mode=OneWay}"
Target="{x:Bind SettingsButton}" />
Target="{x:Bind RootGrid}" />
<uc:SystemTray />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="MusicPlayingStates">
<VisualState x:Name="MusicPlaying">
<VisualState.StateTriggers>
<ui:IsNotEqualStateTrigger Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}" To="3" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DisplayTypeSwitchButton.Visibility" Value="Visible" />
<Setter Target="MusicInfoButton.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NoMusicPlaying">
<!--<VisualStateGroup x:Name="VolumeState">
<VisualState x:Name="Volume0">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.DisplayType, Mode=OneWay}"
To="3" />
Value="{x:Bind ViewModel.Volume, Mode=OneWay}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DisplayTypeSwitchButton.Visibility" Value="Collapsed" />
<Setter Target="MusicInfoButton.Visibility" Value="Collapsed" />
<Setter Target="VolumeLevel0.Opacity" Value="1" />
<Setter Target="VolumeLevel1.Opacity" Value="0" />
<Setter Target="VolumeLevel2.Opacity" Value="0" />
<Setter Target="VolumeLevel3.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualState x:Name="Volume1">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="LessThanOrEqual"
Value="{x:Bind ViewModel.Volume, Mode=OneWay}"
To="32" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="VolumeLevel0.Opacity" Value="0" />
<Setter Target="VolumeLevel1.Opacity" Value="1" />
<Setter Target="VolumeLevel2.Opacity" Value="0" />
<Setter Target="VolumeLevel3.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Volume2">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="LessThanOrEqual"
Value="{x:Bind ViewModel.Volume, Mode=OneWay}"
To="65" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="VolumeLevel0.Opacity" Value="0" />
<Setter Target="VolumeLevel1.Opacity" Value="0" />
<Setter Target="VolumeLevel2.Opacity" Value="1" />
<Setter Target="VolumeLevel3.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Volume3">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="LessThanOrEqual"
Value="{x:Bind ViewModel.Volume, Mode=OneWay}"
To="100" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="VolumeLevel0.Opacity" Value="0" />
<Setter Target="VolumeLevel1.Opacity" Value="0" />
<Setter Target="VolumeLevel2.Opacity" Value="0" />
<Setter Target="VolumeLevel3.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>-->
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

View File

@@ -1,94 +1,30 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
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 System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using WinUIEx.Messaging;
namespace BetterLyrics.WinUI3.Views
{
public sealed partial class LyricsPage : Page
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IPlaybackService _playbackService = Ioc.Default.GetRequiredService<IPlaybackService>();
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
public LyricsPage()
{
this.InitializeComponent();
DataContext = Ioc.Default.GetService<LyricsPageViewModel>();
WeakReferenceMessenger.Default.Register<PropertyChangedMessage<LyricsDisplayType>>(
this,
async (r, m) =>
{
if (m.Sender is LyricsPageViewModel)
{
if (m.PropertyName == nameof(LyricsPageViewModel.DisplayType))
{
switch (m.NewValue)
{
case LyricsDisplayType.AlbumArtOnly:
await SwitchToAlbumArtOnlyDisplayTypeAsync();
break;
case LyricsDisplayType.LyricsOnly:
await SwitchToLyricsOnlyDisplayTypeAsync();
break;
case LyricsDisplayType.SplitView:
await SwitchToSplitViewDisplayTypeAsync();
break;
case LyricsDisplayType.PlaceholderOnly:
await SwitchToPlaceholderOnlyDisplayTypeAsync();
break;
default:
break;
}
}
}
}
);
}
public LyricsPageViewModel ViewModel => (LyricsPageViewModel)DataContext;
private void BottomCommandGrid_PointerEntered(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (BottomCommandGrid.Opacity == 0)
BottomCommandGrid.Opacity = .5;
}
private void BottomCommandGrid_PointerExited(
object sender,
Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e
)
{
if (BottomCommandGrid.Opacity == .5)
BottomCommandGrid.Opacity = 0;
}
private void CoverArea_SizeChanged(object sender, SizeChangedEventArgs e)
{
CoverImageGrid.Width = CoverImageGrid.Height = Math.Min(
CoverArea.ActualWidth,
CoverArea.ActualHeight
);
}
private void CoverImageGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.CoverImageGridActualHeight = e.NewSize.Height;
}
private void LyricsPlaceholderGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.MaxLyricsWidth = e.NewSize.Width;
DataContext = Ioc.Default.GetRequiredService<LyricsPageViewModel>();
}
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args)
@@ -96,78 +32,117 @@ namespace BetterLyrics.WinUI3.Views
ViewModel.IsFirstRun = false;
}
private async void LyricsOnlyRadioButton_Click(object sender, RoutedEventArgs e)
private void LyricsOnlyRadioButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.PreferredDisplayType = ViewModel.DisplayType = LyricsDisplayType.LyricsOnly;
await SwitchToLyricsOnlyDisplayTypeAsync();
ViewModel.DisplayType = LyricsDisplayType.LyricsOnly;
_settingsService.DisplayType = ViewModel.DisplayType;
}
private async void AlbumArtOnlyRadioButton_Click(object sender, RoutedEventArgs e)
private void AlbumArtOnlyRadioButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.PreferredDisplayType = ViewModel.DisplayType = LyricsDisplayType.AlbumArtOnly;
await SwitchToAlbumArtOnlyDisplayTypeAsync();
ViewModel.DisplayType = LyricsDisplayType.AlbumArtOnly;
_settingsService.DisplayType = ViewModel.DisplayType;
}
private async void SplitViewRadioButton_Click(object sender, RoutedEventArgs e)
private void SplitViewRadioButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.PreferredDisplayType = ViewModel.DisplayType = LyricsDisplayType.SplitView;
await SwitchToSplitViewDisplayTypeAsync();
ViewModel.DisplayType = LyricsDisplayType.SplitView;
_settingsService.DisplayType = ViewModel.DisplayType;
}
private async Task SwitchToLyricsOnlyDisplayTypeAsync()
private void PositionOffsetResetButton_Click(object sender, RoutedEventArgs e)
{
await BeforeSwitchDisplayTypeAsync();
Grid.SetColumn(LyricsPlaceholderGrid, 0);
Grid.SetColumnSpan(LyricsPlaceholderGrid, 3);
LyricsPlaceholderGrid.Opacity = 1;
LyricsGrid.Opacity = 1;
ViewModel.PositionOffset = 0;
}
private async Task SwitchToAlbumArtOnlyDisplayTypeAsync()
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
await BeforeSwitchDisplayTypeAsync();
Grid.SetColumn(SongInfoInnerGrid, 0);
Grid.SetColumnSpan(SongInfoInnerGrid, 3);
SongInfoInnerGrid.Opacity = 1;
LyricsGrid.Opacity = 1;
if (ViewModel.IsImmersiveMode && BottomCommandGrid.Children.Count != 0)
{
ViewModel.BottomCommandGridOpacity = 1f;
}
e.Handled = true;
}
private async Task BeforeSwitchDisplayTypeAsync()
private void BottomCommandGrid_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
SongInfoInnerGrid.Opacity = 0;
LyricsPlaceholderGrid.Opacity = 0;
//LyricsGrid.Opacity = 0;
MainPageNoMusicPlayingTextBlock.Opacity = 0;
await Task.Delay(300);
if (ViewModel.IsImmersiveMode && BottomCommandGrid.Children.Count != 0)
{
ViewModel.BottomCommandGridOpacity = 0f;
}
e.Handled = true;
}
private async Task SwitchToSplitViewDisplayTypeAsync()
private void DisplayTypeSwitchButton_Click(object sender, RoutedEventArgs e)
{
await BeforeSwitchDisplayTypeAsync();
Grid.SetColumn(SongInfoInnerGrid, 0);
Grid.SetColumnSpan(SongInfoInnerGrid, 1);
Grid.SetColumn(LyricsPlaceholderGrid, 2);
Grid.SetColumnSpan(LyricsPlaceholderGrid, 1);
SongInfoInnerGrid.Opacity = 1;
LyricsPlaceholderGrid.Opacity = 1;
LyricsGrid.Opacity = 1;
DisplayTypeSwitchFlyout.ShowAt(BottomRightCommandStackPanel);
}
private async Task SwitchToPlaceholderOnlyDisplayTypeAsync()
private void TimelineOffsetButton_Click(object sender, RoutedEventArgs e)
{
await BeforeSwitchDisplayTypeAsync();
TimelineOffsetFlyout.ShowAt(BottomLeftCommandStackPanel);
}
MainPageNoMusicPlayingTextBlock.Opacity = 1;
private void TranslationButton_Click(object sender, RoutedEventArgs e)
{
TranslationFlyout.ShowAt(BottomRightCommandStackPanel);
}
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize.Width < 500 || e.NewSize.Height < 100)
{
if (BottomCommandGrid.Children.Count != 0)
{
BottomCommandGrid.Children.Remove(BottomCommandContent);
BottomCommandFlyoutContainer.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 0, 0);
}
else
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.Hide();
BottomCommandFlyoutContainer.Children.Remove(BottomCommandContent);
BottomCommandGrid.Children.Add(BottomCommandContent);
}
BottomCommandFlyoutTriggerHint.Translation = new Vector3(0, 12, 0);
}
}
//private void VolumeButton_Click(object sender, RoutedEventArgs e)
//{
// VolumeFlyout.ShowAt(BottomRightCommandStackPanel);
//}
private void BottomCommandFlyoutTrigger_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (ViewModel.IsImmersiveMode && BottomCommandFlyoutContainer.Children.Count != 0)
{
ViewModel.BottomCommandFlyoutTriggerOpacity = 1f;
}
}
private void BottomCommandFlyoutTrigger_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (ViewModel.IsImmersiveMode && BottomCommandFlyoutContainer.Children.Count != 0)
{
ViewModel.BottomCommandFlyoutTriggerOpacity = 0f;
}
}
private void BottomCommandFlyoutTrigger_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
if (BottomCommandFlyoutContainer.Children.Count != 0)
{
BottomCommandFlyout.ShowAt(BottomCommandFlyoutTrigger);
}
}
private void TimelineSliderOverlay_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
_playbackService.ChangePosition(TimelineSliderOverlay.Value);
}
}
}

View File

@@ -9,200 +9,199 @@
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:scontrols="using:ShadowViewer.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid
x:Name="RootGrid"
PointerMoved="RootGrid_PointerMoved"
RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}">
PointerEntered="RootGrid_PointerEntered"
PointerExited="RootGrid_PointerExited"
RequestedTheme="{x:Bind ViewModel.ThemeType, Mode=OneWay}"
SizeChanged="RootGrid_SizeChanged">
<local:LyricsPage />
<!-- Top command -->
<Grid
x:Name="TopCommandGrid"
Height="{x:Bind ViewModel.TitleBarHeight, Mode=OneWay}"
VerticalAlignment="Top"
Background="Transparent"
Opacity="0">
Opacity="{x:Bind ViewModel.TopCommandGridOpacity, Mode=OneWay}"
PointerEntered="TopCommandGrid_PointerEntered"
PointerExited="TopCommandGrid_PointerExited">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Grid Padding="3" HorizontalAlignment="Left">
<StackPanel
x:Name="TopLeftCommandStackPanel"
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="3">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<Button
x:Name="ClickThroughButton"
Command="{x:Bind ViewModel.LockWindowCommand}"
Style="{StaticResource TitleBarButtonStyle}">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- Music gallery -->
<Button
Click="MusicGalleryButton_Click"
Style="{StaticResource TitleBarButtonStyle}"
Visibility="Collapsed">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize}"
Glyph="&#xE8A9;" />
</Button>
<!-- Immersive mode -->
<ToggleButton
x:Name="ImmersiveButton"
Command="{x:Bind ViewModel.ImmersiveToggleButtonEnabledChangedCommand}"
IsChecked="{x:Bind ViewModel.IsImmersiveMode, Mode=TwoWay}"
Style="{StaticResource TitleBarToggleButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize}"
Glyph="&#xED1A;" />
</ToggleButton>
</StackPanel>
</Grid>
<Grid Padding="3" HorizontalAlignment="Right">
<StackPanel
x:Name="TopRightCommandStackPanel"
Orientation="Horizontal"
Spacing="3">
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<!-- Look -->
<Button
x:Name="ClickThroughButton"
Command="{x:Bind ViewModel.ToggleLockWindowCommand}"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="Bold"
Glyph="&#xE72E;" />
</Grid>
<ToolTipService.ToolTip>
<ToolTip x:Name="LockToolTip" x:Uid="HostWindowLockToolTip" />
</ToolTipService.ToolTip>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ChangePropertyAction
PropertyName="IsOpen"
TargetObject="{x:Bind LockToolTip}"
Value="True" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ChangePropertyAction
PropertyName="IsOpen"
TargetObject="{x:Bind LockToolTip}"
Value="False" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
<ToolTipService.ToolTip>
<ToolTip x:Name="LockToolTip">
<ToolTip.Content>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="HostWindowLockToolTip" />
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
Opacity="0.7"
Text="Ctrl + Alt + " />
<TextBlock
VerticalAlignment="Center"
Opacity="0.7"
Text="{x:Bind ViewModel.LockHotKey, Mode=OneWay}" />
</StackPanel>
</ToolTip.Content>
</ToolTip>
</ToolTipService.ToolTip>
</Button>
<!-- More -->
<Button x:Name="MoreButton" Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE700;" />
<ToolTipService.ToolTip>
<ToolTip x:Name="MoreButtonToolTip" x:Uid="HostWindowMoreButtonToolTip" />
</ToolTipService.ToolTip>
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem
x:Name="AOTFlyoutItem"
x:Uid="BaseWindowAOTFlyoutItem"
Click="AOTFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="FullScreenFlyoutItem"
x:Uid="BaseWindowFullScreenFlyoutItem"
Click="FullScreenFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="DockFlyoutItem"
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="DesktopFlyoutItem"
x:Uid="HostWindowDesktopFlyoutItem"
Command="{x:Bind ViewModel.ToggleDesktopModeCommand}"
IsChecked="{x:Bind ViewModel.IsDesktopMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="MiniFlyoutItem"
x:Uid="BaseWindowMiniFlyoutItem"
Click="MiniFlyoutItem_Click" />
<MenuFlyoutItem
x:Name="SettingsFlyoutItem"
x:Uid="HostWindowSettingsFlyoutItem"
Click="SettingsMenuFlyoutItem_Click" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button x:Name="MoreButton" Style="{StaticResource TitleBarButtonStyle}">
<Grid>
<FontIcon
Margin="0,0,0,8"
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xEF2D;" />
<!-- Window Minimise -->
<Button
x:Name="MinimiseButton"
Click="MinimiseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xEF2D;" />
Glyph="&#xE654;" />
</Button>
<!-- Window Maximise -->
<Button
x:Name="MaximiseButton"
Click="MaximiseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
Margin="0,8,0,0"
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
FontWeight="ExtraBold"
Glyph="&#xEF2D;" />
</Grid>
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem
x:Name="AOTFlyoutItem"
x:Uid="BaseWindowAOTFlyoutItem"
Click="AOTFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="FullScreenFlyoutItem"
x:Uid="BaseWindowFullScreenFlyoutItem"
Click="FullScreenFlyoutItem_Click" />
<ToggleMenuFlyoutItem
x:Name="DockFlyoutItem"
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="DesktopFlyoutItem"
x:Uid="HostWindowDesktopFlyoutItem"
Command="{x:Bind ViewModel.ToggleDesktopModeCommand}"
IsChecked="{x:Bind ViewModel.IsDesktopMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="MiniFlyoutItem"
x:Uid="BaseWindowMiniFlyoutItem"
Click="MiniFlyoutItem_Click" />
<MenuFlyoutItem
x:Name="SettingsFlyoutItem"
x:Uid="HostWindowSettingsFlyoutItem"
Click="SettingsMenuFlyoutItem_Click" />
</MenuFlyout>
</Button.Flyout>
</Button>
<!-- Window Minimise -->
<Button
x:Name="MinimiseButton"
Click="MinimiseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2D;" />
</Button>
<!-- Window Maximise -->
<Button
x:Name="MaximiseButton"
Click="MaximiseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2E;" />
</Button>
<!-- Window Restore -->
<Button
x:Name="RestoreButton"
Click="RestoreButton_Click"
Style="{StaticResource TitleBarButtonStyle}"
Visibility="Collapsed">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2F;" />
</Button>
<!-- Window Close -->
<Button
x:Name="CloseButton"
Click="CloseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xEF2C;" />
</Button>
</StackPanel>
Glyph="&#xE655;" />
</Button>
<!-- Window Restore -->
<Button
x:Name="RestoreButton"
Click="RestoreButton_Click"
Style="{StaticResource TitleBarButtonStyle}"
Visibility="Collapsed">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE656;" />
</Button>
<!-- Window Close -->
<Button
x:Name="CloseButton"
Click="CloseButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize, Mode=OneWay}"
Glyph="&#xE653;" />
</Button>
</StackPanel>
</Grid>
</Grid>
<InfoBar
x:Name="HostInfoBar"
Margin="36"
<scontrols:NotificationPanel
x:Name="TipContainerCenter"
Margin="0,0,0,52"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Background="{ThemeResource SystemFillColorSolidAttentionBackgroundBrush}"
IsClosable="False"
IsOpen="{x:Bind ViewModel.ShowInfoBar, Mode=OneWay}"
Message="{x:Bind ViewModel.Notification.Message, Mode=OneWay}"
Opacity="0"
Severity="{x:Bind ViewModel.Notification.Severity, Mode=OneWay}">
<InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform>
<InfoBar.Resources>
<Storyboard x:Key="InfoBarShowAndHideStoryboard">
<!-- Opacity -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBar" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="0" />
</DoubleAnimationUsingKeyFrames>
<!-- Y -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBarTransform" Storyboard.TargetProperty="Y">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="20" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="20" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</InfoBar.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource InfoBarShowAndHideStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
Canvas.ZIndex="1"
FlowDirection="RightToLeft"
Loaded="TipContainerCenter_Loaded"
Orientation="Vertical"
Visibility="Collapsed" />
</Grid>
</Window>

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