Compare commits

...

83 Commits

Author SHA1 Message Date
Zhe Fang
6dbf6297dd Merge pull request #72 from jayfunc/dev
v1.0.32.0
2025-07-30 16:59:00 -04:00
Zhe Fang
537584e557 更新版本并优化窗口管理功能
在 `Package.appxmanifest` 中将版本号更新为 `1.0.32.0`。
在 `App.xaml.cs` 中优化 `OnLaunched` 方法,增加对 `lyricsWindow` 的空值检查。
在 `SystemTray.xaml` 中添加双击和左击命令以打开歌词窗口。
在 `SystemTrayViewModel.cs` 中新增 `OpenLyricsWindow` 方法,支持通过命令机制打开歌词窗口。
2025-07-30 16:50:41 -04:00
Zhe Fang
e06a50d8e8 Update releases-to-discord.yml 2025-07-28 19:39:13 -04:00
Zhe Fang
01776ed9a8 Update release-to-telegram.yml 2025-07-28 19:38:47 -04:00
Zhe Fang
1b7f53c055 Create release-to-telegram.yml 2025-07-28 19:34:59 -04:00
Zhe Fang
3233d35a05 Delete .github/workflows/telegram-notification.yml 2025-07-28 19:16:55 -04:00
Zhe Fang
ee52dafa0b Create telegram-notification.yml 2025-07-28 19:11:23 -04:00
Zhe Fang
03ddf42152 fix #30 2025-07-28 18:02:26 -04:00
Zhe Fang
bb4ee6bae7 Enhance image resizing logic to return original bytes if no scaling is needed 2025-07-28 16:27:36 -04:00
Zhe Fang
cb5a9f8042 fix #68 2025-07-28 15:55:18 -04:00
Zhe Fang
1d5d3bfa72 Update README files and enhance SettingsPageViewModel
- Improved the Chinese and English README files for clarity and consistency, including updates to feature descriptions and links.
- Added detailed support information for multiple music players in the README.
- Implemented the SettingsPageViewModel with properties and methods for managing application settings, including language, display options, and playback settings.
- Introduced partial methods for handling property changes in SettingsPageViewModel to ensure settings are saved and applied correctly.
- Enhanced the observable properties for better data binding and UI updates.
2025-07-28 12:10:02 -04:00
Zhe Fang
d6f6a5761f Merge pull request #70 from jayfunc/dev
v1.0.31.0
2025-07-28 09:28:06 -04:00
Zhe Fang
06379ebaba fix #37 2025-07-28 09:18:38 -04:00
Zhe Fang
0e84826a4e Merge pull request #66 from jayfunc/dev
v1.0.30.0
2025-07-27 23:28:12 -04:00
Zhe Fang
45eb2a1506 Enhance application functionality to support single instance and multiple languages
Added single instance support in `App.xaml.cs` to ensure that only one instance of the application can run, and introduced `Mutex` and `EnsureSingleInstance()` methods. Updated multi-language support and added new string resources to serve users of different languages.

Refactored the play queue processing logic, using `PlayQueueItem` instead of `Track`, and introduced a new song tag information class in `SongsTabInfo.cs`. Updated UI components, replaced old image resources, and improved user experience.

In addition, removed the unused `BuildDate` property and simplified the logic of the settings page. Together, these changes improve the stability, usability, and maintainability of the application.
2025-07-27 23:27:04 -04:00
Zhe Fang
0e96c35d2d fix #62 2025-07-27 14:15:40 -04:00
Zhe Fang
152ecc7ff0 Update version and enhance play queue function
- Update the version number in `Package.appxmanifest` to `1.0.30.0`.
- Restore the event handler registration of unhandled exception in `App.xaml.cs`.
- Add `InsertRange` method in `CollectionHelper.cs`.
- Update the text entry in `Resources.resw` and change "play list" to "play queue".
- Add `SelectedTracks` property in `MusicGalleryViewModel.cs` to support multi-selection function.
- Update the data template of `MusicGalleryPage.xaml`, disable some buttons and add new buttons.
- Refactor the event handler in `MusicGalleryPage.xaml.cs` to support play queue logic and select all functions.
- Adjust the maximum value and step size of the slider in `SettingsPage.xaml`.
- Add `FindChildIndex` method in `ListViewHelper.cs`.
2025-07-26 23:04:49 -04:00
Zhe Fang
8b38aa1e33 fix #63 2025-07-26 07:51:27 -04:00
Zhe Fang
79e6995384 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-07-25 16:12:04 -04:00
Zhe Fang
1d6e19a718 Update localization resources and improve Music Gallery functionality
- Added new strings for file information and sorting options in Japanese, Korean, Simplified Chinese, and Traditional Chinese resource files.
- Removed unused property `IsMusicGalleryPageExpanded` from `LyricsWindowViewModel`.
- Enhanced `PlayTrack` method in `MusicGalleryViewModel` to handle null tracks more gracefully.
- Refactored `LyricsWindow` to remove overlay frame and simplify the UI.
- Updated `MusicGalleryPage` to bind new localized strings and improve layout.
- Implemented closing event for `MusicGalleryWindow` to manage resources effectively.
- Revised README files to update feedback group links and improve formatting.
2025-07-25 16:12:03 -04:00
Zhe Fang
23cb4b11a9 Update Crowdin configuration file 2025-07-25 11:30:32 -04:00
Zhe Fang
11461a615b Translate resource files and optimize XML schema
This code change mainly involves translating Chinese text in resource files into English to improve the internationalization support of the application. Specific changes include:
- Translate Chinese prompts, titles, and descriptions in the interface into English.
- Made minor adjustments to the XML schema, mainly adding spaces to comply with XML format standards.
- Added some new prompts for translation failures to ensure that users can get clear feedback when the translation service is not set up or the request fails.
2025-07-25 11:21:49 -04:00
Zhe Fang
e8f0359fb2 fix 2025-07-25 11:09:01 -04:00
Zhe Fang
eba81e8be3 fix 2025-07-25 11:07:34 -04:00
Zhe Fang
2e0d437b08 Merge pull request #57 from jayfunc/l10n_dev
New Crowdin updates
2025-07-25 11:06:46 -04:00
Zhe Fang
cbb0b87392 update readme 2025-07-25 09:54:25 -04:00
Zhe Fang
3f63043150 update readme enhance music gallery 2025-07-25 09:51:43 -04:00
Zhe Fang
77f2474562 add another way of noise byte generation 2025-07-24 12:51:46 -04:00
Zhe Fang
8aa5c392df New translations resources.resw (English) 2025-07-24 10:42:51 -04:00
Zhe Fang
a453f4862d New translations resources.resw (Chinese Traditional) 2025-07-24 10:42:49 -04:00
Zhe Fang
0e6702f3c3 New translations resources.resw (Korean) 2025-07-24 10:42:47 -04:00
Zhe Fang
412cdbdaf4 New translations resources.resw (Japanese) 2025-07-24 10:42:46 -04:00
Zhe Fang
02dbbf983c Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-07-24 10:37:48 -04:00
Zhe Fang
8cf9830644 add search function for music gallery 2025-07-24 10:37:46 -04:00
Zhe Fang
b04dfedf7e Merge pull request #56 from ZHider/dev
新增背景亚克力效果粗糙度调节功能,但未实装。
2025-07-24 10:36:57 -04:00
Ivand
3d4e3209e6 新增背景亚克力效果粗糙度调节功能,但未实装。
涉及文件修改说明:
- 接口层:ISettingsService.cs新增CoverAcrylicEffectAmount属性
- 服务层:SettingsService.cs实现亚克力效果参数存储
- 视图模型:LyricsRendererViewModel系列文件新增噪点贴图处理逻辑
- 设置页面:新增CoverAcrylicEffectAmount绑定属性及通知机制
- 字符串资源:新增多语言支持(en-US, ja-JP, ko-KR, zh-CN, zh-TW)
- XAML界面:SettingsPage.xaml新增调节滑块控件

Signed-off-by: Ivand <littlehider@gmail.com>
2025-07-24 10:52:26 +08:00
Zhe Fang
e92130af85 update readme 2025-07-23 14:14:12 -04:00
Zhe Fang
de3d6f1695 fix 2025-07-23 13:54:30 -04:00
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
6ad79180e4 Merge pull request #19 from jayfunc/dev
github actions
2025-07-11 19:49:17 -04:00
Zhe Fang
42af22a7e3 Merge pull request #18 from jayfunc/dev
v1.0.11.0
2025-07-11 18:08:49 -04:00
Zhe Fang
509079e8c7 Merge pull request #10 from jayfunc/dev
1.0.9.0
2025-07-07 17:32:48 -04:00
Zhe Fang
c50c180aa0 Merge pull request #9 from jayfunc/dev
v1.0.7.0 release
2025-06-30 20:56:34 -04:00
Zhe Fang
5e74468194 Merge pull request #8 from jayfunc/dev
add multiple online lyrics providers; add desktop mode; improve blur/shadow/scrolling effect performance; fix bugs
2025-06-26 21:51:36 -04:00
Zhe Fang
a93b535667 Merge pull request #7 from jayfunc/dev
update to v1.0.5.0
2025-06-23 13:41:33 -04:00
Zhe Fang
2c55b11e70 Merge pull request #6 from jayfunc/dev
fix
2025-06-18 17:16:06 -04:00
Zhe Fang
7bca1d1205 Merge pull request #5 from jayfunc/dev
Add dock mode, improve glow effect, fix bugs ...
2025-06-17 21:50:22 -04:00
98 changed files with 5432 additions and 1564 deletions

View File

@@ -0,0 +1,27 @@
name: Notify Telegram on GitHub Release
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send release info to Telegram
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_BODY: ${{ github.event.release.body }}
run: |
TEXT="🚀 *New Release:* ${RELEASE_TAG} - ${RELEASE_NAME}%0A"
TEXT+="📝 *Description:*%0A${RELEASE_BODY}%0A"
TEXT+="🔗 [View on GitHub](${RELEASE_URL})"
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" \
-d text="${TEXT}" \
-d parse_mode=Markdown

View File

@@ -1,3 +1,5 @@
name: Notify Discord on GitHub Release
on:
release:
types: [published]
@@ -13,7 +15,7 @@ jobs:
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "2105893"
username: "Release Changelog"
username: "GitHub"
avatar_url: "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png"
content: "||@everyone||"
footer_title: "Changelog"

View File

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

View File

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

View File

@@ -10,13 +10,17 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.ApplicationModel.Resources;
using Serilog;
using ShadowViewer.Controls;
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3
{
@@ -33,6 +37,8 @@ namespace BetterLyrics.WinUI3
public NotificationPanel? LyricsWindowNotificationPanel { get; set; }
public NotificationPanel? SettingsWindowNotificationPanel { get; set; }
private static Mutex? _instanceMutex;
public App()
{
this.InitializeComponent();
@@ -41,6 +47,8 @@ namespace BetterLyrics.WinUI3
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
EnsureSingleInstance();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
ConfigureServices();
@@ -53,14 +61,28 @@ namespace BetterLyrics.WinUI3
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void EnsureSingleInstance()
{
bool createdNew;
_instanceMutex = new Mutex(true, MetadataHelper.AppName, out createdNew);
if (!createdNew)
{
User32.MessageBox(HWND.NULL, ResourceLoader!.GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
}
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenOrShowWindow<LyricsWindow>();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow == null) return;
WindowHelper.OpenWindow<LyricsWindow>();
lyricsWindow.ViewModel.InitLockHotKey();
lyricsWindow.AutoSelectLyricsMode();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
lyricsWindow.ViewModel.InitLockHotKey();
lyricsWindow.AutoSelectLyricsMode();
}
}
private static void ConfigureServices()
@@ -85,13 +107,13 @@ namespace BetterLyrics.WinUI3
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ITranslateService, TranslateService>()
// Manager
// ViewModels
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsPageViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<MusicGalleryViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.BuildServiceProvider()
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

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

View File

@@ -14,7 +14,9 @@
x:Name="TrayIcon"
x:FieldModifier="public"
ContextMenuMode="SecondWindow"
DoubleClickCommand="{x:Bind ViewModel.OpenLyricsWindowCommand}"
IconSource="ms-appx:///Assets/Logo.ico"
LeftClickCommand="{x:Bind ViewModel.OpenLyricsWindowCommand}"
NoLeftClickDelay="True"
ToolTipText="{x:Bind ViewModel.ToolTipText, Mode=OneWay}">
<tb:TaskbarIcon.ContextFlyout>
@@ -22,6 +24,7 @@
AreOpenCloseAnimationsEnabled="True"
LightDismissOverlayMode="On"
ShowMode="TransientWithDismissOnPointerMoveAway">
<MenuFlyoutItem x:Uid="SystemTrayMusicGallery" Command="{x:Bind ViewModel.OpenMusicGalleryCommand}" />
<MenuFlyoutItem x:Uid="SystemTraySettings" Command="{x:Bind ViewModel.OpenSettingsCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayExit" Command="{x:Bind ViewModel.ExitAppCommand}" />
<MenuFlyoutItem

View File

@@ -8,7 +8,7 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public class AlbumArtSearchProviderToDisplayNameConverter : IValueConverter
public partial class AlbumArtSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -3,7 +3,7 @@ using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
internal partial class CornerRadiusToDoubleConverter : IValueConverter
public partial class CornerRadiusToDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -5,7 +5,7 @@ using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
internal partial class EnumToIntConverter : IValueConverter
public partial class EnumToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -14,19 +14,19 @@ namespace BetterLyrics.WinUI3.Converter
{
return provider switch
{
LyricsSearchProvider.LrcLib => App.ResourceLoader!.GetString("LyricsSearchProviderLrcLib"),
LyricsSearchProvider.QQ => App.ResourceLoader!.GetString("LyricsSearchProviderQQ"),
LyricsSearchProvider.Netease => App.ResourceLoader!.GetString("LyricsSearchProviderNetease"),
LyricsSearchProvider.Kugou => App.ResourceLoader!.GetString("LyricsSearchProviderKugou"),
LyricsSearchProvider.AmllTtmlDb => App.ResourceLoader!.GetString("LyricsSearchProviderAmllTtmlDb"),
LyricsSearchProvider.LrcLib => "LrcLib",
LyricsSearchProvider.QQ => "QQ",
LyricsSearchProvider.Netease => "Netease",
LyricsSearchProvider.Kugou => "Kugou",
LyricsSearchProvider.AmllTtmlDb => "amll-ttml-db",
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalLrcFile"),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalMusicFile"),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderEslrcFile"),
LyricsSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString("LyricsSearchProviderTtmlFile"),
_ => "",
_ => "N/A",
};
}
return "";
return "N/A";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)

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 partial 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,38 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class TranslationSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TranslationSearchProvider provider)
{
return provider switch
{
TranslationSearchProvider.LrcLib => "LrcLib",
TranslationSearchProvider.QQ => "QQ",
TranslationSearchProvider.Netease => "Netease",
TranslationSearchProvider.Kugou => "Kugou",
TranslationSearchProvider.AmllTtmlDb => "amll-ttml-db",
TranslationSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalLrcFile"),
TranslationSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalMusicFile"),
TranslationSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderEslrcFile"),
TranslationSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString("LyricsSearchProviderTtmlFile"),
TranslationSearchProvider.LibreTranslate => "LibreTranslate",
_ => "N/A",
};
}
return "N/A";
}
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 CommonSongProperty
{
Title,
Album,
Artist
}
}

View File

@@ -61,5 +61,22 @@ namespace BetterLyrics.WinUI3.Enums
{
return !provider.IsLocal();
}
public static TranslationSearchProvider? ToTranslationSearchProvider(this LyricsSearchProvider? provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => TranslationSearchProvider.LrcLib,
LyricsSearchProvider.QQ => TranslationSearchProvider.QQ,
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,
LyricsSearchProvider.LocalTtmlFile => TranslationSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}

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 PlaybackOrder
{
RepeatAll,
RepeatOne,
Shuffle,
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum TranslationSearchProvider
{
QQ,
Kugou,
Netease,
LrcLib,
AmllTtmlDb,
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
LibreTranslate,
}
}

View File

@@ -8,9 +8,9 @@ using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs : EventArgs
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, Color? albumArtAccentColor) : EventArgs
{
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = null;
public Color? AlbumArtAccentColor { get; set; } = null;
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public Color? AlbumArtAccentColor { get; set; } = albumArtAccentColor;
}
}

View File

@@ -4,8 +4,9 @@ using System;
namespace BetterLyrics.WinUI3.Events
{
public class PositionChangedEventArgs(TimeSpan position) : EventArgs()
public class TimelineChangedEventArgs(TimeSpan position, TimeSpan end) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
public TimeSpan End { get; set; } = end;
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.UI.Windowing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class AppWindowHelper
{
public static void SetIcons(this AppWindow appWindow)
{
appWindow.SetIcon(@"Assets/Logo.ico");
appWindow.SetTaskbarIcon(@"Assets/Logo.ico");
appWindow.SetTitleBarIcon(@"Assets/Logo.ico");
}
}
}

View File

@@ -0,0 +1,51 @@
using ATL;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
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 ObservableCollection<GroupInfoList> GetGroupedBy<T>(
this IEnumerable<T> items,
Func<T, object> groupKeySelector,
Func<object, object>? orderSelector = null)
{
var query = from item in items
group item by groupKeySelector(item) into g
orderby g.Key
select new GroupInfoList(g.Cast<object>(), orderSelector) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
}
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
{
if (collection == null) return;
if (items == null) return;
foreach (var item in items)
{
collection.Add(item);
}
}
public static void InsertRange<T>(this IList<T> list, int index, IEnumerable<T> items)
{
if (list == null) return;
if (items == null) return;
if (index < 0 || index > list.Count) return;
foreach (var item in items)
{
list.Insert(index++, item);
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,17 @@ namespace BetterLyrics.WinUI3.Helper
return stream;
}
public static RandomAccessStreamReference ByteArrayToRandomAccessStreamReference(byte[] bytes)
{
var stream = new InMemoryRandomAccessStream();
var writer = new DataWriter(stream);
writer.WriteBytes(bytes);
writer.StoreAsync().GetAwaiter().GetResult();
writer.FlushAsync().GetAwaiter().GetResult();
writer.DetachStream();
return RandomAccessStreamReference.CreateFromStream(stream);
}
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(int width, int height)
{
var device = CanvasDevice.GetSharedDevice();
@@ -167,9 +178,11 @@ namespace BetterLyrics.WinUI3.Helper
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)
@@ -218,5 +231,22 @@ namespace BetterLyrics.WinUI3.Helper
square.Save(ms, new PngEncoder());
return ms.ToArray();
}
public static byte[] Resize(byte[] imageBytes, int size)
{
using Image image = Image.Load(imageBytes);
var factor = Math.Max(size / image.Width, size / image.Height);
if (factor <= 1)
{
return imageBytes;
}
int width = image.Width * factor;
int height = image.Height * factor;
image.Mutate(x => x.Resize(width, height, KnownResamplers.Welch));
using var ms = new MemoryStream();
image.Save(ms, new PngEncoder());
return ms.ToArray();
}
}
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TinyPinyin;
using Windows.Globalization;
namespace BetterLyrics.WinUI3.Services
@@ -124,5 +125,20 @@ namespace BetterLyrics.WinUI3.Services
if (found == -1) found = 7; // 默认使用英语
return found;
}
public static string GetOrderChar(string text)
{
if (string.IsNullOrWhiteSpace(text)) return "#";
char c = text.ElementAtOrDefault(0);
if (char.IsLetter(c) && c < 128)
return char.ToUpper(c).ToString();
if (PinyinHelper.IsChinese(c))
{
return PinyinHelper.GetPinyinInitials($"{c}");
}
return "#";
}
}
}

View File

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

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.System;
namespace BetterLyrics.WinUI3.Helper
{
public class LauncherHelper
{
public static async Task SelectAndShowFile(string filePath)
{
var file = await StorageFile.GetFileFromPathAsync(filePath);
var folder = await file.GetParentAsync();
var folderOptions = new FolderLauncherOptions();
folderOptions.ItemsToSelect.Add(file);
await Launcher.LaunchFolderAsync(folder, folderOptions);
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class ListViewHelper
{
public static int FindChildIndex(this ListView listView, object frameworkElement)
{
var children = listView.ItemsPanelRoot.Children.Select(x => ((ListViewItem)x).ContentTemplateRoot).ToList();
return children.IndexOf((UIElement)frameworkElement);
}
}
}

View File

@@ -47,7 +47,7 @@ namespace BetterLyrics.WinUI3.Helper
break;
}
}
PostProcessLyricsLines(durationMs.Value);
_lyricsDataArr.Add(new LyricsData()); // 为机翻预留
return _lyricsDataArr;
}
@@ -109,12 +109,16 @@ namespace BetterLyrics.WinUI3.Helper
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = grouped.Max(g => g.Count());
int languageCount = 0;
if (grouped != null && grouped.Count > 0)
{
// 计算最大语言数量
languageCount = grouped.Max(g => g.Count());
}
// 初始化每种语言的歌词列表
_lyricsDataArr.Clear();
for (int i = 0; i < languageCount; i++)
_lyricsDataArr.Add(new LyricsData());
for (int i = 0; i < languageCount; i++) _lyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
foreach (var group in grouped)
@@ -160,7 +164,7 @@ namespace BetterLyrics.WinUI3.Helper
{
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
var xdoc = XDocument.Parse(raw);
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
@@ -186,16 +190,18 @@ namespace BetterLyrics.WinUI3.Helper
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
// 原文(非 CJK 语言添加空格)
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
if (!LanguageHelper.IsCJK(originalText))
// 处理原文span后的空白
for (int i = 0; i < originalTextSpans.Count; i++)
{
foreach (var span in originalTextSpans)
var span = originalTextSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += " ";
span.Value += textNode.Value;
}
originalText = string.Concat(originalTextSpans.Select(s => s.Value));
}
// 拼接空白字符后的原文
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
var originalCharTimings = new List<LyricsChar>();
int originalStartIndex = 0;
@@ -374,29 +380,5 @@ namespace BetterLyrics.WinUI3.Helper
_lyricsDataArr.Add(new LyricsData(lyricsLines));
}
private void PostProcessLyricsLines(int durationMs)
{
for (int langIdx = 0; langIdx < _lyricsDataArr.Count; langIdx++)
{
var lines = _lyricsDataArr[langIdx].LyricsLines;
if (lines.Count > 0)
{
if (lines[0].StartMs > 0)
{
lines.Insert(
0,
new LyricsLine
{
StartMs = 0,
EndMs = lines[0].StartMs,
OriginalText = "● ● ●",
LyricsChars = [],
}
);
}
}
}
}
}
}

View File

@@ -29,6 +29,7 @@ namespace BetterLyrics.WinUI3.Helper
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 const string TelegramUrl = "https://t.me/+svhSLZ7awPsxNGY1";
public static async Task<DateTime> GetBuildDate()
{

View File

@@ -0,0 +1,331 @@
using System;
using System.Buffers;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.Storage;
using static BetterLyrics.WinUI3.Helper.NoiseOverlayHelper.BitmapFileCreator;
namespace BetterLyrics.WinUI3.Helper
{
internal static class NoiseOverlayHelper
{
const string NoiseOverlayFileName = "noise_overlay.bmp";
static readonly string NoiseOverlayFilePath = Path.Combine(ApplicationData.Current.LocalFolder.Path, "Assets", NoiseOverlayFileName);
/// <summary>
/// 生成 BGRA 格式的灰阶噪声像素数据
/// </summary>
public static byte[] GenerateNoiseBitmapBGRA(int width, int height)
{
var random = new Random();
var pixelData = new byte[width * height * 4];
for (int i = 0; i < width * height; i++)
{
byte gray = (byte)random.Next(0, 256);
pixelData[i * 4 + 0] = gray; // B
pixelData[i * 4 + 1] = gray; // G
pixelData[i * 4 + 2] = gray; // R
pixelData[i * 4 + 3] = 255; // A
}
return pixelData;
}
/// <summary>
/// 生成单色灰阶随机噪声
/// </summary>
/// <param name="outputPath">输出文件路径</param>
/// <param name="width">图片宽度</param>
/// <param name="height">图片高度</param>
public static BitmapFile GenerateNoiseBitmap(int width, int height)
{
const uint NumOfGrayscale = 16;
uint bitCount = NextPowerOfTwo((uint)Math.Round(Math.Sqrt(NumOfGrayscale)));
var palette = BitmapFileCreator.CreateGrayscalePalette(16);
var pixelData = GenerateRandomNoise(width, height, bitCount);
var fileHeader = BitmapFileCreator.CreateFileHeader(palette, pixelData);
var infoHeader = BitmapFileCreator.CreateInfoHeader(width, height, bitCount);
return new BitmapFile(fileHeader, infoHeader, palette, pixelData);
}
/// <summary>
/// 读取噪声图片的位头信息
/// </summary>
public static BitmapFileCreator.WINBMPINFOHEADER ReadBitmapInfoHeaders(string? FilePath)
{
var _filePath = FilePath ?? NoiseOverlayFilePath;
using var fs = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
using var br = new BinaryReader(fs);
// 跳过文件头
fs.Seek(Marshal.SizeOf<BitmapFileCreator.BITMAPFILEHEADER>(), SeekOrigin.Begin);
// 读取信息头
byte[] infoHeaderBytes = br.ReadBytes(Marshal.SizeOf<BitmapFileCreator.WINBMPINFOHEADER>());
return MemoryMarshal.Read<BitmapFileCreator.WINBMPINFOHEADER>(infoHeaderBytes);
}
public static BitmapFileCreator.WINBMPINFOHEADER ReadBitmapInfoHeaders(byte[] FileBytes)
{
// 跳过文件头
var offset = Marshal.SizeOf<BitmapFileCreator.BITMAPFILEHEADER>();
// 读取信息头
var infoHeaderLength = Marshal.SizeOf<BitmapFileCreator.WINBMPINFOHEADER>();
Span<byte> infoHeaderBytes = new(FileBytes, offset, infoHeaderLength);
return MemoryMarshal.Read<BitmapFileCreator.WINBMPINFOHEADER>(infoHeaderBytes);
}
/// <summary>
/// safe 的写入 struct 到流
/// </summary>
/// <typeparam name="T">值 strcut</typeparam>
/// <param name="stream">要写入的字节流</param>
/// <param name="structure">要被写入的结构</param>
public static void WriteStruct<T>(Stream stream, in T structure) where T : struct
{
int size = Unsafe.SizeOf<T>();
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
MemoryMarshal.Write(buffer, in structure);
stream.Write(buffer, 0, size);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// 随机填充位图内容
/// </summary>
/// <param name="width">填充宽度</param>
/// <param name="height">填充高度</param>
/// <param name="bitCount">单个调色盘索引所占比特位数</param>
/// <returns>字节数据</returns>
private static byte[] GenerateRandomNoise(int width, int height, uint bitCount)
{
// 创建位图行字节数4K 对齐
int rowSize = ((width * (int)bitCount + 31) >> 5) << 2;
// 创建随机位图数据
Random rnd = new();
return Enumerable.Range(0, rowSize * height)
.Select(i => (byte)rnd.Next(0x00, 0xFF))
.ToArray();
}
private static uint NextPowerOfTwo(uint value)
{
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
}
public static class BitmapFileCreator
{
/// <summary>
/// 创建BMP文件头
/// </summary>
/// <param name="palette">调色盘数据</param>
/// <param name="pixelData">像素数据</param>
/// <returns>文件头结构</returns>
public static BITMAPFILEHEADER CreateFileHeader(byte[] palette, byte[] pixelData)
{
return new BITMAPFILEHEADER
{
bfType = 0x4D42,
bfSize = (uint)(Marshal.SizeOf<BITMAPFILEHEADER>() +
Marshal.SizeOf<WINBMPINFOHEADER>() +
palette.Length +
pixelData.Length),
bfReserved1 = 0,
bfReserved2 = 0,
bfOffBits = (uint)(Marshal.SizeOf<BITMAPFILEHEADER>() +
Marshal.SizeOf<WINBMPINFOHEADER>() +
palette.Length)
};
}
/// <summary>
/// 将指定值填充到为最接近的2的幂数
/// </summary>
/// <summary>
/// 生成灰阶调色盘
/// </summary>
/// <param name="colors">灰阶数量</param>
/// <returns>调色盘byte数组</returns>
public static byte[] CreateGrayscalePalette(int colors)
{
return Enumerable.Range(0, colors)
.SelectMany(i => Enumerable.Repeat((byte)(i * 0x10), 4))
.ToArray();
}
/// <summary>
/// 创建BMP信息头
/// </summary>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
/// <param name="bitCount">单个像素(调色盘索引)位数</param>
/// <returns>BMP信息头结构</returns>
public static WINBMPINFOHEADER CreateInfoHeader(int width, int height, uint bitCount)
{
return new WINBMPINFOHEADER
{
biSize = (uint)Marshal.SizeOf<WINBMPINFOHEADER>(),
biWidth = (uint)width,
biHeight = (uint)height,
biPlanes = 1,
biBitCount = (ushort)bitCount,
biCompression = 0,
biSizeImage = 0,
biXPelsPerMeter = 0,
biYPelsPerMeter = 0,
biClrUsed = 0,
biClrImportant = 0
};
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
/// <summary>
/// BMP 位图文件头
/// </summary>
public struct BITMAPFILEHEADER
{
/// <summary>
/// 文件类型标识,用于指定文件格式
/// </summary>
public ushort bfType;
/// <summary>
/// 文件大小,以字节为单位
/// </summary>
public uint bfSize;
/// <summary>
/// 保留字段,未使用
/// </summary>
public ushort bfReserved1;
/// <summary>
/// 保留字段,未使用
/// </summary>
public ushort bfReserved2;
/// <summary>
/// 像素数据的起始位置,以字节为单位,从文件头开始计算
/// </summary>
public uint bfOffBits;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
/// <summary>
/// BMP 位图信息头
/// </summary>
public struct WINBMPINFOHEADER
{
/// <summary>
/// 指定此结构体的字节大小。
/// </summary>
public uint biSize;
/// <summary>
/// 以像素为单位的位图宽度。
/// </summary>
public uint biWidth;
/// <summary>
/// 以像素为单位的位图高度。
/// </summary>
public uint biHeight;
/// <summary>
/// 目标设备的平面数通常为1。
/// </summary>
public ushort biPlanes;
/// <summary>
/// 每个像素的位数表示颜色深度如1、4、8、16、24、32等。
/// </summary>
public ushort biBitCount;
/// <summary>
/// 压缩类型0表示不压缩。
/// </summary>
public uint biCompression;
/// <summary>
/// 位图图像数据的大小以字节为单位若图像未压缩该值可设为0。
/// </summary>
public uint biSizeImage;
/// <summary>
/// 每米X轴方向的像素数水平分辨率通常设为0。
/// </summary>
public uint biXPelsPerMeter;
/// <summary>
/// 每米Y轴方向的像素数垂直分辨率通常设为0。
/// </summary>
public uint biYPelsPerMeter;
/// <summary>
/// 实际使用的颜色索引数若为0则使用位图中实际出现的颜色数。
/// </summary>
public uint biClrUsed;
/// <summary>
/// 重要颜色索引数0表示所有颜色都重要。
/// </summary>
public uint biClrImportant;
}
}
public class BitmapFile(BitmapFileCreator.BITMAPFILEHEADER fileHeader, BitmapFileCreator.WINBMPINFOHEADER infoHeader, byte[] palette, byte[] pixelData)
{
public BITMAPFILEHEADER FileHeader = fileHeader;
public WINBMPINFOHEADER InfoHeader = infoHeader;
public byte[] Palette = palette;
public byte[] PixelData = pixelData;
/// <summary>
/// 转换为byte[]
/// </summary>
/// <param name="bf"></param>
public static explicit operator byte[](BitmapFile bf)
{
var result = new byte[bf.FileHeader.bfSize];
var ms = new MemoryStream(result);
bf.WriteToStream(ms);
return result;
}
public byte[] ToByteArray() => (byte[])this;
public Task<byte[]> ToByteArrayAsync() { return Task.FromResult(ToByteArray());}
/// <summary>
/// 写入此结构到流中
/// </summary>
public void WriteToStream(Stream stream)
{
NoiseOverlayHelper.WriteStruct(stream, FileHeader);
NoiseOverlayHelper.WriteStruct(stream, InfoHeader);
stream.Write(Palette);
stream.Write(PixelData);
stream.Flush();
}
}
}
}

View File

@@ -33,6 +33,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string TranslationCacheDirectory => Path.Combine(CacheFolder, "translations");
public static string QQTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "qq");
public static string NeteaseTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "netease");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
@@ -49,6 +50,7 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(QQTranslationCacheDirectory);
Directory.CreateDirectory(NeteaseTranslationCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}

View File

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

View File

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

View File

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

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class GroupInfoList : List<object>
{
public required object Key { get; set; }
public GroupInfoList(IEnumerable<object> items, Func<object, object>? orderSelector = null)
: base(orderSelector != null
? items.OrderBy(orderSelector)
: items)
{
}
public override string ToString()
{
return $"{Key}";
}
}
}

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

@@ -26,36 +26,39 @@ namespace BetterLyrics.WinUI3.Models
LyricsLines = lyricsLines;
}
public void SetDisplayedTextAlongWith(LyricsData translationData)
public void SetDisplayedTextAlongWith(LyricsData translationData, int toleranceMs = 0)
{
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= translationData.LyricsLines.Count)
// 在翻译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = translationData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
line.DisplayedText = line.OriginalText; // No translation available, keep original text
}
else
{
if (translationData.LanguageCode?.Substring(0, 2) == "zh")
if (translationData.LanguageCode?.StartsWith("zh") == true)
{
string tmp = "";
if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hant")
{
tmp = ChineseConverter.ConvertToTraditionalChinese(translationData.LyricsLines[i].OriginalText);
tmp = ChineseConverter.ConvertToTraditionalChinese(transLine.OriginalText);
}
else if (LanguageHelper.GetUserTargetLanguageCode() == "zh-Hans")
{
tmp = ChineseConverter.ConvertToSimplifiedChinese(translationData.LyricsLines[i].OriginalText);
tmp = ChineseConverter.ConvertToSimplifiedChinese(transLine.OriginalText);
}
line.DisplayedText = $"{line.OriginalText}\n{tmp}";
}
else
{
line.DisplayedText = $"{line.OriginalText}\n{translationData.LyricsLines[i].OriginalText}";
line.DisplayedText = $"{line.OriginalText}\n{transLine.OriginalText}";
}
}
i++;
else
{
// 没有匹配的翻译,翻译部分留空
line.DisplayedText = $"{line.OriginalText}\n";
}
}
}
@@ -71,7 +74,7 @@ namespace BetterLyrics.WinUI3.Models
}
else
{
line.DisplayedText = $"{line.OriginalText}{StringHelper.NewLine}({translationArr[i]})";
line.DisplayedText = $"{line.OriginalText}{StringHelper.NewLine}{translationArr[i]}";
}
i++;
}
@@ -85,6 +88,30 @@ namespace BetterLyrics.WinUI3.Models
}
}
public LyricsData CreateLyricsDataFrom(string translation)
{
var result = new LyricsData(LyricsLines.Select(line => new LyricsLine
{
StartMs = line.StartMs,
EndMs = line.EndMs,
}).ToList());
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in result.LyricsLines)
{
if (i >= translationArr.Count)
{
break;
}
else
{
line.OriginalText = translationArr[i];
}
i++;
}
return result;
}
public static LyricsData GetNotfoundPlaceholder(int durationMs)
{
return new LyricsData([new LyricsLine

View File

@@ -0,0 +1,19 @@
using ATL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public class PlayQueueItem
{
public Track Track { get; set; }
public PlayQueueItem(Track track)
{
Track = track;
}
}
}

View File

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

View File

@@ -0,0 +1,35 @@
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Models
{
public class SongsTabInfo
{
public string Name { get; set; }
public string Icon { get; set; }
public bool IsClosable { get; set; }
public CommonSongProperty FilterProperty { get; set; }
public string FilterValue { get; set; }
public SongsTabInfo()
{
Name = string.Empty;
Icon = string.Empty;
IsClosable = true;
FilterProperty = CommonSongProperty.Title;
FilterValue = string.Empty;
}
public SongsTabInfo(string name, string icon, bool isClosable, CommonSongProperty filterProperty, string filterValue)
{
Name = name;
Icon = icon;
IsClosable = isClosable;
FilterProperty = filterProperty;
FilterValue = filterValue;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ namespace BetterLyrics.WinUI3.Services
public interface IPlaybackService
{
event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
event EventHandler<PositionChangedEventArgs>? PositionChanged;
event EventHandler<TimelineChangedEventArgs>? TimelineChanged;
event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
@@ -19,6 +19,9 @@ namespace BetterLyrics.WinUI3.Services
Task PauseAsync();
Task PreviousAsync();
Task NextAsync();
Task ChangePosition(double seconds);
bool IsPlaying { get; }
SongInfo? SongInfo { get; }
}
}

View File

@@ -19,6 +19,7 @@ namespace BetterLyrics.WinUI3.Services
int CoverOverlayBlurAmount { get; set; }
int CoverOverlayOpacity { get; set; }
bool IsDynamicCoverOverlayEnabled { get; set; }
int CoverAcrylicEffectAmount { get; set; }
bool IsFanLyricsEnabled { get; set; }
bool IsFirstRun { get; set; }
bool IsLyricsGlowEffectEnabled { get; set; }
@@ -41,7 +42,7 @@ namespace BetterLyrics.WinUI3.Services
int PositionOffset { get; set; }
// Lyrics lib
List<LocalLyricsFolder> LocalLyricsFolders { get; set; }
List<LocalMediaFolder> LocalMediaFolders { get; set; }
// Lyrics style and effetc
@@ -60,7 +61,9 @@ namespace BetterLyrics.WinUI3.Services
LyricsFontColorType LyricsFgFontColorType { get; set; }
LyricsFontColorType LyricsStrokeFontColorType { get; set; }
int LyricsFontSize { get; set; }
int LyricsStandardFontSize { get; set; }
int LyricsDockFontSize { get; set; }
int LyricsDesktopFontSize { get; set; }
ElementTheme LyricsBackgroundTheme { get; set; }
@@ -96,5 +99,12 @@ namespace BetterLyrics.WinUI3.Services
bool IsImmersiveMode { get; set; }
string LXMusicServer { get; set; }
DockPlacement DockPlacement { get; set; }
bool HideWindowWhenNotPlaying { get; set; }
int DockWindowHeight { get; set; }
int SelectedFontFamilyIndex { get; set; }
string LyricsFontFamily { get; set; }
bool IsDragEverywhereEnabled { get; set; }
PlaybackOrder PlaybackOrder { get; set; }
bool IsLibreTranslateEnabled { get; set; }
}
}

View File

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

View File

@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Providers.Web.Kugou;
using Lyricify.Lyrics.Searchers;
using Microsoft.Extensions.Logging;
using NTextCat.Commons;
using System;
using System.IO;
using System.Linq;
@@ -89,82 +90,86 @@ namespace BetterLyrics.WinUI3.Services
{
_logger.LogInformation("Searching img for: {Title} - {Artist} (Album: {Album}, Duration: {DurationMs}ms)", title, artist, album, durationMs);
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
try
{
if (!provider.IsEnabled)
foreach (var provider in _settingsService.LyricsSearchProvidersInfo)
{
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))
if (!provider.IsEnabled)
{
return (cachedLyrics, provider.Provider);
continue;
}
}
string? searchedLyrics = null;
string? cachedLyrics;
LyricsFormat lyricsFormat = provider.Provider.GetLyricsFormat();
if (provider.Provider.IsLocal())
{
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
// Check cache first
if (provider.Provider.IsRemote())
{
searchedLyrics = SearchEmbedded(title, artist);
cachedLyrics = FileHelper.ReadLyricsCache(title, artist, lyricsFormat, provider.Provider.GetCacheDirectory());
if (!string.IsNullOrWhiteSpace(cachedLyrics))
{
return (cachedLyrics, provider.Provider);
}
}
string? searchedLyrics = null;
if (provider.Provider.IsLocal())
{
if (provider.Provider == LyricsSearchProvider.LocalMusicFile)
{
searchedLyrics = SearchEmbedded(title, artist);
}
else
{
searchedLyrics = await SearchFile(title, artist, lyricsFormat);
}
}
else
{
searchedLyrics = await SearchFile(title, artist, lyricsFormat);
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;
}
}
}
else
{
switch (provider.Provider)
token.ThrowIfCancellationRequested();
if (!string.IsNullOrWhiteSpace(searchedLyrics))
{
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;
if (provider.Provider.IsRemote())
{
FileHelper.WriteLyricsCache(title, artist, searchedLyrics, lyricsFormat, provider.Provider.GetCacheDirectory());
}
return (searchedLyrics, provider.Provider);
}
}
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 async Task<string?> SearchFile(string title, string artist, LyricsFormat format)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
@@ -186,7 +191,7 @@ namespace BetterLyrics.WinUI3.Services
private string? SearchEmbedded(string title, string artist)
{
foreach (var folder in _settingsService.LocalLyricsFolders)
foreach (var folder in _settingsService.LocalMediaFolders)
{
if (Directory.Exists(folder.Path) && folder.IsEnabled)
{
@@ -343,6 +348,17 @@ namespace BetterLyrics.WinUI3.Services
else if (result is NeteaseSearchResult neteaseResult)
{
var response = await Lyricify.Lyrics.Helpers.ProviderHelper.NeteaseApi.GetLyric(neteaseResult.Id);
var translated = response?.Tlyric?.Lyric;
if (!string.IsNullOrEmpty(translated))
{
FileHelper.WriteLyricsCache(
title,
artist,
translated,
LyricsFormat.Lrc,
PathHelper.NeteaseTranslationCacheDirectory
);
}
return response?.Lrc.Lyric;
}
else if (result is KugouSearchResult kugouResult)

View File

@@ -10,9 +10,11 @@ using CommunityToolkit.Mvvm.Messaging.Messages;
using EvtSource;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
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;
@@ -21,7 +23,9 @@ 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
{
@@ -33,21 +37,24 @@ namespace BetterLyrics.WinUI3.Services
private readonly ILogger<PlaybackService> _logger;
private readonly string _lxMusicId = "cn.toside.music.desktop";
private double _lxMusicPositionSeconds = 0;
private double _lxMusicDurationSeconds = 0;
private bool _cachedIsPlaying = false;
private EventSourceReader? _sse = null;
private readonly MediaManager _mediaManager = new();
private readonly LatestOnlyTaskRunner _AlbumArtRefreshRunner = new();
private readonly LatestOnlyTaskRunner _OnAnyMediaPropertyChangedRunner = new();
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();
private readonly LatestOnlyTaskRunner _onAnyMediaPropertyChangedRunner = new();
private SongInfo? _cachedSongInfo;
private List<MediaSourceProviderInfo> _mediaSourceProvidersInfo;
private byte[]? _SMTCAlbumArtBytes = null;
private AlbumArtChangedEventArgs _albumArtChangedEventArgs = new();
public event EventHandler<IsPlayingChangedEventArgs>? IsPlayingChanged;
public event EventHandler<PositionChangedEventArgs>? PositionChanged;
public event EventHandler<TimelineChangedEventArgs>? TimelineChanged;
public event EventHandler<SongInfoChangedEventArgs>? SongInfoChanged;
public event EventHandler<AlbumArtChangedEventArgs>? AlbumArtChangedChanged;
public event EventHandler<MediaSourceProvidersInfoEventArgs>? MediaSourceProvidersInfoChanged;
@@ -61,6 +68,9 @@ namespace BetterLyrics.WinUI3.Services
InitMediaManager();
}
public bool IsPlaying => _cachedIsPlaying;
public SongInfo? SongInfo => _cachedSongInfo;
private bool IsMediaSourceEnabled(string id)
{
return _mediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id)?.IsEnabled ?? true;
@@ -68,8 +78,6 @@ namespace BetterLyrics.WinUI3.Services
private void InitMediaManager()
{
_mediaManager.Start();
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
_mediaManager.OnAnySessionClosed += MediaManager_OnAnySessionClosed;
_mediaManager.OnFocusedSessionChanged += MediaManager_OnFocusedSessionChanged;
@@ -77,79 +85,85 @@ namespace BetterLyrics.WinUI3.Services
_mediaManager.OnAnyPlaybackStateChanged += MediaManager_OnAnyPlaybackStateChanged;
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
MediaManager_OnFocusedSessionChanged(_mediaManager.GetFocusedSession());
_mediaManager.Start();
Task.Run(() =>
{
MediaManager_OnFocusedSessionChanged(null);
});
}
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession mediaSession)
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
{
if (mediaSession == null || !IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId))
{
SendNullMessages();
}
else
{
_dispatcherQueue.TryEnqueue(async () =>
{
try
{
var props = await mediaSession.ControlSession.TryGetMediaPropertiesAsync();
MediaManager_OnAnyMediaPropertyChanged(mediaSession, props);
MediaManager_OnAnyPlaybackStateChanged(mediaSession, mediaSession.ControlSession.GetPlaybackInfo());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "TryGetMediaPropertiesAsync failed");
SendNullMessages();
}
});
}
if (!_mediaManager.IsStarted) return;
SendFocusedMessagesAsync().ConfigureAwait(false);
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties timelineProperties)
{
if (!IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId) || mediaSession != _mediaManager.GetFocusedSession()) return;
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
_dispatcherQueue.TryEnqueue(
DispatcherQueuePriority.High,
() =>
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(timelineProperties.Position));
}
);
var focusedSession = _mediaManager.GetFocusedSession();
if (!IsMediaSourceEnabled(mediaSession.Id) || mediaSession != focusedSession) return;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(timelineProperties.Position, timelineProperties.EndTime));
});
}
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo playbackInfo)
{
RecordMediaSourceProviderInfo(mediaSession);
if (!IsMediaSourceEnabled(mediaSession.ControlSession.SourceAppUserModelId) || mediaSession != _mediaManager.GetFocusedSession()) return;
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
bool isPlaying = playbackInfo.PlaybackStatus switch
var focusedSession = _mediaManager.GetFocusedSession();
RecordMediaSourceProviderInfo(mediaSession);
if (!IsMediaSourceEnabled(mediaSession.Id) || mediaSession != focusedSession) return;
_cachedIsPlaying = playbackInfo.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(isPlaying));
}
);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(_cachedIsPlaying));
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties mediaProperties)
{
_ = _OnAnyMediaPropertyChangedRunner.RunAsync(async token =>
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
string id = mediaSession.Id;
var focusedSession = _mediaManager.GetFocusedSession();
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);
RecordMediaSourceProviderInfo(mediaSession);
string id = mediaSession.ControlSession.SourceAppUserModelId;
if (!IsMediaSourceEnabled(id) || mediaSession != _mediaManager.GetFocusedSession()) return;
token.ThrowIfCancellationRequested();
if (id == _lxMusicId)
{
StartSSE();
@@ -159,39 +173,36 @@ namespace BetterLyrics.WinUI3.Services
StopSSE();
}
_cachedSongInfo = new SongInfo
{
Title = mediaProperties.Title,
Artist = mediaProperties.Artist,
Album = mediaProperties.AlbumTitle,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
SourceAppUserModelId = id,
};
if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBytes = await ImageHelper.ToByteArrayAsync(streamReference);
token.ThrowIfCancellationRequested();
_SMTCAlbumArtBytes = ImageHelper.Resize(_SMTCAlbumArtBytes, 800);
}
else
{
_SMTCAlbumArtBytes = null;
}
_ = _AlbumArtRefreshRunner.RunAsync(async tokne =>
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});
if (!token.IsCancellationRequested)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
});
}
});
}).ConfigureAwait(false);
}
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
if (_mediaManager.CurrentMediaSessions.Count == 0)
{
SendNullMessages();
@@ -200,12 +211,19 @@ namespace BetterLyrics.WinUI3.Services
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
RecordMediaSourceProviderInfo(mediaSession);
SendFocusedMessagesAsync().ConfigureAwait(false);
}
private void RecordMediaSourceProviderInfo(MediaManager.MediaSession mediaSession)
{
var id = mediaSession?.ControlSession?.SourceAppUserModelId;
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var id = mediaSession?.Id;
if (string.IsNullOrEmpty(id)) return;
var found = _mediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
@@ -213,8 +231,7 @@ namespace BetterLyrics.WinUI3.Services
{
_mediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, true));
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
MediaSourceProvidersInfoChanged?.Invoke(this, new MediaSourceProvidersInfoEventArgs(_mediaSourceProvidersInfo));
});
@@ -223,16 +240,27 @@ namespace BetterLyrics.WinUI3.Services
private void SendNullMessages()
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High,
() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
_cachedSongInfo = null;
_cachedIsPlaying = false;
SongInfoChanged?.Invoke(this, new SongInfoChangedEventArgs(_cachedSongInfo));
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(false));
PositionChanged?.Invoke(this, new PositionChangedEventArgs(TimeSpan.Zero));
IsPlayingChanged?.Invoke(this, new IsPlayingChangedEventArgs(_cachedIsPlaying));
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(TimeSpan.Zero, TimeSpan.Zero));
});
}
private async Task SendFocusedMessagesAsync()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession == null || focusedSession.ControlSession == 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)
@@ -264,22 +292,34 @@ namespace BetterLyrics.WinUI3.Services
var decoder = await BitmapDecoder.CreateAsync(stream);
token.ThrowIfCancellationRequested();
_albumArtChangedEventArgs.AlbumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
var _albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied);
token.ThrowIfCancellationRequested();
_albumArtChangedEventArgs.AlbumArtAccentColor = ImageHelper.GetAccentColorsFromByte(bytes).FirstOrDefault();
var _albumArtAccentColor = ImageHelper.GetAccentColorsFromByte(bytes).FirstOrDefault();
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
AlbumArtChangedChanged?.Invoke(this, _albumArtChangedEventArgs);
AlbumArtChangedChanged?.Invoke(this, new AlbumArtChangedEventArgs(_albumArtSwBitmap, _albumArtAccentColor));
});
}
private void StartSSE()
{
_sse = new EventSourceReader(new Uri($"{_settingsService.LXMusicServer}/subscribe-player-status?filter=progress")).Start();
_sse.MessageReceived += Sse_MessageReceived;
_sse.Disconnected += Sse_Disconnected;
try
{
_sse = new EventSourceReader(new Uri($"{_settingsService.LXMusicServer}/subscribe-player-status?filter=progress,duration")).Start();
_sse.MessageReceived += Sse_MessageReceived;
_sse.Disconnected += Sse_Disconnected;
}
catch (Exception)
{
_logger.LogError("Failed to start SSE connection for LX Music.");
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.LyricsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("FailToStartLXMusicServer"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
});
StopSSE();
}
}
private void StopSSE()
@@ -304,13 +344,23 @@ namespace BetterLyrics.WinUI3.Services
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)
{
if (_cachedSongInfo?.SourceAppUserModelId == _lxMusicId)
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
{
PositionChanged?.Invoke(this, new PositionChangedEventArgs(TimeSpan.FromSeconds(positionSeconds)));
if (e.Event == "progress")
{
_lxMusicPositionSeconds = data.GetDouble();
}
else if (e.Event == "duration")
{
_lxMusicDurationSeconds = data.GetDouble();
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
TimelineChanged?.Invoke(this, new TimelineChangedEventArgs(TimeSpan.FromSeconds(_lxMusicPositionSeconds), TimeSpan.FromSeconds(_lxMusicDurationSeconds)));
});
}
}
}
@@ -320,7 +370,7 @@ namespace BetterLyrics.WinUI3.Services
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
{
await focusedSession.ControlSession.TryPlayAsync();
await focusedSession.ControlSession?.TryPlayAsync();
}
}
@@ -329,7 +379,7 @@ namespace BetterLyrics.WinUI3.Services
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
{
await focusedSession.ControlSession.TryPauseAsync();
await focusedSession.ControlSession?.TryPauseAsync();
}
}
@@ -338,7 +388,7 @@ namespace BetterLyrics.WinUI3.Services
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
{
await focusedSession.ControlSession.TrySkipPreviousAsync();
await focusedSession.ControlSession?.TrySkipPreviousAsync();
}
}
@@ -347,7 +397,16 @@ namespace BetterLyrics.WinUI3.Services
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
{
await focusedSession.ControlSession.TrySkipNextAsync();
await focusedSession.ControlSession?.TrySkipNextAsync();
}
}
public async Task ChangePosition(double seconds)
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession != null)
{
await focusedSession.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
}
@@ -359,12 +418,12 @@ namespace BetterLyrics.WinUI3.Services
{
_mediaSourceProvidersInfo = [.. message.NewValue];
_settingsService.MediaSourceProvidersInfo = _mediaSourceProvidersInfo;
MediaManager_OnFocusedSessionChanged(_mediaManager.GetFocusedSession());
MediaManager_OnFocusedSessionChanged(null);
}
}
}
public void Receive(PropertyChangedMessage<ObservableCollection<AlbumArtSearchProviderInfo>> message)
public async void Receive(PropertyChangedMessage<ObservableCollection<AlbumArtSearchProviderInfo>> message)
{
if (message.Sender is SettingsPageViewModel)
{
@@ -372,7 +431,7 @@ namespace BetterLyrics.WinUI3.Services
{
// Album art search providers info changed, re-fetch album art
_logger.LogInformation("Album art search providers info changed, refreshing album art.");
_ = _AlbumArtRefreshRunner.RunAsync(async tokne =>
await _albumArtRefreshRunner.RunAsync(async tokne =>
{
await UpdateAlbumArtRelated(tokne);
});

View File

@@ -30,6 +30,8 @@ namespace BetterLyrics.WinUI3.Services
private const string CoverOverlayOpacityKey = "CoverOverlayOpacity";
private const string IsCoverOverlayEnabledKey = "IsCoverOverlayEnabled";
private const string CoverAcrylicEffectAmountKey = "CoverAcrylicEffectAmount";
private const string DesktopWindowLeftKey = "DesktopWindowLeft";
private const string DesktopWindowTopKey = "DesktopWindowTop";
private const string DesktopWindowWidthKey = "DesktopWindowWidth";
@@ -60,7 +62,11 @@ namespace BetterLyrics.WinUI3.Services
private const string LyricsFontStrokeWidthKey = "LyricsFontStrokeWidth";
private const string LyricsFontSizeKey = "LyricsFontSize";
// Lyrics font size
private const string LyricsStandardFontSizeKey = "LyricsStandardFontSize";
private const string LyricsDockFontSizeKey = "LyricsDockFontSize";
private const string LyricsDesktopFontSizeKey = "LyricsDesktopFontSize";
private const string LyricsFontWeightKey = "LyricsFontWeightKey";
private const string LyricsGlowEffectScopeKey = "LyricsGlowEffectScope";
private const string LyricsHighlightSopeKey = "LyricsHighlightSope";
@@ -71,11 +77,14 @@ namespace BetterLyrics.WinUI3.Services
private const string MediaSourceProvidersInfoKey = "MediaSourceProvidersInfo";
// Translation
private const string IsTranslationEnabledKey = "IsTranslationEnabled";
private const string ShowTranslationOnlyKey = "ShowTranslationOnly";
private const string IsLibreTranslateEnabledKey = "IsLibreTranslateEnabled";
private const string LibreTranslateServerKey = "LibreTranslateServer";
private const string SelectedTargetLanguageIndexKey = "SelectedTargetLanguageIndex";
// LX Music
private const string LXMusicServerKey = "LXMusicServer";
private const string LyricsBackgroundThemeKey = "LyricsBackgroundTheme";
@@ -90,12 +99,19 @@ namespace BetterLyrics.WinUI3.Services
private const string IsLyricsFloatAnimationEnabledKey = "IsLyricsFloatAnimationEnabled";
private const string ResetPositionOffsetOnSongChangedKey = "ResetPositionOffsetOnSongChanged";
private const string PlaybackOrderKey = "PlaybackOrder";
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;
@@ -175,6 +191,7 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(CoverOverlayOpacityKey, 100); // 100 % = 1.0
SetDefault(CoverOverlayBlurAmountKey, 100);
SetDefault(CoverImageRadiusKey, 12); // 12 %
SetDefault(CoverAcrylicEffectAmountKey, 0);
// Lyrics
SetDefault(LyricsAlignmentTypeKey, (int)TextAlignmentType.Center);
SetDefault(SongInfoAlignmentTypeKey, (int)TextAlignmentType.Left);
@@ -191,7 +208,10 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(LyricsCustomFgFontColorKey, Colors.White.ToInt());
SetDefault(LyricsCustomStrokeFontColorKey, Colors.White.ToInt());
SetDefault(LyricsFontSizeKey, 28);
SetDefault(LyricsStandardFontSizeKey, 28);
SetDefault(LyricsDockFontSizeKey, 20);
SetDefault(LyricsDesktopFontSizeKey, 28);
SetDefault(LyricsLineSpacingFactorKey, 0.5f);
SetDefault(LyricsVerticalEdgeOpacityKey, 0);
SetDefault(IsLyricsGlowEffectEnabledKey, true);
@@ -200,6 +220,7 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(IsFanLyricsEnabledKey, false);
SetDefault(LibreTranslateServerKey, "");
SetDefault(IsLibreTranslateEnabledKey, false);
SetDefault(IsTranslationEnabledKey, true);
SetDefault(ShowTranslationOnlyKey, false);
SetDefault(SelectedTargetLanguageIndexKey, LanguageHelper.GetDefaultTargetLanguageIndex());
@@ -221,6 +242,41 @@ namespace BetterLyrics.WinUI3.Services
SetDefault(LockHotKeyIndexKey, 'U' - 'A');
SetDefault(DockPlacementKey, (int)DockPlacement.Top);
SetDefault(LyricsBgFontOpacityKey, 30); // 30%
SetDefault(HideWindowWhenNotPlayingKey, false);
SetDefault(DockWindowHeightKey, 64); // 64px
SetDefault(SelectedFontFamilyIndexKey, 0);
SetDefault(LyricsFontFamilyKey, FontHelper.SystemFontFamilies.ElementAtOrDefault(0));
SetDefault(IsDragEverywhereEnabledKey, false);
}
public bool IsDragEverywhereEnabled
{
get => GetValue<bool>(IsDragEverywhereEnabledKey);
set => SetValue(IsDragEverywhereEnabledKey, value);
}
public string LyricsFontFamily
{
get => GetValue<string>(LyricsFontFamilyKey)!;
set => SetValue(LyricsFontFamilyKey, value);
}
public int SelectedFontFamilyIndex
{
get => GetValue<int>(SelectedFontFamilyIndexKey);
set => SetValue(SelectedFontFamilyIndexKey, value);
}
public bool HideWindowWhenNotPlaying
{
get => GetValue<bool>(HideWindowWhenNotPlayingKey);
set => SetValue(HideWindowWhenNotPlayingKey, value);
}
public int DockWindowHeight
{
get => GetValue<int>(DockWindowHeightKey);
set => SetValue(DockWindowHeightKey, value);
}
public int LyricsBgFontOpacity
@@ -355,6 +411,12 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(IsDynamicCoverOverlayEnabledKey, value);
}
public int CoverAcrylicEffectAmount
{
get => GetValue<int>(CoverAcrylicEffectAmountKey);
set => SetValue(CoverAcrylicEffectAmountKey, value);
}
public bool IsFanLyricsEnabled
{
get => GetValue<bool>(IsFanLyricsEnabledKey);
@@ -379,19 +441,19 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(LanguageKey, (int)value);
}
public List<LocalLyricsFolder> LocalLyricsFolders
public List<LocalMediaFolder> LocalMediaFolders
{
get =>
System.Text.Json.JsonSerializer.Deserialize(
GetValue<string>(LocalLyricsFoldersKey) ?? "[]",
SourceGenerationContext.Default.ListLocalLyricsFolder
SourceGenerationContext.Default.ListLocalMediaFolder
)!;
set =>
SetValue(
LocalLyricsFoldersKey,
System.Text.Json.JsonSerializer.Serialize(
value,
SourceGenerationContext.Default.ListLocalLyricsFolder
SourceGenerationContext.Default.ListLocalMediaFolder
)
);
}
@@ -456,10 +518,22 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(LyricsFontStrokeWidthKey, value);
}
public int LyricsFontSize
public int LyricsStandardFontSize
{
get => GetValue<int>(LyricsFontSizeKey);
set => SetValue(LyricsFontSizeKey, value);
get => GetValue<int>(LyricsStandardFontSizeKey);
set => SetValue(LyricsStandardFontSizeKey, value);
}
public int LyricsDockFontSize
{
get => GetValue<int>(LyricsDockFontSizeKey);
set => SetValue(LyricsDockFontSizeKey, value);
}
public int LyricsDesktopFontSize
{
get => GetValue<int>(LyricsDesktopFontSizeKey);
set => SetValue(LyricsDesktopFontSizeKey, value);
}
public LyricsFontWeight LyricsFontWeight
@@ -555,6 +629,12 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(IsTranslationEnabledKey, value);
}
public bool IsLibreTranslateEnabled
{
get => GetValue<bool>(IsLibreTranslateEnabledKey);
set => SetValue(IsLibreTranslateEnabledKey, value);
}
public int SelectedTargetLanguageIndex
{
get => GetValue<int>(SelectedTargetLanguageIndexKey);
@@ -591,6 +671,12 @@ namespace BetterLyrics.WinUI3.Services
set => SetValue(ResetPositionOffsetOnSongChangedKey, value);
}
public PlaybackOrder PlaybackOrder
{
get => (PlaybackOrder)GetValue<int>(PlaybackOrderKey);
set => SetValue(PlaybackOrderKey, (int)value);
}
public int PositionOffset
{
get => GetValue<int>(PositionOffsetKey);

View File

@@ -3,6 +3,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.ViewModels;
using Lyricify.Lyrics.Helpers.General;
using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -27,7 +28,7 @@ namespace BetterLyrics.WinUI3.Services
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Text and target language must be provided.");
throw new Exception(text + " is empty or null.");
}
string? originalLangCode = LanguageHelper.DetectLanguageCode(text);
@@ -46,15 +47,7 @@ namespace BetterLyrics.WinUI3.Services
if (string.IsNullOrEmpty(_settingsService.LibreTranslateServer))
{
_dispatcherQueue.TryEnqueue(() =>
{
App.Current.LyricsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString("TranslateServerNotSet"),
Microsoft.UI.Xaml.Controls.InfoBarSeverity.Warning
);
});
throw new InvalidOperationException("LibreTranslate server URL is not configured.");
throw new Exception("LibreTranslate server URL is not set in settings.");
}
var url = $"{_settingsService.LibreTranslateServer}/translate";

View File

@@ -264,8 +264,14 @@
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>x line height</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>Font size</value>
<data name="SettingsPageLyricsStandardFontSize.Header" xml:space="preserve">
<value>Font size (Standard mode)</value>
</data>
<data name="SettingsPageLyricsDockFontSize.Header" xml:space="preserve">
<value>Font size (Dock mode)</value>
</data>
<data name="SettingsPageLyricsDesktopFontSize.Header" xml:space="preserve">
<value>Font size (Desktop mode)</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Lyrics only</value>
@@ -481,9 +487,6 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>Local music files</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
@@ -499,18 +502,6 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains added folders, please delete these folders to add the folder</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Show debug overlay</value>
</data>
@@ -593,7 +584,7 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<value>Lyrics timeline offset (ms)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>Configure translation services</value>
<value>LibreTranslate translation service</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>Server address</value>
@@ -772,4 +763,148 @@ If you encounter any problems, please go to the Settings page, About tab, and vi
<data name="SettingsPageFAQ.Header" xml:space="preserve">
<value>Frequently asked questions</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>Unable to connect to LX Music server, please go to Settings - Advanced options to check if the link is entered correctly</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>Auto-hide window</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>Automatically hide the window when no songs are playing in dock mode or desktop mode</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Window height</value>
</data>
<data name="SettingsPageLyricsFontFamily.Header" xml:space="preserve">
<value>Lyrics font family</value>
</data>
<data name="SettingsPageGlobalDrag.Header" xml:space="preserve">
<value>Global drag</value>
</data>
<data name="SettingsPageGlobalDrag.Description" xml:space="preserve">
<value>Extend the title bar to the entire page so that the window can be dragged in any non-interactive area</value>
</data>
<data name="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>Fluid acrylic effect roughness</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Music gallery - BetterLyrics</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>Requesting translation from LibreTranslate failed, please check the settings or the native LibreTranslate configuration</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>File info</value>
</data>
<data name="MusicGalleryPageFileInfoTitle.Text" xml:space="preserve">
<value>Title</value>
</data>
<data name="MusicGalleryPageFileArtist.Text" xml:space="preserve">
<value>Artist</value>
</data>
<data name="MusicGalleryPageFileAlbum.Text" xml:space="preserve">
<value>Album</value>
</data>
<data name="MusicGalleryPageFileInfoYear.Text" xml:space="preserve">
<value>Year</value>
</data>
<data name="MusicGalleryPageFileInfoDuration.Text" xml:space="preserve">
<value>Duration</value>
</data>
<data name="MusicGalleryPageFileInfoBitrate.Text" xml:space="preserve">
<value>Bitrate</value>
</data>
<data name="MusicGalleryPageFileInfoSampleRate.Text" xml:space="preserve">
<value>Sample rate</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Text" xml:space="preserve">
<value>Bit depth</value>
</data>
<data name="MusicGalleryPageFileInfoFormat.Text" xml:space="preserve">
<value>Format</value>
</data>
<data name="MusicGalleryPageFileInfoEncoder.Text" xml:space="preserve">
<value>Encoder</value>
</data>
<data name="MusicGalleryPageFileInfoPath.Text" xml:space="preserve">
<value>Path</value>
</data>
<data name="MusicGalleryPageSongSearchBox.PlaceholderText" xml:space="preserve">
<value>Search songs</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>Sort type</value>
</data>
<data name="MusicGalleryPageSortByTitle.Content" xml:space="preserve">
<value>Title</value>
</data>
<data name="MusicGalleryPageSortByAlbum.Content" xml:space="preserve">
<value>Album</value>
</data>
<data name="MusicGalleryPageSortByArtist.Content" xml:space="preserve">
<value>Artist</value>
</data>
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>No songs were found in the media library</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Play queue</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Content" xml:space="preserve">
<value>Clear play queue</value>
</data>
<data name="MusicGalleryPageScrollToPlayingItem.Content" xml:space="preserve">
<value>Scroll to playing item</value>
</data>
<data name="MusicGalleryPageQueueLoop.Content" xml:space="preserve">
<value>List loop</value>
</data>
<data name="MusicGalleryPageSingleLoop.Content" xml:space="preserve">
<value>Single loop</value>
</data>
<data name="MusicGalleryPageQueueRandom.Content" xml:space="preserve">
<value>Random</value>
</data>
<data name="MusicGalleryPageRemoveFromPlayingQueue.Text" xml:space="preserve">
<value>Remove from play queue</value>
</data>
<data name="MusicGalleryPagePlayingQueueEmpty.Text" xml:space="preserve">
<value>Play queue is empty</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>Select all</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Content" xml:space="preserve">
<value>Add to play queue</value>
</data>
<data name="MusicGalleryPageAddToNext.Text" xml:space="preserve">
<value>Next items</value>
</data>
<data name="MusicGalleryPageAddToEnd.Text" xml:space="preserve">
<value>End of the list</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>Add to playlist</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Create a playlist</value>
</data>
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Open music gallery</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics is already running</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>All songs</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Text" xml:space="preserve">
<value>Lyrics provider</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Text" xml:space="preserve">
<value>Translation provider</value>
</data>
</root>

View File

@@ -250,7 +250,7 @@
<value>この値を調整すると、アルバム画像のバックグラウンドブラー強度も増加します。</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>現在の値:</value>
<value>現在の値: </value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>ぼかしが有効になっている場合のGPU使用量が大幅に高くなります&gt; 0</value>
@@ -264,8 +264,14 @@
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>x線の高さ</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>フォントサイズ</value>
<data name="SettingsPageLyricsStandardFontSize.Header" xml:space="preserve">
<value>フォントサイズ(標準モード)</value>
</data>
<data name="SettingsPageLyricsDockFontSize.Header" xml:space="preserve">
<value>フォントサイズ(ドックモード)</value>
</data>
<data name="SettingsPageLyricsDesktopFontSize.Header" xml:space="preserve">
<value>フォントサイズ(デスクトップモード)</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>歌詞のみ</value>
@@ -481,9 +487,6 @@
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>ローカル音楽ファイル</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
@@ -499,18 +502,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>このフォルダーには追加されたフォルダーが含まれています。これらのフォルダを削除してフォルダーを追加してください</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>デバッグオーバーレイを表示します</value>
</data>
@@ -593,7 +584,7 @@
<value>歌詞タイムラインオフセット(ミリ秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>翻訳サービスを構成します</value>
<value>リブレットランレート翻訳サービス</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>サーバーアドレス</value>
@@ -772,4 +763,148 @@
<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="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>仿亚克力效果粗糙度</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音楽ギャラリー - BetterLyrics</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>リブレットランスレートからの翻訳のリクエストが失敗しました。設定またはネイティブリブレットランレート構成を確認してください</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>ファイル情報</value>
</data>
<data name="MusicGalleryPageFileInfoTitle.Text" xml:space="preserve">
<value>タイトル</value>
</data>
<data name="MusicGalleryPageFileArtist.Text" xml:space="preserve">
<value>アーティスト</value>
</data>
<data name="MusicGalleryPageFileAlbum.Text" xml:space="preserve">
<value>アルバム</value>
</data>
<data name="MusicGalleryPageFileInfoYear.Text" xml:space="preserve">
<value>年</value>
</data>
<data name="MusicGalleryPageFileInfoDuration.Text" xml:space="preserve">
<value>間隔</value>
</data>
<data name="MusicGalleryPageFileInfoBitrate.Text" xml:space="preserve">
<value>ビットレート</value>
</data>
<data name="MusicGalleryPageFileInfoSampleRate.Text" xml:space="preserve">
<value>サンプルレート</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Text" xml:space="preserve">
<value>ビットの深さ</value>
</data>
<data name="MusicGalleryPageFileInfoFormat.Text" xml:space="preserve">
<value>形式</value>
</data>
<data name="MusicGalleryPageFileInfoEncoder.Text" xml:space="preserve">
<value>エンコーダー</value>
</data>
<data name="MusicGalleryPageFileInfoPath.Text" xml:space="preserve">
<value>パス</value>
</data>
<data name="MusicGalleryPageSongSearchBox.PlaceholderText" xml:space="preserve">
<value>曲を検索します</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>並べ替えタイプ</value>
</data>
<data name="MusicGalleryPageSortByTitle.Content" xml:space="preserve">
<value>タイトル</value>
</data>
<data name="MusicGalleryPageSortByAlbum.Content" xml:space="preserve">
<value>アルバム</value>
</data>
<data name="MusicGalleryPageSortByArtist.Content" xml:space="preserve">
<value>アーティスト</value>
</data>
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>メディアライブラリには歌が見つかりませんでした</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>キューを再生します</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Content" xml:space="preserve">
<value>クリアプレイキュー</value>
</data>
<data name="MusicGalleryPageScrollToPlayingItem.Content" xml:space="preserve">
<value>アイテムを再生するための位置</value>
</data>
<data name="MusicGalleryPageQueueLoop.Content" xml:space="preserve">
<value>ループをリストします</value>
</data>
<data name="MusicGalleryPageSingleLoop.Content" xml:space="preserve">
<value>シングルループ</value>
</data>
<data name="MusicGalleryPageQueueRandom.Content" xml:space="preserve">
<value>ランダム</value>
</data>
<data name="MusicGalleryPageRemoveFromPlayingQueue.Text" xml:space="preserve">
<value>プレイリストから取り外します</value>
</data>
<data name="MusicGalleryPagePlayingQueueEmpty.Text" xml:space="preserve">
<value>キューを再生するのは空です</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>すべてを選択します</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Content" xml:space="preserve">
<value>キューを再生するために追加します</value>
</data>
<data name="MusicGalleryPageAddToNext.Text" xml:space="preserve">
<value>次のアイテム</value>
</data>
<data name="MusicGalleryPageAddToEnd.Text" xml:space="preserve">
<value>リストの終わり</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>プレイリストに追加します</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>プレイリストを作成します</value>
</data>
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>オープンミュージックギャラリー</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics はすでに実行されています!</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>すべての曲</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Text" xml:space="preserve">
<value>歌詞プロバイダー</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Text" xml:space="preserve">
<value>翻訳プロバイダー</value>
</data>
</root>

View File

@@ -250,7 +250,7 @@
<value>이 값을 조정하면 앨범 이미지의 배경 흐림 강도가 증가합니다.</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>현재 가치 :</value>
<value>현재 가치 : </value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>Blur가 활성화 될 때 상당히 높은 GPU 사용량 (&gt; 0)</value>
@@ -264,8 +264,14 @@
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>X 라인 높이</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>글꼴 크기</value>
<data name="SettingsPageLyricsStandardFontSize.Header" xml:space="preserve">
<value>글꼴 크기 (표준 모드)</value>
</data>
<data name="SettingsPageLyricsDockFontSize.Header" xml:space="preserve">
<value>글꼴 크기 (도크 모드)</value>
</data>
<data name="SettingsPageLyricsDesktopFontSize.Header" xml:space="preserve">
<value>글꼴 크기 (데스크탑 모드)</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>가사 만</value>
@@ -481,9 +487,6 @@
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>로컬 음악 파일</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
@@ -499,18 +502,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>이 폴더에는 추가 된 폴더가 포함되어 있습니다. 폴더를 추가하려면이 폴더를 삭제하십시오.</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>디버그 오버레이를 표시하십시오</value>
</data>
@@ -593,7 +584,7 @@
<value>가사 타임 라인 오프셋 (밀리초)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>번역 서비스를 구성하십시오</value>
<value>libretranslate 번역 서비스</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>서버 주소</value>
@@ -772,4 +763,148 @@
<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="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>仿亚克力效果粗糙度</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>음악 갤러리 - BetterLyrics</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>LibreTranslate에서 번역 요청 실패, 설정 또는 기본 LibreTranslate 구성을 확인하십시오.</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>파일 정보</value>
</data>
<data name="MusicGalleryPageFileInfoTitle.Text" xml:space="preserve">
<value>제목</value>
</data>
<data name="MusicGalleryPageFileArtist.Text" xml:space="preserve">
<value>아티스트</value>
</data>
<data name="MusicGalleryPageFileAlbum.Text" xml:space="preserve">
<value>앨범</value>
</data>
<data name="MusicGalleryPageFileInfoYear.Text" xml:space="preserve">
<value>년도</value>
</data>
<data name="MusicGalleryPageFileInfoDuration.Text" xml:space="preserve">
<value>지속</value>
</data>
<data name="MusicGalleryPageFileInfoBitrate.Text" xml:space="preserve">
<value>비트 레이트</value>
</data>
<data name="MusicGalleryPageFileInfoSampleRate.Text" xml:space="preserve">
<value>샘플 속도</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Text" xml:space="preserve">
<value>비트 깊이</value>
</data>
<data name="MusicGalleryPageFileInfoFormat.Text" xml:space="preserve">
<value>체재</value>
</data>
<data name="MusicGalleryPageFileInfoEncoder.Text" xml:space="preserve">
<value>인코더</value>
</data>
<data name="MusicGalleryPageFileInfoPath.Text" xml:space="preserve">
<value>길</value>
</data>
<data name="MusicGalleryPageSongSearchBox.PlaceholderText" xml:space="preserve">
<value>노래 검색</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>정렬 유형</value>
</data>
<data name="MusicGalleryPageSortByTitle.Content" xml:space="preserve">
<value>제목</value>
</data>
<data name="MusicGalleryPageSortByAlbum.Content" xml:space="preserve">
<value>앨범</value>
</data>
<data name="MusicGalleryPageSortByArtist.Content" xml:space="preserve">
<value>아티스트</value>
</data>
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>미디어 라이브러리에는 노래가 없습니다</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>대기열을 재생하십시오</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Content" xml:space="preserve">
<value>플레이 대기열을 클리어합니다</value>
</data>
<data name="MusicGalleryPageScrollToPlayingItem.Content" xml:space="preserve">
<value>아이템을 재생하기위한 포지셔닝</value>
</data>
<data name="MusicGalleryPageQueueLoop.Content" xml:space="preserve">
<value>목록 루프</value>
</data>
<data name="MusicGalleryPageSingleLoop.Content" xml:space="preserve">
<value>단일 루프</value>
</data>
<data name="MusicGalleryPageQueueRandom.Content" xml:space="preserve">
<value>무작위의</value>
</data>
<data name="MusicGalleryPageRemoveFromPlayingQueue.Text" xml:space="preserve">
<value>재생 목록에서 제거하십시오</value>
</data>
<data name="MusicGalleryPagePlayingQueueEmpty.Text" xml:space="preserve">
<value>플레이 대기열이 비어 있습니다</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>모두를 선택하십시오</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Content" xml:space="preserve">
<value>재생 큐에 추가하십시오</value>
</data>
<data name="MusicGalleryPageAddToNext.Text" xml:space="preserve">
<value>다음 항목</value>
</data>
<data name="MusicGalleryPageAddToEnd.Text" xml:space="preserve">
<value>목록의 끝</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>재생 목록에 추가하십시오</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>재생 목록을 만듭니다</value>
</data>
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>오픈 음악 갤러리</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 가 이미 실행 중입니다</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>모든 노래</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Text" xml:space="preserve">
<value>가사 제공자</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Text" xml:space="preserve">
<value>번역 제공자</value>
</data>
</root>

View File

@@ -250,7 +250,7 @@
<value>调整该数值将同步提高专辑图片背景模糊强度</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>当前值:</value>
<value>当前值: </value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>启用模糊(&gt; 0时将显著提升 GPU 占用率</value>
@@ -264,8 +264,14 @@
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> 倍行高</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字体大小</value>
<data name="SettingsPageLyricsStandardFontSize.Header" xml:space="preserve">
<value>字体尺寸(标准模式)</value>
</data>
<data name="SettingsPageLyricsDockFontSize.Header" xml:space="preserve">
<value>字体大小(停靠模式)</value>
</data>
<data name="SettingsPageLyricsDesktopFontSize.Header" xml:space="preserve">
<value>字体尺寸(桌面模式)</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>仅显示歌词</value>
@@ -481,9 +487,6 @@
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音乐文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
@@ -499,18 +502,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>该文件夹包含已添加文件夹,请删除这些文件夹以添加该文件夹</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>显示调试覆盖层</value>
</data>
@@ -593,7 +584,7 @@
<value>歌词时间轴偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻译服务</value>
<value>LibreTranslate 翻译服务</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服务器地址</value>
@@ -650,7 +641,7 @@
<value>设置</value>
</data>
<data name="TranslateServerNotSet" xml:space="preserve">
<value>未设置翻译服务器,请先在设置中进行配置</value>
<value>未设置Translate服务器,请先在设置中进行配置</value>
</data>
<data name="LyricsPagePositionOffsetHint.Text" xml:space="preserve">
<value>切换歌曲时重置为 0</value>
@@ -772,4 +763,148 @@
<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="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>仿亚克力效果粗糙度</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音乐库 - BetterLyrics</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>向 LibreTranslate 请求翻译失败,请检查设置或本机 LibreTranslate 配置</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>文件信息</value>
</data>
<data name="MusicGalleryPageFileInfoTitle.Text" xml:space="preserve">
<value>标题</value>
</data>
<data name="MusicGalleryPageFileArtist.Text" xml:space="preserve">
<value>艺术家</value>
</data>
<data name="MusicGalleryPageFileAlbum.Text" xml:space="preserve">
<value>专辑</value>
</data>
<data name="MusicGalleryPageFileInfoYear.Text" xml:space="preserve">
<value>年</value>
</data>
<data name="MusicGalleryPageFileInfoDuration.Text" xml:space="preserve">
<value>期间</value>
</data>
<data name="MusicGalleryPageFileInfoBitrate.Text" xml:space="preserve">
<value>比特率</value>
</data>
<data name="MusicGalleryPageFileInfoSampleRate.Text" xml:space="preserve">
<value>样本率</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Text" xml:space="preserve">
<value>位深度</value>
</data>
<data name="MusicGalleryPageFileInfoFormat.Text" xml:space="preserve">
<value>格式</value>
</data>
<data name="MusicGalleryPageFileInfoEncoder.Text" xml:space="preserve">
<value>编码器</value>
</data>
<data name="MusicGalleryPageFileInfoPath.Text" xml:space="preserve">
<value>路径</value>
</data>
<data name="MusicGalleryPageSongSearchBox.PlaceholderText" xml:space="preserve">
<value>搜索歌曲</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>排序类型</value>
</data>
<data name="MusicGalleryPageSortByTitle.Content" xml:space="preserve">
<value>标题</value>
</data>
<data name="MusicGalleryPageSortByAlbum.Content" xml:space="preserve">
<value>专辑</value>
</data>
<data name="MusicGalleryPageSortByArtist.Content" xml:space="preserve">
<value>艺术家</value>
</data>
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>未在媒体库内找到任何歌曲</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>播放队列</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Content" xml:space="preserve">
<value>清除播放队列</value>
</data>
<data name="MusicGalleryPageScrollToPlayingItem.Content" xml:space="preserve">
<value>定位到播放项</value>
</data>
<data name="MusicGalleryPageQueueLoop.Content" xml:space="preserve">
<value>列表循环</value>
</data>
<data name="MusicGalleryPageSingleLoop.Content" xml:space="preserve">
<value>单曲循环</value>
</data>
<data name="MusicGalleryPageQueueRandom.Content" xml:space="preserve">
<value>随机</value>
</data>
<data name="MusicGalleryPageRemoveFromPlayingQueue.Text" xml:space="preserve">
<value>从播放列表移除</value>
</data>
<data name="MusicGalleryPagePlayingQueueEmpty.Text" xml:space="preserve">
<value>播放队列是空的</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>选择全部</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Content" xml:space="preserve">
<value>添加到播放队列</value>
</data>
<data name="MusicGalleryPageAddToNext.Text" xml:space="preserve">
<value>下一个项目</value>
</data>
<data name="MusicGalleryPageAddToEnd.Text" xml:space="preserve">
<value>列表的结尾</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>添加到歌单</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>创建歌单</value>
</data>
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>打开音乐库</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 已经在运行</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有歌曲</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Text" xml:space="preserve">
<value>歌词来源</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Text" xml:space="preserve">
<value>翻译来源</value>
</data>
</root>

View File

@@ -250,7 +250,7 @@
<value>調整此數值將同步提升專輯圖片背景模糊強度</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>目前值:</value>
<value>目前值: </value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>啟用模糊(&gt; 0時將顯著提升 GPU 佔用率</value>
@@ -264,8 +264,14 @@
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> 倍行高</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字體大小</value>
<data name="SettingsPageLyricsStandardFontSize.Header" xml:space="preserve">
<value>字體尺寸(標準模式)</value>
</data>
<data name="SettingsPageLyricsDockFontSize.Header" xml:space="preserve">
<value>字體大小(停靠模式)</value>
</data>
<data name="SettingsPageLyricsDesktopFontSize.Header" xml:space="preserve">
<value>字體尺寸(桌面模式)</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>僅顯示歌詞</value>
@@ -481,9 +487,6 @@
<data name="LyricsSearchProviderLocalMusicFile" xml:space="preserve">
<value>本地音樂文件</value>
</data>
<data name="LyricsSearchProviderLrcLib" xml:space="preserve">
<value>LRCLIB</value>
</data>
<data name="SettingsPageJA.Content" xml:space="preserve">
<value>日本語</value>
</data>
@@ -499,18 +502,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>該文件夾包含已添加文件夾,請刪除這些文件夾以添加該文件夾</value>
</data>
<data name="LyricsSearchProviderAmllTtmlDb" xml:space="preserve">
<value>amll-ttml-db</value>
</data>
<data name="LyricsSearchProviderQQ" xml:space="preserve">
<value>QQ</value>
</data>
<data name="LyricsSearchProviderNetease" xml:space="preserve">
<value>Netease</value>
</data>
<data name="LyricsSearchProviderKugou" xml:space="preserve">
<value>Kugou</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>顯示調試覆蓋層</value>
</data>
@@ -593,7 +584,7 @@
<value>歌詞時間軸偏移(毫秒)</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>配置翻譯服務</value>
<value>LibreTranslate 翻譯服務</value>
</data>
<data name="SettingsPageLibreTranslateServer.Header" xml:space="preserve">
<value>服務器地址</value>
@@ -772,4 +763,148 @@
<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="SettingsPageBackgroundAcrylicEffectAmount.Header" xml:space="preserve">
<value>仿亚克力效果粗糙度</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>音樂庫 - BetterLyrics</value>
</data>
<data name="LibreTranslateFailed" xml:space="preserve">
<value>向 LibreTranslate 請求翻譯失敗,請檢查設置或本機 LibreTranslate 配置</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>文件信息</value>
</data>
<data name="MusicGalleryPageFileInfoTitle.Text" xml:space="preserve">
<value>標題</value>
</data>
<data name="MusicGalleryPageFileArtist.Text" xml:space="preserve">
<value>藝術家</value>
</data>
<data name="MusicGalleryPageFileAlbum.Text" xml:space="preserve">
<value>專輯</value>
</data>
<data name="MusicGalleryPageFileInfoYear.Text" xml:space="preserve">
<value>年</value>
</data>
<data name="MusicGalleryPageFileInfoDuration.Text" xml:space="preserve">
<value>期間</value>
</data>
<data name="MusicGalleryPageFileInfoBitrate.Text" xml:space="preserve">
<value>比特率</value>
</data>
<data name="MusicGalleryPageFileInfoSampleRate.Text" xml:space="preserve">
<value>樣本率</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Text" xml:space="preserve">
<value>位深度</value>
</data>
<data name="MusicGalleryPageFileInfoFormat.Text" xml:space="preserve">
<value>格式</value>
</data>
<data name="MusicGalleryPageFileInfoEncoder.Text" xml:space="preserve">
<value>編碼器</value>
</data>
<data name="MusicGalleryPageFileInfoPath.Text" xml:space="preserve">
<value>路徑</value>
</data>
<data name="MusicGalleryPageSongSearchBox.PlaceholderText" xml:space="preserve">
<value>搜索歌曲</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>排序類型</value>
</data>
<data name="MusicGalleryPageSortByTitle.Content" xml:space="preserve">
<value>標題</value>
</data>
<data name="MusicGalleryPageSortByAlbum.Content" xml:space="preserve">
<value>專輯</value>
</data>
<data name="MusicGalleryPageSortByArtist.Content" xml:space="preserve">
<value>藝術家</value>
</data>
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>未在媒體庫內找到任何歌曲</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>清除播放</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Content" xml:space="preserve">
<value>清除播放隊列</value>
</data>
<data name="MusicGalleryPageScrollToPlayingItem.Content" xml:space="preserve">
<value>定位到播放項</value>
</data>
<data name="MusicGalleryPageQueueLoop.Content" xml:space="preserve">
<value>列表循環</value>
</data>
<data name="MusicGalleryPageSingleLoop.Content" xml:space="preserve">
<value>單曲循環</value>
</data>
<data name="MusicGalleryPageQueueRandom.Content" xml:space="preserve">
<value>隨機</value>
</data>
<data name="MusicGalleryPageRemoveFromPlayingQueue.Text" xml:space="preserve">
<value>從播放列表移除</value>
</data>
<data name="MusicGalleryPagePlayingQueueEmpty.Text" xml:space="preserve">
<value>播放隊列是空的</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>選擇全部</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Content" xml:space="preserve">
<value>新增到播放隊列</value>
</data>
<data name="MusicGalleryPageAddToNext.Text" xml:space="preserve">
<value>下一個項目</value>
</data>
<data name="MusicGalleryPageAddToEnd.Text" xml:space="preserve">
<value>列表的結尾</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Content" xml:space="preserve">
<value>添加到歌單</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>建立歌單</value>
</data>
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>開啟音樂庫</value>
</data>
<data name="TryRunMultipleInstance" xml:space="preserve">
<value>BetterLyrics 已經在運作</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有歌曲</value>
</data>
<data name="SettingsPageTelegram.Header" xml:space="preserve">
<value>Telegram</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Text" xml:space="preserve">
<value>歌詞來源</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Text" xml:space="preserve">
<value>翻譯來源</value>
</data>
</root>

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.TemplateSelector;
public class SongOrderTemplateSelector : DataTemplateSelector
{
public DataTemplate ByTitleTemplate { get; set; }
public DataTemplate ByAlbumTemplate { get; set; }
public DataTemplate ByArtistTemplate { get; set; }
public CommonSongProperty SongOrderType { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
return SongOrderType switch
{
CommonSongProperty.Title => ByTitleTemplate,
CommonSongProperty.Album => ByAlbumTemplate,
CommonSongProperty.Artist => ByArtistTemplate,
_ => ByTitleTemplate
};
}
}

View File

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

View File

@@ -9,14 +9,26 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsPageViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<bool>>
public partial class LyricsPageViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsSearchProvider?>>,
IRecipient<PropertyChangedMessage<TranslationSearchProvider?>>
{
private readonly IPlaybackService _playbackService;
private readonly ThrottleHelper _timelineThrottle = new(TimeSpan.FromSeconds(1));
public LyricsPageViewModel(ISettingsService settingsService, IPlaybackService playbackService) : base(settingsService)
{
@@ -28,6 +40,10 @@ namespace BetterLyrics.WinUI3.ViewModels
IsImmersiveMode = _settingsService.IsImmersiveMode;
ShowTranslationOnly = _settingsService.ShowTranslationOnly;
LyricsStandardFontSize = _settingsService.LyricsStandardFontSize;
LyricsFontFamily = _settingsService.LyricsFontFamily;
OnIsImmersiveModeChanged(IsImmersiveMode);
//Volume = SystemVolumeHelper.GetMasterVolume();
@@ -36,6 +52,14 @@ namespace BetterLyrics.WinUI3.ViewModels
_playbackService = playbackService;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.TimelineChanged += PlaybackService_TimelineChanged;
IsSongPlaying = _playbackService.IsPlaying;
}
private void PlaybackService_TimelineChanged(object? sender, Events.TimelineChangedEventArgs e)
{
SongDurationSeconds = (int)e.End.TotalSeconds;
}
//private void SystemVolumeHelper_VolumeChanged(int volume)
@@ -51,14 +75,27 @@ namespace BetterLyrics.WinUI3.ViewModels
private void PlaybackService_SongInfoChanged(object? sender, Events.SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
SongDurationSeconds = SongInfo?.Duration ?? 0;
if (ResetPositionOffsetOnSongChanged)
{
PositionOffset = 0;
}
}
//[ObservableProperty]
//public partial int Volume { get; set; }
[ObservableProperty]
public partial double TimelinePositionSeconds { get; set; }
[ObservableProperty]
public partial int SongDurationSeconds { get; set; }
[ObservableProperty]
public partial int Volume { get; set; }
[ObservableProperty]
public partial string LyricsFontFamily { get; set; }
[ObservableProperty]
public partial int LyricsStandardFontSize { get; set; }
[ObservableProperty]
public partial bool IsImmersiveMode { get; set; }
@@ -67,7 +104,7 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial float BottomCommandGridOpacity { get; set; }
[ObservableProperty]
public partial Thickness BottomCommandGridMargin { get; set; } = new Thickness(12);
public partial float BottomCommandFlyoutTriggerOpacity { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
@@ -101,6 +138,12 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial bool IsSongPlaying { get; set; }
[ObservableProperty]
public partial LyricsSearchProvider? LyricsSearchProvider { get; set; } = null;
[ObservableProperty]
public partial TranslationSearchProvider? TranslationSearchProvider { get; set; } = null;
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is LyricsWindowViewModel)
@@ -115,7 +158,6 @@ namespace BetterLyrics.WinUI3.ViewModels
{
DisplayType = _settingsService.DisplayType;
}
BottomCommandGridMargin = message.NewValue ? new Thickness(0) : new Thickness(12);
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
@@ -138,7 +180,7 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private static void OpenSettingsWindow()
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
[RelayCommand]
@@ -186,10 +228,12 @@ namespace BetterLyrics.WinUI3.ViewModels
if (value)
{
BottomCommandGridOpacity = 0f;
BottomCommandFlyoutTriggerOpacity = 0f;
}
else
{
BottomCommandGridOpacity = 1f;
BottomCommandFlyoutTriggerOpacity = 1f;
}
}
@@ -198,9 +242,70 @@ namespace BetterLyrics.WinUI3.ViewModels
_settingsService.ShowTranslationOnly = value;
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsStandardFontSize))
{
LyricsStandardFontSize = 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;
});
}
}
}
}
public void Receive(PropertyChangedMessage<LyricsSearchProvider?> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.LyricsSearchProvider))
{
LyricsSearchProvider = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<TranslationSearchProvider?> message)
{
if (message.Sender is LyricsRendererViewModel)
{
if (message.PropertyName == nameof(LyricsRendererViewModel.TranslationSearchProvider))
{
TranslationSearchProvider = message.NewValue;
}
}
}
}
}

View File

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -20,16 +21,23 @@ namespace BetterLyrics.WinUI3.ViewModels
_isDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
_albumArtBgOpacity = _settingsService.CoverOverlayOpacity;
_albumArtBgBlurAmount = _settingsService.CoverOverlayBlurAmount;
_coverAcrylicEffectAmount = _settingsService.CoverAcrylicEffectAmount;
_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;
_lyricsStandardFontSize = _settingsService.LyricsStandardFontSize;
_lyricsDockFontSize = _settingsService.LyricsDockFontSize;
_lyricsDesktopFontSize = _settingsService.LyricsDesktopFontSize;
_lyricsBlurAmount = _settingsService.LyricsBlurAmount;
_isLyricsGlowEffectEnabled = _settingsService.IsLyricsGlowEffectEnabled;
_lyricsGlowEffectScope = _settingsService.LyricsGlowEffectScope;
@@ -44,6 +52,7 @@ namespace BetterLyrics.WinUI3.ViewModels
_lyricsFontStrokeWidth = _settingsService.LyricsFontStrokeWidth;
_isTranslationEnabled = _settingsService.IsTranslationEnabled;
_showTranslationOnly = _settingsService.ShowTranslationOnly;
_isLibreTranslateEnabled = _settingsService.IsLibreTranslateEnabled;
_targetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
@@ -62,7 +71,9 @@ namespace BetterLyrics.WinUI3.ViewModels
_playbackService.IsPlayingChanged += PlaybackService_IsPlayingChanged;
_playbackService.SongInfoChanged += PlaybackService_SongInfoChanged;
_playbackService.AlbumArtChangedChanged += PlaybackService_AlbumArtChangedChanged;
_playbackService.PositionChanged += PlaybackService_PositionChanged;
_playbackService.TimelineChanged += PlaybackService_TimelineChanged;
_isPlaying = _playbackService.IsPlaying;
UpdateColorConfig();
}

View File

@@ -7,11 +7,14 @@ using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using Windows.Graphics.Effects;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
@@ -78,7 +81,7 @@ namespace BetterLyrics.WinUI3.ViewModels
$"[DEBUG]\n" +
$"Cur playing {_playingLineIndex}, char start idx {charStartIndex}, length {charLength}, prog {charProgress}\n" +
$"Visible lines [{_startVisibleLineIndex}, {_endVisibleLineIndex}]\n" +
$"Cur time {_totalTime + _positionOffset}\n" +
$"Cur time {TotalTime + _positionOffset}\n" +
$"Lang size {_lyricsDataArr.Count}\n" +
$"Song duration {TimeSpan.FromMilliseconds(SongInfo?.DurationMs ?? 0)}",
new Vector2(10, 10),
@@ -133,8 +136,7 @@ namespace BetterLyrics.WinUI3.ViewModels
BlackPoint = new Vector2(blackX, blackY),
},
Opacity = opacity,
}, new Vector2(x, y)
);
}, new Vector2(x, y));
}
private void DrawForegroundImgae(ICanvasAnimatedControl control, CanvasDrawingSession ds, CanvasBitmap canvasBitmap, float opacity)
@@ -174,10 +176,11 @@ namespace BetterLyrics.WinUI3.ViewModels
private void DrawAlbumArtBackground(ICanvasAnimatedControl control, CanvasDrawingSession ds)
{
ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
//ds.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
using var overlappedCovers = new CanvasCommandList(control.Device);
using var overlappedCoversDs = overlappedCovers.CreateDrawingSession();
overlappedCoversDs.Transform = Matrix3x2.CreateRotation(_rotateAngle, control.Size.ToVector2() * 0.5f);
if (_lastAlbumArtCanvasBitmap != null)
{
@@ -188,20 +191,40 @@ namespace BetterLyrics.WinUI3.ViewModels
DrawBackgroundImgae(control, overlappedCoversDs, _albumArtCanvasBitmap, _albumArtBgTransition.Value);
}
using var coverOverlayEffect = new OpacityEffect
overlappedCoversDs.Transform = Matrix3x2.Identity;
IGraphicsEffectSource blurredCover = new GaussianBlurEffect
{
BlurAmount = _albumArtBgBlurAmount,
Source = overlappedCovers,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Speed,
};
// 应用亚克力噪点效果
// TODO: 没有写_coverAcrylicNoiseCanvasBitmap加载的代码
if (_coverAcrylicEffectAmount > 0 && _coverAcrylicNoiseCanvasBitmap != null)
{
blurredCover = new BlendEffect
{
Mode = BlendEffectMode.SoftLight,
Background = blurredCover,
Foreground = new OpacityEffect
{
Source = _coverAcrylicNoiseCanvasBitmap,
Opacity = _coverAcrylicEffectAmount / 100f,
},
};
}
var coverOverlayEffect = new OpacityEffect
{
Opacity = _albumArtBgOpacity / 100f,
Source = new GaussianBlurEffect
{
BlurAmount = _albumArtBgBlurAmount,
Source = overlappedCovers,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Speed,
},
Source = blurredCover,
};
ds.DrawImage(coverOverlayEffect);
ds.Transform = Matrix3x2.Identity;
//ds.Transform = Matrix3x2.Identity;
}
private void DrawAlbumArt(ICanvasAnimatedControl control, CanvasDrawingSession ds)
@@ -443,7 +466,8 @@ namespace BetterLyrics.WinUI3.ViewModels
else
{
float height = 0f;
var regions = textLayout.GetCharacterRegions(0, string.Join("", line.LyricsChars.Select(x => x.Text)).Length);
//var regions = textLayout.GetCharacterRegions(0, string.Join("", line.LyricsChars.Select(x => x.Text)).Length);
var regions = textLayout.GetCharacterRegions(0, line.OriginalText.Length);
if (regions.Length > 0)
{
height = (float)regions[^1].LayoutBounds.Bottom - (float)regions[0].LayoutBounds.Top;

View File

@@ -6,12 +6,14 @@ using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsRendererViewModel
: IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<float>>,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<Color>>,
@@ -23,13 +25,13 @@ namespace BetterLyrics.WinUI3.ViewModels
IRecipient<PropertyChangedMessage<ElementTheme>>,
IRecipient<PropertyChangedMessage<EasingType>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LyricsSearchProviderInfo>>>,
IRecipient<PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>>>
IRecipient<PropertyChangedMessage<ObservableCollection<LocalMediaFolder>>>
{
public void Receive(PropertyChangedMessage<ObservableCollection<LocalLyricsFolder>> message)
public void Receive(PropertyChangedMessage<ObservableCollection<LocalMediaFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalLyricsFolders))
if (message.PropertyName == nameof(SettingsPageViewModel.LocalMediaFolders))
{
// Music lib changed, re-fetch lyrics
_logger.LogInformation("Local lyrics folders changed, refreshing lyrics.");
@@ -82,6 +84,11 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_isLyricsFloatAnimationEnabled = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.IsLibreTranslateEnabled))
{
_isLibreTranslateEnabled = message.NewValue;
UpdateTranslations();
}
}
else if (message.Sender is LyricsWindowViewModel)
{
@@ -89,20 +96,25 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_isDockMode = message.NewValue;
UpdateColorConfig();
UpdateImmersiveBackgroundOpacity();
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsDesktopMode))
{
_isDesktopMode = message.NewValue;
UpdateColorConfig();
UpdateImmersiveBackgroundOpacity();
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsLyricsWindowLocked))
{
_isLyricsWindowLocked = message.NewValue;
UpdateImmersiveBackgroundOpacity();
}
else if (message.PropertyName == nameof(LyricsWindowViewModel.IsMouseWithinWindow))
{
_isMouseWithinWindow = message.NewValue;
_immersiveBgOpacityTransition.StartTransition(_isDesktopMode ? (_isMouseWithinWindow ? 1f : 0f) : 1f);
UpdateImmersiveBackgroundOpacity();
}
}
else if (message.Sender is LyricsPageViewModel)
@@ -180,6 +192,11 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_albumArtBgBlurAmount = message.NewValue;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.CoverAcrylicEffectAmount))
{
_coverAcrylicEffectAmount = message.NewValue;
_isCoverAcrylicEffectAmountChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsVerticalEdgeOpacity))
{
_lyricsVerticalEdgeOpacity = message.NewValue;
@@ -190,9 +207,19 @@ namespace BetterLyrics.WinUI3.ViewModels
_lyricsBlurAmount = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontSize))
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsStandardFontSize))
{
_lyricsFontSize = message.NewValue;
_lyricsStandardFontSize = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsDockFontSize))
{
_lyricsDockFontSize = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.LyricsDesktopFontSize))
{
_lyricsDesktopFontSize = message.NewValue;
_isLayoutChanged = true;
}
else if (message.PropertyName == nameof(SettingsPageViewModel.SelectedTargetLanguageIndex))
@@ -320,5 +347,17 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LyricsFontFamily))
{
_lyricsTextFormat.FontFamily = _artistTextFormat.FontFamily = _titleTextFormat.FontFamily = message.NewValue;
_isLayoutChanged = true;
}
}
}
}
}

View File

@@ -7,8 +7,12 @@ 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 System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
@@ -29,7 +33,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (_isPlaying)
{
_totalTime += _elapsedTime;
TotalTime += _elapsedTime;
}
var playingLineIndex = GetCurrentPlayingLineIndex();
@@ -69,6 +73,13 @@ namespace BetterLyrics.WinUI3.ViewModels
_albumArtSize = MathF.Max(0, _albumArtSize);
_titleY = _albumArtY + _albumArtSize * 1.05f;
_isCoverAcrylicEffectAmountChanged = true;
}
if (_isCoverAcrylicEffectAmountChanged)
{
UpdateCoverAcrylicOverlay(control);
}
if (_isDisplayTypeChanged || _isCanvasWidthChanged)
@@ -129,7 +140,18 @@ namespace BetterLyrics.WinUI3.ViewModels
if (control == null)
return;
_lyricsTextFormat.FontSize = _lyricsFontSize;
if (_isDockMode)
{
_lyricsTextFormat.FontSize = _lyricsDockFontSize;
}
else if (_isDesktopMode)
{
_lyricsTextFormat.FontSize = _lyricsDesktopFontSize;
}
else
{
_lyricsTextFormat.FontSize = _lyricsStandardFontSize;
}
float y = 0;
@@ -194,48 +216,13 @@ namespace BetterLyrics.WinUI3.ViewModels
_canvasYScrollTransition.Update(_elapsedTime);
int startVisibleLineIndex = -1;
int endVisibleLineIndex = -1;
// Update visible line indices
for (int i = startLineIndex; i <= endLineIndex; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
var lines = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines;
if (lines == null || lines.Count == 0) return;
if (line == null || line.CanvasTextLayout == null)
{
continue;
}
var textLayout = line.CanvasTextLayout;
if (
_canvasYScrollTransition.Value
+ _canvasHeight / 2
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= 0
)
{
if (startVisibleLineIndex == -1)
{
startVisibleLineIndex = i;
}
}
if (
_canvasYScrollTransition.Value
+ _canvasHeight / 2
+ line.Position.Y
+ textLayout.LayoutBounds.Height
>= control.Size.Height
)
{
if (endVisibleLineIndex == -1)
{
endVisibleLineIndex = i;
}
}
}
float offset = _canvasYScrollTransition.Value + _canvasHeight / 2;
int startVisibleLineIndex = FindFirstVisibleLine(lines, offset);
int endVisibleLineIndex = FindLastVisibleLine(lines, offset, _canvasHeight);
if (startVisibleLineIndex != -1 && endVisibleLineIndex == -1)
{
@@ -248,6 +235,52 @@ namespace BetterLyrics.WinUI3.ViewModels
_endVisibleLineIndex = endVisibleLineIndex;
}
private int FindFirstVisibleLine(IList<LyricsLine> lines, float offset)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
var layout = line.CanvasTextLayout;
if (layout == null) break;
float value = offset + line.Position.Y + (float)layout.LayoutBounds.Height;
if (value >= 0)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private int FindLastVisibleLine(IList<LyricsLine> lines, float offset, float canvasHeight)
{
int left = 0, right = lines.Count - 1, result = -1;
while (left <= right)
{
int mid = (left + right) / 2;
var line = lines[mid];
var layout = line.CanvasTextLayout;
if (layout == null) break;
float value = offset + line.Position.Y + (float)layout.LayoutBounds.Height;
if (value >= canvasHeight)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private void UpdateColorConfig()
{
if (_isDesktopMode || _isDockMode)
@@ -398,5 +431,49 @@ namespace BetterLyrics.WinUI3.ViewModels
line.HighlightOpacityTransition.Update(_elapsedTime);
}
}
private void UpdateImmersiveBackgroundOpacity()
{
float targetOpacity;
if (_isDesktopMode)
{
if (_isLyricsWindowLocked)
{
targetOpacity = 0;
}
else
{
if (_isMouseWithinWindow)
{
targetOpacity = 1f;
}
else
{
targetOpacity = 0f;
}
}
}
else
{
targetOpacity = 1f;
}
_immersiveBgOpacityTransition.StartTransition(targetOpacity);
}
private void UpdateCoverAcrylicOverlay(ICanvasAnimatedControl control)
{
if (_coverAcrylicEffectAmount > 0)
{
var ret = NoiseOverlayHelper.GenerateNoiseBitmapBGRA((int)_canvasWidth, (int)_canvasHeight);
_coverAcrylicNoiseCanvasBitmap = CanvasBitmap.CreateFromBytes(
control,
ret,
(int)_canvasWidth,
(int)_canvasHeight,
Windows.Graphics.DirectX.DirectXPixelFormat.B8G8R8A8UIntNormalized
);
}
_isCoverAcrylicEffectAmountChanged = false;
}
}
}

View File

@@ -31,7 +31,11 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial class LyricsRendererViewModel : BaseViewModel
{
private TimeSpan _elapsedTime = TimeSpan.Zero;
private TimeSpan _totalTime = TimeSpan.Zero;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
private TimeSpan _positionOffset = TimeSpan.Zero;
private int _songDurationMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
@@ -42,6 +46,9 @@ namespace BetterLyrics.WinUI3.ViewModels
private CanvasBitmap? _lastAlbumArtCanvasBitmap = null;
private CanvasBitmap? _albumArtCanvasBitmap = null;
private CanvasBitmap? _coverAcrylicNoiseCanvasBitmap = null;
private bool _isCoverAcrylicEffectAmountChanged = false;
private float _albumArtSize = 0f;
private int _albumArtCornerRadius = 0;
@@ -67,6 +74,14 @@ namespace BetterLyrics.WinUI3.ViewModels
private readonly float _coverRotateSpeed = 0.003f;
private float _rotateAngle = 0f;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial LyricsSearchProvider? LyricsSearchProvider { get; set; } = null;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TranslationSearchProvider? TranslationSearchProvider { get; set; } = null;
private TextAlignmentType _lyricsAlignmentType;
private readonly float _lyricsGlowEffectAmount = 8f;
@@ -78,7 +93,9 @@ namespace BetterLyrics.WinUI3.ViewModels
private LineRenderingType _lyricsHighlightScope;
private int _lyricsFontStrokeWidth;
private int _lyricsFontSize;
private int _lyricsStandardFontSize;
private int _lyricsDockFontSize;
private int _lyricsDesktopFontSize;
private float _lyricsLineSpacingFactor;
private LyricsFontColorType _lyricsBgFontColorType;
@@ -145,6 +162,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private bool _isTranslationEnabled;
private bool _showTranslationOnly;
private int _targetLanguageIndex;
private bool _isLibreTranslateEnabled;
private int _timelineSyncThreshold;
@@ -181,6 +199,8 @@ namespace BetterLyrics.WinUI3.ViewModels
private int _albumArtBgBlurAmount;
private int _albumArtBgOpacity;
private int _coverAcrylicEffectAmount;
[ObservableProperty]
public partial bool IsTranslating { get; set; } = false;
@@ -193,12 +213,14 @@ namespace BetterLyrics.WinUI3.ViewModels
private int GetCurrentPlayingLineIndex()
{
var totalMs = TotalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (totalMs < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
for (int i = 0; i < _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.Count; i++)
{
var line = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var nextLine = _lyricsDataArr.ElementAtOrDefault(_langIndex)?.LyricsLines.ElementAtOrDefault(i + 1);
var totalMs = _totalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (nextLine != null && line.StartMs <= totalMs && totalMs < nextLine.StartMs)
{
return i;
@@ -227,7 +249,7 @@ namespace BetterLyrics.WinUI3.ViewModels
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = _songDurationMs;
float now = (float)_totalTime.TotalMilliseconds + (float)_positionOffset.TotalMilliseconds;
float now = (float)TotalTime.TotalMilliseconds + (float)_positionOffset.TotalMilliseconds;
// 1. 还没到本句
if (now < line.StartMs)
@@ -283,11 +305,21 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
// 没有逐字时间轴,直接线性,模拟逐字高亮
// 没有逐字时间轴,均匀分配每个字的高亮时间
int textLength = line.OriginalText.Length;
if (textLength == 0) return;
float lineProgress = (now - line.StartMs) / (lineEndMs - line.StartMs);
charStartIndex = (int)(lineProgress * line.OriginalText.Length);
charProgress = lineProgress - (int)(lineProgress);
lineProgress = Math.Clamp(lineProgress, 0f, 1f);
// 计算当前高亮到第几个字
float charFloatIndex = lineProgress * textLength;
int charIndex = (int)charFloatIndex;
charStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
charLength = 1;
// 当前字的进度0~1
charProgress = charFloatIndex - charIndex;
}
}
@@ -319,11 +351,11 @@ namespace BetterLyrics.WinUI3.ViewModels
_isPlaying = e.IsPlaying;
}
private void PlaybackService_PositionChanged(object? sender, PositionChangedEventArgs e)
private void PlaybackService_TimelineChanged(object? sender, TimelineChangedEventArgs e)
{
if (Math.Abs(_totalTime.TotalMilliseconds - e.Position.TotalMilliseconds) >= _timelineSyncThreshold)
if (Math.Abs(TotalTime.TotalMilliseconds - e.Position.TotalMilliseconds) >= _timelineSyncThreshold)
{
_totalTime = e.Position;
TotalTime = e.Position;
}
}
@@ -349,7 +381,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
await RefreshLyricsAsync(token);
});
_totalTime = TimeSpan.Zero;
TotalTime = TimeSpan.Zero;
}
}
@@ -374,6 +406,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private void UpdateTranslations()
{
TranslationSearchProvider = null;
_lyricsDataArr.ElementAtOrDefault(0)?.SetDisplayedTextInOriginalText();
_isLayoutChanged = true;
@@ -400,7 +433,9 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_logger.LogInformation("Showing translation for lyrics...");
string targetLangCode = LanguageHelper.GetUserTargetLanguageCode();
string originalText = _lyricsDataArr[0].WrappedOriginalText;
string? originalText = _lyricsDataArr.FirstOrDefault()?.WrappedOriginalText;
if (originalText == null) return;
string? originalLangCode = LanguageHelper.DetectLanguageCode(originalText);
if (originalLangCode == targetLangCode)
@@ -421,56 +456,66 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(_lyricsDataArr[found]);
_lyricsDataArr[0].SetDisplayedTextAlongWith(_lyricsDataArr[found], 50);
_langIndex = 0;
}
TranslationSearchProvider = LyricsSearchProvider.ToTranslationSearchProvider();
}
else
else if (_isLibreTranslateEnabled)
{
string translated = string.Empty;
try
{
var translated = await _translateService.TranslateTextAsync(originalText, targetLangCode, token);
token.ThrowIfCancellationRequested();
translated = await _translateService.TranslateTextAsync(originalText, targetLangCode, token);
if (translated == string.Empty) return;
if (_showTranslationOnly)
{
// TODO
_lyricsDataArr[0].SetDisplayedTextAlongWith(translated);
_langIndex = 0;
_lyricsDataArr[^1] = _lyricsDataArr[0].CreateLyricsDataFrom(translated);
_lyricsDataArr[^1].SetDisplayedTextInOriginalText();
_langIndex = _lyricsDataArr.Count - 1;
}
else
{
_lyricsDataArr[0].SetDisplayedTextAlongWith(translated);
_langIndex = 0;
}
TranslationSearchProvider = Enums.TranslationSearchProvider.LibreTranslate;
token.ThrowIfCancellationRequested();
}
catch (Exception)
{
App.Current.LyricsWindowNotificationPanel?.Notify(App.ResourceLoader?.GetString("LibreTranslateFailed")!, Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
}
catch (Exception) { }
}
}
}
private async Task RefreshLyricsAsync(CancellationToken token)
{
LyricsSearchProvider = null;
_logger.LogInformation("Refreshing lyrics...");
_lyricsDataArr = [LyricsData.GetLoadingPlaceholder()];
_isLayoutChanged = true;
string? lyricsRaw = null;
LyricsSearchProvider? provider = null;
LyricsSearchProvider? lyricsSearchProvider = null;
if (SongInfo != null)
{
(lyricsRaw, provider) = await _lyrcsSearchService.SearchAsync(
(lyricsRaw, lyricsSearchProvider) = await _lyrcsSearchService.SearchAsync(
SongInfo.Title,
SongInfo.Artist,
SongInfo.Album ?? "",
SongInfo.DurationMs ?? 0,
token
);
LyricsSearchProvider = lyricsSearchProvider;
_logger.LogInformation("Lyrics search result: {LyricsRaw}", lyricsRaw ?? "null");
token.ThrowIfCancellationRequested();
_lyricsDataArr = new LyricsParser().Parse(lyricsRaw, (int?)SongInfo?.DurationMs);
FillTranslationFromCache(provider);
FillTranslationFromCache(LyricsSearchProvider);
}
else
{
@@ -491,24 +536,25 @@ namespace BetterLyrics.WinUI3.ViewModels
string? translationRaw = null;
switch (provider)
{
case LyricsSearchProvider.QQ:
case Enums.LyricsSearchProvider.QQ:
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.QQTranslationCacheDirectory);
break;
case LyricsSearchProvider.Kugou:
case Enums.LyricsSearchProvider.Kugou:
break;
case LyricsSearchProvider.Netease:
case Enums.LyricsSearchProvider.Netease:
translationRaw = FileHelper.ReadLyricsCache(SongInfo!.Title, SongInfo.Artist, LyricsFormat.Lrc, PathHelper.NeteaseTranslationCacheDirectory);
break;
case LyricsSearchProvider.LrcLib:
case Enums.LyricsSearchProvider.LrcLib:
break;
case LyricsSearchProvider.AmllTtmlDb:
case Enums.LyricsSearchProvider.AmllTtmlDb:
break;
case LyricsSearchProvider.LocalMusicFile:
case Enums.LyricsSearchProvider.LocalMusicFile:
break;
case LyricsSearchProvider.LocalLrcFile:
case Enums.LyricsSearchProvider.LocalLrcFile:
break;
case LyricsSearchProvider.LocalEslrcFile:
case Enums.LyricsSearchProvider.LocalEslrcFile:
break;
case LyricsSearchProvider.LocalTtmlFile:
case Enums.LyricsSearchProvider.LocalTtmlFile:
break;
default:
break;
@@ -516,10 +562,20 @@ namespace BetterLyrics.WinUI3.ViewModels
if (translationRaw != null)
{
var translationData = new LyricsParser().Parse(translationRaw, (int?)SongInfo?.DurationMs);
foreach (var data in translationData)
if (provider == Enums.LyricsSearchProvider.QQ)
{
data.LyricsLines = data.LyricsLines.Where(line => !string.IsNullOrWhiteSpace(line.OriginalText)).ToList();
foreach (var data in translationData)
{
foreach (var item in data.LyricsLines)
{
if (item.OriginalText == "//")
{
item.OriginalText = "";
}
}
}
}
_lyricsDataArr = _lyricsDataArr.Concat(translationData).ToList();
}
}

View File

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

View File

@@ -0,0 +1,420 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
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 CommunityToolkit.WinUI;
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;
// All songs
private List<Track> _tracks = [];
// Songs in current playlist
private List<Track> _playlistTracks = [];
// Filtered songs based on search query for current playlist
private List<Track> _filteredTracks = [];
[ObservableProperty]
public partial bool IsLocalMediaNotFound { get; set; }
/// <summary>
/// Grouped tracks after filtering and sorting for current playlist
/// </summary>
[ObservableProperty]
public partial ObservableCollection<GroupInfoList> GroupedTracks { get; set; } = [];
[ObservableProperty]
public partial List<Track> SelectedTracks { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; } = [];
public PlayQueueItem? PlayingQueueItem => TrackPlayingQueue.ElementAtOrDefault(PlayingSongIndex);
[ObservableProperty]
public partial PlaybackOrder PlaybackOrder { get; set; }
[ObservableProperty]
public partial CommonSongProperty SongOrderType { get; set; } = CommonSongProperty.Title;
[ObservableProperty]
public partial ObservableCollection<SongsTabInfo> SongsTabInfoList { get; set; } = [];
[ObservableProperty]
public partial int SelectedSongsTabInfoIndex { get; set; } = 0;
public SongsTabInfo? SelectedSongsTabInfo => SongsTabInfoList.ElementAtOrDefault(SelectedSongsTabInfoIndex);
[ObservableProperty]
public partial bool IsDataLoading { get; set; } = false;
[ObservableProperty]
public partial Track TrackRightTapped { get; set; } = new();
[ObservableProperty]
public partial int PlayingSongIndex { get; set; } = -1;
[ObservableProperty]
public partial int DisplayedPlayingSongIndex { get; set; } = 0;
[ObservableProperty]
public partial string SongSearchQuery { get; set; } = string.Empty;
public MusicGalleryViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService) : base(settingsService)
{
SongsTabInfoList.Add(new SongsTabInfo(App.ResourceLoader!.GetString("MusicGalleryPageAllSongs"), "\uE8A9", false, CommonSongProperty.Title, string.Empty));
RefreshSongs();
PlaybackOrder = _settingsService.PlaybackOrder;
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
_timelineController = _mediaPlayer.TimelineController = new();
_timelineController.PositionChanged += TimelineController_PositionChanged;
_smtc = _mediaPlayer.SystemMediaTransportControls;
_mediaPlayer.CommandManager.IsEnabled = false;
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
_smtc.ButtonPressed += Smtc_ButtonPressed;
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
_libWatcherService = libWatcherService;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
}
private void MediaPlayer_MediaEnded(MediaPlayer sender, object args)
{
PlayNextTrack();
}
public void PlayNextTrack()
{
switch (PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (PlayingSongIndex < TrackPlayingQueue.Count - 1)
{
PlayingSongIndex++;
}
else
{
PlayingSongIndex = 0;
}
PlayTrack(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (TrackPlayingQueue.Count > 0)
{
PlayingSongIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
PlayTrack(PlayingQueueItem);
});
break;
default:
break;
}
}
private void PlayPreviousTrack()
{
switch (PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (PlayingSongIndex > 0)
{
PlayingSongIndex--;
}
else
{
PlayingSongIndex = TrackPlayingQueue.Count - 1;
}
PlayTrack(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (TrackPlayingQueue.Count > 0)
{
PlayingSongIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
PlayTrack(PlayingQueueItem);
});
break;
default:
break;
}
}
private void Smtc_PlaybackPositionChangeRequested(SystemMediaTransportControls sender, PlaybackPositionChangeRequestedEventArgs args)
{
_timelineController.Position = args.RequestedPlaybackPosition;
}
private void MediaPlayer_MediaOpened(MediaPlayer sender, object args)
{
_timelineController.Start();
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
}
private void TimelineController_PositionChanged(MediaTimelineController sender, object args)
{
_smtc.UpdateTimelineProperties(new SystemMediaTransportControlsTimelineProperties()
{
Position = sender.Position,
EndTime = _mediaPlayer.PlaybackSession.NaturalDuration
});
}
private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
switch (args.Button)
{
case SystemMediaTransportControlsButton.Play:
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
_timelineController.Resume();
break;
case SystemMediaTransportControlsButton.Pause:
_smtc.PlaybackStatus = MediaPlaybackStatus.Paused;
_timelineController.Pause();
break;
case SystemMediaTransportControlsButton.Next:
PlayNextTrack();
break;
case SystemMediaTransportControlsButton.Previous:
PlayPreviousTrack();
break;
}
}
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, Events.LibChangedEventArgs e)
{
RefreshSongs();
}
public void RefreshSongs()
{
_dispatcherQueueTimer.Debounce(() =>
{
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);
if (track.Duration <= 0) continue;
_tracks.Add(track);
}
}
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
ApplyPlaylist();
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
IsDataLoading = false;
});
});
}, TimeSpan.FromMilliseconds(100));
}
public void ApplyPlaylist()
{
if (SelectedSongsTabInfo?.FilterValue == string.Empty)
{
_playlistTracks = _tracks;
}
else
{
switch (SelectedSongsTabInfo?.FilterProperty)
{
case CommonSongProperty.Title:
_playlistTracks = _tracks.Where(t => t.Title.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
case CommonSongProperty.Album:
_playlistTracks = _tracks.Where(t => t.Album.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
case CommonSongProperty.Artist:
_playlistTracks = _tracks.Where(t => t.Artist.Equals(SelectedSongsTabInfo.FilterValue, StringComparison.OrdinalIgnoreCase)).ToList();
break;
default:
break;
}
}
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
}
public void ApplySongSearchQuery()
{
if (string.IsNullOrWhiteSpace(SongSearchQuery))
{
_filteredTracks = _playlistTracks;
return;
}
_filteredTracks = _playlistTracks.Where(t =>
t.Title.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Artist.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase) ||
t.Album.Contains(SongSearchQuery, StringComparison.OrdinalIgnoreCase)).ToList();
}
private void ApplySongOrderType()
{
switch (SongOrderType)
{
case CommonSongProperty.Title:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Title),
o => ((Track)o).Title
);
break;
case CommonSongProperty.Artist:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Artist),
o => ((Track)o).Artist
);
break;
case CommonSongProperty.Album:
GroupedTracks = _filteredTracks.GetGroupedBy(
t => LanguageHelper.GetOrderChar(t.Album),
o => ((Track)o).Album
);
break;
}
}
public void UpdateSelectedPlaylist(SongsTabInfo playlist)
{
var found = SongsTabInfoList.Where(x => x.FilterProperty == playlist.FilterProperty && x.FilterValue == playlist.FilterValue)
.ToList().FirstOrDefault();
if (found == null)
{
SongsTabInfoList.Add(playlist);
SelectedSongsTabInfoIndex = SongsTabInfoList.Count - 1;
}
else
{
SelectedSongsTabInfoIndex = SongsTabInfoList.IndexOf(found);
}
ApplyPlaylist();
}
public void PlayTrackAt(int index)
{
PlayTrack(TrackPlayingQueue.ElementAtOrDefault(index));
}
public void PlayTrack(PlayQueueItem? playQueueItem)
{
_timelineController.Pause();
_mediaPlayer.Source = null;
if (playQueueItem == null)
{
_smtc.IsEnabled = false;
}
else
{
var track = playQueueItem.Track;
var updater = _smtc.DisplayUpdater;
_smtc.IsEnabled = true;
_mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(track.Path));
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);
}
else
{
updater.Thumbnail = null;
}
updater.Update();
}
}
partial void OnSongOrderTypeChanged(CommonSongProperty value)
{
ApplySongOrderType();
IsLocalMediaNotFound = !_filteredTracks.Any();
}
partial void OnSongSearchQueryChanged(string value)
{
ApplySongSearchQuery();
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
}
partial void OnPlayingSongIndexChanged(int value)
{
DisplayedPlayingSongIndex = value + 1;
}
partial void OnPlaybackOrderChanged(PlaybackOrder value)
{
_settingsService.PlaybackOrder = value;
}
public void Receive(PropertyChangedMessage<ObservableCollection<LocalMediaFolder>> message)
{
if (message.Sender is SettingsPageViewModel)
{
if (message.PropertyName == nameof(SettingsPageViewModel.LocalMediaFolders))
{
RefreshSongs();
}
}
}
}
}

View File

@@ -0,0 +1,91 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsPageViewModel
{
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, IPlaybackService playbackService, ITranslateService libreTranslateService) : base(settingsService)
{
_libWatcherService = libWatcherService;
_playbackService = playbackService;
_libreTranslateService = libreTranslateService;
IsLibreTranslateEnabled = _settingsService.IsLibreTranslateEnabled;
LibreTranslateServer = _settingsService.LibreTranslateServer;
SelectedTargetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
LocalMediaFolders = [.. _settingsService.LocalMediaFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
AlbumArtSearchProvidersInfo = [.. _settingsService.AlbumArtSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
AutoStartWindowType = _settingsService.AutoStartWindowType;
AutoLockOnDesktopMode = _settingsService.AutoLockOnDesktopMode;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
CoverOverlayOpacity = _settingsService.CoverOverlayOpacity;
CoverOverlayBlurAmount = _settingsService.CoverOverlayBlurAmount;
CoverAcrylicEffectAmount = _settingsService.CoverAcrylicEffectAmount;
LyricsAlignmentType = _settingsService.LyricsAlignmentType;
SongInfoAlignmentType = _settingsService.SongInfoAlignmentType;
LyricsFontWeight = _settingsService.LyricsFontWeight;
LyricsBlurAmount = _settingsService.LyricsBlurAmount;
LyricsVerticalEdgeOpacity = _settingsService.LyricsVerticalEdgeOpacity;
LyricsLineSpacingFactor = _settingsService.LyricsLineSpacingFactor;
// Font size
LyricsStandardFontSize = _settingsService.LyricsStandardFontSize;
LyricsDockFontSize = _settingsService.LyricsDockFontSize;
LyricsDesktopFontSize = _settingsService.LyricsDesktopFontSize;
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;
}
}
}

View File

@@ -0,0 +1,227 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Xaml;
using Windows.Globalization;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsPageViewModel
{
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 OnCoverAcrylicEffectAmountChanged(int value)
{
_settingsService.CoverAcrylicEffectAmount = 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)
{
case Enums.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Enums.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Enums.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Enums.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
case Enums.Language.Japanese:
ApplicationLanguages.PrimaryLanguageOverride = "ja-JP";
break;
case Enums.Language.Korean:
ApplicationLanguages.PrimaryLanguageOverride = "ko-KR";
break;
default:
break;
}
_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 OnLyricsStandardFontSizeChanged(int value)
{
_settingsService.LyricsStandardFontSize = value;
}
partial void OnLyricsDockFontSizeChanged(int value)
{
_settingsService.LyricsDockFontSize = value;
}
partial void OnLyricsDesktopFontSizeChanged(int value)
{
_settingsService.LyricsDesktopFontSize = 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();
}
}
partial void OnIsLibreTranslateEnabledChanged(bool value)
{
_settingsService.IsLibreTranslateEnabled = value;
}
}
}

View File

@@ -0,0 +1,237 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class SettingsPageViewModel
{
public string Version { get; set; } = MetadataHelper.AppVersion;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial bool IsLibreTranslateEnabled { get; set; }
[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; }
[ObservableProperty]
public partial bool AutoLockOnDesktopMode { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayOpacity { get; set; }
[ObservableProperty]
[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; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverAcrylicEffectAmount { get; set; }
[ObservableProperty]
public partial Enums.Language Language { get; set; }
[ObservableProperty]
public partial ObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; }
[ObservableProperty]
public partial ObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { 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 LyricsStandardFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsDockFontSize { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int LyricsDesktopFontSize { 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; }
[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; }
}
}

View File

@@ -7,24 +7,17 @@ using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using ShadowViewer.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Globalization;
using Windows.Media.Playback;
using Windows.System;
using Windows.UI;
using Windows.UI.Popups;
using WinRT.Interop;
using MetadataHelper = BetterLyrics.WinUI3.Helper.MetadataHelper;
@@ -38,262 +31,11 @@ namespace BetterLyrics.WinUI3.ViewModels
private readonly string _autoStartupTaskId = "AutoStartup";
public SettingsPageViewModel(ISettingsService settingsService, ILibWatcherService libWatcherService, IPlaybackService playbackService, ITranslateService libreTranslateService) : base(settingsService)
{
_libWatcherService = libWatcherService;
_playbackService = playbackService;
_libreTranslateService = libreTranslateService;
LibreTranslateServer = _settingsService.LibreTranslateServer;
SelectedTargetLanguageIndex = _settingsService.SelectedTargetLanguageIndex;
LocalLyricsFolders = [.. _settingsService.LocalLyricsFolders];
LyricsSearchProvidersInfo = [.. _settingsService.LyricsSearchProvidersInfo];
AlbumArtSearchProvidersInfo = [.. _settingsService.AlbumArtSearchProvidersInfo];
Language = _settingsService.Language;
CoverImageRadius = _settingsService.CoverImageRadius;
AutoStartWindowType = _settingsService.AutoStartWindowType;
AutoLockOnDesktopMode = _settingsService.AutoLockOnDesktopMode;
IsDynamicCoverOverlayEnabled = _settingsService.IsDynamicCoverOverlayEnabled;
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;
_playbackService.MediaSourceProvidersInfoChanged += PlaybackService_SessionIdsChanged;
Task.Run(async () =>
{
BuildDate = (await Helper.MetadataHelper.GetBuildDate()).ToString("(yyyy/MM/dd HH:mm:ss)");
});
}
private void PlaybackService_SessionIdsChanged(object? sender, Events.MediaSourceProvidersInfoEventArgs e)
{
MediaSourceProvidersInfo = [.. e.MediaSourceProviersInfo];
}
[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; }
[ObservableProperty]
public partial bool AutoLockOnDesktopMode { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverImageRadius { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayBlurAmount { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial int CoverOverlayOpacity { get; set; }
[ObservableProperty]
[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; }
[ObservableProperty]
public partial Enums.Language Language { get; set; }
[ObservableProperty]
public partial ObservableCollection<LocalLyricsFolder> LocalLyricsFolders { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<LyricsSearchProviderInfo> LyricsSearchProvidersInfo { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ObservableCollection<AlbumArtSearchProviderInfo> AlbumArtSearchProvidersInfo { get; set; }
[ObservableProperty]
public partial ObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { 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; }
[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; }
public void OnLyricsSearchProvidersReordered()
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
@@ -314,21 +56,21 @@ namespace BetterLyrics.WinUI3.ViewModels
);
}
public void RemoveFolderAsync(LocalLyricsFolder folder)
public void RemoveFolderAsync(LocalMediaFolder folder)
{
LocalLyricsFolders.Remove(folder);
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
LocalMediaFolders.Remove(folder);
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
_libWatcherService.UpdateWatchers([.. LocalMediaFolders]);
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
public void ToggleLocalLyricsFolder(LocalLyricsFolder folder)
public void ToggleLocalLyricsFolder()
{
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
public void ToggleLyricsSearchProvider(LyricsSearchProviderInfo providerInfo)
public void ToggleLyricsSearchProvider()
{
_settingsService.LyricsSearchProvidersInfo = [.. LyricsSearchProvidersInfo];
Broadcast(
@@ -361,16 +103,16 @@ namespace BetterLyrics.WinUI3.ViewModels
{
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (LocalLyricsFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
if (LocalMediaFolders.Any(x => Path.GetFullPath(x.Path).TrimEnd(Path.DirectorySeparatorChar).Equals(normalizedPath.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)))
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathExistedInfo"));
}
else if (LocalLyricsFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
else if (LocalMediaFolders.Any(item => normalizedPath.StartsWith(Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
{
// 添加的文件夹是现有文件夹的子文件夹
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPagePathBeIncludedInfo"));
}
else if (LocalLyricsFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
else if (LocalMediaFolders.Any(item => Path.GetFullPath(item.Path).TrimEnd(Path.DirectorySeparatorChar).StartsWith(normalizedPath, StringComparison.OrdinalIgnoreCase))
)
{
// 添加的文件夹是现有文件夹的父文件夹
@@ -378,23 +120,23 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
LocalLyricsFolders.Add(new LocalLyricsFolder(path, true));
_settingsService.LocalLyricsFolders = [.. LocalLyricsFolders];
_libWatcherService.UpdateWatchers([.. LocalLyricsFolders]);
Broadcast(LocalLyricsFolders, LocalLyricsFolders, nameof(LocalLyricsFolders));
LocalMediaFolders.Add(new LocalMediaFolder(path, true));
_settingsService.LocalMediaFolders = [.. LocalMediaFolders];
_libWatcherService.UpdateWatchers([.. LocalMediaFolders]);
Broadcast(LocalMediaFolders, LocalMediaFolders, nameof(LocalMediaFolders));
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync()
{
await Launcher.LaunchUriAsync(new Uri(MetadataHelper.GithubUrl));
await Windows.System.Launcher.LaunchUriAsync(new Uri(MetadataHelper.GithubUrl));
}
[RelayCommand]
private static async Task OpenCacheFolderAsync()
{
await Launcher.LaunchFolderPathAsync(PathHelper.CacheFolder);
await Windows.System.Launcher.LaunchFolderPathAsync(PathHelper.CacheFolder);
}
[RelayCommand]
@@ -433,7 +175,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
string targetLangCode = LanguageHelper.SupportedTargetLanguages[SelectedTargetLanguageIndex].Code;
string result = await _libreTranslateService.TranslateTextAsync("Hello, world!", targetLangCode, null);
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestSuccessInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Success);
IsLibreTranslateServerTesting = false;
@@ -441,7 +183,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
catch (Exception)
{
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageServerTestFailedInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error);
IsLibreTranslateServerTesting = false;
@@ -457,7 +199,7 @@ namespace BetterLyrics.WinUI3.ViewModels
Task.Run(async () =>
{
bool testResult = await NetHelper.CheckConnectivity($"{LXMusicServer}/status");
_dispatcherQueue.TryEnqueue(() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString($"SettingsPageServerTest{(testResult ? "Success" : "Failed")}Info"),
@@ -498,177 +240,5 @@ namespace BetterLyrics.WinUI3.ViewModels
}
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)
{
case Enums.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Enums.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Enums.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Enums.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
case Enums.Language.Japanese:
ApplicationLanguages.PrimaryLanguageOverride = "ja-JP";
break;
case Enums.Language.Korean:
ApplicationLanguages.PrimaryLanguageOverride = "ko-KR";
break;
default:
break;
}
_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;
}
}
}

View File

@@ -5,6 +5,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using System;
namespace BetterLyrics.WinUI3.ViewModels
{
@@ -34,13 +36,30 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private static void ExitApp()
{
WindowHelper.ExitAllWindows();
LyricsWindow? lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
DockModeHelper.Disable(lyricsWindow);
}
Environment.Exit(0);
}
[RelayCommand]
private static void OpenSettings()
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
[RelayCommand]
private static void OpenMusicGallery()
{
WindowHelper.OpenWindow<MusicGalleryWindow>();
}
[RelayCommand]
private static void OpenLyricsWindow()
{
WindowHelper.OpenWindow<LyricsWindow>();
}
[RelayCommand]

View File

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

View File

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

View File

@@ -25,8 +25,8 @@
<!-- Top command -->
<Grid
x:Name="TopCommandGrid"
Margin="12"
VerticalAlignment="Top"
Background="Transparent"
Opacity="{x:Bind ViewModel.TopCommandGridOpacity, Mode=OneWay}"
PointerEntered="TopCommandGrid_PointerEntered"
PointerExited="TopCommandGrid_PointerExited">
@@ -34,10 +34,7 @@
<ScalarTransition />
</Grid.OpacityTransition>
<Grid
Padding="3"
HorizontalAlignment="Left"
Style="{StaticResource CardGridStyle}">
<Grid Padding="3" HorizontalAlignment="Left">
<StackPanel
x:Name="TopLeftCommandStackPanel"
HorizontalAlignment="Left"
@@ -46,6 +43,15 @@
<StackPanel.OpacityTransition>
<ScalarTransition />
</StackPanel.OpacityTransition>
<!-- Music gallery -->
<Button Click="MusicGalleryButton_Click" Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="{x:Bind ViewModel.TitleBarFontSize}"
Glyph="&#xE8A9;" />
</Button>
<!-- Immersive mode -->
<ToggleButton
x:Name="ImmersiveButton"
Command="{x:Bind ViewModel.ImmersiveToggleButtonEnabledChangedCommand}"
@@ -59,10 +65,7 @@
</StackPanel>
</Grid>
<Grid
Padding="3"
HorizontalAlignment="Right"
Style="{StaticResource CardGridStyle}">
<Grid Padding="3" HorizontalAlignment="Right">
<StackPanel
x:Name="TopRightCommandStackPanel"
Orientation="Horizontal"
@@ -74,7 +77,7 @@
<!-- Look -->
<Button
x:Name="ClickThroughButton"
Command="{x:Bind ViewModel.ToggleLockWindowCommand}"
Click="ClickThroughButton_Click"
Style="{StaticResource TitleBarButtonStyle}">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
@@ -121,12 +124,12 @@
<ToggleMenuFlyoutItem
x:Name="DockFlyoutItem"
x:Uid="HostWindowDockFlyoutItem"
Command="{x:Bind ViewModel.ToggleDockModeCommand}"
Click="DockFlyoutItem_Click"
IsChecked="{x:Bind ViewModel.IsDockMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="DesktopFlyoutItem"
x:Uid="HostWindowDesktopFlyoutItem"
Command="{x:Bind ViewModel.ToggleDesktopModeCommand}"
Click="DesktopFlyoutItem_Click"
IsChecked="{x:Bind ViewModel.IsDesktopMode, Mode=OneWay}" />
<ToggleMenuFlyoutItem
x:Name="MiniFlyoutItem"

View File

@@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using System;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
@@ -28,18 +29,32 @@ namespace BetterLyrics.WinUI3.Views
{
this.InitializeComponent();
AppWindow.SetIcons();
AppWindow.Changed += AppWindow_Changed;
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
Title = App.ResourceLoader!.GetString("LyricsPageTitle");
SetTitleBar(TopCommandGrid);
//SetTitleBar(RootGrid);
UpdateTitleBarArea();
_wmm = new WindowMessageMonitor(this);
_wmm.WindowMessageReceived += Wmm_WindowMessageReceived;
}
public void UpdateTitleBarArea()
{
if (_settingsService.IsDragEverywhereEnabled)
{
SetTitleBar(RootGrid);
}
else
{
SetTitleBar(TopCommandGrid);
}
}
private void Wmm_WindowMessageReceived(object? sender, WindowMessageEventArgs e)
{
if (e.Message.MessageId == (uint)User32.WindowMessage.WM_HOTKEY)
@@ -57,6 +72,14 @@ namespace BetterLyrics.WinUI3.Views
switch (type!)
{
case AutoStartWindowType.StandardMode:
if (_settingsService.StandardWindowLeft < 0 || _settingsService.StandardWindowTop < 0 ||
_settingsService.StandardWindowWidth <= 0 || _settingsService.StandardWindowHeight <= 0)
{
_settingsService.StandardWindowLeft = 200;
_settingsService.StandardWindowTop = 200;
_settingsService.StandardWindowWidth = 1600;
_settingsService.StandardWindowHeight = 800;
}
AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(
_settingsService.StandardWindowLeft,
_settingsService.StandardWindowTop,
@@ -97,25 +120,27 @@ namespace BetterLyrics.WinUI3.Views
var rect = AppWindow.Position;
var size = AppWindow.Size;
if (ViewModel.IsDesktopMode)
{
_settingsService.DesktopWindowLeft = rect.X;
_settingsService.DesktopWindowTop = rect.Y;
_settingsService.DesktopWindowWidth = size.Width;
_settingsService.DesktopWindowHeight = size.Height;
}
else if (ViewModel.IsDockMode)
if (rect.X >= 0 && rect.Y >= 0 && size.Width > 0 && size.Height > 0)
{
if (ViewModel.IsDesktopMode)
{
_settingsService.DesktopWindowLeft = rect.X;
_settingsService.DesktopWindowTop = rect.Y;
_settingsService.DesktopWindowWidth = size.Width;
_settingsService.DesktopWindowHeight = size.Height;
}
else if (ViewModel.IsDockMode)
{
}
else
{
_settingsService.StandardWindowLeft = rect.X;
_settingsService.StandardWindowTop = rect.Y;
_settingsService.StandardWindowWidth = size.Width;
_settingsService.StandardWindowHeight = size.Height;
}
}
else
{
_settingsService.StandardWindowLeft = rect.X;
_settingsService.StandardWindowTop = rect.Y;
_settingsService.StandardWindowWidth = size.Width;
_settingsService.StandardWindowHeight = size.Height;
}
}
}
@@ -152,13 +177,11 @@ namespace BetterLyrics.WinUI3.Views
private void SettingsMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenOrShowWindow<SettingsWindow>();
WindowHelper.OpenWindow<SettingsWindow>();
}
private void UpdateTitleBarWindowButtonsVisibility()
{
TopCommandGrid.Margin = new Thickness(12);
switch (AppWindow.Presenter.Kind)
{
case AppWindowPresenterKind.Default:
@@ -197,7 +220,6 @@ namespace BetterLyrics.WinUI3.Views
Visibility.Collapsed;
ViewModel.IsImmersiveMode = true;
TopCommandGrid.Margin = new Thickness();
}
else if (DesktopFlyoutItem.IsChecked)
{
@@ -250,7 +272,8 @@ namespace BetterLyrics.WinUI3.Views
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
WindowHelper.ExitAllWindows();
DockModeHelper.Disable(this);
Environment.Exit(0);
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e)
@@ -301,27 +324,38 @@ namespace BetterLyrics.WinUI3.Views
private void RootGrid_PointerEntered(object sender, PointerRoutedEventArgs e)
{
ViewModel.IsMouseWithinWindow = true;
e.Handled = true;
}
private void RootGrid_PointerExited(object sender, PointerRoutedEventArgs e)
{
ViewModel.IsMouseWithinWindow = false;
e.Handled = true;
}
private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (ClickThroughButton == null) return;
}
// <20><>ȡ<EFBFBD><C8A1><EFBFBD>ؼ<EFBFBD><D8BC>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD>е<EFBFBD>λ<EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڴ<EFBFBD><DAB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͻǣ<CFBD>
var transform = ClickThroughButton.TransformToVisual(Content);
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
var btnRect = new Rectangle(
(int)point.X,
(int)point.Y,
(int)ClickThroughButton.ActualWidth,
(int)ClickThroughButton.ActualHeight
);
DesktopModeHelper.SetInteractiveRects([btnRect]);
private void MusicGalleryButton_Click(object sender, RoutedEventArgs e)
{
WindowHelper.OpenWindow<MusicGalleryWindow>();
}
private void ClickThroughButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.ToggleLockWindowCommand.Execute(null);
}
private void DockFlyoutItem_Click(object sender, RoutedEventArgs e)
{
ViewModel.ToggleDockModeCommand.Execute(null);
}
private void DesktopFlyoutItem_Click(object sender, RoutedEventArgs e)
{
ViewModel.ToggleDesktopModeCommand.Execute(null);
UpdateTitleBarWindowButtonsVisibility();
}
}
}

View File

@@ -0,0 +1,425 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="BetterLyrics.WinUI3.Views.MusicGalleryPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:atl="using:ATL"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:labs="using:CommunityToolkit.Labs.WinUI"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:templateselector="using:BetterLyrics.WinUI3.TemplateSelector"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Page.Resources>
<CollectionViewSource
x:Name="GroupedTracksCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.GroupedTracks, Mode=OneWay}" />
</Page.Resources>
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid
x:Name="SongViewer"
Grid.Column="0"
Padding="12">
<Grid.Tag>
<Flyout
x:Name="SongFileInfoFlyout"
Placement="Bottom"
ShouldConstrainToRootBounds="False">
<ScrollViewer Width="300" Height="300">
<StackPanel Spacing="12">
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoTitle" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Title, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileArtist" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Artist, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileAlbum" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Album, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoYear" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Year, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoDuration" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoBitrate" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Bitrate, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoSampleRate" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.SampleRate, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoBitDepth" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.BitDepth, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoFormat" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.AudioFormat.Name, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoEncoder" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="{x:Bind ViewModel.TrackRightTapped.Encoder, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock x:Uid="MusicGalleryPageFileInfoPath" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<HyperlinkButton
Margin="-12,0,0,0"
Click="SongPathHyperlinkButton_Click"
Content="{x:Bind ViewModel.TrackRightTapped.Path, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Flyout>
</Grid.Tag>
<ListView
VerticalAlignment="Top"
ItemsSource="{x:Bind ViewModel.SongsTabInfoList, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
SelectedIndex="{x:Bind ViewModel.SelectedSongsTabInfoIndex, Mode=TwoWay}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:SongsTabInfo">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Background="Transparent"
ColumnSpacing="6"
Tapped="PlaylistGrid_Tapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="{Binding Icon}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{Binding Name}" />
</Grid>
<Button
Grid.Column="1"
Click="PlaylistCloseButton_Click"
Content="{ui:FontIcon FontSize=10,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8BB;}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{Binding IsClosable, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Margin="0,48,0,0" VerticalAlignment="Top">
<AutoSuggestBox
x:Name="SongSearchBox"
x:Uid="MusicGalleryPageSongSearchBox"
HorizontalAlignment="Stretch"
QueryIcon="Find"
Text="{x:Bind ViewModel.SongSearchQuery, Mode=TwoWay}" />
</Grid>
<Grid Margin="0,96,0,0" VerticalAlignment="Top">
<StackPanel
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="12">
<ToggleButton
x:Name="SelectAllToggleButton"
x:Uid="MusicGalleryPageSelectAll"
Click="SelectAllToggleButton_Click" />
<Button x:Uid="MusicGalleryPageAddToPlayingQueue">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem x:Uid="MusicGalleryPageAddToNext" Click="AddSongToQueueNextMenuFlyoutItem_Click" />
<MenuFlyoutItem x:Uid="MusicGalleryPageAddToEnd" Click="AddSongToQueueEndMenuFlyoutItem_Click" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button x:Uid="MusicGalleryPageAddToCustomList" Visibility="Collapsed">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem x:Uid="MusicGalleryPageNewPlaylist" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE710;}" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
<StackPanel
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock
x:Uid="MusicGalleryPageSortType"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<controls:Segmented
x:Name="Segmented"
SelectedIndex="{x:Bind ViewModel.SongOrderType, Converter={StaticResource EnumToIntConverter}, Mode=TwoWay}"
SelectionMode="Single">
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByTitle" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEC4F;}" />
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByAlbum" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE93C;}" />
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByArtist" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEFA9;}" />
</controls:Segmented>
</StackPanel>
</StackPanel>
</Grid>
<SemanticZoom Margin="0,140,0,0">
<SemanticZoom.ZoomedInView>
<ListView
x:Name="SongListView"
ItemsSource="{x:Bind GroupedTracksCVS.View, Mode=OneWay}"
SelectionChanged="SongListView_SelectionChanged"
SelectionMode="Multiple">
<ListView.ItemTemplate>
<DataTemplate x:DataType="atl:Track">
<Grid Padding="12" RightTapped="SongListViewItemGrid_RightTapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<!-- 歌曲名 -->
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{Binding Title}"
TextWrapping="Wrap" />
<!-- 歌手名 -->
<HyperlinkButton
Grid.Column="1"
VerticalAlignment="Center"
Click="ArtistHyperlibkButton_Click"
Tag="{Binding Artist}">
<TextBlock Text="{Binding Artist}" TextWrapping="Wrap" />
</HyperlinkButton>
<!-- 专辑名 -->
<HyperlinkButton
Grid.Column="2"
VerticalAlignment="Center"
Click="AlbumHyperlibkButton_Click"
Tag="{Binding Album}">
<TextBlock Text="{Binding Album}" TextWrapping="Wrap" />
</HyperlinkButton>
<!-- 年份 -->
<TextBlock
Grid.Column="3"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Year}"
TextWrapping="Wrap" />
<!-- 歌曲时长 -->
<TextBlock
Grid.Column="4"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Duration, Converter={StaticResource SecondsToFormattedTimeConverter}}"
TextAlignment="Right"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="models:GroupInfoList">
<Border AutomationProperties.AccessibilityView="Raw">
<TextBlock
AutomationProperties.AccessibilityView="Raw"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{Binding}" />
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<GridView
MaxWidth="500"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{x:Bind GroupedTracksCVS.View.CollectionGroups, Mode=OneWay}"
ScrollViewer.IsHorizontalScrollChainingEnabled="False"
SelectionMode="None">
<GridView.ItemTemplate>
<DataTemplate x:DataType="models:GroupInfoList">
<TextBlock Style="{ThemeResource TitleTextBlockStyle}" Text="{Binding}" />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
<Grid Margin="0,140,0,0" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="200" Source="/Assets/EmptyState.png" />
<TextBlock
x:Uid="MusicGalleryPageFileNotFound"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
<Grid
x:Name="PlayQueue"
Grid.Column="1"
Margin="0,0,12,0">
<StackPanel Margin="0,10,0,0" Spacing="12">
<Grid Margin="0,6,0,0" VerticalAlignment="Top">
<TextBlock x:Uid="MusicGalleryPagePlayingQueue" Style="{StaticResource BodyStrongTextBlockStyle}" />
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.DisplayedPlayingSongIndex, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="12">
<Button x:Uid="MusicGalleryPageEmptyPlayingQueue" Click="EmptyPlayingQueueButton_Click" />
<Button x:Uid="MusicGalleryPageScrollToPlayingItem" Click="ScrollToPlayingItemButton_Click" />
</StackPanel>
<controls:Segmented HorizontalAlignment="Stretch" SelectedIndex="{x:Bind ViewModel.PlaybackOrder, Converter={StaticResource EnumToIntConverter}, Mode=TwoWay}">
<controls:Segmented.Items>
<controls:SegmentedItem x:Uid="MusicGalleryPageQueueLoop" />
<controls:SegmentedItem x:Uid="MusicGalleryPageSingleLoop" />
<controls:SegmentedItem x:Uid="MusicGalleryPageQueueRandom" />
</controls:Segmented.Items>
</controls:Segmented>
</StackPanel>
<ListView
x:Name="PlayingQueueListView"
Margin="0,136,0,0"
ItemsSource="{x:Bind ViewModel.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.PlayingSongIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel>
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{StaticResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
Click="RemoveFromPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Margin="0,136,0,0">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
</Grid>
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}" Visibility="{x:Bind ViewModel.IsDataLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<labs:Shimmer Grid.Row="0" CornerRadius="6" />
<labs:Shimmer Grid.Row="2" CornerRadius="6" />
<labs:Shimmer Grid.Row="4" CornerRadius="6" />
<labs:Shimmer Grid.Row="6" CornerRadius="6" />
</Grid>
<ProgressRing IsActive="{x:Bind ViewModel.IsDataLoading, Mode=OneWay}" />
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,171 @@
using ATL;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.System;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MusicGalleryPage : Page
{
public MusicGalleryViewModel ViewModel => (MusicGalleryViewModel)DataContext;
public MusicGalleryPage()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<MusicGalleryViewModel>();
}
private void SongListViewItemGrid_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
ViewModel.TrackRightTapped = (Track)((FrameworkElement)sender).DataContext;
SongFileInfoFlyout.ShowAt(sender as FrameworkElement);
}
private async void SongPathHyperlinkButton_Click(object sender, RoutedEventArgs e)
{
await LauncherHelper.SelectAndShowFile($"{((HyperlinkButton)sender).Content}");
}
private void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
ViewModel.PlayTrack(item);
PlayingQueueListView.ScrollIntoView(item);
}
private void EmptyPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.TrackPlayingQueue.Clear();
ViewModel.PlayingSongIndex = -1;
ViewModel.PlayTrackAt(ViewModel.PlayingSongIndex);
}
private void ScrollToPlayingItemButton_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.PlayingQueueItem == null) return;
PlayingQueueListView.ScrollIntoView(ViewModel.PlayingQueueItem);
}
private void RemoveFromPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
bool playNext = false;
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
int index = ViewModel.TrackPlayingQueue.IndexOf(item);
if (item == ViewModel.PlayingQueueItem)
{
playNext = true;
}
ViewModel.TrackPlayingQueue.Remove(item);
if (playNext)
{
if (ViewModel.TrackPlayingQueue.Count == 0)
{
index = -1;
}
else if (index >= ViewModel.TrackPlayingQueue.Count)
{
index = ViewModel.TrackPlayingQueue.Count - 1;
}
ViewModel.PlayingSongIndex = index;
ViewModel.PlayTrackAt(ViewModel.PlayingSongIndex);
}
}
private void SongFileInfoMenuFlyoutSubItem_Tapped(object sender, TappedRoutedEventArgs e)
{
SongFileInfoFlyout.ShowAt(sender as FrameworkElement);
}
private void AddSongToQueueNextMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.PlayingSongIndex + 1, SongListView.SelectedItems.Cast<Track>().Select(x => new PlayQueueItem(x)));
if (startPlaying)
{
ViewModel.PlayingSongIndex = ViewModel.PlayingSongIndex + 1;
ViewModel.PlayTrackAt(ViewModel.PlayingSongIndex);
}
}
private void AddSongToQueueEndMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
ViewModel.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<Track>().Select(x => new PlayQueueItem(x)));
if (startPlaying)
{
ViewModel.PlayingSongIndex = ViewModel.PlayingSongIndex + 1;
ViewModel.PlayTrackAt(ViewModel.PlayingSongIndex);
}
}
private void SongListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ViewModel.SelectedTracks = SongListView.SelectedItems.Cast<Track>().ToList();
SelectAllToggleButton.IsChecked = SongListView.SelectedItems.Count == SongListView.Items.Count;
}
private void SelectAllToggleButton_Click(object sender, RoutedEventArgs e)
{
if (SelectAllToggleButton.IsChecked == true)
{
SongListView.SelectAll();
}
else
{
SongListView.SelectedItems.Clear();
}
}
private void ArtistHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var artist = (string)((HyperlinkButton)sender).Tag;
var playlist = new SongsTabInfo(artist, "\uEFA9", true, CommonSongProperty.Artist, artist);
ViewModel.UpdateSelectedPlaylist(playlist);
}
private void AlbumHyperlibkButton_Click(object sender, RoutedEventArgs e)
{
var album = (string)((HyperlinkButton)sender).Tag;
var playlist = new SongsTabInfo(album, "\uE93C", true, CommonSongProperty.Album, album);
ViewModel.UpdateSelectedPlaylist(playlist);
}
private void PlaylistGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var playlist = (SongsTabInfo)((FrameworkElement)sender).DataContext;
ViewModel.UpdateSelectedPlaylist(playlist);
}
private void PlaylistCloseButton_Click(object sender, RoutedEventArgs e)
{
var playlist = (SongsTabInfo)((FrameworkElement)sender).DataContext;
ViewModel.SongsTabInfoList.Remove(playlist);
ViewModel.SelectedSongsTabInfoIndex = 0;
ViewModel.ApplyPlaylist();
}
}
}

View File

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

View File

@@ -0,0 +1,45 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Views
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MusicGalleryWindow : Window
{
public MusicGalleryWindow()
{
InitializeComponent();
Title = App.ResourceLoader?.GetString("MusicGalleryPageTitle");
AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
AppWindow.SetIcons();
AppWindow.Closing += AppWindow_Closing;
}
private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
}
}
}

View File

@@ -138,6 +138,14 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.IgnoreFullscreenWindow, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageHideWindow" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED1A;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.HideWindowWhenNotPlaying, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageGlobalDrag" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE7C2;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsDragEverywhereEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Desktop mode -->
<TextBlock x:Uid="SettingsPageAppDesktop" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -183,6 +191,25 @@
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageDockWindowHeight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xED5E;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.DockWindowHeight, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" px" />
<Slider
Maximum="200"
Minimum="64"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.DockWindowHeight, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<!-- Dock mode -->
<TextBlock x:Uid="SettingsPageAppDock" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -250,6 +277,24 @@
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageBackgroundAcrylicEffectAmount" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.CoverAcrylicEffectAmount, Mode=OneWay}" />
<Slider
Maximum="10"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.CoverAcrylicEffectAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</StackPanel>
</controls:Case>
@@ -306,18 +351,12 @@
CanDragItems="True"
CanReorderItems="True"
DragItemsCompleted="AlbumArtSearchProvidersListView_DragItemsCompleted"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.AlbumArtSearchProvidersInfo, Mode=OneWay}"
SelectionMode="None">
<ListView.OpacityTransition>
<ScalarTransition />
</ListView.OpacityTransition>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:AlbumArtSearchProviderInfo">
<controls:SettingsCard Padding="60,0,48,0" Header="{Binding Provider, Converter={StaticResource AlbumArtSearchProviderToDisplayNameConverter}, Mode=OneWay}">
@@ -364,7 +403,7 @@
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8B7;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.LocalLyricsFolders, Mode=OneWay}">
ItemsSource="{x:Bind ViewModel.LocalMediaFolders, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard>
@@ -399,13 +438,13 @@
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.LocalLyricsFolders.Count, Mode=OneWay}"
Binding="{x:Bind ViewModel.LocalMediaFolders.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -484,6 +523,16 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontFamily" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8D2;}">
<ComboBox ItemsSource="{x:Bind ViewModel.SystemFontNames, Mode=OneWay}" SelectedIndex="{x:Bind ViewModel.SelectedFontFamilyIndex, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontWeight" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8DD;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.LyricsFontWeight, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsThin" />
@@ -639,22 +688,60 @@
</interactivity:Interaction.Behaviors>
</ColorPicker>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<controls:SettingsCard x:Uid="SettingsPageLyricsStandardFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{Binding ElementName=LyricsFontSizeSlider, Path=Value, Mode=OneWay}" />
Text="{Binding ElementName=LyricsStandardFontSizeSlider, Path=Value, Mode=OneWay}" />
<Slider
x:Name="LyricsFontSizeSlider"
x:Name="LyricsStandardFontSizeSlider"
Maximum="96"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.LyricsFontSize, Mode=TwoWay}" />
Value="{x:Bind ViewModel.LyricsStandardFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsDockFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{Binding ElementName=LyricsDockFontSizeSlider, Path=Value, Mode=OneWay}" />
<Slider
x:Name="LyricsDockFontSizeSlider"
Maximum="96"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.LyricsDockFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsDesktopFontSize" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8E9;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{Binding ElementName=LyricsDesktopFontSizeSlider, Path=Value, Mode=OneWay}" />
<Slider
x:Name="LyricsDesktopFontSizeSlider"
Maximum="96"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.LyricsDesktopFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
@@ -845,9 +932,10 @@
x:Uid="SettingsPageTranslationConfig"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8C1;}"
IsExpanded="True">
IsExpanded="{x:Bind ViewModel.IsLibreTranslateEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsLibreTranslateEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLibreTranslateServer">
<controls:SettingsCard x:Uid="SettingsPageLibreTranslateServer" IsEnabled="{x:Bind ViewModel.IsLibreTranslateEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBox
x:Name="LibreTranslateServerTextBox"
@@ -885,7 +973,6 @@
<Paragraph>
<Run x:Uid="SettingsPageVersion" />
<Run Text="{x:Bind ViewModel.Version, Mode=OneWay}" />
<Run Text="{x:Bind ViewModel.BuildDate, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</controls:SettingsCard.Description>
@@ -912,6 +999,10 @@
<Button x:Uid="SettingsPageJoinNowButton" Click="DiscodGroupButton_Click" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageTelegram" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/Telegram.png}">
<Button x:Uid="SettingsPageJoinNowButton" Click="TelegramGroupButton_Click" />
</controls:SettingsCard>
</StackPanel>
</controls:Case>

View File

@@ -26,9 +26,9 @@ namespace BetterLyrics.WinUI3.Views
{
if (sender is ToggleSwitch toggleSwitch)
{
if (toggleSwitch.DataContext is LocalLyricsFolder localLyricsFolder)
if (toggleSwitch.DataContext is LocalMediaFolder localLyricsFolder)
{
ViewModel.ToggleLocalLyricsFolder(localLyricsFolder);
ViewModel.ToggleLocalLyricsFolder();
}
}
}
@@ -47,7 +47,7 @@ namespace BetterLyrics.WinUI3.Views
{
if (toggleSwitch.DataContext is LyricsSearchProviderInfo providerInfo)
{
ViewModel.ToggleLyricsSearchProvider(providerInfo);
ViewModel.ToggleLyricsSearchProvider();
}
}
}
@@ -65,7 +65,7 @@ namespace BetterLyrics.WinUI3.Views
Microsoft.UI.Xaml.RoutedEventArgs e
)
{
ViewModel.RemoveFolderAsync((LocalLyricsFolder)(sender as HyperlinkButton)!.Tag);
ViewModel.RemoveFolderAsync((LocalMediaFolder)(sender as HyperlinkButton)!.Tag);
}
private void MediaSourceProviderToggleSwitch_Toggled(object sender, RoutedEventArgs e)
@@ -131,5 +131,10 @@ namespace BetterLyrics.WinUI3.Views
{
Launcher.LaunchUriAsync(new Uri(MetadataHelper.DiscordUrl));
}
private void TelegramGroupButton_Click(object sender, RoutedEventArgs e)
{
Launcher.LaunchUriAsync(new Uri(MetadataHelper.TelegramUrl));
}
}
}

View File

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

View File

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

View File

@@ -1,10 +1,28 @@
### I couldn't see any button that I can interact with
### I couldn't see any button that I can interact with / 我找不到任何可交互的按钮
This app is built with immersive experience, just hover your mouse on the top/bottom area of the app and then you'll see everything.
By default, all the buttons are shown but if you click on the "Immersive" button (as shown in the picture), all the buttons will disappear. But don't worry, one you hover again, it will shown up to you!
![alt text](image-3.png)
默认情况下,所有按钮都会显示,但如果您点击 “沉浸” 按钮(如图左上位置所示),所有按钮都会消失。不过不用担心,当您再次将鼠标悬停在按钮上时,它们就会重新显示出来!
![alt text](image-4.png)
![alt text](image-9.png)
It is important to note that when you enter "Docked Mode", the action buttons are hidden. Hover your mouse over the top to access the "Immerse", "More", and "Close" buttons.
需要特别注意的是,当您进入 “停靠模式” 时,会隐藏操作按钮。将鼠标悬停在顶部以访问 “沉浸” 按钮、“更多” 按钮以及 “关闭” 按钮
![alt text](image-10.png)
Hover the mouse slightly above the bottom edge of the window to display the white control floating window at the bottom
将鼠标悬浮在窗口下边缘稍靠上位置以显示底部控制浮窗小白条
![alt text](image-11.png)
Tap the "little white bar" to display the bottom control bar in floating window form (including current playback progress view, timeline offset adjustment; previous song, pause/play, next song; translation, layout, settings)
点按 “小白条” 即可以浮窗形式显示底部控制栏(包括当前播放进度查看、时间轴偏移调整;上一首、暂停/播放、下一首; 翻译、布局、设置)
![alt text](image-12.png)
### I have set up all the settings related to translation but there is no translation at all
@@ -32,11 +50,7 @@ Hover you mouse on the very bottom of the app,
And then click on the first icon button (Lyrics timeline offset), here you can adjust the offset freely.
### I'm using Apple Music, the lyrics is moving forward and afterward constantly
![alt text](image-1.png)
Hover your mouse at the very bottom of the app and then click on the very last icon button to go to settings page.
### The lyrics jump back and forth frequently / 歌词频繁前后跳动
![alt text](image-2.png)

BIN
FAQ/image-10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
FAQ/image-11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
FAQ/image-12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
FAQ/image-9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -1,151 +1,207 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.md">_**Click here to see the English version**_</a>
> 注:以下内容含有大语言模型翻译内容
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**点此处查看常见问题与解答FAQ**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.md">_**🌐 Click here to see the English version**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**❓ 点此查看常见问题FAQ**_</a>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<h2 align="center">
BetterLyrics
</h2>
<h4 align="center">
一个基于 WinUI 3 和 Win2D 构建的动态歌词显示工具 —— 支持本地播放,并可监听多种播放器
</h4>
<h3 align="center">
基于 WinUI 3 打造的丝滑动态歌词显示工具
</h3>
## 🎉 本项目已被少数派推荐!
---
查看文章:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)
- [「BetterLyrics」反馈交流群(简体中文)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (QQ群号1054700388)
- [「BetterLyrics」反馈交流群繁体中文/英文)](https://discord.gg/5yAQPnyCKv) (Discord)
## 🔈 反馈交流群
---
- [<img src="BetterLyrics.WinUI3\BetterLyrics.WinUI3\Assets\QQ.png" height="20"> QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info)群号1054700388
- [<img src="BetterLyrics.WinUI3\BetterLyrics.WinUI3\Assets\Discord.png" height="12"> Discord](https://discord.gg/5yAQPnyCKv)
- [<img src="BetterLyrics.WinUI3\BetterLyrics.WinUI3\Assets\Telegram.png" height="16"> Telegram](https://t.me/+svhSLZ7awPsxNGY1)
🎉 本项目已被少数派精选推荐!欢迎阅读:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)
## 🌟 核心特色
---
- 🌠 **优雅的用户界面**
- 丰富流畅的动画与特效
- ↔️ **强大的歌词翻译功能**
- 支持离线翻译(支持 30 种语言)
- 自动读取本地歌词文件中的双语翻译内容
- 🧩 **多种歌词来源支持**
- 本地存储
- 音乐文件(嵌入式歌词)
- [.lrc](https://zh.wikipedia.org/wiki/LRC) 文件(支持标准与增强格式)
- [.eslrc](https://github.com/ESLyric/release) 文件
- [.ttml](https://zh.wikipedia.org/wiki/Timed_Text_Markup_Language) 文件
- 在线歌词源
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
## 核心特色
- 🎶 **支持多种播放器**
- 动态模糊专辑封面背景
- 丝滑的歌词淡入/淡出、缩放效果
- 歌曲切换时的流畅界面过渡
- 逐字渐变的卡拉OK效果带光晕
- 沉浸式桌面歌词(停靠模式)
- 本地化歌词翻译(支持 30 种语言)
- <details><summary>网易云音乐</summary>
> 项目仍在开发中,最新分支可能存在未修复的 Bug 或异常行为
- 请先安装 [BetterNCM 插件](https://microblock.cc/betterncm)。若安装后出现降级提示,请按提示将网易云降级至 2.10.13
- 然后在插件市场安装 InfLink 插件,安装完成后重启网易云即可!
## 支持的歌词来源
</details>
- 本地资源
- 音乐文件(内嵌歌词)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) 文件(标准格式与增强格式)
- [.eslrc](https://github.com/ESLyric/release) 文件
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) 文件
- <details><summary>酷狗音乐</summary>
(歌词下载推荐工具:[LDDC](https://github.com/chenmozhijin/LDDC)
- 请确保酷狗设置中已开启“支持系统播放控制,如锁屏界面控制”
- 当前版本无法获取时间轴信息,意味着无法检测进度条的跳转
- 在线歌词源
- QQ音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
</details>
## 效果展示
- <details><summary>Apple Music</summary>
- 建议在设置中将“时间轴同步容差”调至 600ms 左右(设置 → 高级选项),否则歌词会前后晃动
</details>
- <details><summary>foobar2000</summary>
- 请安装插件https://github.com/dumbie/foo_mediacontrol
</details>
- Spotify
- QQ 音乐
- PotPlayer
- Windows 自带媒体播放器
- <details><summary>LX Music</summary>
- 请在 LX Music 设置中开启“开放 API”
- 然后在 BetterLyrics 设置中填写 LX 服务器地址(通常是 http://127.0.0.1:23330
</details>
- <details><summary>MusicBee</summary>
- 请安装插件https://github.com/HenryPDT/mb_MediaControl
</details>
- <details><summary>iTunes</summary>
- 请安装插件https://github.com/thewizrd/iTunes-SMTC
</details>
- <details><summary>AIMP</summary>
- 请安装插件https://www.aimp.ru/?do=catalog&rec_id=1097
</details>
- 🪟 **多种展示模式**
- **标准模式**:沉浸式动画歌词 + 动态背景
- **吸附模式**:屏幕边缘浮动歌词条
- **桌面模式**:歌词悬浮在所有窗口之上
- 🧠 **智能行为**
- 检测播放器关闭自动隐藏歌词
> 本项目仍在开发中,开发分支可能存在 bug 或异常行为。
## 软件截图
### 标准模式
![标准模式](Screenshots/image.png)
![alt text](Screenshots/image.png)
![光晕浮动效果](Screenshots/glow-float.gif)
![alt text](Screenshots/glow-float.gif)
![歌词面板](Screenshots/fan.png)
![alt text](Screenshots/fan.png)
![纯歌词模式](Screenshots/lyrics-only.png)
![alt text](Screenshots/lyrics-only.png)
![纯专辑封面模式](Screenshots/album-art-only.png)
![alt text](Screenshots/album-art-only.png)
### 停靠模式
### 吸附模式
![停靠模式1](Screenshots/dock-1.png)
![alt text](Screenshots/dock-1.png)
![停靠模式2](Screenshots/dock-2.png)
![alt text](Screenshots/dock-2.png)
### 桌面模式
![桌面模式1](Screenshots/desktop-1.png)
![alt text](Screenshots/desktop-1.png)
![桌面模式2](Screenshots/desktop-2.png)
![alt text](Screenshots/desktop-2.png)
## 演示视频
## 使用演示
观看 B 站演示视频2025年7月7日发布[点击此处](https://www.bilibili.com/video/BV1zjGjzfEXh)
观看我们于 2025 年 7 月 7 日上传的哔哩哔哩介绍视频:[点击这里](https://www.bilibili.com/video/BV1zjGjzfEXh)
## 立即体验
## 立即下载试用
- 稳定版
### Microsoft Store 推荐安装方式
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/zh-cn%20dark.svg" width="200"/>
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
> **最便**的获取方式,提供**无限制**免费试用购买(免费版与付费版**功能完全一致**,购买视为对开发者的支持
**最便**的获取方式,支持**永久免费试用**与购买(免费版与付费版**功能差异**
或通过 Google Drive 获取(链接见[发布页](https://github.com/jayfunc/BetterLyrics/releases)
☕ 如果你觉得本软件对你有帮助,欢迎在 Microsoft Store **支持我一杯奶茶** 🧧🥰
> 下载的是 ".zip" 压缩包,安装指南请参阅[此文档](How2Install/How2Install.md)
> 一旦有稳定版发布Microsoft Store 将是第一更新渠道。
- 开发版
可通过 `git clone` 拉取项目源码自行编译
### 备用下载方式Google Drive
## 已测试播放器
你也可以在 [发布页](https://github.com/jayfunc/BetterLyrics/releases) 找到 Google Drive 下载链接
- 酷狗音乐
- **时间轴同步限制**:酷狗不广播时间轴信息,调整进度时歌词无法实时跟随
- Apple Music
- 需在设置中调整时间轴阈值至 600 ms 左右(路径:设置→高级选项)
- foobar2000
- 需配合安装 [foo_mediacontrol](https://github.com/dumbie/foo_mediacontrol) 插件
- Spotify
- QQ音乐
- PotPlayer
- 系统自带媒体播放器
> 下载为 `.zip` 压缩包,安装方法请参阅:[安装指南](How2Install/How2Install.md)
##
##
- [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper)
- 提供 QQ/网易云/酷狗歌词获取与解密
- [LRCLIB](https://lrclib.net/)
- 歌词 API 服务
- [ATL.NET](https://github.com/Zeugma440/atldotnet)
- 音乐文件封面提取
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- Win32 窗口 API 封装
- [TagLib#](https://github.com/mono/taglib-sharp)
- 歌词内容解析
- [Vanara](https://github.com/dahall/Vanara)
- Win32 API 封装库
- [Stackoverflow - WPF边距动画实现](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [B - WinUI3系统背景效果教程](https://www.bilibili.com/video/BV1PY4FevEkS)
- [博客园 - .NET应用与SMTC交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D游戏循环实现](https://www.cnblogs.com/walterlv/p/10236395.html)
- [Win2D高级示例](https://github.com/r2d2rigo/Win2D-Samples)
- [CommunityToolkit开发指南](https://mvvm.coldwind.top/)
- [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper)
- 提供 QQ网易酷狗歌词解析与解密支持
- [LRCLIB](https://lrclib.net/)
- 歌词 API 提供方
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- 用于提取音乐封面图像
- [WinUIEx](https://github.com/dotMorten/WinUIEx)
- 提供窗口相关 Win32 API 简化访问方式
- [TagLib#](https://github.com/mono/taglib-sharp)
- 用于读取音频文件中的歌词内容
- [Vanara](https://github.com/dahall/Vanara)
- Win32 API 封装库
- [StackOverflow - 如何为 WPF 动画 Margin 属性](https://stackoverflow.com/a/21542882/11048731)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Bilibili -WinUI3】SystemBackdropController 讲解](https://www.bilibili.com/video/BV1PY4FevEkS)
- [博客园 - .NET App 与 SMTC 控制](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环介绍](https://www.cnblogs.com/walterlv/p/10236395.html)
- [Win2D 示例 - Iris Blur](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit 教程](https://mvvm.coldwind.top/)
## 灵感来源
- [网易云歌词增强](https://github.com/solstice23/refined-now-playing-netease)
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐播放器](https://moriafly.com/program/salt-player)
- [MyToolBar任务栏工具](https://github.com/TwilightLemon/MyToolBar)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## 项目星标历史
## ✍️ 欢迎协助翻译!
找不到你的语言?
没关系!快来成为贡献者的一员吧!😆
点击此 [链接](https://crowdin.com/project/betterlyrics/invite?h=d767e4f2dbd832d8fcdb6f7e5a198b402502866) 使用 Crowdin 在线翻译!
## Star 增长趋势
[![Star History Chart](https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date)
## 欢迎反馈与贡献
## 欢迎反馈和 PR
如遇问题请提交 Issue如有改进建议欢迎提交 PR
发现 bug 请提交 Issues,如有想法欢迎一起参与开发!

166
README.md
View File

@@ -1,58 +1,117 @@
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md">_**点此处查看中文说明**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md">_**🌐 点此处查看中文说明**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**Click here to view frequently asked questions (FAQ)**_</a>
<a href="https://github.com/jayfunc/BetterLyrics/blob/dev/FAQ/FAQ.md">_**Click here to view frequently asked questions (FAQ)**_</a>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64"/>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="64">
</div>
<h2 align="center">
BetterLyrics
</h2>
<h3 align="center">
Your smooth dynamic lyrics display tool built with WinUI 3
<h4 align="center">
Your dynamic lyrics display tool built with WinUI 3 and Win2D — works with local playback and other players
</h3>
---
## 🎉 This project was featured by SSPAI!
- [BetterLyrics」反馈交流群(简体中文)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) on QQ
- [「BetterLyrics」Feedback Chat Group (Traditional Chinese / English)](https://discord.gg/5yAQPnyCKv) on Discord
Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028)
---
## 🔈 Feedback and chat group
🎉 This project was featured by SSPAI! Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028)
- [<img src="BetterLyrics.WinUI3\BetterLyrics.WinUI3\Assets\QQ.png" height="20"> QQ](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388)
- [<img src="BetterLyrics.WinUI3\BetterLyrics.WinUI3\Assets\Discord.png" height="12"> Discord](https://discord.gg/5yAQPnyCKv)
- [<img src="BetterLyrics.WinUI3\BetterLyrics.WinUI3\Assets\Telegram.png" height="16"> Telegram](https://t.me/+svhSLZ7awPsxNGY1)
---
## 🌟 Highlighted features
## Highlighted features
- 🌠 **Pleasing User Interface**
- Fluent animations and effects
- ↔️ **Strong Lyrics Translation**
- Offline machine translation (supporting 30 languages)
- Auto reading local lyrics files for embedded translation
- 🧩 **Various Lyrics Source**
- Local storage
- Music files (with embedded lyrics)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) files (with both core format and enhanced format)
- [.eslrc](https://github.com/ESLyric/release) files
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) files
- Online lyrics providers
- QQ Music
- 网易云音乐 NetEase Cloud Music
- 酷狗音乐 Kugou Music
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- 🎶 **Multiple Music Players Supported**
- Dynamic blur album art as background
- Smooth lyrics fade in/out, zoom in/out effects
- Smooth user interface change from song to song
- Gradient Karaoke (with glow) effect on every single character
- Immersive desktop lyrics (dock mode)
- Local translation (supporting 30 languages)
- <details><summary>NetEase Cloud Music</summary>
- Please install the [BetterNCM plugin](https://microblock.cc/betterncm) first. If a downgrade guide pops up after the installation, please follow the guide to complete the downgrade of NetEase Cloud Music (downgrade to 2.10.13);
- After that, please install the InfLink plugin in PluginMarket. After the installation is complete, please restart NetEase Cloud Music. At this point, all preparatory operations have been completed, enjoy it!
</details>
- <details><summary>Kugou Music</summary>
- Please make sure that the Kugou Music setting "Support system playback controls, such as lock screen interface" is turned on
- No timeline information broadcasted, which means when you change timeline position in Kugou Music, BetterLyrics has no way to detect this change.
</details>
- <details><summary>Apple Music</summary>
- Make sure you have set timeline threshold to around 600 ms in settings (Go to "Settings" - "Advanced option" to change), otherwise, the lyrics will be moving forward and afterward constantly.
</details>
- <details><summary>foobar2000</summary>
- Make sure you have https://github.com/dumbie/foo_mediacontrol installed with it
</details>
- Spotify
- QQ Music
- PotPlayer
- Media Player (System)
- <details><summary>LX Music</summary>
- Please make sure you have enabled "Open API" in LX Music settings page
- Then open BetterLyrics, go to settings, go to "Advanced options", input your LX Music server address (mostly like http://127.0.0.1:23330) and there you go!
</details>
- <details><summary>MusicBee</summary>
- Please install https://github.com/HenryPDT/mb_MediaControl before using
</details>
- <details><summary>iTunes</summary>
- Please install https://github.com/thewizrd/iTunes-SMTC before using
</details>
- <details><summary>AIMP</summary>
- Please install https://www.aimp.ru/?do=catalog&rec_id=1097 before using
</details>
- 🪟 **Multiple Display Modes**
- **Standard Mode**
- Enjoy an immersive listening journey with rich lyrics animations and beautifully dynamic backgrounds
- **Dock Mode**
- A smart animated lyrics bar docked to your screen edge
- **Desktop Mode**
- Enjoy immersive lyrics floating above your apps
- 🧠 **Smart Behaviors**
- Auto hide when music player closed
> This project is still under development, bugs and unexpected behaviors may be existed in the latest branch.
## Supported lyrics source
- From your local storage
- Music files (with embedded lyrics)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) files (with both core format and enhanced format)
- [.eslrc](https://github.com/ESLyric/release) files
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) files
(For lyrics downloading, you can use [LDDC](https://github.com/chenmozhijin/LDDC))
- From online lyrics providers
- QQ Music
- 网易云音乐 NetEase Cloud Music
- 酷狗音乐 Kugou Music
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
## Screenshots
### Standard mode
@@ -85,35 +144,24 @@ Watch our introduction video (uploaded on 7 July 2025) on Bilibili [here](https:
## Try it now
- Stable version
### Microsoft Store
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
> **Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version, if you like you can purchase to support me)
**Easiest** way to get it. **Unlimited** free trail or purchase (there is **no difference** between free and paid version)
Or alternatively get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases) page for the link)
☕ If you find it useful, please consider purchasing 🧧 it in **Microsoft Store**, I'll appreciate it! 🥰
> When there's a stable version built, Microsoft Store will be the first channel to get updated.
### Google Drive
Or get it from Google Drive (see [release](https://github.com/jayfunc/BetterLyrics/releases) page for the link)
> Please note you are downloading ".zip" file, for guide on how to install it, please kindly follow [this doc](How2Install/How2Install.md).
- Latest dev version
You can `git clone` this project and build it yourself.
## Tested music player
- Kugou Music
- No timeline information broadcasted, which means when you change timeline position in Kugou Music, BetterLyrics has no way to detect this change.
- Apple Music
- Make sure you have set timeline threshold to around 600 ms in settings (Go to "Settings" - "Advanced option" to change), otherwise, the lyrics will be moving forward and afterward constantly.
- foobar2000
- Make sure you have https://github.com/dumbie/foo_mediacontrol installed with it
- Spofity
- QQ Music
- PotPlayer
- Media Player (System)
## Many thanks to
- [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper)
@@ -143,6 +191,12 @@ You can `git clone` this project and build it yourself.
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ✍️ Help us translate into your language
Cannot find your language?
Don't worry! Start translating and become one of the contributors! 😆
Click the [link](https://crowdin.com/project/betterlyrics/invite?h=d767e4f2dbd832d8fcdb6f7e5a198b402502866) to translate this app into your language via Crowdin now!
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date)

3
crowdin.yml Normal file
View File

@@ -0,0 +1,3 @@
files:
- source: /BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw
translation: /BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/%locale%/Resources.resw

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB