Compare commits

...

125 Commits

Author SHA1 Message Date
Zhe Fang
abca9ae5fb fix: auto-play in music gallery wont show song title and artist when first play 2026-01-03 12:50:33 -05:00
Zhe Fang
a062897e1a fix: music gallery play issue (and improve ui/ux) 2026-01-03 12:14:54 -05:00
Zhe Fang
8b4748df1b fix: stats dashboard data selection issue 2026-01-02 12:05:10 -05:00
Zhe Fang
1df5ea6bab fix: playback realtime info encoded cache path 2026-01-02 08:27:03 -05:00
Zhe Fang
c576635af2 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-01 21:49:33 -05:00
Zhe Fang
c8590202ec chores: improve music stats 2026-01-01 21:49:31 -05:00
Zhe Fang
2dc8b1283f Update README.CN.md 2026-01-01 12:38:31 -05:00
Zhe Fang
c482edea0f Update README.md 2026-01-01 12:36:39 -05:00
Zhe Fang
315722252c Update README.md 2026-01-01 12:32:01 -05:00
Zhe Fang
32ba453264 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 16:30:16 -05:00
Zhe Fang
d4902329bb chores: update readme 2025-12-31 16:30:14 -05:00
Zhe Fang
83aee8948b Update README.CN.md 2025-12-31 16:13:48 -05:00
Zhe Fang
1f9fab3228 Update README.CN.md 2025-12-31 16:10:42 -05:00
Zhe Fang
7a3a659dfc Update README.CN.md 2025-12-31 16:09:17 -05:00
Zhe Fang
a14afd3eb5 Update README.md 2025-12-31 16:01:37 -05:00
Zhe Fang
c2af7f3186 Update README.CN.md 2025-12-31 16:00:16 -05:00
Zhe Fang
cd026dd2bf Update README.CN.md 2025-12-31 15:56:08 -05:00
Zhe Fang
4bc1a9975d Update README.md 2025-12-31 15:52:05 -05:00
Zhe Fang
07eecf0930 Update README.md 2025-12-31 15:49:22 -05:00
Zhe Fang
35fba5abb0 chores 2025-12-31 15:44:58 -05:00
Zhe Fang
03ef231a3f chores 2025-12-31 15:39:44 -05:00
Zhe Fang
f41879f4e5 chores: add pic 2025-12-31 15:38:18 -05:00
Zhe Fang
bda7510ed6 chores: update readme 2025-12-31 14:28:54 -05:00
Zhe Fang
5ec8c7c61f chores: bump to 1.2.236 2025-12-31 13:32:45 -05:00
Zhe Fang
7e6bd9dade fix: _scrobbleStopwatch 2025-12-31 10:55:56 -05:00
Zhe Fang
56244cb793 fix: scrollable timer 2025-12-31 10:47:43 -05:00
Zhe Fang
cb5f70ab55 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 10:12:29 -05:00
Zhe Fang
3cc018bb1f chores: bump to 1.2.234.0 2025-12-31 10:12:28 -05:00
Zhe Fang
c517d2b008 Update README.md 2025-12-31 10:08:58 -05:00
Zhe Fang
e79f2a0223 Update README.CN.md 2025-12-31 10:07:53 -05:00
Zhe Fang
39122b9147 Update README.CN.md 2025-12-31 10:07:01 -05:00
Zhe Fang
accbdc1806 Update README.md 2025-12-31 10:06:01 -05:00
Zhe Fang
de014d1ad7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 09:52:12 -05:00
Zhe Fang
cc2ce5f8cf chores: i18n 2025-12-31 09:52:10 -05:00
Zhe Fang
2a2d80436e Update README.CN.md 2025-12-31 09:34:29 -05:00
Zhe Fang
ce3f79f35c Update README.md 2025-12-31 09:33:01 -05:00
Zhe Fang
12e6000cb3 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:43:44 -05:00
Zhe Fang
c1dc684411 fix: scrobble timer is still running when music is paused 2025-12-31 08:43:43 -05:00
Zhe Fang
69ea2cb495 Update README.md 2025-12-31 08:21:33 -05:00
Zhe Fang
e2ee03c4be Update README.CN.md 2025-12-31 08:21:03 -05:00
Zhe Fang
c6fe33d6ae Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:12:12 -05:00
Zhe Fang
7744e145fa chores: i18n 2025-12-31 08:12:10 -05:00
Zhe Fang
0284b1de81 Update README.CN.md 2025-12-30 21:46:32 -05:00
Zhe Fang
108c2cd34b Update README.md 2025-12-30 21:45:56 -05:00
Zhe Fang
390e30f7f5 chores: bump to v1.2.232.0 2025-12-30 20:54:40 -05:00
Zhe Fang
900774668d chores: i18n 2025-12-30 20:09:02 -05:00
Zhe Fang
6ca2d1f897 chores 2025-12-30 19:49:54 -05:00
Zhe Fang
164bd077b8 fix: app crash when audio device was not found 2025-12-30 14:30:10 -05:00
Zhe Fang
8ec71fcfb7 chores: update StatsDashboardControl 2025-12-30 14:04:08 -05:00
Zhe Fang
f39ad54df8 fix: record play history 2025-12-30 13:02:23 -05:00
Zhe Fang
9b809983df chores: i18n 2025-12-30 08:59:30 -05:00
Zhe Fang
8006b3a443 chores: i18n 2025-12-30 08:41:12 -05:00
Zhe Fang
26a7454de2 feat: play history 2025-12-30 08:02:48 -05:00
Zhe Fang
0793a074cf fix: add to playlists 2025-12-29 16:11:21 -05:00
Zhe Fang
125bf1682e chores 2025-12-29 12:15:39 -05:00
Zhe Fang
48bdffb2fe Merge pull request #179 from jayfunc/l10n_dev
New Crowdin updates
2025-12-28 21:37:57 -05:00
Zhe Fang
d324a7552f New translations resources.resw (Chinese Traditional) 2025-12-28 21:37:17 -05:00
Zhe Fang
78c308c393 New translations resources.resw (Japanese) 2025-12-28 21:37:15 -05:00
Zhe Fang
a1bba00db6 chores: i18n 2025-12-28 21:31:37 -05:00
Zhe Fang
0787f5b111 fix 2025-12-28 21:10:02 -05:00
Zhe Fang
884026594b fix: ignore system, hidden files and unsupported format files 2025-12-28 21:03:56 -05:00
Zhe Fang
b0a777db8d chores: adjust layout and fix bugs 2025-12-28 20:01:41 -05:00
Zhe Fang
83f3a3bd6d chores: fix SMB and local file system, add auto-sync, improve lyruics search, album art search, local music gallery load speed (after 1st time) 2025-12-27 15:25:49 -05:00
Zhe Fang
bfb2ed29e5 fix: window title is synced with config name 2025-12-25 13:44:43 -05:00
Zhe Fang
131a0f0eb1 Merge pull request #172 from jayfunc/l10n_dev
New Crowdin updates
2025-12-24 19:24:20 -05:00
Zhe Fang
ac2a7b3f7b New translations resources.resw (Malay) 2025-12-24 19:22:52 -05:00
Zhe Fang
36eea7f8f2 New translations resources.resw (Hindi) 2025-12-24 19:22:52 -05:00
Zhe Fang
6b338deb55 New translations resources.resw (Thai) 2025-12-24 19:22:51 -05:00
Zhe Fang
af323ecd00 New translations resources.resw (Indonesian) 2025-12-24 19:22:50 -05:00
Zhe Fang
c79d01c75b New translations resources.resw (Vietnamese) 2025-12-24 19:22:49 -05:00
Zhe Fang
b51ec1e60f New translations resources.resw (Chinese Traditional) 2025-12-24 19:22:48 -05:00
Zhe Fang
7fe925bcba New translations resources.resw (Chinese Simplified) 2025-12-24 19:22:47 -05:00
Zhe Fang
0626472d66 New translations resources.resw (Russian) 2025-12-24 19:22:46 -05:00
Zhe Fang
33099bc186 New translations resources.resw (Portuguese) 2025-12-24 19:22:45 -05:00
Zhe Fang
e653efc227 New translations resources.resw (Korean) 2025-12-24 19:22:44 -05:00
Zhe Fang
074fef3faf New translations resources.resw (Japanese) 2025-12-24 19:22:43 -05:00
Zhe Fang
029cbbd343 New translations resources.resw (German) 2025-12-24 19:22:42 -05:00
Zhe Fang
802b2a4c1c New translations resources.resw (Arabic) 2025-12-24 19:22:41 -05:00
Zhe Fang
eccc4d519c New translations resources.resw (Spanish) 2025-12-24 19:22:39 -05:00
Zhe Fang
5f274ea28a New translations resources.resw (French) 2025-12-24 19:22:37 -05:00
Zhe Fang
aa1a1f5d58 chores: i18n 2025-12-24 19:22:16 -05:00
Zhe Fang
3a56d53487 New translations resources.resw (Malay) 2025-12-24 19:15:10 -05:00
Zhe Fang
bbc5eb772c New translations resources.resw (Hindi) 2025-12-24 19:15:09 -05:00
Zhe Fang
05b491052b New translations resources.resw (Thai) 2025-12-24 19:15:08 -05:00
Zhe Fang
8accbf0431 New translations resources.resw (Indonesian) 2025-12-24 19:15:07 -05:00
Zhe Fang
1174209c2a New translations resources.resw (Vietnamese) 2025-12-24 19:15:06 -05:00
Zhe Fang
23ed719046 New translations resources.resw (Chinese Traditional) 2025-12-24 19:15:05 -05:00
Zhe Fang
a34f00662e New translations resources.resw (Chinese Simplified) 2025-12-24 19:15:04 -05:00
Zhe Fang
f783314258 New translations resources.resw (Russian) 2025-12-24 19:15:03 -05:00
Zhe Fang
215a39c5d5 New translations resources.resw (Portuguese) 2025-12-24 19:15:02 -05:00
Zhe Fang
16bcef5f64 New translations resources.resw (Korean) 2025-12-24 19:15:01 -05:00
Zhe Fang
fbba9a3c36 New translations resources.resw (Japanese) 2025-12-24 19:15:00 -05:00
Zhe Fang
f205ab0364 New translations resources.resw (German) 2025-12-24 19:14:59 -05:00
Zhe Fang
10314f3c2f New translations resources.resw (Arabic) 2025-12-24 19:14:58 -05:00
Zhe Fang
b4710e87d3 New translations resources.resw (Spanish) 2025-12-24 19:14:57 -05:00
Zhe Fang
282a934cd2 New translations resources.resw (French) 2025-12-24 19:14:56 -05:00
Zhe Fang
b4c4e394ef Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-24 18:36:28 -05:00
Zhe Fang
17cfdf37bd chores: i18n 2025-12-24 18:36:26 -05:00
Zhe Fang
900a8e1e7c Update credit wording in CONTRIBUTING.md 2025-12-24 11:55:18 -05:00
Zhe Fang
ea9a9c2f5f Update translation contribution instructions in README
Added a link for contributors to find more information.
2025-12-24 11:52:00 -05:00
Zhe Fang
0c4d02b337 Revise translation assistance information
Updated translation assistance section with a link for contributions.
2025-12-24 11:51:38 -05:00
Zhe Fang
d137d82ecf Update translation assistance section in README
Added a section for translation assistance and removed the previous translation section.
2025-12-24 11:51:07 -05:00
Zhe Fang
02551e2053 Add translation contribution section to README
Added a section encouraging users to help translate the project and contribute.
2025-12-24 11:50:10 -05:00
Zhe Fang
026926e9b8 Update CONTRIBUTING.md 2025-12-24 11:39:08 -05:00
Zhe Fang
4c811db16a Update README.CN.md with translation and donation info
Added a section for translation contributions and donation support.
2025-12-24 11:36:29 -05:00
Zhe Fang
6f83fa11db Revise contributing guidelines for translations
Updated the contributing guidelines to include translation information in both English and Chinese. Added a status table for languages and contributors.
2025-12-24 11:34:55 -05:00
Zhe Fang
bc8e15c144 Update translation section in README.md
Added a section for translation contributions and removed the previous translation details.
2025-12-24 11:32:16 -05:00
Zhe Fang
85de1eb2cd Add Chinese translation guidelines to CONTRIBUTING.md 2025-12-24 11:29:00 -05:00
Zhe Fang
d2bf19ed3d Add translation contribution guidelines
Added instructions for translating BetterLyrics using Crowdin.
2025-12-24 11:27:23 -05:00
Zhe Fang
43c205c839 Update Japanese language support status in README 2025-12-24 11:07:35 -05:00
Zhe Fang
9664b1ab78 Update Japanese language entry in README.CN.md 2025-12-24 11:07:00 -05:00
Zhe Fang
08c5f6b515 Add contributor link for Japanese language support 2025-12-24 11:06:31 -05:00
Zhe Fang
260de40f81 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-24 11:05:41 -05:00
Zhe Fang
c00d0eb005 fix: duration issue 2025-12-24 11:05:39 -05:00
Zhe Fang
32e761724c Merge pull request #170 from jayfunc/l10n_dev
New Crowdin updates
2025-12-24 11:05:05 -05:00
Zhe Fang
9fd08af582 New translations resources.resw (Japanese) 2025-12-24 11:03:57 -05:00
Zhe Fang
266dcfc930 New translations resources.resw (Chinese Simplified) 2025-12-24 10:43:52 -05:00
Zhe Fang
8764585f2c New translations resources.resw (Japanese) 2025-12-24 10:43:50 -05:00
Zhe Fang
91ab3a48c0 New translations resources.resw (Chinese Simplified) 2025-12-24 09:23:59 -05:00
Zhe Fang
80fa34d9e8 New translations resources.resw (Japanese) 2025-12-24 09:23:58 -05:00
Zhe Fang
b4ca4fd990 New translations resources.resw (Chinese Traditional) 2025-12-24 08:00:35 -05:00
Zhe Fang
86527f6b82 New translations resources.resw (Japanese) 2025-12-24 08:00:34 -05:00
Zhe Fang
d8066bc683 New translations resources.resw (Japanese) 2025-12-24 06:04:12 -05:00
Zhe Fang
b261a86791 New translations resources.resw (Japanese) 2025-12-24 04:35:20 -05:00
Zhe Fang
34f2a51b74 New translations resources.resw (Japanese) 2025-12-24 00:10:01 -05:00
143 changed files with 9248 additions and 6485 deletions

View File

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

View File

@@ -3,8 +3,6 @@
x:Class="BetterLyrics.WinUI3.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:globalization="using:Windows.Globalization"
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:media="using:CommunityToolkit.WinUI.Media">
@@ -13,8 +11,14 @@
<ResourceDictionary.MergedDictionaries>
<!-- Merged dictionaries here -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<ResourceDictionary Source="ms-appx:///DevWinUI.Controls/Themes/Generic.xaml" />
<ResourceDictionary Source="/Styles/Converters.xaml" />
<ResourceDictionary Source="/Styles/InteractiveListViewHeaderStyle.xaml" />
<ResourceDictionary Source="/Styles/GhostSliderStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Theme -->
@@ -42,45 +46,6 @@
<ExponentialEase x:Key="EaseOut" EasingMode="EaseOut" />
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:EnumToIntConverter x:Key="EnumToIntConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converter:TranslationSearchProviderToDisplayNameConverter x:Key="TranslationSearchProviderToDisplayNameConverter" />
<converter:TransliterationSearchProviderToDisplayNameConverter x:Key="TransliterationSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converter:MillisecondsToFormattedTimeConverter x:Key="MillisecondsToFormattedTimeConverter" />
<converter:FPSToTimeSpanConverter x:Key="FPSToTimeSpanConverter" />
<converter:ShortcutToStringConverter x:Key="ShortcutToStringConverter" />
<converter:BoolNegationToVisibilityConverter x:Key="BoolNegationToVisibilityConverter" />
<converter:BoolToOpacityConverter x:Key="BoolToOpacityConverter" />
<converter:BoolToPartialOpacityConverter x:Key="BoolToPartialOpacityConverter" />
<converter:BoolNegationToOpacityConverter x:Key="BoolNegationToOpacityConverter" />
<converter:RectToMarginConverter x:Key="RectToMarginConverter" />
<converter:LanguageCodeToDisplayedNameConverter x:Key="LanguageCodeToDisplayedNameConverter" />
<converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
<converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
<converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
<converter:IntToBoolConverter x:Key="IntToBoolConverter" />
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
<converter:MillisecondsToSecondsConverter x:Key="MillisecondsToSecondsConverter" />
<converter:PictureInfosToImageSourceConverter x:Key="PictureInfosToImageSourceConverter" />
<converter:LyricsFontWeightToFontWeightConverter x:Key="LyricsFontWeightToFontWeightConverter" />
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style -->
@@ -96,7 +61,7 @@
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="14,6,14,9" />
<Setter Property="Padding" Value="16,9,16,9" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
@@ -108,7 +73,7 @@
</Style>
<Style
x:Key="TitleBarToggleButtonStyle"
BasedOn="{StaticResource ToggleButtonRevealStyle}"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
TargetType="ToggleButton">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
@@ -116,7 +81,10 @@
<Setter Property="Padding" Value="14,6,14,9" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Style
x:Key="GhostToggleButtonStyle"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
@@ -130,190 +98,6 @@
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource ControlStrokeColorOnAccentDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="ListViewStretchedItemContainerStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
@@ -357,10 +141,6 @@
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />
<!-- Dimensions -->
<!-- Fonts -->

View File

@@ -1,51 +1,56 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.Globalization;
using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理
using Serilog;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3
{
public partial class App : Application
{
private Window? m_window;
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
private static Mutex? _instanceMutex;
private readonly string _appKey = Windows.ApplicationModel.Package.Current.Id.FamilyName;
public App()
{
this.InitializeComponent();
// Must be done before InitializeComponent
if (!TryHandleSingleInstance())
{
// 如果移交成功直接退出当前进程
Environment.Exit(0);
return;
}
EnsureSingleInstance();
this.InitializeComponent();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
@@ -53,29 +58,94 @@ namespace BetterLyrics.WinUI3
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
// 注册全局异常捕获
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void EnsureSingleInstance()
/// <summary>
/// 处理单实例逻辑。
/// 返回 true 表示我是主实例,继续运行。
/// 返回 false 表示我是第二个实例,已通知主实例,我应该退出。
/// </summary>
private bool TryHandleSingleInstance()
{
_instanceMutex = new Mutex(true, Constants.App.AppName, out bool createdNew);
// 尝试查找或注册当前实例
var mainInstance = AppInstance.FindOrRegisterForKey(_appKey);
if (!createdNew)
// 如果当前实例就是注册的那个主实例
if (mainInstance.IsCurrent)
{
User32.MessageBox(HWND.NULL, new ResourceLoader().GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
// 监听 "Activated" 事件。
// 当第二个实例启动并重定向过来时,这个事件会被触发。
mainInstance.Activated += OnMainInstanceActivated;
return true;
}
else
{
// 我不是主实例,我是后来者。
// 获取当前实例的激活参数(比如是通过文件双击打开的,这里能拿到文件路径)
var args = AppInstance.GetCurrent().GetActivatedEventArgs();
// 将激活请求重定向给主实例
// 注意:这里是同步等待,确保发送成功后再退出
try
{
mainInstance.RedirectActivationToAsync(args).AsTask().Wait();
}
catch (Exception)
{
// 即使重定向失败,作为第二个实例也应该退出
}
return false;
}
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
/// <summary>
/// 当第二个实例试图启动时,主实例会收到此回调
/// </summary>
private void OnMainInstanceActivated(object? sender, AppActivationArguments e)
{
// 这个事件是在后台线程触发的,必须切回 UI 线程操作窗口
m_window?.DispatcherQueue.TryEnqueue(() =>
{
HandleActivation();
});
}
/// <summary>
/// 唤醒逻辑
/// </summary>
private void HandleActivation()
{
WindowHook.OpenOrShowWindow<LyricsWindowSwitchWindow>();
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
// 初始化数据库
await EnsureDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 开始后台扫描任务
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
{
_ = Task.Run(async () => await fileSystemService.ScanMediaFolderAsync(item, CancellationToken.None));
}
}
fileSystemService.StartAllFolderTimers();
// 初始化托盘
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 根据设置打开歌词窗口
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -91,12 +161,101 @@ namespace BetterLyrics.WinUI3
}
}
}
// 根据设置自动打开主界面
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
private async Task EnsureDatabasesAsync()
{
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var fileCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
await SafeInitDatabaseAsync(
"PlayHistory",
PathHelper.PlayHistoryPath,
async () =>
{
using var db = await playHistoryFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: true
);
await SafeInitDatabaseAsync(
"FileCache",
PathHelper.FilesIndexPath,
async () =>
{
using var db = await fileCacheFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: false
);
}
private async Task SafeInitDatabaseAsync(string dbName, string dbPath, Func<Task> initAction, bool isCritical)
{
try
{
await initAction();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DB Error] {dbName} init failed: {ex.Message}");
try
{
if (File.Exists(dbPath))
{
// 尝试清理连接池
SqliteConnection.ClearAllPools();
if (isCritical)
{
var backupPath = dbPath + ".bak_" + DateTime.Now.ToString("yyyyMMddHHmmss");
File.Move(dbPath, backupPath, true);
await ShowErrorDialogAsync("Database Recovery", $"Database {dbName} is damaged, the old database has been backed up to {backupPath}, and the program will create a new database.");
}
else
{
File.Delete(dbPath);
}
}
await initAction();
System.Diagnostics.Debug.WriteLine($"[DB Info] {dbName} recovered successfully.");
}
catch (Exception fatalEx)
{
System.Diagnostics.Debug.WriteLine($"[] : {fatalEx.Message}");
await ShowErrorDialogAsync("Fatal Error", $"{dbName} recovery failed, please delete the file at {dbPath} and try again by restarting the program. ({fatalEx.Message})");
}
}
}
private async Task ShowErrorDialogAsync(string title, string content)
{
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
if (m_window != null)
{
m_window.DispatcherQueue.TryEnqueue(async () =>
{
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{
Title = title,
Content = content,
CloseButtonText = "OK",
XamlRoot = m_window.Content?.XamlRoot // 确保 Content 不为空
};
if (dialog.XamlRoot != null) await dialog.ShowAsync();
});
}
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
@@ -104,25 +263,33 @@ namespace BetterLyrics.WinUI3
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
// 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
// 日志
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
.AddSingleton<ISMTCService, SMTCService>()
.AddSingleton<IGSMTCService, GSMTCService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ITranslationService, TranslationService>()
.AddSingleton<ITransliterationService, TransliterationService>()
.AddSingleton<ILastFMService, LastFMService>()
.AddSingleton<IDiscordService, DiscordService>()
.AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>()
@@ -137,6 +304,8 @@ namespace BetterLyrics.WinUI3
.AddSingleton<MusicGalleryPageViewModel>()
.AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddSingleton<PlayQueueViewModel>()
.AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>()
@@ -154,7 +323,8 @@ namespace BetterLyrics.WinUI3
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
// FirstChance 异常非常多(比如内部 try-catch 也会触发),通常建议只在 Debug 模式记录,或者过滤特定类型
// _logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
@@ -167,4 +337,4 @@ namespace BetterLyrics.WinUI3
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -43,11 +43,15 @@
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\StatsDashboardControl.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Controls\WindowSettingsControl.xaml" />
<None Remove="Styles\GhostSliderStyle.xaml" />
<None Remove="Styles\InteractiveListViewHeaderStyle.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
<None Remove="Views\LyricsWindowSwitchWindow.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
@@ -69,6 +73,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
@@ -84,7 +89,12 @@
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.4.1" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc6.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
@@ -127,6 +137,10 @@
<ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Abstractions" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Relational" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Sqlite" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
@@ -169,6 +183,9 @@
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -211,6 +228,9 @@
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -242,6 +262,26 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -387,6 +427,11 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>

View File

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

View File

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

View File

@@ -118,13 +118,21 @@
</HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="*" />
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="*" />
<TextBlock
x:Uid="SetingsPageThanks"
Grid.Column="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</StackPanel>
</dev:SettingsCard>
@@ -182,6 +190,12 @@
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -8,7 +8,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -41,8 +41,7 @@ namespace BetterLyrics.WinUI3.Controls
IRecipient<PropertyChangedMessage<IRandomAccessStream?>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
@@ -99,8 +98,6 @@ namespace BetterLyrics.WinUI3.Controls
private TimeSpan _songPositionWithOffset;
private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1>
private TimeSpan _totalPlayedTime; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD>ŵ<EFBFBD>ʱ<EFBFBD>
private bool _isLastFMTracked = false;
private double _renderLyricsStartX = 0;
private double _renderLyricsStartY = 0;
@@ -346,7 +343,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0;
double songDuration = _gsmtcService.CurrentSongInfo?.DurationMs ?? 0;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor;
@@ -460,7 +457,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsBg = _lyricsWindowStatus.LyricsBackgroundSettings;
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
var lyricsData = _mediaSessionsService.CurrentLyricsData;
var lyricsData = _gsmtcService.CurrentLyricsData;
TimeSpan elapsedTime = args.Timing.ElapsedTime;
@@ -655,41 +652,22 @@ namespace BetterLyrics.WinUI3.Controls
private void UpdatePlaybackState(TimeSpan elapsedTime)
{
if (_mediaSessionsService.CurrentIsPlaying)
if (_gsmtcService.CurrentIsPlaying)
{
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_gsmtcService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
}
}
private void ResetPlaybackState()
{
_songPosition = TimeSpan.Zero;
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
private void UpdateRenderLyricsLines()
{
_renderLyricsLines = null;
_renderLyricsLines = _mediaSessionsService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
_renderLyricsLines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
{
LyricsSyllables = x.LyricsSyllables,
StartMs = x.StartMs,
@@ -702,7 +680,7 @@ namespace BetterLyrics.WinUI3.Controls
private async Task ReloadCoverBackgroundResourcesAsync()
{
if (_mediaSessionsService.AlbumArtBitmapStream is IRandomAccessStream stream)
if (_gsmtcService.AlbumArtBitmapStream is IRandomAccessStream stream)
{
stream.Seek(0);
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
@@ -712,26 +690,19 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
{
var realPosition = message.NewValue;
var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds);
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
var timelineSyncThreshold = _gsmtcService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
// ƫ<><C6AB> or seek
if (diff >= timelineSyncThreshold)
{
_songPosition = realPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˿<EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD> LastFM ͳ<><CDB3>״̬
if (_songPosition.TotalSeconds <= 1)
{
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>
@@ -745,9 +716,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<LyricsData?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentLyricsData))
if (message.PropertyName == nameof(IGSMTCService.CurrentLyricsData))
{
UpdateRenderLyricsLines();
_isLayoutChanged = true;
@@ -757,9 +728,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
ResetPlaybackState();
}
@@ -912,9 +883,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}

View File

@@ -25,7 +25,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<ScrollViewer>
<ScrollViewer Padding="8,0">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="LyricsSearchControlSongInfoMapping" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -153,12 +153,11 @@
<CheckBox x:Uid="LyricsSearchControlMarkAsPureMusic" IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsSearchControlTargetSearchProvider">
<Button
x:Uid="LyricsSearchControlSearch"
Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard>
<Button
x:Uid="LyricsSearchControlSearch"
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" />
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreCacheWhenSearching, Mode=TwoWay}" />
@@ -183,10 +182,7 @@
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind Duration, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
@@ -244,8 +240,6 @@
<ProgressBar
VerticalAlignment="Top"
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.IsSearching, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</Grid>
<Grid Grid.Column="2">
@@ -346,8 +340,8 @@
</Grid>
<Grid Grid.Row="1" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;
using Windows.System;
// To learn more about WinUI, the WinUI project structure,
@@ -22,18 +23,14 @@ namespace BetterLyrics.WinUI3.Controls
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ViewModel.RemoveFolderAsync((MediaFolder)(sender as HyperlinkButton)!.Tag);
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
ViewModel.RemoveFolder(folder);
}
private async void LocalFolderHyperlinkButton_Click(object sender, RoutedEventArgs e)
private void SyncNowButton_Click(object sender, RoutedEventArgs e)
{
if (sender is HyperlinkButton button && button.Tag is string uriStr)
{
if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri))
{
await Launcher.LaunchUriAsync(uri);
}
}
var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
ViewModel.SyncFolder(folder);
}
}
}

View File

@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +12,6 @@
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid
x:Name="BottomCommandGrid"
Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
@@ -113,7 +113,58 @@
x:Name="BottomCenterCommandStackPanel"
Padding="16"
Orientation="Horizontal"
Spacing="3">
Spacing="12">
<!-- Playback order -->
<Button
Grid.Column="2"
Click="PlaybackOrderButton_Click"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlaybackOrderButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<!-- 上一曲目 -->
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -126,13 +177,13 @@
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
@@ -146,13 +197,13 @@
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -164,6 +215,17 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<Button
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPagePlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
@@ -174,6 +236,19 @@
Orientation="Horizontal"
Spacing="3">
<!-- Stop media session -->
<Button
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowStopButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Volume -->
<Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}">
<Grid>
@@ -345,7 +420,7 @@
Margin="0,-14,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Maximum="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" />
@@ -427,7 +502,58 @@
</Grid.ContextFlyout>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PlaybackOrderState">
<VisualState x:Name="RepeatAll">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="1" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Visible" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="RepeatOne">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="1" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Visible" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Shuffle">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="1" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -1,6 +1,8 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
@@ -12,6 +14,7 @@ using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Numerics;
using BetterLyrics.WinUI3.Extensions;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -27,6 +30,7 @@ public sealed partial class NowPlayingBar : UserControl,
public event EventHandler? SongInfoTapped;
public event EventHandler? TimeTapped;
public event EventHandler? PlayQueueButtonClick;
public bool ShowTime
{
@@ -46,6 +50,42 @@ public sealed partial class NowPlayingBar : UserControl,
public static readonly DependencyProperty ShowSongInfoProperty =
DependencyProperty.Register(nameof(ShowSongInfo), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlayingQueueButton
{
get { return (bool)GetValue(ShowPlayingQueueButtonProperty); }
set { SetValue(ShowPlayingQueueButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlayingQueueButtonProperty =
DependencyProperty.Register(nameof(ShowPlayingQueueButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlaybackOrderButton
{
get { return (bool)GetValue(ShowPlaybackOrderButtonProperty); }
set { SetValue(ShowPlaybackOrderButtonProperty, value); }
}
public static readonly DependencyProperty ShowStopButtonProperty =
DependencyProperty.Register(nameof(ShowStopButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowStopButton
{
get { return (bool)GetValue(ShowStopButtonProperty); }
set { SetValue(ShowStopButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public PlaybackOrder PlaybackOrder
{
get { return (PlaybackOrder)GetValue(PlaybackOrderProperty); }
set { SetValue(PlaybackOrderProperty, value); }
}
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsCompactMode
{
get { return (bool)GetValue(IsCompactModeProperty); }
@@ -167,7 +207,7 @@ public sealed partial class NowPlayingBar : UserControl,
var grid = (Grid)sender;
var pos = e.GetCurrentPoint(grid).Position;
var ratio = pos.X / grid.ActualWidth;
ViewModel.MediaSessionsService.ChangePosition(TimelineSlider.Maximum * ratio);
ViewModel.GSMTCService.ChangePosition(TimelineSlider.Maximum * ratio);
}
private void TimelineSliderOverlay_PointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -210,12 +250,12 @@ public sealed partial class NowPlayingBar : UserControl,
private void SongInfoStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
SongInfoTapped?.Invoke(this, EventArgs.Empty);
SongInfoTapped?.Invoke(sender, EventArgs.Empty);
}
private void TimeStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
TimeTapped?.Invoke(this, EventArgs.Empty);
TimeTapped?.Invoke(sender, EventArgs.Empty);
}
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -262,11 +302,21 @@ public sealed partial class NowPlayingBar : UserControl,
}
}
private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
PlayQueueButtonClick?.Invoke(sender, EventArgs.Empty);
}
private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
{
PlaybackOrder = PlaybackOrder.GetNext();
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
TitleTextBlock.Text = message.NewValue?.Title;
ArtistsTextBlock.Text = message.NewValue?.DisplayArtists;
@@ -275,9 +325,9 @@ public sealed partial class NowPlayingBar : UserControl,
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapImage))
{
AlbumArtImageSwitcher.Source = message.NewValue;
}
@@ -286,9 +336,9 @@ public sealed partial class NowPlayingBar : UserControl,
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
}

View File

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

View File

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

View File

@@ -314,8 +314,8 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.GSMTCService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -325,13 +325,10 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Duration, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -341,29 +338,26 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<local:PropertyRow
x:Uid="LyricsPageLyricsProviderPrefix"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay}"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}" />
</StackPanel>
</Expander>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum AutoScanInterval
{
Disabled,
Every15Minutes,
EveryHour,
Every6Hours,
Daily
}
}

View File

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

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum StatsRange
{
Today,
ThisWeek,
ThisMonth,
ThisQuarter,
ThisYear,
Custom
}
}

View File

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

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Extensions
{
@@ -30,6 +31,22 @@ namespace BetterLyrics.WinUI3.Extensions
songInfo.Album = value;
return songInfo;
}
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{
if (songInfo == null) return null;
return new PlayHistoryItem
{
Title = songInfo.Title,
Artist = songInfo.DisplayArtists,
Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.Now.AddMilliseconds(-actualPlayedMs)
};
}
}
}
}

View File

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

View File

@@ -4,6 +4,7 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
@@ -15,6 +16,16 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
{
public static Color GetSystemAccentColor()
{
if (Application.Current.Resources.TryGetValue("SystemAccentColor", out var resource) &&
resource is Color uiColor)
{
return uiColor;
}
return Color.FromArgb(255, 0, 120, 215);
}
public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor)
{
// 计算亮度YIQ公式

View File

@@ -5,8 +5,11 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper
@@ -27,6 +30,18 @@ namespace BetterLyrics.WinUI3.Helper
return Encoding.GetEncoding(encoding);
}
public static async Task CopyFileAsync(string sourcePath, string destinationPath)
{
var dir = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await sourceStream.CopyToAsync(destinationStream);
}
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
@@ -86,5 +101,15 @@ namespace BetterLyrics.WinUI3.Helper
".wav", ".aiff", ".aif", ".pcm", ".cda", ".dsf", ".dff", ".au", ".snd",
".mid", ".midi", ".mod", ".xm", ".it", ".s3m"
};
public static readonly string[] LyricExtensions =
Enum.GetValues(typeof(LyricsSearchProvider)).Cast<LyricsSearchProvider>()
.Where(x => x.IsLocal())
.Select(x => x.GetLyricsFormat())
.Where(x => x != LyricsFormat.NotSpecified)
.Select(x => x.ToFileExtension())
.ToArray();
public static readonly HashSet<string> AllSupportedExtensions = new(MusicExtensions.Union(LyricExtensions));
}
}

View File

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

View File

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

View File

@@ -33,6 +33,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string SaltPlayerForWindowsLogoPath => Path.Combine(AssetsFolder, "SaltPlayerForWindows.png");
public static string MoeKoeMusicLogoPath => Path.Combine(AssetsFolder, "MoeKoeMusic.png");
public static string Listen1LogoPath => Path.Combine(AssetsFolder, "Listen1.png");
public static string OriginalSoundHQPlayerLogoPath => Path.Combine(AssetsFolder, "OriginalSoundHQPlayer.png");
public static string UnknownPlayerLogoPath => Path.Combine(AssetsFolder, "Question.png");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
@@ -54,8 +55,11 @@ namespace BetterLyrics.WinUI3.Helper
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
public static string LocalAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "local");
public static string PlayQueuePath => Path.Combine(CacheFolder, "play-queue.m3u");
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.db");
public static void EnsureDirectories()
{
@@ -75,6 +79,8 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(LocalTtmlCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
using System;
using System.IO;
namespace BetterLyrics.WinUI3.Models.FileSystem
namespace BetterLyrics.WinUI3.Helper
{
public class StreamFileAbstraction : TagLib.File.IFileAbstraction
{
@@ -9,7 +9,7 @@ namespace BetterLyrics.WinUI3.Models.FileSystem
private readonly Stream _stream;
private readonly bool _closeStreamOnDispose;
public StreamFileAbstraction(string path, Stream stream, bool closeStreamOnDispose = false)
public StreamFileAbstraction(string path, Stream? stream, bool closeStreamOnDispose = false)
{
_name = Path.GetFileName(path);
_stream = stream ?? throw new ArgumentNullException(nameof(stream));

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ using FlaUI.Core.EventHandlers;
using FlaUI.UIA3;
using Microsoft.UI.Dispatching;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
@@ -186,7 +187,8 @@ namespace BetterLyrics.WinUI3.Hooks
if (width < 20) return Rectangle.Empty;
return new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height);
var finalRect = new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height);
return finalRect;
}
catch (Exception ex)
{

View File

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

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class PlayHistoryDbContext : DbContext
{
public PlayHistoryDbContext(DbContextOptions<PlayHistoryDbContext> options) : base(options) { }
public DbSet<PlayHistoryItem> PlayHistory { get; set; }
}
}

View File

@@ -1,28 +1,200 @@
using BetterLyrics.WinUI3.Models.FileSystem;
using ATL;
using BetterLyrics.WinUI3.Helper;
using System;
using System.IO;
using System.Linq;
namespace BetterLyrics.WinUI3.Models
{
public class ExtendedTrack : ATL.Track
public class ExtendedTrack
{
public new string Path { get; private set; } = "";
public string RawLyrics { get; set; } = "";
public string ParentFolderName => Directory.GetParent(Path)?.Name ?? "";
public string ParentFolderPath => Directory.GetParent(Path)?.FullName ?? "";
public string FileName => System.IO.Path.GetFileName(Path);
public string Uri { get; private set; } = "";
public string? RawLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public byte[]? AlbumArtByteArray { get; set; }
public string ParentFolderName
{
get
{
if (string.IsNullOrEmpty(Uri)) return "";
try
{
// 使用 Uri Segments 安全获取倒数第二层 (文件夹名)
// Segments 示例: "/", "Music/", "Artist/", "Song.mp3"
var u = new System.Uri(Uri);
if (u.Segments.Length > 1)
{
// 取倒数第二个 segment (如果是文件)
// 注意处理末尾斜杠
string folder = u.Segments[u.Segments.Length - 2];
return System.Net.WebUtility.UrlDecode(folder.TrimEnd('/', '\\'));
}
return "";
}
catch
{
return "";
}
}
}
public string ParentFolderPath
{
get
{
if (string.IsNullOrEmpty(Uri)) return "";
try
{
var u = new System.Uri(Uri);
if (u.IsFile)
{
// 本地文件:返回目录路径 C:\Music
return System.IO.Path.GetDirectoryName(u.LocalPath) ?? "";
}
else
{
// 远程文件:返回去掉文件名的 URI
// new Uri(u, ".") 表示当前目录
return new System.Uri(u, ".").AbsoluteUri;
}
}
catch
{
return "";
}
}
}
public string FileName
{
get
{
if (string.IsNullOrEmpty(Uri)) return "";
try
{
var u = new System.Uri(Uri);
if (u.IsFile) return System.IO.Path.GetFileName(u.LocalPath);
// 远程文件:获取 AbsolutePath 的最后一段并解码
// 例如: /Music/My%20Song.mp3 -> My Song.mp3
string rawName = System.IO.Path.GetFileName(u.AbsolutePath);
return System.Net.WebUtility.UrlDecode(rawName);
}
catch
{
return System.IO.Path.GetFileName(Uri);
}
}
}
public string MediaFolderId { get; set; } = "";
public string Title { get; set; } = "";
public string Artist { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; }
public string AudioFormatName { get; set; } = "";
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
public ExtendedTrack() : base() { }
public ExtendedTrack(string path) : base(path)
public ExtendedTrack(string decodedUriString) : base()
{
Path = path;
string atlPath = decodedUriString;
try
{
var u = new Uri(decodedUriString);
Uri = u.AbsoluteUri;
if (u.IsFile) atlPath = u.LocalPath;
}
catch { }
// 用于本地文件
var track = new Track(atlPath);
SetFromTrack(track);
}
public ExtendedTrack(string path, Stream stream) : base(stream, System.IO.Path.GetExtension(path))
public ExtendedTrack(FilesIndexItem? entity, Stream? stream = null) : base()
{
Path = path;
SetRawLyrics(new StreamFileAbstraction(path, stream));
if (entity == null) return;
this.MediaFolderId = entity.MediaFolderId;
this.Uri = entity.Uri;
this.Title = entity.Title;
this.Artist = entity.Artists;
this.Album = entity.Album;
this.Year = entity.Year;
this.Bitrate = entity.Bitrate;
this.SampleRate = entity.SampleRate;
this.BitDepth = entity.BitDepth;
this.Duration = entity.Duration;
this.AudioFormatName = entity.AudioFormatName;
this.AudioFormatShortName = entity.AudioFormatShortName;
this.Encoder = entity.Encoder;
this.RawLyrics = entity.EmbeddedLyrics;
this.LocalAlbumArtPath = entity.LocalAlbumArtPath;
if (stream != null)
{
var track = new Track(stream, Path.GetExtension(FileName));
SetFromTrack(track);
SetRawLyrics(new StreamFileAbstraction(Uri, stream));
}
}
private void SetFromTrack(Track? track)
{
if (track == null) return;
this.Title = track.Title;
this.Artist = track.Artist;
this.Album = track.Album;
this.Year = track.Year;
this.Bitrate = track.Bitrate;
this.SampleRate = track.SampleRate;
this.BitDepth = track.BitDepth;
this.Duration = track.Duration;
this.AudioFormatName = track.AudioFormat.Name;
this.AudioFormatShortName = track.AudioFormat.ShortName;
this.Encoder = track.Encoder;
this.AlbumArtByteArray = null;
if (track.EmbeddedPictures != null && track.EmbeddedPictures.Count > 0)
{
try
{
var validPics = track.EmbeddedPictures.Where(p => p != null).ToList();
if (validPics.Count > 0)
{
var cover = validPics.FirstOrDefault(p => p.PicType == PictureInfo.PIC_TYPE.Front);
if (cover == null)
{
cover = validPics.First();
}
this.AlbumArtByteArray = cover.PictureData;
}
}
catch (Exception) { }
}
}
private void SetRawLyrics(StreamFileAbstraction streamFileAbstraction)
@@ -34,4 +206,4 @@ namespace BetterLyrics.WinUI3.Models
catch (Exception) { }
}
}
}
}

View File

@@ -1,53 +0,0 @@
using FluentFTP;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.FileSystem
{
public partial class FTPFileSystem : IUnifiedFileSystem
{
private readonly AsyncFtpClient _client;
private readonly string _rootPath; // 服务器上的根路径 (例如 /pub/music)
public FTPFileSystem(string host, string user, string pass, int port, string remotePath)
{
// 如果 path 是 "192.168.1.5/Music",我们需要把 /Music 拆出来
// 但为了简单,假设 host 仅仅是 IPremotePath 才是路径
_rootPath = remotePath ?? "/";
var config = new FtpConfig { ConnectTimeout = 5000 };
_client = new AsyncFtpClient(host, user ?? "anonymous", pass ?? "", port > 0 ? port : 21, config);
}
public async Task<bool> ConnectAsync()
{
await _client.AutoConnect();
return _client.IsConnected;
}
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
{
string targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
var items = await _client.GetListing(targetPath);
return items.Select(i => new UnifiedFileItem
{
Name = i.Name,
FullPath = i.FullName,
IsFolder = i.Type == FtpObjectType.Directory,
Size = i.Size,
LastModified = i.Modified
}).ToList();
}
public async Task<Stream> OpenReadAsync(string fullPath)
{
return await _client.OpenRead(fullPath);
}
public async Task DisconnectAsync() => await _client.Disconnect();
public void Dispose() => _client?.Dispose();
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.FileSystem
{
public interface IUnifiedFileSystem : IDisposable
{
Task<bool> ConnectAsync();
Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath);
Task<Stream> OpenReadAsync(string fullPath);
Task DisconnectAsync();
}
}

View File

@@ -1,56 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.FileSystem
{
public partial class LocalFileSystem : IUnifiedFileSystem
{
private readonly string _rootPath;
public LocalFileSystem(string rootPath)
{
_rootPath = rootPath;
}
public Task<bool> ConnectAsync()
{
return Task.FromResult(Directory.Exists(_rootPath));
}
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
{
var result = new List<UnifiedFileItem>();
var targetPath = string.IsNullOrWhiteSpace(relativePath)
? _rootPath
: Path.Combine(_rootPath, relativePath);
if (!Directory.Exists(targetPath)) return result;
var dirInfo = new DirectoryInfo(targetPath);
foreach (var item in dirInfo.GetFileSystemInfos())
{
bool isDir = (item.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
result.Add(new UnifiedFileItem
{
Name = item.Name,
FullPath = item.FullName,
IsFolder = isDir,
Size = isDir ? 0 : ((FileInfo)item).Length,
LastModified = item.LastWriteTime
});
}
return result;
}
public async Task<Stream> OpenReadAsync(string fullPath)
{
return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
public async Task DisconnectAsync() => await Task.CompletedTask;
public void Dispose() { }
}
}

View File

@@ -1,130 +0,0 @@
using SMBLibrary;
using SMBLibrary.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models.FileSystem
{
public partial class SMBFileSystem : IUnifiedFileSystem
{
private SMB2Client _client;
private ISMBFileStore _fileStore;
private readonly string _ip;
private readonly string _shareName;
private readonly string _pathInsideShare; // 共享里的子路径
private readonly string _username;
private readonly string _password;
// fullPathInput 例如: "192.168.1.5/Music/Pop"
public SMBFileSystem(string fullPathInput, string user, string pass)
{
_username = user;
_password = pass;
// 解析路径:分离 IP 和 共享名
var parts = fullPathInput.Replace("\\", "/").Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 1) _ip = parts[0];
if (parts.Length >= 2) _shareName = parts[1];
// 剩下的部分重新拼起来作为子路径
if (parts.Length > 2)
_pathInsideShare = string.Join("\\", parts.Skip(2));
else
_pathInsideShare = "";
}
public async Task<bool> ConnectAsync()
{
_client = new SMB2Client();
bool connected = _client.Connect(_ip, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
var status = _client.Login(string.Empty, _username, _password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 连接具体的共享文件夹
if (string.IsNullOrEmpty(_shareName)) return true; // 只连了服务器,没连共享
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
{
var result = new List<UnifiedFileItem>();
if (_fileStore == null) return result;
// 拼接完整路径: Root里面的子路径 + 传入的相对路径
string queryPath = Path.Combine(_pathInsideShare, relativePath).Replace("/", "\\").TrimStart('\\');
// 打开目录
var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, queryPath,
AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read,
CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
if (statusRet != NTStatus.STATUS_SUCCESS) return result;
List<QueryDirectoryFileInformation> fileInfo;
do
{
statusRet = _fileStore.QueryDirectory(out fileInfo, handle, "*", FileInformationClass.FileDirectoryInformation);
List<FileDirectoryInformation> list = fileInfo.Select(x => (FileDirectoryInformation)x).ToList();
foreach (var item in list)
{
// 排除当前目录和父目录
if (item.FileName == "." || item.FileName == "..") continue;
result.Add(new UnifiedFileItem
{
Name = item.FileName,
FullPath = Path.Combine(queryPath, item.FileName),
IsFolder = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
Size = item.AllocationSize,
LastModified = item.LastWriteTime
});
}
if (statusRet == NTStatus.STATUS_NO_MORE_FILES)
{
break;
}
if (statusRet != NTStatus.STATUS_SUCCESS)
{
// Log
break;
}
} while (statusRet == NTStatus.STATUS_SUCCESS);
_fileStore.CloseFile(handle);
return result;
}
public async Task<Stream> OpenReadAsync(string fullPath)
{
var ret = _fileStore.CreateFile(out object handle, out FileStatus status, fullPath,
AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, 0, null);
if (ret != NTStatus.STATUS_SUCCESS) throw new IOException($"SMB Open Error: {ret}");
return new SMBReadOnlyStream(_fileStore, handle);
}
public async Task DisconnectAsync()
{
_client?.Disconnect();
await Task.CompletedTask;
}
public void Dispose()
{
_client?.Disconnect();
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
namespace BetterLyrics.WinUI3.Models.FileSystem
{
public class UnifiedFileItem
{
public string Name { get; set; }
public string FullPath { get; set; }
public long Size { get; set; }
public bool IsFolder { get; set; }
public DateTime? LastModified { get; set; }
}
}

View File

@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using WebDav;
namespace BetterLyrics.WinUI3.Models.FileSystem
{
public partial class WebDavFileSystem : IUnifiedFileSystem
{
private readonly WebDavClient _client;
private readonly string _baseUrl;
private readonly string _rootPath;
// host: http://192.168.1.5:5005
// path: /music
public WebDavFileSystem(string host, string user, string pass, int port, string path)
{
if (!host.StartsWith("http")) host = $"http://{host}";
if (port > 0) host = $"{host}:{port}";
_baseUrl = host;
_rootPath = path ?? "/";
_client = new WebDavClient(new WebDavClientParams
{
BaseAddress = new Uri(_baseUrl),
Credentials = new System.Net.NetworkCredential(user, pass)
});
}
public async Task<bool> ConnectAsync()
{
// WebDAV 无状态Propfind 测试根目录连通性
var result = await _client.Propfind(_rootPath);
return result.IsSuccessful;
}
public async Task<List<UnifiedFileItem>> GetFilesAsync(string relativePath)
{
var targetPath = Path.Combine(_rootPath, relativePath).Replace("\\", "/");
var result = await _client.Propfind(targetPath);
var list = new List<UnifiedFileItem>();
if (result.IsSuccessful)
{
foreach (var res in result.Resources)
{
if (res == null || res.Uri == null) continue;
// 排除掉文件夹自身 (WebDAV 通常会把当前请求的文件夹作为第一个结果返回)
// 通过判断 URL 结尾是否一致来简单过滤,或者判断 IsCollection 且 Uri 相同
// 这里简单处理:只要名字不为空
var name = System.Net.WebUtility.UrlDecode(res.Uri.Split('/').LastOrDefault());
if (string.IsNullOrEmpty(name)) continue;
// 如果名字和请求的目录名一样,可能是它自己,跳过 (这需要根据具体服务器响应调整)
// 更稳妥的是比较 Uri
list.Add(new UnifiedFileItem
{
Name = name,
FullPath = res.Uri.ToString(), // WebDAV 需要完整 URI
IsFolder = res.IsCollection,
Size = res.ContentLength ?? 0,
LastModified = res.LastModifiedDate
});
}
}
return list;
}
public async Task<Stream> OpenReadAsync(string fullPath)
{
// WebDAV 获取流
var res = await _client.GetRawFile(fullPath);
if (!res.IsSuccessful) throw new IOException($"WebDAV Error: {res.StatusCode}");
return res.Stream;
}
public async Task DisconnectAsync() => await Task.CompletedTask;
public void Dispose() => _client?.Dispose();
}
}

View File

@@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
[Index(nameof(MediaFolderId))] // 普通索引
[Index(nameof(ParentUri))] // 普通索引
[Index(nameof(Uri), IsUnique = true)] // 唯一索引
public class FilesIndexItem
{
[Key] // 主键
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 明确指定为自增 (Identity)
public int Id { get; set; }
// 关联到 MediaFolder.Id
// 注意:作为索引列,必须限制长度,否则 SQL Server 会报错 (索引最大900字节)
[MaxLength(450)]
public string MediaFolderId { get; set; }
// 存储父文件夹的标准 URI
// 允许为空
[MaxLength(450)]
public string? ParentUri { get; set; }
// 唯一索引列
// 必须限制长度。450字符 * 2字节/字符 = 900字节 (正好卡在 SQL Server 限制内)
[Required]
[MaxLength(450)]
public string Uri { get; set; }
public string FileName { get; set; } = "";
public bool IsDirectory { get; set; }
public long FileSize { get; set; }
public DateTime? LastModified { get; set; }
// 下面的元数据字段通常不需要索引,可以使用 MaxLength 稍微优化空间,
// 或者直接留空(默认为 nvarchar(max)
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; }
[MaxLength(50)] // 格式名称通常很短,限制一下是个好习惯
public string AudioFormatName { get; set; } = "";
[MaxLength(20)]
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
// 歌词可能会很长,保留默认的 nvarchar(max) 即可
public string? EmbeddedLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public bool IsMetadataParsed { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public partial class FolderNode : ObservableObject
{
public FileSourceType SourceType { get; set; } = FileSourceType.Local;
public string FolderName { get; set; } = "";
public string FolderPath { get; set; } = "";
public string MediaFolderId { get; set; } = "";
public ObservableCollection<FolderNode> SubFolders { get; set; } = new();
[ObservableProperty] public partial bool IsExpanded { get; set; }
}
}

View File

@@ -29,84 +29,6 @@ namespace BetterLyrics.WinUI3.Models
LyricsLines = lyricsLines;
}
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 50)
{
foreach (var line in LyricsLines)
{
// 在翻译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = translationData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
// 此处 transLine.OriginalText 指翻译中的“原文”属性
line.TranslatedText = transLine.OriginalText;
}
else
{
// 没有匹配的翻译
line.TranslatedText = "";
}
}
}
public void SetPhoneticText(LyricsData phoneticData, int toleranceMs = 50)
{
foreach (var line in LyricsLines)
{
// 在音译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = phoneticData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
// 此处 transLine.OriginalText 指音译中的“原文”属性
line.PhoneticText = transLine.OriginalText;
}
else
{
// 没有匹配的音译
line.PhoneticText = "";
}
}
}
public void SetTranslation(string translation)
{
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= translationArr.Count)
{
line.TranslatedText = ""; // No translation available, keep empty
}
else
{
line.TranslatedText = translationArr[i];
}
i++;
}
}
public void SetTransliteration(string transliteration)
{
List<string> transliterationArr = transliteration.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= transliterationArr.Count)
{
line.PhoneticText = ""; // No transliteration available, keep empty
}
else
{
line.PhoneticText = transliterationArr[i];
}
i++;
}
}
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
@@ -117,34 +39,5 @@ namespace BetterLyrics.WinUI3.Models
}]);
}
public static LyricsData GetLoadingPlaceholder()
{
return new LyricsData()
{
LyricsLines = [
new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = "● ● ●",
},
],
LanguageCode = "N/A",
};
}
public LyricsLine? GetLyricsLine(double sec)
{
for (int i = 0; i < LyricsLines.Count; i++)
{
var line = LyricsLines[i];
if (line.StartMs > sec * 1000)
{
return LyricsLines.ElementAtOrDefault(i - 1);
}
}
return null;
}
}
}

View File

@@ -1,11 +1,12 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.FileSystem;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Text.Json.Serialization;
using System.Threading;
namespace BetterLyrics.WinUI3.Models
{
@@ -14,32 +15,83 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty] public partial string Id { get; set; } = Guid.NewGuid().ToString();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients][NotifyPropertyChangedFor(nameof(ConnectionSummary))] public partial string Path { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
[NotifyPropertyChangedFor(nameof(IsLocal))]
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
[NotifyPropertyChangedFor(nameof(UriString))]
public partial FileSourceType SourceType { get; set; } = FileSourceType.Local;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Name { get; set; }
[ObservableProperty] public partial string UserName { get; set; }
// 连接属性
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UserName { get; set; }
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UriScheme { get; set; }
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UriHost { get; set; }
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial int UriPort { get; set; } = -1;
[ObservableProperty] public partial int Port { get; set; } = -1;
[JsonPropertyName("Path")]
[ObservableProperty]
[NotifyPropertyChangedRecipients]
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
[NotifyPropertyChangedFor(nameof(UriString))]
public partial string UriPath { get; set; }
[JsonIgnore] public string Password { get; set; }
[JsonIgnore] public bool IsLocal => SourceType == FileSourceType.Local;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsProcessing { get; set; } = false;
[ObservableProperty] public partial double IndexingProgress { get; set; } = 0;
[ObservableProperty] public partial string StatusText { get; set; } = "";
[ObservableProperty] public partial InfoBarSeverity StatusSeverity { get; set; } = InfoBarSeverity.Informational;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DateTime? LastSyncTime { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AutoScanInterval ScanInterval { get; set; } = AutoScanInterval.Disabled;
public Uri GetStandardUri()
{
try
{
if (IsLocal)
{
return new Uri(UriPath);
}
var builder = new UriBuilder
{
Scheme = UriScheme ?? "file",
Host = UriHost,
Port = UriPort,
};
if (!string.IsNullOrEmpty(UriPath))
{
string cleanPath = UriPath.Replace("\\", "/");
if (!cleanPath.StartsWith("/")) cleanPath = "/" + cleanPath;
builder.Path = cleanPath;
}
return builder.Uri;
}
catch (Exception)
{
return new Uri("about:blank");
}
}
// 例smb://user@host:445/share/path
[JsonIgnore]
public string UriString => GetStandardUri().AbsoluteUri;
[JsonIgnore]
public string ConnectionSummary
{
get
{
if (IsLocal) return Path;
return $"{SourceType} - {Path} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
if (IsLocal) return UriPath;
return $"{UriScheme}://{UriHost}{(UriPort > 0 ? ":" + UriPort : "")}/{UriPath?.TrimStart('/', '\\')} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
}
}
@@ -49,7 +101,8 @@ namespace BetterLyrics.WinUI3.Models
public MediaFolder(string path)
{
Path = path;
UriPath = path;
SourceType = FileSourceType.Local;
}
public IUnifiedFileSystem? CreateFileSystem()
@@ -62,12 +115,13 @@ namespace BetterLyrics.WinUI3.Models
return SourceType switch
{
FileSourceType.Local => new LocalFileSystem(Path),
FileSourceType.SMB => new SMBFileSystem(Path, UserName, Password),
FileSourceType.FTP => new FTPFileSystem(Path, UserName, Password, Port, Path),
FileSourceType.WebDav => new WebDavFileSystem(Path, UserName, Password, Port, Path),
FileSourceType.Local => new LocalFileSystem(this),
FileSourceType.SMB => new SMBFileSystem(this),
FileSourceType.FTP => new FTPFileSystem(this),
FileSourceType.WebDAV => new WebDavFileSystem(this),
_ => throw new NotImplementedException()
};
}
}
}
}

View File

@@ -35,11 +35,11 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchType LyricsSearchType { get; set; } = LyricsSearchType.Sequential;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int MatchingThreshold { get; set; } = 40;
[JsonIgnore] public string LogoPath => PlayerIDHelper.GetLogoPath(Provider);
[JsonIgnore] public string LogoPath => PlayerIdHelper.GetLogoPath(Provider);
[JsonIgnore] public string? DisplayName => PlayerIDHelper.GetDisplayName(Provider);
[JsonIgnore] public string? DisplayName => PlayerIdHelper.GetDisplayName(Provider);
[JsonIgnore] public bool IsLXMusic => PlayerIDHelper.IsLXMusic(Provider);
[JsonIgnore] public bool IsLXMusic => PlayerIdHelper.IsLXMusic(Provider);
public MediaSourceProviderInfo()
{
@@ -53,7 +53,7 @@ namespace BetterLyrics.WinUI3.Models
IsEnabled = isEnable;
switch (provider)
{
case Constants.PlayerID.AppleMusic:
case Constants.PlayerId.AppleMusic:
// Apple Music 的特性
TimelineSyncThreshold = 1000;
PositionOffset = 1000;

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
[Index(nameof(Title))]
[Index(nameof(Artist))]
[Index(nameof(StartedAt))] // 用于按时间排序查询(如:最近播放)
[Index(nameof(PlayerId))]
public class PlayHistoryItem
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // AutoIncrement
public int Id { get; set; }
// 注意:作为索引列,必须加 MaxLength。
// 如果不加,默认为 nvarchar(max)SQL Server 无法对其建立高效索引。
[MaxLength(450)]
public string Title { get; set; } = "";
[MaxLength(450)]
public string Artist { get; set; } = "";
// Album 没有索引,可以不限制长度,或者为了规范也限制一下
public string Album { get; set; } = "";
public DateTime StartedAt { get; set; }
public double DurationPlayedMs { get; set; }
public double TotalDurationMs { get; set; }
// PlayerId 通常是个 GUID 或者短字符串,给 100 长度通常足够了,节省索引空间
[MaxLength(100)]
public string PlayerId { get; set; } = "";
}
}

View File

@@ -0,0 +1,15 @@
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public class PlayerStatDisplayItem
{
public string PlayerId { get; set; }
public int PlayCount { get; set; }
public string PlayerName => PlayerIdHelper.GetDisplayName(PlayerId);
}
}

View File

@@ -1,5 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.ObjectModel;
namespace BetterLyrics.WinUI3.Models.Settings

View File

@@ -6,37 +6,18 @@ namespace BetterLyrics.WinUI3.Models
{
public partial class SongsTabInfo : BaseViewModel
{
public string Name { get; set; }
public string Name { get; set; } = "";
public string Icon { get; set; }
public string Icon { get; set; } = "";
public bool IsClosable { get; set; }
public CommonSongProperty FilterProperty { get; set; } = CommonSongProperty.Title;
[ObservableProperty]
public partial bool IsStarred { get; set; }
public string FilterValue { get; set; } = "";
public CommonSongProperty FilterProperty { get; set; }
public string FilterValue { get; set; }
public bool IsDefault => Icon == "\uE8A9";
public SongsTabInfo()
{
Name = string.Empty;
Icon = string.Empty;
IsClosable = true;
IsStarred = false;
FilterProperty = CommonSongProperty.Title;
FilterValue = string.Empty;
}
public SongsTabInfo(string name, string icon, bool isClosable, bool isStarred, CommonSongProperty filterProperty, string filterValue)
{
Name = name;
Icon = icon;
IsClosable = isClosable;
IsStarred = isStarred;
FilterProperty = filterProperty;
FilterValue = filterValue;
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class ArtistPlayCount
{
public string Artist { get; set; }
public int PlayCount { get; set; }
public double TotalDurationSeconds { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class HourlyStatBar
{
public int Hour { get; set; }
public double NormalizedHeight { get; set; } // 0 - 100用于UI高度
public int RawCount { get; set; } // 实际播放数
public string Label { get; set; } // Tooltip: "09:00 - 15 plays"
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class PlayerStats
{
public string PlayerId { get; set; }
public int Count { get; set; }
public double DisplayWidth => (TotalCount > 0) ? (Count / (double)TotalCount) * 150 : 0;
public int TotalCount { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class SongPlayCount
{
public string Title { get; set; }
public string Artist { get; set; }
public int PlayCount { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
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

@@ -39,6 +39,8 @@ namespace BetterLyrics.WinUI3.Renderer
var blur = line.BlurAmountTransition.Value;
var bounds = line.PhoneticCanvasTextLayout.LayoutBounds;
if (double.IsNaN(opacity)) return;
var destRect = new Rect(
bounds.X + line.PhoneticPosition.X,
bounds.Y + line.PhoneticPosition.Y,
@@ -71,6 +73,8 @@ namespace BetterLyrics.WinUI3.Renderer
var blur = line.BlurAmountTransition.Value;
var bounds = line.TranslatedCanvasTextLayout.LayoutBounds;
if (double.IsNaN(opacity)) return;
var destRect = new Rect(
bounds.X + line.TranslatedPosition.X,
bounds.Y + line.TranslatedPosition.Y,

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.SettingsService;
using Microsoft.Extensions.Logging;
using System;
@@ -22,11 +23,13 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
private readonly HttpClient _iTunesHttpClinet;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private readonly ILogger _logger;
public AlbumArtSearchService(ISettingsService settingsService, ILogger<AlbumArtSearchService> logger)
public AlbumArtSearchService(ISettingsService settingsService, IFileSystemService fileSystemService, ILogger<AlbumArtSearchService> logger)
{
_settingsService = settingsService;
_fileSystemService = fileSystemService;
_logger = logger;
_iTunesHttpClinet = new();
}
@@ -77,70 +80,54 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
private async Task<byte[]?> SearchFile(SongInfo songInfo)
{
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
var enabledIds = _settingsService.AppSettings.LocalMediaFolders
.Where(f => f.IsEnabled)
.Select(f => f.Id)
.ToList();
if (enabledIds.Count == 0) return null;
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FilesIndexItem? bestMatch = null;
foreach (var item in allFiles)
{
if (!folder.IsEnabled) continue;
var ext = Path.GetExtension(item.FileName).ToLower();
if (!FileHelper.MusicExtensions.Contains(ext)) continue;
try
bool isMetadataMatch = (item.Title == songInfo.Title && item.Artists == songInfo.DisplayArtists);
bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
Path.GetFileNameWithoutExtension(item.FileName),
songInfo.DisplayArtists,
songInfo.Title
);
if (isMetadataMatch || isFilenameMatch)
{
using var fs = folder.CreateFileSystem();
if (fs == null) continue;
if (!await fs.ConnectAsync()) continue;
// 递归扫描
var foldersToScan = new Queue<string>();
foldersToScan.Enqueue(""); // 根目录
while (foldersToScan.Count > 0)
{
var currentPath = foldersToScan.Dequeue();
var items = await fs.GetFilesAsync(currentPath);
foreach (var item in items)
{
if (item.IsFolder)
{
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
continue;
}
var ext = Path.GetExtension(item.Name).ToLower();
if (FileHelper.MusicExtensions.Contains(ext))
{
try
{
using (var stream = await fs.OpenReadAsync(item.FullPath))
{
var track = new ExtendedTrack(item.FullPath, stream);
bool isMetadataMatch = (track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists);
bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
Path.GetFileNameWithoutExtension(item.Name),
songInfo.DisplayArtists,
songInfo.Title
);
if (isMetadataMatch || isFilenameMatch)
{
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
if (bytes != null && bytes.Length > 0)
{
return bytes;
}
}
}
}
catch
{
}
}
}
}
bestMatch = item;
break;
}
catch
}
if (bestMatch == null || string.IsNullOrEmpty(bestMatch.LocalAlbumArtPath))
{
return null;
}
try
{
if (File.Exists(bestMatch.LocalAlbumArtPath))
{
return await File.ReadAllBytesAsync(bestMatch.LocalAlbumArtPath);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"读取本地缓存失败: {ex.Message}");
}
return null;
}

View File

@@ -0,0 +1,605 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService
{
public partial class FileSystemService : BaseViewModel, IFileSystemService,
IRecipient<PropertyChangedMessage<AutoScanInterval>>,
IRecipient<PropertyChangedMessage<bool>>
{
private readonly ISettingsService _settingsService;
private readonly ILocalizationService _localizationService;
private readonly ILogger<FileSystemService> _logger;
private readonly IDbContextFactory<FilesIndexDbContext> _contextFactory;
// 定时器字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _folderTimerTokens = new();
// 当前正在执行的扫描任务字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeScanTokens = new();
private static readonly SemaphoreSlim _folderScanLock = new(1, 1);
public FileSystemService(
ISettingsService settingsService,
ILocalizationService localizationService,
ILogger<FileSystemService> logger,
IDbContextFactory<FilesIndexDbContext> contextFactory)
{
_logger = logger;
_localizationService = localizationService;
_settingsService = settingsService;
_contextFactory = contextFactory;
}
public async Task<List<FilesIndexItem>> GetFilesAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId, bool forceRefresh = false)
{
string queryParentUri = parentFolder == null ? "" : parentFolder.Uri;
if (parentFolder == null && !forceRefresh) forceRefresh = true;
using var context = await _contextFactory.CreateDbContextAsync();
var cachedEntities = await context.FilesIndex
.AsNoTracking() // 读操作不追踪,提升性能
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
.ToListAsync();
bool needSync = forceRefresh || cachedEntities.Count == 0;
if (needSync)
{
// SyncAsync 内部自己管理 Context
cachedEntities = await SyncAsync(provider, parentFolder, configId);
}
return cachedEntities;
}
/// <summary>
/// 从远端/本地同步文件至数据库
/// </summary>
private async Task<List<FilesIndexItem>> SyncAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId)
{
List<FilesIndexItem> remoteItems;
try
{
remoteItems = await provider.GetFilesAsync(parentFolder);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Network sync error: {ex.Message}");
return [];
}
if (remoteItems == null) return [];
string targetParentUri = "";
if (remoteItems.Count > 0)
targetParentUri = remoteItems[0].ParentUri ?? "";
else if (parentFolder != null)
targetParentUri = parentFolder.Uri;
else
return [];
try
{
using var context = await _contextFactory.CreateDbContextAsync();
// 开启事务 (EF Core 也能管理事务)
using var transaction = await context.Database.BeginTransactionAsync();
// 1. 获取数据库中现有的该目录下的文件
var dbItems = await context.FilesIndex
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToListAsync();
var dbMap = dbItems.ToDictionary(x => x.Uri, x => x);
// 2. 远端数据去重(防止 Provider 返回重复 Uri
var remoteDistinct = remoteItems
.GroupBy(x => x.Uri)
.Select(g => g.First())
.ToList();
var remoteUris = new HashSet<string>();
// 3. 处理 新增 和 更新
foreach (var remote in remoteDistinct)
{
remoteUris.Add(remote.Uri);
if (dbMap.TryGetValue(remote.Uri, out var existing))
{
// 检查是否变更
bool isChanged = existing.FileSize != remote.FileSize ||
existing.LastModified != remote.LastModified;
if (isChanged)
{
existing.FileSize = remote.FileSize;
existing.LastModified = remote.LastModified;
existing.IsMetadataParsed = false; // 标记重新解析
// EF Core 自动追踪 existing 的变化,无需手动 Update
}
}
else
{
// 新增
// 注意:如果 Id 是自增的,不要手动赋值 Id除非是 Guid
context.FilesIndex.Add(remote);
}
}
// 4. 处理 删除 (数据库有,远端没有)
foreach (var dbItem in dbItems)
{
if (!remoteUris.Contains(dbItem.Uri))
{
context.FilesIndex.Remove(dbItem);
}
}
await context.SaveChangesAsync();
await transaction.CommitAsync();
// 5. 返回最新数据
// 这里的 dbItems 已经被 Update 更新了内存状态,但 Remove 的还在列表里Add 的不在列表里
// 所以最稳妥的是重新查一次,或者手动维护列表。为了准确性,重新查询 (AsNoTracking)
var finalItems = await context.FilesIndex
.AsNoTracking()
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToListAsync();
FolderUpdated?.Invoke(this, targetParentUri);
return finalItems;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Database sync error: {ex.Message}");
return [];
}
}
public async Task UpdateMetadataAsync(FilesIndexItem entity)
{
using var context = await _contextFactory.CreateDbContextAsync();
// 使用 EF Core 7.0+ 的 ExecuteUpdateAsync 高效更新
// 这会直接生成 UPDATE SQL不经过内存加载性能极高
await context.FilesIndex
.Where(x => x.Id == entity.Id) // 优先用 Id
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Title, entity.Title)
.SetProperty(p => p.Artists, entity.Artists)
.SetProperty(p => p.Album, entity.Album)
.SetProperty(p => p.Year, entity.Year)
.SetProperty(p => p.Bitrate, entity.Bitrate)
.SetProperty(p => p.SampleRate, entity.SampleRate)
.SetProperty(p => p.BitDepth, entity.BitDepth)
.SetProperty(p => p.Duration, entity.Duration)
.SetProperty(p => p.AudioFormatName, entity.AudioFormatName)
.SetProperty(p => p.AudioFormatShortName, entity.AudioFormatShortName)
.SetProperty(p => p.Encoder, entity.Encoder)
.SetProperty(p => p.EmbeddedLyrics, entity.EmbeddedLyrics)
.SetProperty(p => p.LocalAlbumArtPath, entity.LocalAlbumArtPath)
.SetProperty(p => p.IsMetadataParsed, true)
);
}
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FilesIndexItem entity)
{
return await provider.OpenReadAsync(entity);
}
public async Task DeleteCacheForMediaFolderAsync(MediaFolder folder)
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.IndexingProgress = 0;
folder.StatusSeverity = InfoBarSeverity.Informational;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServicePrepareToClean");
folder.IsProcessing = true;
});
if (_folderTimerTokens.TryRemove(folder.Id, out var timerCts))
{
timerCts.Cancel();
timerCts.Dispose();
_logger.LogInformation("DeleteCacheForMediaFolderAsync: {}", "cts.Dispose();");
}
if (_activeScanTokens.TryGetValue(folder.Id, out var activeScanCts))
{
activeScanCts.Cancel();
}
try
{
await _folderScanLock.WaitAsync();
try
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
});
using var context = await _contextFactory.CreateDbContextAsync();
await context.FilesIndex
.Where(x => x.MediaFolderId == folder.Id)
.ExecuteDeleteAsync();
// VACUUM 是 SQLite 特有的命令
if (context.Database.IsSqlite())
{
await context.Database.ExecuteSqlRawAsync("VACUUM");
}
}
finally
{
_folderScanLock.Release();
}
}
catch (Exception ex)
{
_logger.LogError("DeleteCacheForMediaFolderAsync: {}", ex.Message);
}
finally
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.IsProcessing = false;
folder.LastSyncTime = null;
});
}
}
public async Task ScanMediaFolderAsync(MediaFolder folder, CancellationToken token = default)
{
if (folder == null || !folder.IsEnabled) return;
using var scanCts = CancellationTokenSource.CreateLinkedTokenSource(token);
_activeScanTokens[folder.Id] = scanCts;
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusSeverity = InfoBarSeverity.Informational;
folder.IsProcessing = true;
folder.IndexingProgress = 0;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceWaitingForScan");
});
try
{
await _folderScanLock.WaitAsync(scanCts.Token);
_dispatcherQueue.TryEnqueue(() => folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
using var fs = folder.CreateFileSystem();
if (fs == null || !await fs.ConnectAsync())
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusSeverity = InfoBarSeverity.Error;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceConnectFailed");
});
return;
}
_dispatcherQueue.TryEnqueue(() => folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceFetchingFileList"));
var filesToProcess = new List<FilesIndexItem>();
var foldersToScan = new Queue<FilesIndexItem?>();
foldersToScan.Enqueue(null); // 根目录
while (foldersToScan.Count > 0)
{
if (scanCts.Token.IsCancellationRequested) return;
var currentParent = foldersToScan.Dequeue();
var items = await GetFilesAsync(fs, currentParent, folder.Id, forceRefresh: true);
foreach (var item in items)
{
if (item.IsDirectory)
{
foldersToScan.Enqueue(item);
}
else
{
var ext = Path.GetExtension(item.FileName).ToLower();
if (FileHelper.AllSupportedExtensions.Contains(ext))
{
filesToProcess.Add(item);
}
}
}
}
int total = filesToProcess.Count;
int current = 0;
foreach (var item in filesToProcess)
{
if (scanCts.Token.IsCancellationRequested) return;
current++;
if (current % 10 == 0 || current == total)
{
double progress = (double)current / total * 100;
_dispatcherQueue.TryEnqueue(() =>
{
folder.IndexingProgress = progress;
folder.StatusText = $"{_localizationService.GetLocalizedString("FileSystemServiceParsing")} {current}/{total}";
});
}
if (item.IsMetadataParsed) continue;
var ext = Path.GetExtension(item.FileName).ToLower();
try
{
if (FileHelper.MusicExtensions.Contains(ext))
{
using var originalStream = await OpenFileAsync(fs, item);
if (originalStream == null) continue;
ExtendedTrack track;
if (originalStream.CanSeek)
{
track = new ExtendedTrack(item, originalStream);
}
else
{
using var memStream = new MemoryStream();
await originalStream.CopyToAsync(memStream, scanCts.Token);
memStream.Position = 0;
track = new ExtendedTrack(item, memStream);
}
if (track.Duration > 0)
{
string? artPath = await SaveAlbumArtToDiskAsync(track);
item.Title = track.Title;
item.Artists = track.Artist;
item.Album = track.Album;
item.Year = track.Year;
item.Bitrate = track.Bitrate;
item.SampleRate = track.SampleRate;
item.BitDepth = track.BitDepth;
item.Duration = track.Duration;
item.AudioFormatName = track.AudioFormatName;
item.AudioFormatShortName = track.AudioFormatShortName;
item.Encoder = track.Encoder;
item.EmbeddedLyrics = track.RawLyrics;
item.LocalAlbumArtPath = artPath;
item.IsMetadataParsed = true;
}
}
else if (FileHelper.LyricExtensions.Contains(ext))
{
using var stream = await OpenFileAsync(fs, item);
if (stream != null)
{
using var reader = new StreamReader(stream);
string content = await reader.ReadToEndAsync();
item.EmbeddedLyrics = content;
item.IsMetadataParsed = true;
}
}
if (item.IsMetadataParsed)
{
// 更新操作:直接调用 UpdateMetadataAsync
// 此时不需要 _dbLock因为 UpdateMetadataAsync 内部会 CreateDbContextAsync
// 而 _folderScanLock 已经保证了当前文件夹扫描的独占性
await UpdateMetadataAsync(item);
}
}
catch (Exception ex)
{
_logger.LogError("ScanMediaFolderAsync: {}", ex.Message);
}
}
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusSeverity = InfoBarSeverity.Success;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceReady");
folder.LastSyncTime = DateTime.Now;
});
}
catch (OperationCanceledException)
{
// 正常取消
}
catch (Exception ex)
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusText = ex.Message;
folder.StatusSeverity = InfoBarSeverity.Error;
});
}
finally
{
_folderScanLock.Release();
_activeScanTokens.TryRemove(folder.Id, out _);
_dispatcherQueue.TryEnqueue(() =>
{
folder.IsProcessing = false;
folder.IndexingProgress = 0;
});
}
}
public async Task<List<FilesIndexItem>> GetParsedFilesAsync()
{
using var context = await _contextFactory.CreateDbContextAsync();
// SQL: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
return await context.FilesIndex
.AsNoTracking()
.Where(x => x.IsMetadataParsed)
.ToListAsync();
}
public async Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
{
if (enabledConfigIds == null || !enabledConfigIds.Any())
{
return new List<FilesIndexItem>();
}
var idList = enabledConfigIds.ToList();
using var context = await _contextFactory.CreateDbContextAsync();
// SQL: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
return await context.FilesIndex
.AsNoTracking()
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
.ToListAsync();
}
public void StartAllFolderTimers()
{
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
{
if (folder.IsEnabled)
{
UpdateFolderTimer(folder);
}
}
}
private void UpdateFolderTimer(MediaFolder folder)
{
if (_folderTimerTokens.TryRemove(folder.Id, out var oldCts))
{
oldCts.Cancel();
oldCts.Dispose();
}
if (!folder.IsEnabled || folder.ScanInterval == AutoScanInterval.Disabled)
{
return;
}
var newCts = new CancellationTokenSource();
_folderTimerTokens[folder.Id] = newCts;
TimeSpan period = folder.ScanInterval switch
{
AutoScanInterval.Every15Minutes => TimeSpan.FromMinutes(15),
AutoScanInterval.EveryHour => TimeSpan.FromHours(1),
AutoScanInterval.Every6Hours => TimeSpan.FromHours(6),
AutoScanInterval.Daily => TimeSpan.FromDays(1),
_ => TimeSpan.FromHours(1)
};
Task.Run(async () =>
{
try
{
using var timer = new PeriodicTimer(period);
while (await timer.WaitForNextTickAsync(newCts.Token))
{
await ScanMediaFolderAsync(folder, newCts.Token);
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"文件夹 {folder.Name} 定时扫描出错: {ex.Message}");
}
}, newCts.Token);
}
public event EventHandler<string>? FolderUpdated;
private async Task<string?> SaveAlbumArtToDiskAsync(ExtendedTrack track)
{
// 代码未变,纯 IO 操作
var picData = track.AlbumArtByteArray;
if (picData == null || picData.Length == 0) return null;
try
{
string hash = ComputeHashForBytes(picData);
string safeName = hash + ".jpg";
string localPath = Path.Combine(PathHelper.LocalAlbumArtCacheDirectory, safeName);
if (File.Exists(localPath)) return localPath;
await File.WriteAllBytesAsync(localPath, picData);
return localPath;
}
catch (Exception)
{
return null;
}
}
private string ComputeHashForBytes(byte[] data)
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var hashBytes = md5.ComputeHash(data);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
public void Receive(PropertyChangedMessage<AutoScanInterval> message)
{
if (message.Sender is MediaFolder mediaFolder)
{
if (message.PropertyName == nameof(MediaFolder.ScanInterval))
{
UpdateFolderTimer(mediaFolder);
}
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is MediaFolder mediaFolder)
{
if (message.PropertyName == nameof(MediaFolder.IsEnabled))
{
UpdateFolderTimer(mediaFolder);
}
}
}
}
}

View File

@@ -0,0 +1,69 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService
{
public interface IFileSystemService
{
/// <summary>
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)
/// </summary>
/// <param name="provider"></param>
/// <param name="parentFolder"></param>
/// <param name="configId"></param>
/// <param name="forceRefresh">强制需要从远端/本地同步至数据库</param>
/// <returns></returns>
Task<List<FilesIndexItem>> GetFilesAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId, bool forceRefresh = false);
/// <summary>
/// 打开文件(通过远端/本地流)
/// </summary>
/// <param name="provider"></param>
/// <param name="entity"></param>
/// <returns></returns>
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FilesIndexItem entity);
/// <summary>
/// 更新数据库(单个文件)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
Task UpdateMetadataAsync(FilesIndexItem entity);
/// <summary>
/// 从数据库删除
/// </summary>
/// <param name="folder"></param>
/// <returns></returns>
Task DeleteCacheForMediaFolderAsync(MediaFolder folder);
/// <summary>
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)。对于需要解析的文件,打开流填充元数据并回写至数据库。
/// </summary>
/// <param name="folder"></param>
/// <returns></returns>
Task ScanMediaFolderAsync(MediaFolder folder, CancellationToken token = default);
/// <summary>
/// 从数据库拉取全部已解析的数据
/// </summary>
/// <returns></returns>
Task<List<FilesIndexItem>> GetParsedFilesAsync();
/// <summary>
/// 从数据库拉取全部已解析的且其所属的 MediaFolder 在应用内处于开启状态的数据
/// </summary>
/// <param name="enabledConfigIds"></param>
/// <returns></returns>
Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
void StartAllFolderTimers();
event EventHandler<string> FolderUpdated;
}
}

View File

@@ -0,0 +1,26 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService
{
public interface IUnifiedFileSystem : IDisposable
{
Task<bool> ConnectAsync();
/// <summary>
/// 从流拉取
/// </summary>
/// <param name="parentFolder"></param>
/// <returns></returns>
Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null);
/// <summary>
/// 打开流
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
Task<Stream?> OpenReadAsync(FilesIndexItem file);
Task DisconnectAsync();
}
}

View File

@@ -0,0 +1,169 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using FluentFTP;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net; // 用于 WebUtility.UrlDecode
using System.Text; // ★ 修复 Encoding 报错的关键
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
public partial class FTPFileSystem : IUnifiedFileSystem
{
private readonly AsyncFtpClient _client;
private readonly MediaFolder _config;
public FTPFileSystem(MediaFolder config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
var ftpConfig = new FtpConfig
{
ConnectTimeout = 5000,
DataConnectionConnectTimeout = 5000,
ReadTimeout = 10000,
// 忽略证书错误
ValidateAnyCertificate = true
};
int port = _config.UriPort > 0 ? _config.UriPort : 0;
_client = new AsyncFtpClient(
_config.UriHost,
_config.UserName ?? "anonymous",
_config.Password ?? "",
port,
ftpConfig
);
}
public async Task<bool> ConnectAsync()
{
if (_client.IsConnected) return true;
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
return _client.IsConnected;
}
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FilesIndexItem>();
string targetServerPath;
Uri parentUri;
if (parentFolder == null)
{
var rootUri = _config.GetStandardUri();
targetServerPath = rootUri.AbsolutePath;
parentUri = rootUri;
}
else
{
targetServerPath = GetServerPathFromUri(parentFolder.Uri);
parentUri = new Uri(parentFolder.Uri);
}
targetServerPath = WebUtility.UrlDecode(targetServerPath).Replace("\\", "/");
if (string.IsNullOrEmpty(targetServerPath)) targetServerPath = "/";
try
{
var items = await _client.GetListing(targetServerPath, FtpListOption.Auto);
string baseUriSchema = $"{parentUri.Scheme}://{parentUri.Host}";
if (parentUri.Port > 0) baseUriSchema += $":{parentUri.Port}";
foreach (var item in items)
{
// 跳过 . 和 ..
if (item.Name == "." || item.Name == "..") continue;
// 只处理文件和文件夹
if (item.Type != FtpObjectType.File && item.Type != FtpObjectType.Directory) continue;
// 只处理特定后缀文件
if (item.Type == FtpObjectType.File)
{
string extension = Path.GetExtension(item.Name);
if (string.IsNullOrEmpty(extension) || !FileHelper.AllSupportedExtensions.Contains(extension)) continue;
}
var builder = new UriBuilder(baseUriSchema)
{
Path = item.FullName
};
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
// 如果是根目录扫描ParentUri 用 Config 的;否则用传入文件夹的
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
Uri = builder.Uri.AbsoluteUri, // 标准化 URI
FileName = item.Name,
IsDirectory = item.Type == FtpObjectType.Directory,
FileSize = item.Size,
// 防止某些服务器返回 MinValue
LastModified = item.Modified == DateTime.MinValue ? DateTime.Now : item.Modified,
IsMetadataParsed = false
});
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"FTP列表获取失败: {targetServerPath} - {ex.Message}");
}
return result;
}
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (file == null) return null;
try
{
// 1. 还原服务器路径
string serverPath = GetServerPathFromUri(file.Uri);
// 2. 解码 (Uri 里的空格是 %20FTP 需要真实空格)
serverPath = WebUtility.UrlDecode(serverPath);
// 3. 返回流
// 注意FluentFTP 的 OpenRead 依赖于连接保持活跃
return await _client.OpenRead(serverPath);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"打开文件流失败: {file.FileName} - {ex.Message}");
return null;
}
}
public async Task DisconnectAsync()
{
if (_client.IsConnected)
{
await _client.Disconnect();
}
}
public void Dispose()
{
_client?.Dispose();
GC.SuppressFinalize(this);
}
// 私有辅助方法
private string GetServerPathFromUri(string uriString)
{
var uri = new Uri(uriString);
return uri.AbsolutePath; // 这里拿到的比如是 "/Music/Song%201.mp3"
}
}
}

View File

@@ -0,0 +1,119 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
public partial class LocalFileSystem : IUnifiedFileSystem
{
private readonly MediaFolder _config;
private readonly string _rootLocalPath;
public LocalFileSystem(MediaFolder config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_rootLocalPath = config.UriPath;
}
public Task<bool> ConnectAsync()
{
var isExisted = Directory.Exists(_rootLocalPath);
if (isExisted)
{
return Task.FromResult(true);
}
else
{
throw new FileNotFoundException(null, _rootLocalPath);
}
}
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FilesIndexItem>();
string targetPath;
string parentUriString;
try
{
if (parentFolder == null)
{
targetPath = _rootLocalPath;
parentUriString = _config.GetStandardUri().AbsoluteUri;
}
else
{
var uri = new Uri(parentFolder.Uri);
targetPath = uri.LocalPath;
parentUriString = parentFolder.Uri;
}
if (!Directory.Exists(targetPath)) return result;
var dirInfo = new DirectoryInfo(targetPath);
foreach (var item in dirInfo.EnumerateFileSystemInfos())
{
// 跳过系统/隐藏文件
if ((item.Attributes & FileAttributes.Hidden) != 0 || (item.Attributes & FileAttributes.System) != 0) continue;
bool isDir = (item.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
if (!isDir)
{
// 过滤后缀名
if (string.IsNullOrEmpty(item.Extension) || !FileHelper.AllSupportedExtensions.Contains(item.Extension)) continue;
}
var itemUri = new Uri(item.FullName).AbsoluteUri;
long size = 0;
if (!isDir && item is FileInfo fi)
{
size = fi.Length;
}
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id, // 关联配置 ID
ParentUri = parentUriString, // 记录父级 URI
Uri = itemUri,
FileName = item.Name,
IsDirectory = isDir,
FileSize = size,
LastModified = item.LastWriteTime
});
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Local scan error: {ex.Message}");
}
return await Task.FromResult(result);
}
public async Task<Stream?> OpenReadAsync(FilesIndexItem entity)
{
if (entity == null) return null;
string localPath = new Uri(entity.Uri).LocalPath;
// 使用 FileShare.Read 允许其他程序同时读取
// 使用 useAsync: true 优化异步读写性能
return new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
}
public async Task DisconnectAsync() => await Task.CompletedTask;
public void Dispose() { }
}
}

View File

@@ -0,0 +1,204 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using SMBLibrary;
using SMBLibrary.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
public partial class SMBFileSystem : IUnifiedFileSystem
{
private SMB2Client? _client;
private ISMBFileStore? _fileStore;
// 保存配置对象的引用
private readonly MediaFolder _config;
// 缓存解析出来的 Share 名称,因为 TreeConnect 要用
private string _shareName;
public SMBFileSystem(MediaFolder config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
// 在构造时就解析好 Share 名称,避免后续重复解析
var uri = _config.GetStandardUri();
// Segments[0] 是 "/", Segments[1] 是 "ShareName/"
if (uri.Segments.Length > 1)
{
_shareName = uri.Segments[1].TrimEnd('/');
}
else
{
// 如果没有 ShareName这在 SMB 中通常是不合法的,但在根目录下可能发生
_shareName = "";
}
}
public async Task<bool> ConnectAsync()
{
_client = new SMB2Client();
// 连接主机
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
// 登录
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 连接共享目录 (TreeConnect)
// SMBLibrary 必须先连接到 Share后续所有文件操作都是基于这个 Share 的相对路径
if (string.IsNullOrEmpty(_shareName)) return false;
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
/// <summary>
/// 获取文件列表
/// </summary>
/// <param name="parentFolder">
/// 传入要列出的文件夹实体。
/// 如果传入 null则默认列出 MediaFolder 配置的根目录。
/// </param>
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FilesIndexItem>();
if (_fileStore == null) return result;
string smbPath = GetPathRelativeToShare(parentFolder);
var statusRet = _fileStore.CreateFile(out object handle, out FileStatus status, smbPath,
AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read,
CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
if (statusRet != NTStatus.STATUS_SUCCESS) return result;
string parentUriString = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri;
List<QueryDirectoryFileInformation> fileInfo;
do
{
statusRet = _fileStore.QueryDirectory(out fileInfo, handle, "*", FileInformationClass.FileDirectoryInformation);
// 如果查询失败或者没有更多文件fileInfo 可能是 null直接跳出
if (statusRet != NTStatus.STATUS_SUCCESS && statusRet != NTStatus.STATUS_NO_MORE_FILES)
{
break;
}
// 如果是 NO_MORE_FILES 但 fileInfo 依然有残留数据(极少见),或者是 SUCCESS
if (fileInfo != null)
{
foreach (var item in fileInfo.Cast<FileDirectoryInformation>())
{
if (item.FileName == "." || item.FileName == "..") continue;
// 过滤隐藏文件和系统文件
if ((item.FileAttributes & SMBLibrary.FileAttributes.Hidden) == SMBLibrary.FileAttributes.Hidden ||
(item.FileAttributes & SMBLibrary.FileAttributes.System) == SMBLibrary.FileAttributes.System)
{
continue;
}
bool isDir = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory;
// 后缀名过滤
if (!isDir)
{
string extension = Path.GetExtension(item.FileName);
if (string.IsNullOrEmpty(extension) || !FileHelper.AllSupportedExtensions.Contains(extension)) continue;
}
if (!parentUriString.EndsWith("/")) parentUriString += "/";
var baseUri = new Uri(parentUriString);
var newUri = new Uri(baseUri, item.FileName);
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
Uri = newUri.AbsoluteUri,
FileName = item.FileName,
IsDirectory = (item.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory,
FileSize = item.AllocationSize,
LastModified = item.ChangeTime
});
}
}
if (statusRet == NTStatus.STATUS_NO_MORE_FILES) break;
} while (statusRet == NTStatus.STATUS_SUCCESS);
_fileStore.CloseFile(handle);
return result;
}
/// <summary>
/// 打开文件流
/// </summary>
/// <param name="file">只需要传入文件实体即可</param>
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (_fileStore == null || file == null) return null;
string smbPath = GetPathRelativeToShare(file);
var ret = _fileStore.CreateFile(out object handle, out FileStatus status, smbPath,
AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, 0, null);
if (ret != NTStatus.STATUS_SUCCESS)
throw new IOException($"SMB Open Error: {ret}");
return new SMBReadOnlyStream(_fileStore, handle);
}
public async Task DisconnectAsync()
{
_client?.Disconnect();
await Task.CompletedTask;
}
public void Dispose()
{
_client?.Disconnect();
}
private string GetPathRelativeToShare(FilesIndexItem? entity)
{
Uri targetUri;
if (entity == null)
{
targetUri = _config.GetStandardUri();
}
else
{
targetUri = new Uri(entity.Uri);
}
string absolutePath = Uri.UnescapeDataString(targetUri.AbsolutePath);
string cleanPath = absolutePath.TrimStart('/');
int slashIndex = cleanPath.IndexOf('/');
if (slashIndex == -1)
{
return string.Empty;
}
string relativePath = cleanPath.Substring(slashIndex + 1);
return relativePath.Replace("/", "\\");
}
}
}

View File

@@ -3,14 +3,17 @@ using SMBLibrary.Client;
using System;
using System.IO;
namespace BetterLyrics.WinUI3.Models.FileSystem
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
public partial class SMBReadOnlyStream : Stream
{
private readonly ISMBFileStore _store;
private readonly object _handle;
private long _position;
private long _length; // 新增:缓存文件长度
private long _length;
// SMB 协议建议的最大读取块大小 (64KB 是最安全的通用值)
private const int MaxReadChunkSize = 65536;
public SMBReadOnlyStream(ISMBFileStore store, object handle)
{
@@ -25,18 +28,15 @@ namespace BetterLyrics.WinUI3.Models.FileSystem
}
else
{
// 如果获取失败,这是一个严重问题,意味着无法 Seek 到末尾
// 暂时设为 0但后续读取可能会出问题
_length = 0;
_length = 0; // 这是一个风险点,但为了不 crash 先设为 0
System.Diagnostics.Debug.WriteLine($"SMB GetLength Error: {status}");
}
}
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position
{
get => _position;
@@ -45,30 +45,49 @@ namespace BetterLyrics.WinUI3.Models.FileSystem
public override int Read(byte[] buffer, int offset, int count)
{
// 保护:如果位置已经超过文件末尾,直接返回 0 (EOF)
if (_position >= _length) return 0;
// 保护:防止读取越界 (请求读取量不能超过剩余量)
long remaining = _length - _position;
int bytesToRequest = (int)Math.Min(count, remaining);
int totalBytesRead = 0;
int remainingRequest = count;
// 为了安全,保留对 remaining 的检查是必须的
if (bytesToRequest <= 0) return 0;
var status = _store.ReadFile(out byte[] data, _handle, _position, bytesToRequest);
if (status == NTStatus.STATUS_END_OF_FILE) return 0;
if (status != NTStatus.STATUS_SUCCESS)
// 循环读取,直到读完请求的数量,或者文件结束
while (remainingRequest > 0)
{
throw new IOException($"SMB Read failed. Status: {status} (Pos: {_position}, Req: {bytesToRequest})");
// 计算剩余文件长度
long remainingFile = _length - _position;
if (remainingFile <= 0) break; // 已到末尾
// 计算本次 SMB 请求的大小 (取三者最小值请求剩余量、文件剩余量、SMB最大块限制)
int bytesToReadThisChunk = (int)Math.Min(Math.Min(remainingRequest, remainingFile), MaxReadChunkSize);
// 发送 SMB 请求
var status = _store.ReadFile(out byte[] data, _handle, _position, bytesToReadThisChunk);
// 处理结果
if (status == NTStatus.STATUS_END_OF_FILE) break;
if (status != NTStatus.STATUS_SUCCESS)
{
// 遇到错误抛出详细信息
throw new IOException($"SMB Read failed. Status: {status}, Position: {_position}, ChunkReq: {bytesToReadThisChunk}");
}
if (data == null || data.Length == 0) break;
// 复制数据到输出 buffer
Array.Copy(data, 0, buffer, offset + totalBytesRead, data.Length);
// 更新指针和计数器
_position += data.Length;
totalBytesRead += data.Length;
remainingRequest -= data.Length;
// 如果实际读到的比请求的少,通常意味着提前到了 EOF或者网络包较小
// 这里选择继续循环尝试,直到读不够或者明确 EOF
if (data.Length < bytesToReadThisChunk) break;
}
if (data == null || data.Length == 0) return 0;
Array.Copy(data, 0, buffer, offset, data.Length);
_position += data.Length;
return data.Length;
return totalBytesRead;
}
public override long Seek(long offset, SeekOrigin origin)
@@ -88,10 +107,9 @@ namespace BetterLyrics.WinUI3.Models.FileSystem
break;
}
// 允许 Seek 超过 EOF (标准 Stream 行为),但在 Read 时会返回 0
if (newPos < 0)
{
throw new IOException("An attempt was made to move the file pointer before the beginning of the file.");
throw new IOException("Seek before beginning.");
}
_position = newPos;
@@ -111,4 +129,4 @@ namespace BetterLyrics.WinUI3.Models.FileSystem
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using WebDav;
namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
{
public partial class WebDavFileSystem : IUnifiedFileSystem
{
private readonly WebDavClient _client;
private readonly MediaFolder _config;
private readonly Uri _baseAddress;
public WebDavFileSystem(MediaFolder config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
// 构建 BaseAddress (只包含 http://host:port/)
// MediaFolder.GetStandardUri() 返回的是带路径的完整 URI (http://host:port/path)
// 提取出根用于初始化 WebDavClient
var fullUri = _config.GetStandardUri();
// 提取 "http://host:port"
_baseAddress = new Uri($"{fullUri.Scheme}://{fullUri.Authority}");
_client = new WebDavClient(new WebDavClientParams
{
BaseAddress = _baseAddress,
Credentials = new System.Net.NetworkCredential(_config.UserName, _config.Password)
});
}
public async Task<bool> ConnectAsync()
{
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
return result.IsSuccessful;
}
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var list = new List<FilesIndexItem>();
Uri targetUri;
if (parentFolder == null)
{
targetUri = _config.GetStandardUri();
}
else
{
targetUri = new Uri(parentFolder.Uri);
}
var result = await _client.Propfind(targetUri.AbsoluteUri);
if (result.IsSuccessful)
{
string parentUriString = targetUri.AbsoluteUri;
if (!parentUriString.EndsWith("/")) parentUriString += "/";
string targetPathClean = targetUri.AbsolutePath.TrimEnd('/');
foreach (var res in result.Resources)
{
var itemUri = new Uri(_baseAddress, res.Uri);
// 过滤掉文件夹自身
if (itemUri.AbsolutePath.TrimEnd('/') == targetPathClean) continue;
string? name = res.DisplayName;
if (string.IsNullOrEmpty(name))
{
name = itemUri.AbsolutePath.TrimEnd('/').Split('/').Last();
name = System.Net.WebUtility.UrlDecode(name);
}
if (string.IsNullOrEmpty(name)) continue;
if (name.StartsWith(".")) continue;
bool isDir = res.IsCollection;
if (!isDir)
{
string extension = System.IO.Path.GetExtension(name);
// 如果后缀为空或不在白名单,跳过
if (string.IsNullOrEmpty(extension) || !FileHelper.AllSupportedExtensions.Contains(extension)) continue;
}
list.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
Uri = itemUri.AbsoluteUri,
FileName = name,
IsDirectory = res.IsCollection,
FileSize = res.ContentLength ?? 0,
LastModified = res.LastModifiedDate ?? DateTime.MinValue,
});
}
}
return list;
}
public async Task<Stream?> OpenReadAsync(FilesIndexItem entity)
{
if (entity == null) return null;
// WebDAV 获取流,直接使用完整 URI
var res = await _client.GetRawFile(entity.Uri);
if (!res.IsSuccessful)
throw new IOException($"WebDAV Error {res.StatusCode}: {res.Description}");
return res.Stream;
}
public async Task DisconnectAsync() => await Task.CompletedTask;
public void Dispose() => _client?.Dispose();
}
}

View File

@@ -9,6 +9,7 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -16,9 +17,9 @@ using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public partial class MediaSessionsService : IMediaSessionsService
public partial class GSMTCService : IGSMTCService
{
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();

View File

@@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public partial class MediaSessionsService : IMediaSessionsService
public partial class GSMTCService : IGSMTCService
{
private LatestOnlyTaskRunner _refreshLyricsRunner = new();

View File

@@ -11,8 +11,9 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
@@ -27,6 +28,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
@@ -36,12 +38,13 @@ using Windows.Media.Control;
using Windows.Storage.Streams;
using WindowsMediaController;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
public partial class GSMTCService : BaseViewModel, IGSMTCService,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<ChineseRomanization>>
IRecipient<PropertyChangedMessage<ChineseRomanization>>,
IRecipient<PropertyChangedMessage<DateTime?>>
{
private EventSourceReader? _sse = null;
private readonly MediaManager _mediaManager = new();
@@ -52,38 +55,43 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly ITranslationService _translationService;
private readonly ITransliterationService _transliterationService;
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly IDiscordService _discordService;
private readonly ILogger<MediaSessionsService> _logger;
private readonly IPlayHistoryService _playHistoryService;
private readonly ILastFMService _lastFMService;
private readonly ILogger<GSMTCService> _logger;
private double _lxMusicPositionSeconds = 0;
private byte[]? _lxMusicAlbumArtBytes = null;
private readonly DispatcherQueueTimer? _onMediaPropsChangedTimer;
private readonly Stopwatch _scrobbleStopwatch = new();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool CurrentIsPlaying { get; private set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TimeSpan CurrentPosition { get; private set; } = TimeSpan.Zero;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo? CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
[ObservableProperty] public partial MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; set; }
public MediaSessionsService(
public GSMTCService(
ISettingsService settingsService,
IAlbumArtSearchService albumArtSearchService,
ILyricsSearchService lyricsSearchService,
ILibWatcherService libWatcherService,
IDiscordService discordService,
ITranslationService libreTranslateService,
ITransliterationService transliterationService,
ILogger<MediaSessionsService> logger)
IPlayHistoryService playHistoryService,
ILastFMService lastFMService,
ILogger<GSMTCService> logger)
{
_settingsService = settingsService;
_albumArtSearchService = albumArtSearchService;
_lyrcsSearchService = lyricsSearchService;
_libWatcherService = libWatcherService;
_translationService = libreTranslateService;
_transliterationService = transliterationService;
_discordService = discordService;
_playHistoryService = playHistoryService;
_lastFMService = lastFMService;
_logger = logger;
_onMediaPropsChangedTimer = _dispatcherQueue.CreateTimer();
@@ -91,13 +99,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_settingsService.AppSettings.MappedSongSearchQueries.CollectionChanged += MappedSongSearchQueries_CollectionChanged;
_settingsService.AppSettings.MappedSongSearchQueries.ItemPropertyChanged += MappedSongSearchQueries_ItemPropertyChanged;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
InitMediaManager();
}
@@ -111,12 +116,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
UpdateLyrics();
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateAlbumArt();
@@ -144,12 +143,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
{
var desiredSession = GetCurrentSession();
@@ -161,7 +154,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id);
if (_settingsService.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened)
{
if (PlayerIDHelper.IsBetterLyrics(found?.Provider))
if (PlayerIdHelper.IsBetterLyrics(found?.Provider))
{
return found?.IsEnabled ?? true;
}
@@ -205,11 +198,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties? timelineProperties)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
_dispatcherQueue.TryEnqueue(() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
return;
}
@@ -220,6 +214,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
}
else
@@ -235,7 +230,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo? playbackInfo)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, (() =>
_dispatcherQueue.TryEnqueue(() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
@@ -246,7 +241,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
@@ -261,14 +255,23 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_ => false,
};
}
}));
if (CurrentIsPlaying)
{
_scrobbleStopwatch.Start();
}
else
{
_scrobbleStopwatch.Stop();
}
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProperties)
{
_onMediaPropsChangedTimer?.Debounce(() =>
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
_dispatcherQueue.TryEnqueue(async () =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
@@ -286,7 +289,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
if (PlayerIDHelper.IsLXMusic(sessionId))
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StopSSE();
}
@@ -305,20 +308,20 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
string? fixedAlbum = mediaProperties?.AlbumTitle;
string? songId = null;
if (PlayerIDHelper.IsAppleMusic(sessionId))
if (PlayerIdHelper.IsAppleMusic(sessionId))
{
fixedArtist = mediaProperties?.Artist.Split(" — ").FirstOrDefault();
fixedAlbum = mediaProperties?.Artist.Split(" — ").LastOrDefault();
fixedAlbum = fixedAlbum?.Replace(" - Single", "");
fixedAlbum = fixedAlbum?.Replace(" - EP", "");
}
else if (PlayerIDHelper.IsNeteaseFamily(sessionId))
else if (PlayerIdHelper.IsNeteaseFamily(sessionId))
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerID.QQMusic)
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
@@ -329,6 +332,35 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
// 写入播放记录
if (CurrentSongInfo != null && !string.IsNullOrWhiteSpace(CurrentSongInfo.Title) && CurrentSongInfo.Title != "N/A")
{
// 必须捕获一个副本给异步任务,因为 CurrentSongInfo 马上就要变了
var lastSong = CurrentSongInfo;
// 当前秒表时间 >= 上一首总时长 / 2
if (lastSong.DurationMs > 0 &&
_scrobbleStopwatch.Elapsed.TotalMilliseconds >= (lastSong.DurationMs / 2))
{
// 写入本地播放记录
var playHistoryItem = lastSong.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
_ = Task.Run(() => _playHistoryService.AddLogAsync(playHistoryItem));
_logger.LogInformation($"[Scrobble] 结算成功: {lastSong.Title}");
}
// 写入 Last.fm 播放记录
var isLastFMEnabled = CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (isLastFMEnabled)
{
// 后台
_ = Task.Run(() => _lastFMService.TrackAsync(lastSong));
}
}
}
_scrobbleStopwatch.Restart();
CurrentSongInfo = new SongInfo
{
Title = mediaProperties?.Title ?? "N/A",
@@ -340,7 +372,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
LinkedFileName = linkedFileName
};
if (PlayerIDHelper.IsLXMusic(sessionId))
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
@@ -349,7 +381,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
StopSSE();
}
if (PlayerIDHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
@@ -387,13 +419,13 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
private async void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
RecordMediaSourceProviderInfo(mediaSession);
SendFocusedMessagesAsync().ConfigureAwait(false);
await SendFocusedMessagesAsync();
}
private MediaManager.MediaSession? GetCurrentSession()
@@ -447,6 +479,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
_discordService.Disable();
@@ -540,7 +573,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (PlayerIDHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
if (PlayerIdHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
@@ -693,6 +726,14 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
MediaManager_OnFocusedSessionChanged(null);
}
}
else if (message.Sender is MediaFolder)
{
if (message.PropertyName == nameof(MediaFolder.IsEnabled))
{
UpdateAlbumArt();
UpdateLyrics();
}
}
}
public void Receive(PropertyChangedMessage<string> message)
@@ -726,5 +767,16 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
public void Receive(PropertyChangedMessage<DateTime?> message)
{
if (message.Sender is MediaFolder)
{
if (message.PropertyName == nameof(MediaFolder.LastSyncTime))
{
UpdateAlbumArt();
UpdateLyrics();
}
}
}
}
}

View File

@@ -8,9 +8,12 @@ using System.Threading.Tasks;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public interface IMediaSessionsService : INotifyPropertyChanged
/// <summary>
/// Interface for GlobalSystemMediaTransportControlsSession Service
/// </summary>
public interface IGSMTCService : INotifyPropertyChanged
{
Task PlayAsync();
Task PauseAsync();

View File

@@ -1,12 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Events;
using System;
namespace BetterLyrics.WinUI3.Services.LibWatcherService
{
public interface ILibWatcherService
{
event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
}
}

View File

@@ -1,91 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.UI.Dispatching;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace BetterLyrics.WinUI3.Services.LibWatcherService
{
public class LibWatcherService : BaseViewModel, IDisposable, ILibWatcherService
{
private readonly ISettingsService _settingsService;
private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
public LibWatcherService(ISettingsService settingsService)
{
_settingsService = settingsService;
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
UpdateWatchers();
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, Collections.ItemPropertyChangedEventArgs e)
{
UpdateWatchers();
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateWatchers();
}
public event EventHandler<LibChangedEventArgs>? MusicLibraryFilesChanged;
public void Dispose()
{
foreach (var watcher in _watchers.Values)
{
watcher.Dispose();
}
_watchers.Clear();
}
private void UpdateWatchers()
{
var folders = _settingsService.AppSettings.LocalMediaFolders;
// 移除不再监听的
foreach (var key in _watchers.Keys.ToList())
{
if (!folders.Any(x => x.Path == key && x.IsEnabled && x.IsRealTimeWatchEnabled))
{
_watchers[key].Dispose();
_watchers.Remove(key);
}
}
// 添加新的监听
foreach (var folder in folders)
{
if (!_watchers.ContainsKey(folder.Path) && Directory.Exists(folder.Path) && folder.IsEnabled && folder.IsRealTimeWatchEnabled)
{
var watcher = new FileSystemWatcher(folder.Path)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true,
};
watcher.Created += (s, e) => OnChanged(folder.Path, e);
watcher.Changed += (s, e) => OnChanged(folder.Path, e);
watcher.Deleted += (s, e) => OnChanged(folder.Path, e);
watcher.Renamed += (s, e) => OnChanged(folder.Path, e);
_watchers[folder.Path] = watcher;
}
}
}
private void OnChanged(string folder, FileSystemEventArgs e)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
MusicLibraryFilesChanged?.Invoke(
this,
new LibChangedEventArgs(folder, e.FullPath, e.ChangeType)
);
});
}
}
}

View File

@@ -5,6 +5,7 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Providers;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.SettingsService;
using Lyricify.Lyrics.Helpers;
using Lyricify.Lyrics.Searchers;
@@ -28,11 +29,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private readonly AppleMusic _appleMusic;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private readonly ILogger _logger;
public LyricsSearchService(ISettingsService settingsService, ILogger<LyricsSearchService> logger)
public LyricsSearchService(ISettingsService settingsService, IFileSystemService fileSystemService, ILogger<LyricsSearchService> logger)
{
_settingsService = settingsService;
_fileSystemService = fileSystemService;
_logger = logger;
_lrcLibHttpClient = new();
@@ -276,92 +279,50 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
int maxScore = 0;
MediaFolder? bestFolder = null;
string? bestFilePath = null;
FilesIndexItem? bestFileEntity = null;
MediaFolder? bestFolderConfig = null;
var lyricsSearchResult = new LyricsSearchResult();
if (format.ToLyricsSearchProvider() is LyricsSearchProvider lyricsSearchProvider)
{
lyricsSearchResult.Provider = lyricsSearchProvider;
}
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
string targetExt = format.ToFileExtension();
var enabledFolders = _settingsService.AppSettings.LocalMediaFolders
.Where(f => f.IsEnabled)
.ToList();
var enabledIds = enabledFolders.Select(f => f.Id).ToList();
if (enabledIds.Count == 0) return lyricsSearchResult;
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.LyricExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
foreach (var item in allFiles)
{
if (!folder.IsEnabled) continue;
try
if (item.FileName.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
{
using var fs = folder.CreateFileSystem();
if (fs == null) continue;
if (!await fs.ConnectAsync()) continue;
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FileName });
// 递归扫描
var foldersToScan = new Queue<string>();
foldersToScan.Enqueue(""); // 从根目录开始
string targetExt = format.ToFileExtension();
while (foldersToScan.Count > 0)
if (score > maxScore)
{
var currentPath = foldersToScan.Dequeue();
var items = await fs.GetFilesAsync(currentPath);
maxScore = score;
bestFileEntity = item;
foreach (var item in items)
{
if (item.IsFolder)
{
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
continue;
}
if (item.Name.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
{
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FullPath });
if (score > maxScore)
{
maxScore = score;
bestFilePath = item.FullPath;
bestFolder = folder;
}
}
}
bestFolderConfig = enabledFolders.FirstOrDefault(f => f.Id == item.MediaFolderId);
}
}
catch (Exception ex)
{
// 日志记录...
}
}
// 4. 如果找到了最佳匹配,读取内容
if (bestFolder != null && bestFilePath != null)
if (bestFileEntity != null)
{
try
{
// 重新连接以读取文件 (因为之前的 fs 已经在 using 结束时释放)
using var fs = bestFolder.CreateFileSystem();
if (fs != null && await fs.ConnectAsync())
{
using var stream = await fs.OpenReadAsync(bestFilePath);
lyricsSearchResult.Raw = bestFileEntity.EmbeddedLyrics;
// 使用 StreamReader 读取文本
// 注意:这里简单使用 Default 编码,如果需要探测编码(FileHelper.GetEncoding)
// 可能需要先读一部分字节来判断,或者使用带编码探测的库。
using var reader = new StreamReader(stream);
string raw = await reader.ReadToEndAsync();
lyricsSearchResult.Reference = bestFilePath;
lyricsSearchResult.MatchPercentage = maxScore;
lyricsSearchResult.Raw = raw;
}
}
catch (Exception)
{
// 读取失败处理
}
lyricsSearchResult.Reference = bestFileEntity.Uri;
lyricsSearchResult.MatchPercentage = maxScore;
}
return lyricsSearchResult;
@@ -369,107 +330,53 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private async Task<LyricsSearchResult> SearchEmbedded(SongInfo songInfo)
{
int bestScore = 0;
string? bestFilePath = null;
string? bestRaw = null;
// 用于最后回填 Metadata
string? bestTitle = null;
string[]? bestArtists = null;
string? bestAlbum = null;
double bestDuration = 0;
var lyricsSearchResult = new LyricsSearchResult
{
Provider = LyricsSearchProvider.LocalMusicFile,
};
foreach (var folder in _settingsService.AppSettings.LocalMediaFolders)
var enabledIds = _settingsService.AppSettings.LocalMediaFolders
.Where(f => f.IsEnabled)
.Select(f => f.Id)
.ToList();
if (enabledIds.Count == 0) return lyricsSearchResult;
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FilesIndexItem? bestFile = null;
int maxScore = 0;
foreach (var item in allFiles)
{
if (!folder.IsEnabled) continue;
if (string.IsNullOrEmpty(item.EmbeddedLyrics)) continue;
try
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
{
using var fs = folder.CreateFileSystem();
if (fs == null) continue;
if (!await fs.ConnectAsync()) continue;
Title = item.Title,
Artists = item.Artists?.Split(ATL.Settings.DisplayValueSeparator),
Album = item.Album,
Duration = item.Duration
});
var foldersToScan = new Queue<string>();
foldersToScan.Enqueue("");
while (foldersToScan.Count > 0)
{
var currentPath = foldersToScan.Dequeue();
var items = await fs.GetFilesAsync(currentPath);
foreach (var item in items)
{
if (item.IsFolder)
{
foldersToScan.Enqueue(Path.Combine(currentPath, item.Name));
continue;
}
var ext = Path.GetExtension(item.Name).ToLower();
if (FileHelper.MusicExtensions.Contains(ext))
{
try
{
using var stream = await fs.OpenReadAsync(item.FullPath);
var track = new ExtendedTrack(item.FullPath, stream);
var raw = track.RawLyrics;
if (!string.IsNullOrEmpty(raw))
{
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
{
Title = track.Title,
Artists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator),
Album = track.Album,
Duration = track.Duration,
Reference = item.FullPath,
});
if (score > bestScore)
{
bestScore = score;
bestFilePath = item.FullPath;
bestRaw = raw;
// 缓存当前最佳的元数据,避免最后还需要重新打开文件读一次
bestTitle = track.Title;
bestArtists = track.Artist?.Split(ATL.Settings.DisplayValueSeparator);
bestAlbum = track.Album;
bestDuration = track.Duration;
}
}
}
catch
{
// 单个文件解析失败忽略
}
}
}
}
}
catch
if (score > maxScore)
{
// 文件夹扫描失败忽略
maxScore = score;
bestFile = item;
}
}
if (bestFilePath != null)
if (bestFile != null && maxScore > 0)
{
// 直接使用缓存的数据,不需要 new Track(bestFile) 了
lyricsSearchResult.Title = bestTitle;
lyricsSearchResult.Artists = bestArtists;
lyricsSearchResult.Album = bestAlbum;
lyricsSearchResult.Duration = bestDuration;
lyricsSearchResult.Title = bestFile.Title;
lyricsSearchResult.Artists = bestFile.Artists?.Split(ATL.Settings.DisplayValueSeparator);
lyricsSearchResult.Album = bestFile.Album;
lyricsSearchResult.Duration = bestFile.Duration;
lyricsSearchResult.Raw = bestRaw;
lyricsSearchResult.Reference = bestFilePath;
lyricsSearchResult.MatchPercentage = bestScore;
lyricsSearchResult.Raw = bestFile.EmbeddedLyrics;
lyricsSearchResult.Reference = bestFile.Uri;
lyricsSearchResult.MatchPercentage = maxScore;
}
return lyricsSearchResult;
@@ -651,11 +558,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
ISearchResult? result;
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIDHelper.IsNeteaseFamily(songInfo.PlayerId))
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIdHelper.IsNeteaseFamily(songInfo.PlayerId))
{
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
}
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerID.QQMusic)
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerId.QQMusic)
{
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
public interface IPlayHistoryService
{
Task AddLogAsync(PlayHistoryItem item);
Task<List<PlayHistoryItem>> GetRecentLogsAsync(int limit = 50);
Task<List<PlayHistoryItem>> GetLogsByDateRangeAsync(DateTime start, DateTime end);
Task<List<SongPlayCount>> GetTopSongsAsync(DateTime start, DateTime end, int limit = 10);
Task<List<ArtistPlayCount>> GetTopArtistsAsync(DateTime start, DateTime end, int limit = 10);
Task<TimeSpan> GetTotalListeningDurationAsync(DateTime start, DateTime end);
Task<List<PlayerStats>> GetPlayerDistributionAsync(DateTime start, DateTime end);
Task DeleteLogAsync(int id);
Task ClearHistoryAsync();
Task GenerateTestDataAsync(int count = 100);
}
}

View File

@@ -0,0 +1,266 @@
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Models.Stats;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
public class PlayHistoryService : IPlayHistoryService
{
private readonly IDbContextFactory<PlayHistoryDbContext> _contextFactory;
public PlayHistoryService(IDbContextFactory<PlayHistoryDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task AddLogAsync(PlayHistoryItem item)
{
using var context = await _contextFactory.CreateDbContextAsync();
// 确保 UTC
if (item.StartedAt.Kind != DateTimeKind.Utc)
{
item.StartedAt = item.StartedAt.ToUniversalTime();
}
context.PlayHistory.Add(item);
await context.SaveChangesAsync();
}
public async Task<List<PlayHistoryItem>> GetRecentLogsAsync(int limit = 50)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking() // 读操作,不需要追踪状态,提升性能
.OrderByDescending(x => x.StartedAt)
.Take(limit)
.ToListAsync();
}
public async Task<List<PlayHistoryItem>> GetLogsByDateRangeAsync(DateTime start, DateTime end)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.ToListAsync();
}
public async Task<List<SongPlayCount>> GetTopSongsAsync(DateTime start, DateTime end, int limit = 10)
{
using var context = await _contextFactory.CreateDbContextAsync();
// EF Core 会自动将这个 LINQ 翻译成高效的 GROUP BY SQL
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => new { x.Title, x.Artist }) // 组合分组
.Select(g => new SongPlayCount
{
Title = g.Key.Title,
Artist = g.Key.Artist,
PlayCount = g.Count()
})
.OrderByDescending(x => x.PlayCount)
.Take(limit)
.ToListAsync();
}
public async Task<List<ArtistPlayCount>> GetTopArtistsAsync(DateTime start, DateTime end, int limit = 10)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => x.Artist)
.Select(g => new ArtistPlayCount
{
Artist = g.Key,
PlayCount = g.Count(),
// 注意SQLite 存储 double 精度,这里求和后转秒
TotalDurationSeconds = g.Sum(x => x.DurationPlayedMs) / 1000.0
})
.OrderByDescending(x => x.PlayCount)
.Take(limit)
.ToListAsync();
}
public async Task<TimeSpan> GetTotalListeningDurationAsync(DateTime start, DateTime end)
{
using var context = await _contextFactory.CreateDbContextAsync();
var totalMs = await context.PlayHistory
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.SumAsync(x => Math.Min(x.DurationPlayedMs, x.TotalDurationMs)); // 防止超过歌曲本身时长
return TimeSpan.FromMilliseconds(totalMs);
}
public async Task<List<PlayerStats>> GetPlayerDistributionAsync(DateTime start, DateTime end)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => x.PlayerId)
.Select(g => new PlayerStats
{
PlayerId = g.Key,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.ToListAsync();
}
public async Task DeleteLogAsync(int id)
{
using var context = await _contextFactory.CreateDbContextAsync();
// EF Core 删除需要先查询,或者使用 ExecuteDeleteAsync (EF Core 7+)
// 写法 1 (传统):
// var item = await context.PlayHistory.FindAsync(id);
// if (item != null) { context.PlayHistory.Remove(item); await context.SaveChangesAsync(); }
// 写法 2 (EF Core 7.0+ 高效写法,直接生成 DELETE SQL):
await context.PlayHistory
.Where(x => x.Id == id)
.ExecuteDeleteAsync();
}
public async Task ClearHistoryAsync()
{
using var context = await _contextFactory.CreateDbContextAsync();
// 高效清空表
await context.PlayHistory.ExecuteDeleteAsync();
}
public async Task GenerateTestDataAsync(int count = 100)
{
// 这里的逻辑稍微重构了一下,使用批量插入提升性能
var random = new Random();
var presetSongs = new List<(string Title, string Artist, string Album)>
{
("Anti-Hero", "Taylor Swift", "Midnights"),
("Cruel Summer", "Taylor Swift", "Lover"),
("Blank Space", "Taylor Swift", "1989"),
("As It Was", "Harry Styles", "Harry's House"),
("Late Night Talking", "Harry Styles", "Harry's House"),
("Die For You", "The Weeknd", "Starboy"),
("Blinding Lights", "The Weeknd", "After Hours"),
("Starboy", "The Weeknd", "Starboy"),
("Shape of You", "Ed Sheeran", "Divide"),
("Bad Guy", "Billie Eilish", "When We All Fall Asleep, Where Do We Go?"),
("Flowers", "Miley Cyrus", "Endless Summer Vacation"),
("Stay", "The Kid LAROI & Justin Bieber", "F*ck Love 3: Over You"),
("七里香", "周杰伦", "七里香"),
("晴天", "周杰伦", "叶惠美"),
("一路向北", "周杰伦", "11月的肖邦"),
("告白气球", "周杰伦", "周杰伦的床边故事"),
("十年", "陈奕迅", "黑·白·灰"),
("富士山下", "陈奕迅", "What's Going On...?"),
("孤勇者", "陈奕迅", "孤勇者"),
("修炼爱情", "林俊杰", "因你而在"),
("江南", "林俊杰", "第二天堂"),
("光年之外", "G.E.M. 邓紫棋", "摩天动物园"),
("泡沫", "G.E.M. 邓紫棋", "Xposed"),
("因为爱情", "王菲 & 陈奕迅", "Stranger Under My Skin"),
("红豆", "王菲", "唱游"),
("Bohemian Rhapsody", "Queen", "A Night at the Opera"),
("Don't Stop Me Now", "Queen", "Jazz"),
("Numb", "Linkin Park", "Meteora"),
("In the End", "Linkin Park", "Hybrid Theory"),
("Yellow", "Coldplay", "Parachutes"),
("Viva La Vida", "Coldplay", "Viva La Vida"),
("Smells Like Teen Spirit", "Nirvana", "Nevermind"),
("Hotel California", "Eagles", "Hotel California"),
("Lemon", "米津玄師", "Lemon"),
("Kick Back", "米津玄師", "KICK BACK"),
("アイドル", "YOASOBI", "アイドル"),
("夜に駆ける", "YOASOBI", "THE BOOK"),
("First Love", "宇多田ヒカル", "First Love"),
("Dynamite", "BTS", "BE"),
("Butter", "BTS", "Butter"),
("How You Like That", "BLACKPINK", "The Album"),
("Ditto", "NewJeans", "OMG"),
("Get Lucky", "Daft Punk", "Random Access Memories"),
("The Nights", "Avicii", "The Days / Nights"),
("Summer", "Calvin Harris", "Motion"),
};
var playerIds = new[]
{
PlayerId.Spotify, PlayerId.Spotify, PlayerId.Spotify,
PlayerId.MusicBee, PlayerId.MusicBee,
PlayerId.QQMusic,
PlayerId.NetEaseCloudMusic,
PlayerId.AppleMusic,
};
var batchList = new List<PlayHistoryItem>();
// 我们尝试生成 count 条有效数据
// 为了防止死循环,加个硬上限
int attempts = 0;
while (batchList.Count < count && attempts < count * 5)
{
attempts++;
var song = presetSongs[random.Next(presetSongs.Count)];
var playerId = playerIds[random.Next(playerIds.Length)];
var daysBack = random.Next(0, 365);
var hoursBack = random.Next(0, 24);
var minutesBack = random.Next(0, 60);
var secondsBack = random.Next(0, 60);
var startedAt = DateTime.UtcNow // 直接用 UTC
.AddDays(-daysBack)
.AddHours(-hoursBack)
.AddMinutes(-minutesBack)
.AddSeconds(-secondsBack);
var totalDurationMs = random.Next(180, 300) * 1000.0;
double playedRatio;
double roll = random.NextDouble();
if (roll > 0.3) playedRatio = 0.9 + (random.NextDouble() * 0.1);
else if (roll > 0.1) playedRatio = 0.3 + (random.NextDouble() * 0.5);
else playedRatio = 0.05 + (random.NextDouble() * 0.25);
var playedDurationMs = totalDurationMs * playedRatio;
// 只有听了一半以上的才算作记录
if (playedDurationMs >= (totalDurationMs / 2))
{
batchList.Add(new PlayHistoryItem
{
Title = song.Title,
Artist = song.Artist,
Album = song.Album,
PlayerId = playerId,
StartedAt = startedAt,
TotalDurationMs = totalDurationMs,
DurationPlayedMs = playedDurationMs
});
}
}
if (batchList.Count > 0)
{
using var context = await _contextFactory.CreateDbContextAsync();
await context.PlayHistory.AddRangeAsync(batchList);
await context.SaveChangesAsync();
}
}
}
}

View File

@@ -0,0 +1,21 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.SMTCService
{
/// <summary>
/// Interface for SystemMediaTransportControlsSession Service
/// </summary>
public interface ISMTCService
{
public ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; }
public ExtendedTrack? PlayingTrack { get; set; }
Task PlayTrackAsync(PlayQueueItem? playQueueItem);
Task PlayTrackAtAsync(int index);
}
}

View File

@@ -0,0 +1,330 @@
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
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;
using Windows.Storage;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.SMTCService
{
public partial class SMTCService : BaseViewModel, ISMTCService
{
private readonly MediaPlayer _mediaPlayer;
private readonly MediaTimelineController _timelineController;
private readonly SystemMediaTransportControls _smtc;
private IRandomAccessStream? _currentStream;
private Stream? _currentNetStream;
private IUnifiedFileSystem? _currentProvider;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
[ObservableProperty] public partial ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; } = [];
[ObservableProperty] public partial ExtendedTrack? PlayingTrack { get; set; }
public SMTCService(ISettingsService settingsService, IFileSystemService fileSystemService)
{
_settingsService = settingsService;
_fileSystemService = fileSystemService;
_mediaPlayer = new MediaPlayer();
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
_mediaPlayer.CommandManager.IsEnabled = false;
_timelineController = _mediaPlayer.TimelineController = new();
_timelineController.PositionChanged += TimelineController_PositionChanged;
_smtc = _mediaPlayer.SystemMediaTransportControls;
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
_smtc.ButtonPressed += Smtc_ButtonPressed;
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
_ = Task.Run(async () =>
{
var parsedFiles = await _fileSystemService.GetParsedFilesAsync();
var playQueue = _settingsService.AppSettings.MusicGallerySettings.PlayQueuePaths
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x =>
{
var encodedUri = new Uri(x).AbsoluteUri;
return new PlayQueueItem(new ExtendedTrack(parsedFiles.FirstOrDefault(y => y.Uri == encodedUri)));
});
_dispatcherQueue.TryEnqueue(() =>
{
TrackPlayingQueue = [.. playQueue];
TrackPlayingQueue.CollectionChanged += TrackPlayingQueue_CollectionChanged;
});
});
}
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 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 MediaPlayer_MediaEnded(MediaPlayer sender, object args)
{
PlayNextTrack();
}
private void TimelineController_PositionChanged(MediaTimelineController sender, object args)
{
_smtc.UpdateTimelineProperties(new SystemMediaTransportControlsTimelineProperties()
{
Position = sender.Position,
EndTime = _mediaPlayer.PlaybackSession.NaturalDuration
});
}
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_settingsService.AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.Uri.ToDecodedAbsoluteUri())];
}
private string GetMimeType(string path)
{
var ext = Path.GetExtension(path).ToLower();
return ext switch
{
".mp3" => "audio/mpeg",
".flac" => "audio/flac",
".wav" => "audio/wav",
".m4a" => "audio/mp4",
".aac" => "audio/aac",
".ogg" => "audio/ogg",
".wma" => "audio/x-ms-wma",
_ => "application/octet-stream"
};
}
private void PlayNextTrack()
{
var musicGallerySettings = _settingsService.AppSettings.MusicGallerySettings;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
switch (musicGallerySettings.PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
if (musicGallerySettings.PlayQueueIndex < TrackPlayingQueue.Count - 1)
{
musicGallerySettings.PlayQueueIndex++;
}
else
{
musicGallerySettings.PlayQueueIndex = 0;
}
break;
case PlaybackOrder.RepeatOne:
//_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
if (TrackPlayingQueue.Count > 0)
{
musicGallerySettings.PlayQueueIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
break;
default:
break;
}
await PlayTrackAtAsync(musicGallerySettings.PlayQueueIndex);
});
}
private void PlayPreviousTrack()
{
var musicGallerySettings = _settingsService.AppSettings.MusicGallerySettings;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
switch (musicGallerySettings.PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
if (musicGallerySettings.PlayQueueIndex > 0)
{
musicGallerySettings.PlayQueueIndex--;
}
else
{
musicGallerySettings.PlayQueueIndex = TrackPlayingQueue.Count - 1;
}
break;
case PlaybackOrder.RepeatOne:
//_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
if (TrackPlayingQueue.Count > 0)
{
musicGallerySettings.PlayQueueIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
break;
default:
break;
}
await PlayTrackAtAsync(musicGallerySettings.PlayQueueIndex);
});
}
public async Task PlayTrackAsync(PlayQueueItem? playQueueItem)
{
_timelineController.Pause();
_mediaPlayer.Source = null;
// 清理旧资源
_currentStream?.Dispose();
_currentNetStream?.Dispose();
_currentStream = null;
_currentNetStream = null;
if (playQueueItem == null)
{
_smtc.IsEnabled = false;
_smtc.DisplayUpdater.ClearAll();
}
else
{
PlayingTrack = playQueueItem.Track;
_smtc.IsEnabled = true;
try
{
var targetFolder = _settingsService.AppSettings.LocalMediaFolders.FirstOrDefault(f =>
{
var fUri = f.GetStandardUri().AbsoluteUri;
return PlayingTrack.Uri.StartsWith(fUri, StringComparison.OrdinalIgnoreCase);
});
if (targetFolder == null)
{
throw new FileNotFoundException(null, PlayingTrack.Uri.ToDecodedAbsoluteUri());
}
_currentProvider = targetFolder.CreateFileSystem();
if (_currentProvider == null) return;
await _currentProvider.ConnectAsync();
var fileCacheStub = new FilesIndexItem
{
Uri = PlayingTrack.Uri
};
var sourceStream = await _fileSystemService.OpenFileAsync(_currentProvider, fileCacheStub);
if (sourceStream == null)
{
throw new FileNotFoundException(null, fileCacheStub.Uri);
}
if (sourceStream.CanSeek)
{
_currentNetStream = sourceStream;
}
else
{
var memStream = new MemoryStream();
await sourceStream.CopyToAsync(memStream);
memStream.Position = 0;
sourceStream.Dispose();
_currentNetStream = memStream;
}
_currentStream = _currentNetStream.AsRandomAccessStream();
string contentType = GetMimeType(PlayingTrack.FileName);
var mediaSource = MediaSource.CreateFromStream(_currentStream, contentType);
_mediaPlayer.Source = mediaSource;
var updater = _smtc.DisplayUpdater;
updater.Type = MediaPlaybackType.Music;
updater.MusicProperties.Title = PlayingTrack.Title ?? PlayingTrack.FileName;
updater.MusicProperties.Artist = PlayingTrack.Artist ?? "";
updater.MusicProperties.AlbumTitle = PlayingTrack.Album ?? "";
updater.MusicProperties.Genres.Clear();
updater.MusicProperties.Genres.Add($"{ExtendedGenreFiled.FileName}{Path.GetFileNameWithoutExtension(PlayingTrack.FileName)}");
updater.AppMediaId = Package.Current.Id.FullName;
if (!string.IsNullOrEmpty(PlayingTrack.LocalAlbumArtPath) && File.Exists(PlayingTrack.LocalAlbumArtPath))
{
var storageFile = await StorageFile.GetFileFromPathAsync(PlayingTrack.LocalAlbumArtPath);
updater.Thumbnail = RandomAccessStreamReference.CreateFromFile(storageFile);
}
else
{
updater.Thumbnail = null;
}
updater.Update();
}
catch (Exception ex)
{
ToastHelper.ShowToast("Error", ex.Message, InfoBarSeverity.Error);
_timelineController.Pause();
}
}
}
public async Task PlayTrackAtAsync(int index)
{
await PlayTrackAsync(TrackPlayingQueue.ElementAtOrDefault(index));
}
}
}

View File

@@ -24,11 +24,13 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
public partial class SettingsService : BaseViewModel, ISettingsService
{
private readonly DispatcherQueueTimer _writeAppSettingsTimer;
private readonly ILocalizationService _localizationService;
public AppSettings AppSettings { get; set; }
public SettingsService()
public SettingsService(ILocalizationService localizationService)
{
_localizationService = localizationService;
_writeAppSettingsTimer = _dispatcherQueue.CreateTimer();
AppSettings = ReadAppSettings();
@@ -60,6 +62,7 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
AppSettings.Version = MetadataHelper.AppVersion;
EnsureMediaSourceProvidersInfo();
EnsureStarredPlaylists();
}
private void EnsureMediaSourceProvidersInfo()
@@ -102,6 +105,20 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
}
}
private void EnsureStarredPlaylists()
{
if (!AppSettings.StarredPlaylists.Any(x => x.IsDefault))
{
AppSettings.StarredPlaylists.Insert(0, new SongsTabInfo
{
Name = _localizationService.GetLocalizedString("MusicGalleryPageAllSongs"),
Icon = "\uE8A9",
FilterProperty = CommonSongProperty.Title,
FilterValue = string.Empty
});
}
}
private void AppSettings_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
WriteAppSettings();

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>مشغل الموسيقى</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>وضع صورة داخل صورة (PiP)</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>النافذة</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>عند إدخال فنانين متعددين، يرجى استخدام أحد الفواصل التالية للفصل بينهم (لا تخلط بينها)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>لا تظهر هذه الرسالة مرة أخرى</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>وضع صورة داخل صورة (PiP)</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>إلغاء</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>الوضع المثبت (Docked)</value>
</data>
<data name="Error" xml:space="preserve">
<value>خطأ</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>تم التصدير بنجاح</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>تعذر الاتصال بخادم موسيقى LX، يرجى الانتقال إلى الإعدادات - مصدر التشغيل - LX Music - خادم موسيقى LX للتحقق مما إذا تم إدخال الرابط بشكل صحيح</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>تنظيف ذاكرة التخزين المؤقت...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>فشل الاتصال</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>الاتصال جارِ...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>جلب قائمة الملفات...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>جاري التحليل...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>التحضير لتنظيف ذاكرة التخزين المؤقت...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>جاهز</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>تم اكتشاف مسار الدليل الجذر. قد يحتوي فهرس القرص الكامل على عدد كبير من الملفات غير الوسائط ويتسبب في استغراق الفحص وقتاً طويلاً جداً. يوصى بتحديد دليل فرعي محدد.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>في انتظار المسح...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>وضع ملء الشاشة</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>قفل</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>قفل</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>قفل</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>مكتبة الموسيقى</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>جيتبنغ (كانتونية)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>يرجى التأكد من تعيين حالة واحدة على الأقل كافتراضية</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>فشل التفويض، يرجى المحاولة مرة أخرى</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>الإعدادات</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>إزاحة الجدول الزمني للكلمات</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>ستُعطى الأولوية لقراءة الترجمة داخل ملف الكلمات، وإذا لم يوجد تطابق، سيتم طلب ترجمة آلية من خادم LibreTranslate</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>تفعيل الترجمة</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>عرض الترجمة فقط</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>مُزود الترجمة</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>تعيين معلومات الأغنية</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>مزود بحث الكلمات المستهدف</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>العنوان</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>ملف TTML محلي</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>الوضع الغامر</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>التكوين</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>وضع نافذة الكلمات</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>تعيين كافتراضي</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>إظهار منطقة غلاف الألبوم فقط</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>حرك المؤشر مرة أخرى لإظهار زر التبديل</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>إظهار الكلمات فقط</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>عرض مقسم</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>آخر مزامنة للوقت</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>المجلد المحلي</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>الاسم</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>مزامنة الآن</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>إضافة إلى قائمة التشغيل</value>
</data>
@@ -406,11 +409,17 @@
<value>العنصر التالي</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>إضافة إلى قائمة الانتظار</value>
<value>إضافة إلى قائمة انتظار اللعب</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>كل الموسيقى</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>مزامنة مكتبة الوسائط جارية...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>توجد مشكلة في مزامنة مكتبة الوسائط</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>مسح قائمة الانتظار</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>الفنان</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>معلومات الملف</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>عمق البت (Bit Depth)</value>
</data>
@@ -456,15 +462,15 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>لم يتم العثور على أغاني في مكتبة الوسائط</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>المجلدات</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>استيراد من ملف</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>إنشاء قائمة تشغيل</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>تشغيل الكل</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>قائمة الانتظار</value>
</data>
@@ -472,7 +478,7 @@
<value>قائمة الانتظار فارغة</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>قائمة التشغيل</value>
<value>قوائم التشغيل</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>تكرار القائمة</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>التمرير إلى العنصر المشغل</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>تحديد الكل</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>تكرار أغنية واحدة</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>نوع الفرز</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>قوائم التشغيل المميزة بنجمة</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>إيقاف</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>مكتبة الموسيقى - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>سحب</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>تردد المزامنة التلقائية</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>توسيع</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>مطلقًا</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>كل يوم</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>كل 15 دقيقة</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>كُل ساعة</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>كُل 6 ساعات</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>وضع الشاشة الضيقة</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>سياسة الخصوصية</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>تصفح</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>الاسم</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>سيؤدي تركه فارغاً إلى إنشاء اسم افتراضي تلقائياً.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>كلمة المرور</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>المسار</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>تعذر العثور على مسار المجلد المحدد</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>المسار مطلوب</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>المنفذ</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>تعليقات</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>دليل التشغيل</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>شكر خاص</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>إضافة</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>خيارات متقدمة</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>الألبوم</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>تكوين مصادر غلاف الألبوم</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>حجم غلاف الألبوم</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>تأثير غلاف الألبوم</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>مصدر غلاف الألبوم</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>نصف قطر الزاوية</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>مظهر التطبيق</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>السلوك العام</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>تطبيق</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>الفنان</value>
</data>
@@ -687,23 +696,14 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>بدء التشغيل التلقائي</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>عند بدء تشغيل التطبيق</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>مادة خلفية الكلمات</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>خلفية الكلمات</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>كمية الضبابية (Blur)</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value>نافذة بلا حدود</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>يشمل ملفات السجل وذاكرة التخزين المؤقت للكلمات عبر الشبكة</value>
<value>يتضمن ملفات السجلات، وذاكرة التخزين المؤقت للكلمات عبر الإنترنت</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>ذاكرة التخزين المؤقت (Cache)</value>
@@ -738,9 +738,6 @@
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>طي القائمة المنسدلة</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>مدمج</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>تسمية حالة النافذة المسجلة يمكن أن تساعدك على تمييزها بشكل أفضل</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>تلاشي متقاطع (Crossfade)</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>حالة نافذة الكلمات الحالية</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>خدمة النقل الصوتي cutlet-docker</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>داكن</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>إظهار تراكب التصحيح (Debug)</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>حذف</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>أعلى</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>ارتفاع النافذة</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>منطقة قابلة للسحب</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>نوع حركة التسهيل (Easing)</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>الخروج من البرنامج عند إغلاق نافذة الكلمات</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>تصدير تاريخ اللعب</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>تصدير الإعدادات</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>موسع</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>كلمات مروحية الشكل</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>الأسئلة الشائعة</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>تصيير بخطوة زمنية ثابتة</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>الارتفاع</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>مرحباً</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>ساعدنا في ترجمة هذا التطبيق</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>الصوتيات اليابانية</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>انضم الآن</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>جيتبنغ (كانتونية)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>فاتح</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>الملفات المحلية المرتبطة</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>تمكين الاستماع لمصادر التشغيل الجديدة</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>مجلد محلي</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>التسجيل (Log)</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>عتبة المقطع الطويل</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>السطر الحالي</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>التكيف مع خلفية الكلمات (ملون)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>التكيف مع خلفية الكلمات (رمادي)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>مخصص</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>الرسوم المتحركة العائمة</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>تأثير التوهج</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>تمييز</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>نطاق تمييز النص الأصلي</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>يسار</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>خفيف</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>تدرج حافة منطقة التشغيل</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>تباعد الأسطر</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> مرات ارتفاع السطر</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>متوسط</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>شبه خفيف</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>الظل</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>نمط الكلمات</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>عتبة مزامنة الجدول الزمني للكلمات</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>تمييز الترجمة</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>فاصل النص الأصلي والمترجم</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>شفافية الحافة الرأسية</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>نافذة الكلمات</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>مدير نافذة الكلمات</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>مدير نافذة الكلمات</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>اختصار تبديل حالة نافذة الكلمات</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>نافذة الكلمات</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>سيؤثر ضبط هذه القيمة على نتائج البحث المتسلسل والبحث بأفضل تطابق، ولكنه لن يؤثر على نتائج البحث في واجهة البحث اليدوي عن الكلمات</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>مكتبة الوسائط المحلية</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>مراقبة تغييرات الملفات في الوقت الحقيقي</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>وضع الشاشة الضيقة</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>اختصار الأغنية التالية</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>لا شيء</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>عدواني</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>يحتوي هذا المجلد على مجلدات مضافة مسبقاً، يرجى حذف تلك المجلدات لإضافة هذا المجلد</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>لا يمكن العثور على المسار في جهاز الكمبيوتر الخاص بك</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>الرعاة</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>الحالة في الوقت الحقيقي</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>سجل</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>حالة النافذة المسجلة</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>روابط مرجعية</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>تحديث القائمة المنسدلة</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>يمين</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>الصوتيات اليابانية</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>النطاق</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>مدير الإعدادات</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>تاريخ اللعب</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>تصفح مركز مشاركة الموارد عبر الإنترنت</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>إظهار في بيئة النظام</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>إظهار مقسم التخطيط</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>إظهار العنوان</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>انزلاق (Slide)</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>القيمة الحالية: </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>طبقة الثلج</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>بدء التشغيل</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>الإحصائيات</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>إيقاف التشغيل عند إغلاق نافذة مكتبة الموسيقى</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>موضع تثبيت شريط المهام</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>شكراً لشرائك BetterLyrics</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>قائمة الشكر</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>كامل النافذة</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>حجم شريط العنوان</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>اختصار التبديل للداخل والخارج</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>الترجمة</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>خدمة ترجمة LibreTranslate</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>قم بزيارة https://github.com/LibreTranslate/LibreTranslate للحصول على دروس التثبيت والمزيد من المعلومات (هذا البرنامج ليس له أي صلة بخدمة الترجمة هذه)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>والمستخدمين الذين اشتروا ودعموا BetterLyrics</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>العرض</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>حدود النافذة</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>كمساحة عمل منفصلة، يتم تثبيتها في الحافة العلوية/السفلية للشاشة</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>الوضع القياسي</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>النشاط بالساعة</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>مخصص</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>النهاية</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>الأقل نشاطاً</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>الأكثر نشاطاً</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>المصادر</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>ابدأ</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>هذا الشهر</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>هذا الربع</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>هذا الأسبوع</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>هذا العام</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>النطاق الزمني</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>التايمز</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>اليوم</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>كبار الفنانين</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>أعلى المسارات</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>المصدر الأعلى</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>المدة الإجمالية</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>التايمز</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>التايمز</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>المسارات التي تم تشغيلها</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>خروج</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>فتح مكتبة الموسيقى</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>علبة النظام - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>إعادة تعيين موضع النافذة</value>
</data>

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>Musikplayer</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>Bild-im-Bild-Modus</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>Fenster</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>Wenn Sie mehrere Künstler eingeben, trennen Sie diese bitte mit einem der folgenden Trennzeichen (nicht gemischt verwenden)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Diese Nachricht nicht mehr anzeigen</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Bild-im-Bild-Modus</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Abbrechen</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>Angedockter Modus</value>
</data>
<data name="Error" xml:space="preserve">
<value>Fehler</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Export erfolgreich</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>Verbindung zum LX Music Server fehlgeschlagen. Bitte prüfen Sie unter Einstellungen - Wiedergabequelle - LX Music - LX Music Server, ob der Link korrekt ist</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Cache wird gereinigt...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Verbindung fehlgeschlagen</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>Verbinde...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Lade Dateiliste...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Analysiere...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Bereinigung des Caches vorbereiten...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Bereit</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Der Pfad zum Stammverzeichnis wurde erkannt. Ein vollständiger Festplattenindex kann eine große Anzahl von Nicht-Mediendateien enthalten und dazu führen, dass die Suche zu lange dauert. Es wird empfohlen, ein bestimmtes Unterverzeichnis anzugeben.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Auf Scans warten...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Vollbildmodus</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Sperren</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Sperren</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>Sperren</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>Musikgalerie</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>Jyutping (Kantonesisch)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>Bitte stellen Sie sicher, dass mindestens ein Status als Standard festgelegt ist</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>Autorisierung fehlgeschlagen, bitte versuchen Sie es erneut</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>Songtext-Zeitversatz</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>Priorisiere Übersetzungen innerhalb des Songtextes; falls keine Übereinstimmung gefunden wird, fordere maschinelle Übersetzung vom LibreTranslate-Server an</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>Übersetzung aktivieren</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>Nur Übersetzung anzeigen</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Übersetzungsanbieter</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>Song-Info-Zuordnung</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Ziel-Suchanbieter für Songtexte</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Titel</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Lokale .TTML-Datei</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>Immersiver Modus</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>Konfiguration</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>Songtext-Fenstermodus</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>Als Standard festlegen</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Nur Albumcover-Bereich anzeigen</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Erneut darüberfahren, um den Umschalt-Button anzuzeigen</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Nur Songtext anzeigen</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Geteilte Ansicht</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Letzte Sync-Zeit</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Lokaler Ordner</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Jetzt synchronisieren</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>Zur Wiedergabeliste hinzufügen</value>
</data>
@@ -411,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Alle Musikstücke</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Synchronisierung der Medienbibliothek läuft...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Es gibt ein Problem mit der Synchronisierung der Medienbibliothek</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Warteschlange leeren</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>Künstler</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>Datei-Info</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>Bittiefe</value>
</data>
@@ -456,15 +462,15 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Keine Songs in der Medienbibliothek gefunden</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Ordner</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Aus Datei importieren</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Wiedergabeliste erstellen</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>Alle abspielen</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Wiedergabewarteschlange</value>
</data>
@@ -472,7 +478,7 @@
<value>Wiedergabewarteschlange ist leer</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Wiedergabeliste</value>
<value>Wiedergabelisten</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Liste wiederholen</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>Zum aktuellen Titel scrollen</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>Alle auswählen</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>Einzeltitel wiederholen</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>Sortiertyp</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>Favoriten-Wiedergabeliste</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>Stopp</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Musikgalerie - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>Einklappen</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Auto-Sync-Frequenz</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>Ausklappen</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>Niemals</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>Täglich</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>Alle 15 Minuten</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>Stündlich</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>Alle 6 Stunden</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>Schmaler Modus</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Datenschutzrichtlinie</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Durchsuchen Sie</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Wenn Sie das Feld leer lassen, wird automatisch ein Standardname generiert.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Pfad</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>Der angegebene Ordnerpfad konnte nicht gefunden werden</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Pfad ist erforderlich</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>Feedback</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>Anleitung</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>Besonderer Dank</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Hinzufügen</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>Erweiterte Optionen</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>Album</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>Albumcover-Quellen konfigurieren</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>Albumcover-Größe</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>Albumcover-Effekt</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>Albumcover-Quellen</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Eckenradius</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>Erscheinungsbild</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>Allgemeines Verhalten</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>Übernehmen</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>Künstler</value>
</data>
@@ -687,23 +696,14 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Autostart</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>Beim Anwendungsstart</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Material für Songtext-Hintergrund</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>Songtext-Hintergrund</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>Unschärfe-Stärke</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value>Rahmenloses Fenster</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Einschließlich Protokolldateien, Online-Songtext-Cache</value>
<value>Enthält Protokolldateien, Online-Liedtext-Cache</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -738,9 +738,6 @@
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>Dropdown einklappen</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Kompakt</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>Das Benennen aufgezeichneter Fensterstatus hilft Ihnen, diese besser zu unterscheiden</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>Überblenden</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>Aktueller Status des Songtext-Fensters</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>cutlet-docker Transliterationsdienst</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Dunkel</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Debug-Overlay anzeigen</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>Löschen</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>Oben</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Fensterhöhe</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Ziehbereich</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Easing-Funktionstyp</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>App beenden, wenn Songtext-Fenster geschlossen wird</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Spielverlauf exportieren</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Einstellungen exportieren</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Erweitert</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Fächerförmiger Songtext</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>Häufig gestellte Fragen (FAQ)</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>Rendering mit festem Zeitschritt</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>Höhe</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>Hallo</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>Helfen Sie uns, diese App zu übersetzen</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Japanische Lautschrift</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>Jetzt beitreten</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>Jyutping (Kantonesisch)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Hell</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>Verknüpfte lokale Datei</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>Auf neue Wiedergabesitzungen hören</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>Lokaler Ordner</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Protokollierung</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>Schwellenwert für lange Silben</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>Aktuelle Zeile</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>An Songtext-Hintergrund anpassen (Farbig)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>An Songtext-Hintergrund anpassen (Grau)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>Benutzerdefiniert</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>Schwebe-Animation</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Leuchteffekt</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>Hervorhebung</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>Hervorhebungsbereich für Originaltext</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Links</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Leicht</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>Verblassen am Rand des Spielbereichs</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Zeilenabstand</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> x Zeilenhöhe</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Mittel</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Halb-Leicht</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>Schatten</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Songtext-Stil</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>Schwellenwert für Songtext-Zeitachsensynchronisation</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>Übersetzungshervorhebung</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>Trennzeichen Original-Übersetzung</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Deckkraft der vertikalen Kanten</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>Songtext-Fenster</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>Songtext-Fenstermanager</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>Songtext-Fenstermanager</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Tastenkürzel für Songtext-Fensterstatuswechsel</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>Songtext-Fenster</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Das Anpassen dieses Wertes beeinflusst die sequenzielle Suche und die Suche nach der besten Übereinstimmung, hat jedoch keinen Einfluss auf die Suchergebnisse in der manuellen Songtext-Suchoberfläche</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Lokale Medienbibliothek</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>Echtzeit-Dateiüberwachung</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>Schmaler Modus</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>Tastenkürzel für nächsten Titel</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>Keiner</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>Aggressiv</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>Dieser Ordner enthält bereits hinzugefügte Ordner, bitte entfernen Sie diese, um diesen Ordner hinzuzufügen</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>Dieser Pfad konnte auf Ihrem Computer nicht gefunden werden</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>Sponsoring</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>Echtzeit-Status</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>Aufzeichnen</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>Aufgezeichneter Fensterstatus</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>Referenzen</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>Dropdown aktualisieren</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>Rechts</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>Japanische Lautschrift</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>Bereich</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Einstellungsmanager</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Geschichte spielen</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Online Share Hub durchsuchen</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>In Systemumgebung anzeigen</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>Layout-Splitter anzeigen</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>Titel anzeigen</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>Gleiten</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Aktueller Wert: </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>Schneeflocken-Ebene</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Start</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistik</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Wiedergabe stoppen, wenn Musikgalerie-Fenster geschlossen wird</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>Taskleisten-Anheftposition</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>Vielen Dank für den Kauf von BetterLyrics</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>Danksagung</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>Ganzes Fenster</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Größe der Titelleiste</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>Tastenkürzel zum Aktivieren/Deaktivieren</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>Übersetzung</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>LibreTranslate-Dienst</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>Besuchen Sie https://github.com/LibreTranslate/LibreTranslate für eine Installationsanleitung und weitere Infos (Diese Software steht in keiner Verbindung zu diesem Dienst)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>Und Benutzer, die BetterLyrics gekauft haben</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>Breite</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>Fenstergrenzen</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>Als separater Arbeitsbereich, angedockt an die obere/untere Kante des Bildschirms</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>Standard-Modus</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktivität nach Stunden</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Benutzerdefiniert</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Ende</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Am wenigsten aktiv</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Aktivste</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Quellen</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Start</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Dieser Monat</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Dieses Quartal</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Diese Woche</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Dieses Jahr</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Zeitspanne</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Zeiten</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Heute</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Top Künstler</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Oberste Quelle</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Gesamtdauer</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Zeiten</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Zeiten</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Gespielte Tracks</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Beenden</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Musikgalerie öffnen</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>System-Tray - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>Fensterposition zurücksetzen</value>
</data>

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>Music player</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>Picture-in-Picture mode</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>Window</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>When entering multiple artists, please separate them using one of the following delimiters (do not mix use)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Do not show this message again</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Picture-in-Picture mode</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>Docked Mode</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Export successful</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>Unable to connect to LX Music Server. Please go to Settings - Playback Source - LX Music - LX Music Server to check if the link is entered correctly</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Clearing cache...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Connection failed</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>Connecting...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Fetching file list...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparing to clear cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Ready</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>The root directory path has been detected. A full disk index may contain a large number of non-media files and cause the scan to take too long. It is recommended to specify a specific subdirectory.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Preparing to scan...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Fullscreen Mode</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Lock</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Lock</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>Lock</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>Music Gallery</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>Jyutping (Cantonese)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>Please ensure at least one status is set as default</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>Authorization failed, please try again</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>Settings</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>Lyrics Timeline Offset</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>Prioritize reading translations within lyrics; if no match, request machine translation from LibreTranslate server</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>Enable Translation</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>Show Translation Only</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Translation Provider</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>Song Info Mapping</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Target Lyrics Search Provider</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Title</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Local .TTML File</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>Immersive Mode</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>Configuration</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>Lyrics Window Mode</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>Set as Default</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Show album art area only</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Hover again to show the toggle button</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Show lyrics only</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Split View</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Last Sync Time</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Local Folder</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Sync now</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>Add to playlist</value>
</data>
@@ -406,11 +409,17 @@
<value>Next item</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>Add to queue</value>
<value>Add to playing queue</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>All Music</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Media Library sync in progress...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>There is a problem with Media Library sync</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Clear queue</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>Artist</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>File Info</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>Bit Depth</value>
</data>
@@ -456,14 +462,14 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>No songs found in media library</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Folders</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Import from file</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Create Playlist</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>Play all</value>
<value>Create playlist</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Playing Queue</value>
@@ -472,7 +478,7 @@
<value>Playing queue is empty</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Playlist</value>
<value>Playlists</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Loop List</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>Scroll to playing item</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>Select all</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>Loop Single</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>Sort Type</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>Starred Playlist</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>Stop</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Music Gallery - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>Collapse</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Auto-sync Frequency</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>Expand</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>Never</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>Every Day</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>Every 15 Minutes</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>Every Hour</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>Every 6 Hours</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>Narrow Mode</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Privacy Policy</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Browse</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Name</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Leaving it blank will automatically generate a default name.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Password</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Path</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>The specified folder path could not be found</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Path is required</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>Feedback</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>Instructions</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>Special Thanks</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Add</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>Advanced Options</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>Album</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>Configure Album Art Sources</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>Album Art Size</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>Album Art Effect</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>Album Art Sources</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Corner Radius</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>App Appearance</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>General Behavior</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>Apply</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>Artist</value>
</data>
@@ -687,21 +696,12 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Auto Start</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>On application startup</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Lyrics Background Material</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>Lyrics Background</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>Blur Amount</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value>Borderless Window</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Includes log files, online lyrics cache</value>
</data>
@@ -733,14 +733,11 @@
<value>Clear Cache Files</value>
</data>
<data name="SettingsPageCloseStatus.Text" xml:space="preserve">
<value>Closed</value>
<value>Close</value>
</data>
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>Collapse Dropdown</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Compact</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>Naming recorded window statuses helps you distinguish them better</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>Crossfade</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>Current Lyrics Window Status</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>cutlet-docker Transliteration Service</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Dark</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Show debug overlay</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>Delete</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>Top</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Window Height</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Draggable Area</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Easing Function Type</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Exit app when lyrics window is closed</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Export play history</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Export Settings</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Extended</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Fan-shaped Lyrics</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>FAQ</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>Fixed Time Step Rendering</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>Height</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>Hello</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>Help us translate this app</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Japanese Phonetic</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>Join Now</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>Jyutping (Cantonese)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Light</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>Linked Local File</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>Enable listening for new playback sessions</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>Local Folder</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Logging</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>Long Syllable Threshold</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>Current Line</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adapt to Lyrics Background (Colored)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adapt to Lyrics Background (Grayed)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>Custom</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>Float Animation</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Glow Effect</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>Highlight</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>Original Text Highlight Scope</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Left</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Light</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>Play Area Edge Fade</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Line Spacing</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> x Line Height</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Medium</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Semi Light</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>Shadow</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Lyrics Style</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>Lyrics Timeline Sync Threshold</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>Translation Highlight</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>Original-Translation Separator</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Vertical Edge Opacity</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>Lyrics Window</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>Lyrics Window Manager</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>Lyrics Window Manager</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Lyrics Window Status Switch Shortcut</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>Lyrics Window</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Adjusting this value will affect sequential search and best match search results, but will not affect search results in the manual lyrics search interface</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Local Media Library</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>Real-time file monitoring</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>Narrow Mode</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>Next Track Shortcut</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>None</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>Aggressive</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>This folder contains already added folders, please remove them to add this folder</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>Unable to find this path on your computer</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>Sponsorship</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>Real-time Status</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>Record</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>Recorded Window Status</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>References</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>Refresh Dropdown</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>Japanese Phonetic</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>Scope</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Settings Manager</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Play History</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Browse Online Share Hub</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>Show in System Environment</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>Show Layout Splitter</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>Show Title</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>Slide</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Current Value: </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>Snowflake Layer</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Startup</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistics</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Stop playback when Music Gallery window is closed</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>Taskbar Pin Position</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>Thanks for purchasing BetterLyrics</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>Credits</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>Whole Window</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Title Bar Size</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>Toggle Active/Inactive Shortcut</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>Translation</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>LibreTranslate Service</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>Visit https://github.com/LibreTranslate/LibreTranslate for installation tutorial and more info (This software is not affiliated with this service)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>And users who purchased BetterLyrics</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>Width</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>Window Bounds</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>As a separate work area, docked to the top/bottom edge of the screen</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>Standard Mode</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Activity by Hour</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Custom</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>End</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Least Active</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Most Active</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Start</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>This Month</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>This Quarter</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>This Week</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>This Year</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Time Range</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Today</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Top Artists</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Top Source</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Total Duration</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Tracks Played</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Exit</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Open Music Gallery</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>System Tray - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>Reset Window Position</value>
</data>

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>Reproductor de música</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>Modo Imagen en Imagen (PiP)</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>Ventana</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>Al ingresar varios artistas, sepárelos usando uno de los siguientes delimitadores (no mezclar)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>No volver a mostrar este mensaje</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Modo Imagen en Imagen</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancelar</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>Modo Acoplado</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Exportación exitosa</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>No se puede conectar al servidor LX Music. Vaya a Configuración - Fuente de reproducción - LX Music - Servidor LX Music para verificar si el enlace es correcto</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Limpiando caché...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Conexión fallida</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>Conectando...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Obteniendo lista de archivos...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparando la limpieza de caché...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Listo</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Se ha detectado la ruta del directorio raíz. Un índice de disco completo puede contener un gran número de archivos no multimedia y hacer que la exploración dure demasiado tiempo. Se recomienda especificar un subdirectorio concreto.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Esperando para escanear...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Modo Pantalla Completa</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Bloquear</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Bloquear</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>Bloquear</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>Galería de Música</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>Jyutping (Cantonés)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>Asegúrese de que al menos un estado esté configurado como predeterminado</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>Error de autorización, inténtelo de nuevo</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>Configuración</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>Desplazamiento de la línea de tiempo de la letra</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>Priorizar la lectura de traducciones dentro de la letra; si no hay coincidencia, solicitar traducción automática al servidor LibreTranslate</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>Habilitar traducción</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>Mostrar solo traducción</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Proveedor de traducción</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>Mapeo de información de la canción</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Proveedor de búsqueda de letras de destino</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Título</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Archivo .TTML local</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>Modo Inmersivo</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>Configuración</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>Modo de ventana de letras</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>Establecer como predeterminado</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Mostrar solo área de portada del álbum</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Pase el cursor de nuevo para mostrar el botón de alternancia</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Mostrar solo letra</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Vista dividida</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Última sincronización</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Carpeta local</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Nombre</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Sincronizar ahora</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>Añadir a lista de reproducción</value>
</data>
@@ -406,11 +409,17 @@
<value>Siguiente elemento</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>Añadir a la cola</value>
<value>Añadir a la cola de reproducción</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Toda la música</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sincronización de la biblioteca multimedia en curso...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Hay un problema con la sincronización de la biblioteca multimedia</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Borrar cola</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>Artista</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>Información del archivo</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>Profundidad de bits</value>
</data>
@@ -456,15 +462,15 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>No se encontraron canciones en la biblioteca multimedia</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Carpetas</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Importar desde archivo</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Crear lista de reproducción</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>Reproducir todo</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Cola de reproducción</value>
</data>
@@ -472,7 +478,7 @@
<value>La cola de reproducción está vacía</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Lista de reproducción</value>
<value>Listas de reproducción</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Repetir lista</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>Desplazarse al elemento en reproducción</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>Seleccionar todo</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>Repetir una</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>Tipo de orden</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>Lista de destacados</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>Detener</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Galería de Música - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>Contraer</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Frecuencia de sincronización automática</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>Expandir</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>Nunca</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>Todos los días</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>Cada 15 minutos</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>Cada hora</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>Cada 6 horas</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>Modo Estrecho</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Política de privacidad</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Visite</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Nombre</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Si lo deja en blanco, se generará automáticamente un nombre por defecto.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Contraseña</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Ruta</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>No se ha encontrado la ruta de la carpeta especificada</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>La ruta es obligatoria</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Puerto</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>Comentarios</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>Instrucciones</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>Agradecimientos especiales</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Agregar</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>Opciones avanzadas</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>Álbum</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>Configurar fuentes de portada de álbum</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>Tamaño de la portada del álbum</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>Efecto de portada del álbum</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>Fuentes de portada de álbum</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Radio de esquina</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>Apariencia de la aplicación</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>Comportamiento general</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>Aplicar</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>Artista</value>
</data>
@@ -687,21 +696,12 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Inicio automático</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>Al iniciar la aplicación</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Material de fondo de letras</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>Fondo de letras</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>Cantidad de desenfoque</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value>Ventana sin bordes</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Incluye archivos de registro, caché de letras en línea</value>
</data>
@@ -738,9 +738,6 @@
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>Contraer menú desplegable</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Compacto</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>Nombrar los estados de ventana grabados le ayuda a distinguirlos mejor</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>Fundido cruzado</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>Estado actual de la ventana de letras</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>Servicio de transliteración cutlet-docker</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Oscuro</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Mostrar superposición de depuración</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>Eliminar</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>Arriba</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Altura de la ventana</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Área arrastrable</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Tipo de función de aceleración</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Salir de la app al cerrar la ventana de letras</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exportar historial de jugadas</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exportar configuración</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Extendido</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Letras en abanico</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>Preguntas frecuentes</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>Renderizado de paso de tiempo fijo</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>Altura</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>Hola</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>Ayúdanos a traducir esta aplicación</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Fonética japonesa</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>Unirse ahora</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>Jyutping (Cantonés)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Claro</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>Archivo local vinculado</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>Habilitar escucha de nuevas sesiones de reproducción</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>Carpeta local</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Registro</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>Umbral de sílaba larga</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>Línea actual</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adaptar al fondo de letras (Coloreado)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adaptar al fondo de letras (Grisáceo)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>Personalizado</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>Animación flotante</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Efecto de resplandor</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>Resaltado</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>Alcance de resaltado del texto original</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Izquierda</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Ligero</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>Desvanecimiento de borde del área de reproducción</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Espaciado entre líneas</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> x Altura de línea</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Medio</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Semiligero</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>Sombra</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Estilo de letras</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>Umbral de sincronización de línea de tiempo de letras</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>Resaltado de traducción</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>Separador Original-Traducción</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Opacidad de borde vertical</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>Ventana de letras</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>Gestor de ventanas de letras</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>Gestor de ventanas de letras</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Atajo de cambio de estado de ventana de letras</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>Ventana de letras</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Ajustar este valor afectará a la búsqueda secuencial y a los resultados de la búsqueda de mejor coincidencia, pero no afectará a los resultados de búsqueda en la interfaz de búsqueda manual de letras</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Biblioteca multimedia local</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>Monitoreo de archivos en tiempo real</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>Modo Estrecho</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>Atajo de pista siguiente</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>Ninguno</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>Agresivo</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>Esta carpeta contiene carpetas ya agregadas, por favor elimínelas para agregar esta carpeta</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>No se puede encontrar esta ruta en su computadora</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>Patrocinio</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>Estado en tiempo real</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>Grabar</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>Estado de ventana grabado</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>Referencias</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>Actualizar menú desplegable</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>Derecha</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>Fonética japonesa</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>Alcance</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestor de configuración</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Historia del juego</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Explorar Centro de recursos en línea</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>Mostrar en entorno del sistema</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>Mostrar divisor de diseño</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>Mostrar título</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>Deslizar</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Valor actual: </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>Capa de copos de nieve</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Inicio</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Estadísticas</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Detener reproducción al cerrar ventana de Galería de Música</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>Posición de anclaje en barra de tareas</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>Gracias por comprar BetterLyrics</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>Créditos</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>Ventana completa</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Tamaño de la barra de título</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>Atajo de Activar/Desactivar</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>Traducción</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>Servicio LibreTranslate</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>Visite https://github.com/LibreTranslate/LibreTranslate para tutorial de instalación y más información (Este software no está afiliado a este servicio)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>Y usuarios que compraron BetterLyrics</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>Ancho</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>Límites de la ventana</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>Como un área de trabajo separada, acoplada al borde superior/inferior de la pantalla</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>Modo Estándar</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Actividad por horas</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>A medida</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Fin</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Menos activo</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Más activos</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fuentes</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Inicio</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Este mes</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Este trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Esta semana</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Este año</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Intervalo de tiempo</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hoy</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artistas principales</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Fuente superior</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Duración total</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Pistas reproducidas</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Salir</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Abrir Galería de Música</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>Bandeja del sistema - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>Restablecer posición de ventana</value>
</data>

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>Lecteur de musique</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>Mode Image dans l'image</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>Fenêtre</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>Lors de la saisie de plusieurs artistes, veuillez les séparer en utilisant l'un des délimiteurs suivants (ne pas mélanger)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Ne plus afficher ce message</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Mode Image dans l'image</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Annuler</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>Mode Ancré</value>
</data>
<data name="Error" xml:space="preserve">
<value>Erreur</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Exportation réussie</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>Impossible de se connecter au serveur LX Music. Veuillez aller dans Paramètres - Source de lecture - LX Music - Serveur LX Music pour vérifier si le lien est correct</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Nettoyage du cache...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Échec de la connexion</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>Connexion en cours...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Récupération de la liste des fichiers...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Préparation du nettoyage du cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Prêt</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Le chemin du répertoire racine a été détecté. Un index de disque complet peut contenir un grand nombre de fichiers non multimédias et faire durer l'analyse trop longtemps. Il est recommandé de spécifier un sous-répertoire spécifique.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>En attente d'analyse...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Mode Plein écran</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Verrouiller</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Verrouiller</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>Verrouiller</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>Galerie de musique</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>Jyutping (Cantonais)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>Veuillez vous assurer qu'au moins un statut est défini par défaut</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>Échec de l'autorisation, veuillez réessayer</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>Paramètres</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>Décalage temporel des paroles</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>Prioriser la lecture des traductions dans les paroles ; si aucune correspondance n'est trouvée, demander une traduction automatique au serveur LibreTranslate</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>Activer la traduction</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>Afficher uniquement la traduction</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Fournisseur de traduction</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>Mappage des informations de la chanson</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Fournisseur de recherche de paroles cible</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Titre</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>Fichier .TTML local</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>Mode Immersif</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>Configuration</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>Mode de fenêtre des paroles</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>Définir par défaut</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Afficher uniquement la zone de la pochette</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Survolez à nouveau pour afficher le bouton de basculement</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Afficher uniquement les paroles</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Vue scindée</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Date de la dernière synchronisation</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Dossier local</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Nom</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Synchroniser maintenant</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>Ajouter à la liste de lecture</value>
</data>
@@ -411,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Toute la musique</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Synchronisation de la médiathèque en cours...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Il y a un problème avec la synchronisation de la médiathèque</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Vider la file d'attente</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>Artiste</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>Info fichier</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>Profondeur de bits</value>
</data>
@@ -456,15 +462,15 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Aucune chanson trouvée dans la bibliothèque multimédia</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Dossiers</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Importer depuis un fichier</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Créer une liste de lecture</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>Tout lire</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>File d'attente de lecture</value>
</data>
@@ -472,7 +478,7 @@
<value>La file d'attente est vide</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Liste de lecture</value>
<value>Listes de lecture</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Boucle de liste</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>Faire défiler jusqu'à l'élément en cours</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>Tout sélectionner</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>Boucle unique</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>Type de tri</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>Liste de lecture favorite</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>Arrêter</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Galerie de musique - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>Réduire</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Fréquence de synchronisation automatique</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>Agrandir</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>Jamais</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>Tous les jours</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>Toutes les 15 minutes</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>Toutes les heures</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>Toutes les 6 heures</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>Mode Étroit</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Politique de confidentialité</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Parcourir</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Nom</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Si vous laissez le champ vide, un nom par défaut sera automatiquement généré.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Mot de passe</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Chemin</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>Le chemin d'accès au dossier spécifié n'a pas été trouvé</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Le chemin d'accès est requis</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>Commentaires</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>Instructions</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>Remerciements spéciaux</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Ajouter</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>Options avancées</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>Album</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>Configurer les sources de pochette</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>Taille de la pochette</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>Effet de pochette d'album</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>Sources de pochette</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Rayon des coins</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>Apparence de l'application</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>Comportement général</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>Appliquer</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>Artiste</value>
</data>
@@ -687,23 +696,14 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Démarrage automatique</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>Au lancement de l'application</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Matériau d'arrière-plan des paroles</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>Arrière-plan des paroles</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>Quantité de flou</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value>Fenêtre sans bordure</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Comprend les fichiers journaux, le cache des paroles en ligne</value>
<value>Inclut les fichiers journaux, le cache des paroles en ligne</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -738,9 +738,6 @@
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>Réduire le menu déroulant</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Compact</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>Nommer les statuts de fenêtre enregistrés vous aide à mieux les distinguer</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>Fondu enchaîné</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>État actuel de la fenêtre des paroles</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>Service de translittération cutlet-docker</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Sombre</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Afficher la superposition de débogage</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>Supprimer</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>Haut</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Hauteur de la fenêtre</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Zone déplaçable</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Type de fonction d'assouplissement</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Quitter l'app à la fermeture de la fenêtre des paroles</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exporter l'historique des jeux</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exporter les paramètres</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Étendu</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Paroles en éventail</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>FAQ</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>Rendu à pas de temps fixe</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>Hauteur</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>Bonjour</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>Aidez-nous à traduire cette application</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Phonétique japonaise</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>Rejoindre maintenant</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>Jyutping (Cantonais)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Clair</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>Fichier local lié</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>Écouter les nouvelles sessions de lecture</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>Dossier local</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Journalisation</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>Seuil de syllabe longue</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>Ligne actuelle</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adapter à l'arrière-plan (Coloré)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adapter à l'arrière-plan (Grismé)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>Personnalisé</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>Animation flottante</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Effet de lueur</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>Surlignage</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>Portée du surlignage du texte original</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Gauche</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Léger</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>Fondu des bords de la zone de lecture</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Interligne</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> x Hauteur de ligne</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Moyen</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Demi-Léger</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>Ombre</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Style des paroles</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>Seuil de sync de la chronologie</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>Surlignage de la traduction</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>Séparateur Original-Traduction</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Opacité des bords verticaux</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>Fenêtre des paroles</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>Gestionnaire de fenêtres de paroles</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>Gestionnaire de fenêtres de paroles</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Raccourci changement état fenêtre paroles</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>Fenêtre des paroles</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>L'ajustement de cette valeur affectera les résultats de la recherche séquentielle et de la meilleure correspondance, mais n'affectera pas les résultats de la recherche manuelle</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Bibliothèque multimédia locale</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>Surveillance des fichiers en temps réel</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>Mode Étroit</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>Raccourci piste suivante</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>Aucun</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>Agressif</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>Ce dossier contient des dossiers déjà ajoutés, veuillez les supprimer pour ajouter celui-ci</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>Impossible de trouver ce chemin sur votre ordinateur</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>Parrainage</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>Statut en temps réel</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>Enregistrer</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>État de la fenêtre enregistré</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>Références</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>Actualiser la liste déroulante</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>Droite</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>Phonétique japonaise</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>Portée</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestionnaire de paramètres</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Histoire du jeu</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Parcourir le Hub de partage en ligne</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>Afficher dans l'environnement système</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>Afficher le séparateur de mise en page</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>Afficher le titre</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>Glisser</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Valeur actuelle : </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>Couche de flocons de neige</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Démarrage</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistiques</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Arrêter la lecture à la fermeture de la Galerie</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>Position d'épinglage barre des tâches</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>Merci d'avoir acheté BetterLyrics</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>Crédits</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>Fenêtre entière</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Taille de la barre de titre</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>Raccourci Activer/Désactiver</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>Traduction</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>Service LibreTranslate</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>Visitez https://github.com/LibreTranslate/LibreTranslate pour le tutoriel d'installation et plus d'infos (Ce logiciel n'est pas affilié à ce service)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>Et les utilisateurs ayant acheté BetterLyrics</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>Largeur</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>Limites de la fenêtre</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>Comme un espace de travail séparé, ancré au bord supérieur/inférieur de l'écran</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>Mode Standard</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Activité par heure</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Sur mesure</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Fin</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Le moins actif</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Les plus actifs</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources d'information</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Démarrage</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Ce mois-ci</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Ce trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Cette semaine</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Cette année</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Plage de temps</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Temps</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Aujourd'hui</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artistes de haut niveau</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Source supérieure</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Durée totale</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Temps</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Temps</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Pistes jouées</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Quitter</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Ouvrir la Galerie de musique</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>Zone de notification - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>Réinitialiser la position de la fenêtre</value>
</data>

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>म्यूजिक प्लेयर</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>पिक्चर-इन-पिक्चर मोड</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>विंडो</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>कई कलाकारों को इनपुट करते समय, कृपया उन्हें अलग करने के लिए निम्नलिखित विभाजकों में से किसी एक का उपयोग करें (कृपया मिश्रण न करें)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>यह संदेश दोबारा न दिखाएं</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>पिक्चर-इन-पिक्चर मोड</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>रद्द करें</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>डॉक्ड मोड</value>
</data>
<data name="Error" xml:space="preserve">
<value>त्रुटि</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>निर्यात सफल रहा</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>LX Music सर्वर से कनेक्ट नहीं हो सकता, कृपया सेटिंग्स - प्लेबैक स्रोत - LX Music - LX Music सर्वर पर जाएं और जांचें कि लिंक सही तरीके से दर्ज किया गया है या नहीं</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>कैश साफ़ हो रहा है...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>कनेक्शन विफल रहा</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>कनेक्टिंग ...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>फ़ाइल लिस्ट लाई जा रही है...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>पार्सिंग...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>कैश साफ़ करने की तैयारी...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>तैयार</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>रूट डायरेक्टरी पाथ का पता चल गया है। एक फुल डिस्क इंडेक्स में बड़ी संख्या में नॉन-मीडिया फ़ाइलें हो सकती हैं और स्कैन में बहुत ज़्यादा समय लग सकता है। एक खास सबडायरेक्टरी बताने की सलाह दी जाती है।</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>स्कैन करने की तैयारी...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>फुलस्क्रीन मोड</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>लॉक</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>लॉक</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>लॉक</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>संगीत गैलरी</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>ज्यूतपिंग (केंटोनीज़)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>कृपया सुनिश्चित करें कि कम से कम एक स्थिति डिफ़ॉल्ट के रूप में सेट है</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>प्राधिकरण विफल, कृपया पुनः प्रयास करें</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>सेटिंग्स</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>बोल समयरेखा ऑफसेट</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>बोल के भीतर अनुवाद पढ़ने को प्राथमिकता दी जाएगी, यदि कोई मेल नहीं मिलता है तो LibreTranslate सर्वर से मशीनी अनुवाद का अनुरोध किया जाएगा</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>अनुवाद सक्षम करें</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>केवल अनुवाद दिखाएं</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>अनुवाद प्रदाता</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>गीत सूचना मैपिंग</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>लक्ष्य बोल खोज प्रदाता</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>शीर्षक</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>स्थानीय .TTML फ़ाइल</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>इमर्सिव मोड</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>कॉन्फ़िगरेशन</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>बोल विंडो मोड</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>डिफ़ॉल्ट के रूप में सेट करें</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>केवल एल्बम आर्ट क्षेत्र दिखाएं</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>स्विच बटन दिखाने के लिए फिर से होवर करें</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>केवल बोल दिखाएं</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>विभाजित दृश्य</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>अंतिम सिंक समय</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>स्थानीय फोल्डर</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>नाम</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>अभी सिंक्रनाइज़ करें</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>प्लेलिस्ट में जोड़ें</value>
</data>
@@ -406,11 +409,17 @@
<value>अगला आइटम</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>प्लेइंग कतार में जोड़ें</value>
<value>प्लेइंग क्यू में जोड़ें</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>सभी गाने</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>मीडिया लाइब्रेरी सिंक चल रहा है...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>मीडिया लाइब्रेरी सिंक में कोई समस्या है</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>प्लेइंग कतार साफ़ करें</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>कलाकार</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>फ़ाइल की जानकारी</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>बिट गहराई</value>
</data>
@@ -456,14 +462,14 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>मीडिया लाइब्रेरी में कोई गाना नहीं मिला</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>फ़ोल्डर्स</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>फ़ाइल से आयात करें</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>प्लेलिस्ट बनां</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>सभी चलाएं</value>
<value>प्लेलिस्ट बनायें</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>प्लेइंग कतार</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>प्लेइंग आइटम पर स्क्रॉल करें</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>सभी चुनें</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>एकल लूप</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>क्रमबद्ध प्रकार</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>तारांकित प्लेलिस्ट</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>रोकें</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>संगीत लाइब्रेरी - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>वापस लें</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>ऑटो-सिंक आवृत्ति</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>विस्तार करें</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>कभी नहीं</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>प्रतिदिन</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>हर 15 मिनट</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>हर घंटे</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>हर ६ घंटे</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>संकीर्ण मोड</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>गोपनीयता नीति</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>ब्राउज करें</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>नाम</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>इसे खाली छोड़ने से अपने आप एक डिफ़ॉल्ट नाम बन जाएगा।</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>पासवर्ड</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>पथ</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>बताया गया फ़ोल्डर पाथ नहीं मिला</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>पथ आवश्यक है</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>पोर्ट</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>फीडबैक</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>उपयोगकर्ता गाइड</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>विशेष आभार</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>जोड़ें</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>उन्नत विकल्प</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>एल्बम</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>एल्बम कला स्रोत कॉन्फ़िगर करें</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>एल्बम कला का आकार</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>एल्बम कला प्रभाव</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>एल्बम कला स्रोत</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>कोने की त्रिज्या</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>ऐप का रूप</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>सामान्य व्यवहार</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>लागू करें</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>कलाकार</value>
</data>
@@ -687,23 +696,14 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>ऑटो स्टार्ट</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>एप्लिकेशन शुरू करते समय</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>बोल पृष्ठभूमि सामग्री</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>बोल पृष्ठभूमि</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>धुंधलापन मात्रा</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value> borderless विंडो</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>लॉग फ़ाइलें, नेटवर्क बोल कैश शामिल हैं</value>
<value>लॉग फ़ाइलें, ऑनलाइन लिरिक्स कैश शामिल हैं</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>कैश</value>
@@ -738,9 +738,6 @@
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>ड्रॉपडाउन संक्षिप्त करें</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>कॉम्पैक्ट</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>रिकॉर्ड की गई विंडो स्थिति का नामकरण करने से आपको उन्हें बेहतर ढंग से अलग करने में मदद मिल सकती है</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>क्रॉसफ़ेड</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>वर्तमान बोल विंडो स्थिति</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>cutlet-docker लिप्यंतरण सेवा</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>डार्क</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>डीबग ओवरले दिखाएं</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>हटाएं</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>ऊपरी</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>विंडो की ऊंचाई</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>खींचने योग्य क्षेत्र</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>ईज़िंग एनीमेशन प्रकार</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>बोल विंडो बंद होने पर बाहर निकलें</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>प्ले इतिहास निर्यात करें</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>सेटिंग्स निर्यात करें</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>विस्तारित</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>पंखे जैसे बोल</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>अक्सर पूछे जाने वाले प्रश्न</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>निश्चित समय चरण रेंडरिंग</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>ऊंचाई</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>नमस्ते</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>इस एप्लिकेशन का अनुवाद करने में हमारी सहायता करें</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>जापानी ध्वन्यात्मकता</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>अभी शामिल हों</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>ज्यूतपिंग (केंटोनीज़)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>लाइट</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>लिंक की गई स्थानीय फ़ाइलें</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>नए प्लेबैक स्रोतों को सुनना सक्षम करें</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>स्थानीय फ़ोल्डर</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>लॉगिंग</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>दीर्घ शब्दांश थ्रेसहोल्ड</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>वर्तमान पंक्ति</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>बोल पृष्ठभूमि के अनुकूल (रंगीन)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>बोल पृष्ठभूमि के अनुकूल (धूसर)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>कस्टम</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>फ़्लोट एनीमेशन</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>चमक प्रभाव</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>हाइलाइट</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>मूल पाठ हाइलाइटिंग स्कोप</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>बायां</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>लाइट</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>प्लेबैक क्षेत्र किनारे का ग्रेडिएंट</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>लाइन स्पेसिंग</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> गुना लाइन ऊंचाई</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>मध्यम</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>सेमी लाइट</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>छाया</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>बोल शैली</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>बोल समयरेखा सिंक थ्रेसहोल्ड</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>अनुवाद हाइलाइट</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>मूल अनुवाद विभाजक</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>लंबवत किनारे की अस्पष्टता</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>बोल विंडो</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>बोल विंडो प्रबंधक</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>बोल विंडो प्रबंधक</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>बोल विंडो स्थिति स्विच शॉर्टकट</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>बोल विंडो</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>इस मान को समायोजित करने से अनुक्रमिक खोज और सर्वोत्तम मिलान खोज परिणाम प्रभावित होंगे, लेकिन मैनुअल बोल खोज इंटरफ़ेस में खोज परिणाम प्रभावित नहीं होंगे</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>स्थानीय मीडिया लाइब्रेरी</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>फ़ाइल परिवर्तनों की वास्तविक समय निगरानी</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>संकीर्ण मोड</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>अगला ट्रैक शॉर्टकट</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>कोई नहीं</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>आक्रामक</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>इस फ़ोल्डर में पहले से जोड़े गए फ़ोल्डर शामिल हैं, कृपया इस फ़ोल्डर को जोड़ने के लिए उन फ़ोल्डरों को हटा दें</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>आपके कंप्यूटर में पथ नहीं मिल सका</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>प्रायोजक</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>वास्तविक समय स्थिति</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>रिकॉर्ड</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>रिकॉर्ड की गई विंडो स्थिति</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>संदर्भ लिंक</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>ड्रॉपडाउन रीफ्रेश करें</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>दायां</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>जापानी ध्वन्यात्मकता</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>स्कोप</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>सेटिंग्स प्रबंधक</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>प्ले इतिहास</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>ऑनलाइन संसाधन शेयरिंग हब ब्राउज़ करें</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>सिस्टम वातावरण में दिखाएं</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>लेआउट स्प्लिटर दिखाएं</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>शीर्षक दिखाएं</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>स्लाइड</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>वर्तमान मान: </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>स्नोफ्लेक परत</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>स्टार्टअप</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>आंकड़े</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>संगीत गैलरी विंडो बंद होने पर प्लेबैक रोकें</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>टास्कबार फिक्सिंग स्थिति</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>BetterLyrics खरीदने के लिए धन्यवाद</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>आभार सूची</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>पूरी विंडो</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>शीर्षक बार का आकार</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>कट-इन और कट-आउट शॉर्टकट</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>अनुवाद</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>LibreTranslate अनुवाद सेवा</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>इंस्टॉलेशन ट्यूटोरियल और अधिक जानकारी के लिए https://github.com/LibreTranslate/LibreTranslate पर जाएं (इस सॉफ़्टवेयर का इस अनुवाद सेवा से कोई संबंध नहीं है)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>और वे उपयोगकर्ता जिन्होंने BetterLyrics को खरीदा और समर्थन किया</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>चौड़ाई</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>विंडो सीमाएं</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>एक अलग कार्यक्षेत्र के रूप में, स्क्रीन के ऊपरी/निचले किनारे पर डॉक किया गया</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>मानक मोड</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>घंटे के हिसाब से गतिविधि</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>कस्टम</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>समाप्त</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>सबसे कम सक्रिय</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>सर्वाधिक सक्रिय</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>स्रोत</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>शुरू करें</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>इस महीने</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>इस तिमाही</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>इस सप्ताह</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>इस वर्ष</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>समय सीमा</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>दूसरे दर्जे का</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>आज</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>शीर्ष कलाकार</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>शीर्ष रास्ता</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>शीर्ष स्रोत</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>कुल अवधि</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>दूसरे दर्जे का</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>दूसरे दर्जे का</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>बजाए गए ट्रैक</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>कार्यक्रम से बाहर निकलें</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>संगीत गैलरी खोलें</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>सिस्टम ट्रे - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>विंडो स्थिति रीसेट करें</value>
</data>

View File

@@ -129,21 +129,12 @@
<data name="AlbumArtSearchSMTCProvider" xml:space="preserve">
<value>Pemutar musik</value>
</data>
<data name="AllLyricsSettingsControlPictureInPicture.Content" xml:space="preserve">
<value>Mode Gambar-dalam-Gambar (PiP)</value>
</data>
<data name="AppSettingsControlGeneral.Text" xml:space="preserve">
<value>Jendela</value>
</data>
<data name="ArtistsSplitHint.Text" xml:space="preserve">
<value>Saat memasukkan beberapa artis, silakan gunakan salah satu pemisah berikut untuk memisahkannya (jangan dicampur)</value>
</data>
<data name="BaseWindowHostInfoBarCheckBox.Content" xml:space="preserve">
<value>Jangan tampilkan pesan ini lagi</value>
</data>
<data name="BaseWindowMiniFlyoutItem.Text" xml:space="preserve">
<value>Mode Gambar-dalam-Gambar (PiP)</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Batal</value>
</data>
@@ -168,24 +159,45 @@
<data name="DockedMode" xml:space="preserve">
<value>Mode Dok</value>
</data>
<data name="Error" xml:space="preserve">
<value>Kesalahan</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Ekspor berhasil</value>
</data>
<data name="FailToStartLXMusicServer" xml:space="preserve">
<value>Tidak dapat terhubung ke server LX Music, silakan buka Pengaturan - Sumber Pemutaran - LX Music - Server LX Music untuk memeriksa apakah tautan telah dimasukkan dengan benar</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Membersihkan cache...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Koneksi gagal</value>
</data>
<data name="FileSystemServiceConnecting" xml:space="preserve">
<value>Menghubungkan...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Mengambil daftar file...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Penguraian...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Bersiap untuk membersihkan cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Siap</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Jalur direktori root telah terdeteksi. Indeks disk penuh mungkin berisi sejumlah besar file non-media dan menyebabkan pemindaian memakan waktu terlalu lama. Disarankan untuk menentukan subdirektori tertentu.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Menunggu pemindaian...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Mode Layar Penuh</value>
</data>
<data name="HostWindowClickThroughButton.Content" xml:space="preserve">
<value>Kunci</value>
</data>
<data name="HostWindowClickThroughFlyoutItem.Text" xml:space="preserve">
<value>Kunci</value>
</data>
<data name="HostWindowLockToolTip.Text" xml:space="preserve">
<value>Kunci</value>
</data>
<data name="HostWindowMusicGalleryButtonToolTip.Content" xml:space="preserve">
<value>Galeri Musik</value>
</data>
@@ -204,9 +216,6 @@
<data name="Jyutping" xml:space="preserve">
<value>Jyutping (Kanton)</value>
</data>
<data name="KeepAtLeastOneStatusDefault" xml:space="preserve">
<value>Pastikan setidaknya satu status diatur sebagai default</value>
</data>
<data name="LastFMAuthFailed" xml:space="preserve">
<value>Otorisasi gagal, silakan coba lagi</value>
</data>
@@ -270,18 +279,12 @@
<data name="LyricsPageSettings.Text" xml:space="preserve">
<value>Pengaturan</value>
</data>
<data name="LyricsPageTimelineOffsetButtonToolTip.Content" xml:space="preserve">
<value>Offset Waktu Lirik</value>
</data>
<data name="LyricsPageTranslationEnabled.Description" xml:space="preserve">
<value>Akan memprioritaskan membaca terjemahan di dalam lirik, jika tidak ada kecocokan maka akan meminta terjemahan mesin dari server LibreTranslate</value>
</data>
<data name="LyricsPageTranslationEnabled.Header" xml:space="preserve">
<value>Aktifkan Terjemahan</value>
</data>
<data name="LyricsPageTranslationOnly.Header" xml:space="preserve">
<value>Hanya Tampilkan Terjemahan</value>
</data>
<data name="LyricsPageTranslationProviderPrefix.Header" xml:space="preserve">
<value>Penyedia Terjemahan</value>
</data>
@@ -330,9 +333,6 @@
<data name="LyricsSearchControlSongInfoMapping.Text" xml:space="preserve">
<value>Pemetaan Info Lagu</value>
</data>
<data name="LyricsSearchControlTargetSearchProvider.Header" xml:space="preserve">
<value>Penyedia Pencarian Lirik Target</value>
</data>
<data name="LyricsSearchControlTitle.Header" xml:space="preserve">
<value>Judul</value>
</data>
@@ -351,15 +351,9 @@
<data name="LyricsSearchProviderTtmlFile" xml:space="preserve">
<value>File .TTML Lokal</value>
</data>
<data name="LyricsWindowImmersiveButtonToolTip.Content" xml:space="preserve">
<value>Mode Imersif</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowConfig.Text" xml:space="preserve">
<value>Konfigurasi</value>
</data>
<data name="LyricsWindowSettingsControlLyricsWindowMode.Header" xml:space="preserve">
<value>Mode Jendela Lirik</value>
</data>
<data name="LyricsWindowSettingsControlSetDefault.Text" xml:space="preserve">
<value>Tetapkan sebagai Default</value>
</data>
@@ -378,9 +372,6 @@
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>Hanya tampilkan area sampul album</value>
</data>
<data name="MainPageEnterImmersiveModeHint" xml:space="preserve">
<value>Arahkan kursor lagi untuk menampilkan tombol sakelar</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>Hanya tampilkan lirik</value>
</data>
@@ -396,6 +387,18 @@
<data name="MainPageSplitView.Content" xml:space="preserve">
<value>Tampilan Terpisah</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Waktu Sinkronisasi Terakhir</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Folder Lokal</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Nama</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Sinkronisasi sekarang</value>
</data>
<data name="MusicGalleryPageAddToCustomList.Text" xml:space="preserve">
<value>Tambahkan ke daftar putar</value>
</data>
@@ -406,11 +409,17 @@
<value>Item berikutnya</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>Tambahkan ke antrean putar</value>
<value>Tambahkan ke antrian bermain</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Semua Musik</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sinkronisasi Perpustakaan Media sedang berlangsung...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Ada masalah dengan sinkronisasi Perpustakaan Media</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Bersihkan antrean putar</value>
</data>
@@ -420,9 +429,6 @@
<data name="MusicGalleryPageFileArtist.Header" xml:space="preserve">
<value>Artis</value>
</data>
<data name="MusicGalleryPageFileInfo.Text" xml:space="preserve">
<value>Info File</value>
</data>
<data name="MusicGalleryPageFileInfoBitDepth.Header" xml:space="preserve">
<value>Kedalaman Bit</value>
</data>
@@ -456,14 +462,14 @@
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>Tidak ada lagu yang ditemukan di pustaka media</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Folder</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Impor dari file</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Buat daftar putar</value>
</data>
<data name="MusicGalleryPagePlayAll.Content" xml:space="preserve">
<value>Putar Semua</value>
<value>Membuat daftar putar</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Antrean Putar</value>
@@ -472,7 +478,7 @@
<value>Antrean putar kosong</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Daftar Putar</value>
<value>Daftar putar</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Loop daftar</value>
@@ -489,9 +495,6 @@
<data name="MusicGalleryPageScrollToPlayingItem.Text" xml:space="preserve">
<value>Gulir ke item yang diputar</value>
</data>
<data name="MusicGalleryPageSelectAll.Content" xml:space="preserve">
<value>Pilih Semua</value>
</data>
<data name="MusicGalleryPageSingleLoop.Text" xml:space="preserve">
<value>Loop tunggal</value>
</data>
@@ -513,20 +516,29 @@
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>Jenis Urutan</value>
</data>
<data name="MusicGalleryPageStarredPlaylist.Content" xml:space="preserve">
<value>Daftar Putar Berbintang</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>Berhenti</value>
</data>
<data name="MusicGalleryPageTitle" xml:space="preserve">
<value>Galeri Musik - BetterLyrics</value>
</data>
<data name="MusicGalleryWindowDownButtonToolTip.Content" xml:space="preserve">
<value>Tarik kembali</value>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Frekuensi sinkronisasi otomatis</value>
</data>
<data name="MusicGalleryWindowUpButtonToolTip.Content" xml:space="preserve">
<value>Perluas</value>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>Tidak pernah</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>Setiap hari</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>Setiap 15 Menit</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>Setiap Jam</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>Setiap 6 Jam</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>Mode Sempit</value>
@@ -543,12 +555,27 @@
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Kebijakan Privasi</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Jelajahi</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Nama</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Membiarkannya kosong akan secara otomatis menghasilkan nama default.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Kata Sandi</value>
</data>
<data name="RemoteServerConfigControlPath.Header" xml:space="preserve">
<value>Jalur</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>Jalur folder yang ditentukan tidak dapat ditemukan</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Diperlukan jalur</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
</data>
@@ -576,9 +603,6 @@
<data name="SetingsPageFeedback.Text" xml:space="preserve">
<value>Umpan Balik</value>
</data>
<data name="SetingsPageInstructions.Text" xml:space="preserve">
<value>Panduan Penggunaan</value>
</data>
<data name="SetingsPageSpecialThanks.Text" xml:space="preserve">
<value>Terima Kasih Khusus</value>
</data>
@@ -606,9 +630,6 @@
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Tambah</value>
</data>
<data name="SettingsPageAdvanced.Text" xml:space="preserve">
<value>Opsi Lanjutan</value>
</data>
<data name="SettingsPageAlbum.Header" xml:space="preserve">
<value>Album</value>
</data>
@@ -624,15 +645,9 @@
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>Konfigurasi Sumber Sampul Album</value>
</data>
<data name="SettingsPageAlbumArtSize.Header" xml:space="preserve">
<value>Ukuran Sampul Album</value>
</data>
<data name="SettingsPageAlbumEffect.Text" xml:space="preserve">
<value>Efek Sampul Album</value>
</data>
<data name="SettingsPageAlbumLib.Content" xml:space="preserve">
<value>Sumber Sampul Album</value>
</data>
<data name="SettingsPageAlbumRadius.Header" xml:space="preserve">
<value>Radius Sudut</value>
</data>
@@ -663,12 +678,6 @@
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>Tampilan Aplikasi</value>
</data>
<data name="SettingsPageAppBehavior.Text" xml:space="preserve">
<value>Perilaku Umum</value>
</data>
<data name="SettingsPageApply.Content" xml:space="preserve">
<value>Terapkan</value>
</data>
<data name="SettingsPageArtist.Header" xml:space="preserve">
<value>Artis</value>
</data>
@@ -687,23 +696,14 @@
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>Mulai Otomatis</value>
</data>
<data name="SettingsPageAutoStartWindow.Header" xml:space="preserve">
<value>Saat memulai aplikasi</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Material Latar Belakang Lirik</value>
</data>
<data name="SettingsPageBackgroundOverlay.Text" xml:space="preserve">
<value>Latar Belakang Lirik</value>
</data>
<data name="SettingsPageBlurAmount.Header" xml:space="preserve">
<value>Tingkat Keburaman</value>
</data>
<data name="SettingsPageBorderless.Header" xml:space="preserve">
<value>Jendela Tanpa Batas</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Termasuk file log, cache lirik jaringan</value>
<value>Termasuk file log, cache lirik online</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -738,9 +738,6 @@
<data name="SettingsPageCollapseDropdown.Content" xml:space="preserve">
<value>Ciutkan Dropdown</value>
</data>
<data name="SettingsPageCompactTitleBar.Content" xml:space="preserve">
<value>Kompak</value>
</data>
<data name="SettingsPageConfigName.Description" xml:space="preserve">
<value>Menamai status jendela yang direkam dapat membantu Anda membedakannya dengan lebih baik</value>
</data>
@@ -759,18 +756,12 @@
<data name="SettingsPageCrossfade.Content" xml:space="preserve">
<value>Crossfade</value>
</data>
<data name="SettingsPageCurrentLyricsWindowStatus.Text" xml:space="preserve">
<value>Status Jendela Lirik Saat Ini</value>
</data>
<data name="SettingsPageCutletDockerServer.Header" xml:space="preserve">
<value>Layanan Transliterasi cutlet-docker</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Gelap</value>
</data>
<data name="SettingsPageDebugOverlay.Header" xml:space="preserve">
<value>Tampilkan Overlay Debug</value>
</data>
<data name="SettingsPageDelete.Text" xml:space="preserve">
<value>Hapus</value>
</data>
@@ -801,15 +792,9 @@
<data name="SettingsPageDockPlacementTop.Content" xml:space="preserve">
<value>Atas</value>
</data>
<data name="SettingsPageDockWindowHeight.Header" xml:space="preserve">
<value>Tinggi Jendela</value>
</data>
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Area yang Dapat Diseret</value>
</data>
<data name="SettingsPageEasingFuncType.Header" xml:space="preserve">
<value>Jenis Animasi Easing</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
@@ -873,18 +858,15 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Keluar dari program saat jendela lirik ditutup</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Riwayat permainan ekspor</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Ekspor Pengaturan</value>
</data>
<data name="SettingsPageExtendedTitleBar.Content" xml:space="preserve">
<value>Diperluas</value>
</data>
<data name="SettingsPageFan.Header" xml:space="preserve">
<value>Lirik Kipas</value>
</data>
<data name="SettingsPageFAQ.Content" xml:space="preserve">
<value>FAQ</value>
</data>
<data name="SettingsPageFixedTimeStep.Header" xml:space="preserve">
<value>Rendering Langkah Waktu Tetap</value>
</data>
@@ -921,9 +903,6 @@
<data name="SettingsPageHeight.Header" xml:space="preserve">
<value>Tinggi</value>
</data>
<data name="SettingsPageHello.Text" xml:space="preserve">
<value>Halo</value>
</data>
<data name="SettingsPageHelpUsTranslate.Content" xml:space="preserve">
<value>Bantu kami menerjemahkan aplikasi ini</value>
</data>
@@ -954,9 +933,6 @@
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Fonetik Bahasa Jepang</value>
</data>
<data name="SettingsPageJoinNowButton.Content" xml:space="preserve">
<value>Gabung Sekarang</value>
</data>
<data name="SettingsPageJyutping.Content" xml:space="preserve">
<value>Jyutping (Kanton)</value>
</data>
@@ -1005,18 +981,12 @@
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Terang</value>
</data>
<data name="SettingsPageLinkedFile.Text" xml:space="preserve">
<value>File Lokal Tertaut</value>
</data>
<data name="SettingsPageListenNewSession.Header" xml:space="preserve">
<value>Aktifkan mendengarkan sumber pemutaran baru</value>
</data>
<data name="SettingsPageLocalFolder.Text" xml:space="preserve">
<value>Folder Lokal</value>
</data>
<data name="SettingsPageLog.Header" xml:space="preserve">
<value>Pencatatan (Logging)</value>
</data>
<data name="SettingsPageLongSyllableDuration.Header" xml:space="preserve">
<value>Ambang Batas Suku Kata Panjang</value>
</data>
@@ -1071,15 +1041,6 @@
<data name="SettingsPageLyricsFgFontColor.Header" xml:space="preserve">
<value>Baris Saat Ini</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveColored.Content" xml:space="preserve">
<value>Adaptif terhadap Latar Belakang Lirik (Berwarna)</value>
</data>
<data name="SettingsPageLyricsFgFontColorAdaptiveGrayed.Content" xml:space="preserve">
<value>Adaptif terhadap Latar Belakang Lirik (Abu-abu)</value>
</data>
<data name="SettingsPageLyricsFgFontColorCustom.Content" xml:space="preserve">
<value>Kustom</value>
</data>
<data name="SettingsPageLyricsFloatAnimation.Header" xml:space="preserve">
<value>Animasi Mengambang</value>
</data>
@@ -1107,27 +1068,12 @@
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Efek Bersinar</value>
</data>
<data name="SettingsPageLyricsHighlight.Header" xml:space="preserve">
<value>Sorot</value>
</data>
<data name="SettingsPageLyricsHighlightScope.Header" xml:space="preserve">
<value>Cakupan Penyorotan Teks Asli</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Kiri</value>
</data>
<data name="SettingsPageLyricsLight.Content" xml:space="preserve">
<value>Tipis</value>
</data>
<data name="SettingsPageLyricsLineFade.Header" xml:space="preserve">
<value>Gradien Tepi Area Pemutaran</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Spasi Baris</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> kali tinggi baris</value>
</data>
<data name="SettingsPageLyricsMedium.Content" xml:space="preserve">
<value>Sedang</value>
</data>
@@ -1170,9 +1116,6 @@
<data name="SettingsPageLyricsSemiLight.Content" xml:space="preserve">
<value>Semi Tipis</value>
</data>
<data name="SettingsPageLyricsShadow.Header" xml:space="preserve">
<value>Bayangan</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Gaya Lirik</value>
</data>
@@ -1188,30 +1131,15 @@
<data name="SettingsPageLyricsTimelineThreshold.Header" xml:space="preserve">
<value>Ambang Sinkronisasi Waktu Lirik</value>
</data>
<data name="SettingsPageLyricsTranslationHighlight.Header" xml:space="preserve">
<value>Sorotan Terjemahan</value>
</data>
<data name="SettingsPageLyricsTranslationSeparator.Header" xml:space="preserve">
<value>Pemisah Teks Asli dan Terjemahan</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Opasitas Tepi Vertikal</value>
</data>
<data name="SettingsPageLyricsWindow.Text" xml:space="preserve">
<value>Jendela Lirik</value>
</data>
<data name="SettingsPageLyricsWindowManager.Text" xml:space="preserve">
<value>Manajer Jendela Lirik</value>
</data>
<data name="SettingsPageLyricsWindowMgr.Content" xml:space="preserve">
<value>Manajer Jendela Lirik</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>Pintasan Pengalih Status Jendela Lirik</value>
</data>
<data name="SettingsPageLyricsWindowToolTip.Content" xml:space="preserve">
<value>Jendela Lirik</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>Menyesuaikan nilai ini akan memengaruhi hasil pencarian berurutan dan pencarian kecocokan terbaik, tetapi tidak akan memengaruhi hasil pencarian di antarmuka pencarian manual lirik</value>
</data>
@@ -1248,18 +1176,12 @@
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Pustaka Media Lokal</value>
</data>
<data name="SettingsPageMusicLibRealTimeWatch.Header" xml:space="preserve">
<value>Pemantauan Perubahan File Secara Real-time</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>Mode Sempit</value>
</data>
<data name="SettingsPageNextSongHotKey.Header" xml:space="preserve">
<value>Pintasan Lagu Berikutnya</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>Tidak Ada</value>
</data>
<data name="SettingsPageOctTree.Content" xml:space="preserve">
<value>Agresif</value>
</data>
@@ -1287,9 +1209,6 @@
<data name="SettingsPagePathIncludingOthersInfo" xml:space="preserve">
<value>Folder ini berisi folder yang sudah ditambahkan, harap hapus folder tersebut untuk menambahkan folder ini</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>Tidak dapat menemukan jalur di komputer Anda</value>
</data>
<data name="SettingsPagePatrons.Text" xml:space="preserve">
<value>Sponsor</value>
</data>
@@ -1335,15 +1254,9 @@
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>Status Waktu Nyata</value>
</data>
<data name="SettingsPageRecord.Content" xml:space="preserve">
<value>Rekam</value>
</data>
<data name="SettingsPageRecordedWindowStatus.Text" xml:space="preserve">
<value>Status Jendela yang Direkam</value>
</data>
<data name="SettingsPageReference.Header" xml:space="preserve">
<value>Tautan Referensi</value>
</data>
<data name="SettingsPageRefreshDropdown.Content" xml:space="preserve">
<value>Segarkan Dropdown</value>
</data>
@@ -1362,9 +1275,6 @@
<data name="SettingsPageRight.Content" xml:space="preserve">
<value>Kanan</value>
</data>
<data name="SettingsPageRomaji.Header" xml:space="preserve">
<value>Fonetik Bahasa Jepang</value>
</data>
<data name="SettingsPageScope.Header" xml:space="preserve">
<value>Cakupan</value>
</data>
@@ -1404,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Manajer Pengaturan</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Riwayat Bermain</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Jelajahi Hub Berbagi Sumber Daya Online</value>
</data>
@@ -1431,18 +1344,12 @@
<data name="SettingsPageShowInSwitchers.Header" xml:space="preserve">
<value>Tampilkan di Lingkungan Sistem</value>
</data>
<data name="SettingsPageShowLayoutDragger.Header" xml:space="preserve">
<value>Tampilkan Pemisah Tata Letak</value>
</data>
<data name="SettingsPageShowTitle.Header" xml:space="preserve">
<value>Tampilkan Judul</value>
</data>
<data name="SettingsPageSlide.Content" xml:space="preserve">
<value>Geser</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Nilai saat ini: </value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>Lapisan Kepingan Salju</value>
</data>
@@ -1485,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Startup</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistik</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Hentikan pemutaran saat jendela galeri musik ditutup</value>
</data>
@@ -1503,9 +1413,6 @@
<data name="SettingsPageTaskbarPlacement.Header" xml:space="preserve">
<value>Posisi Pemasangan Bilah Tugas</value>
</data>
<data name="SettingsPageThanksForPurchasing.Text" xml:space="preserve">
<value>Terima kasih telah membeli BetterLyrics</value>
</data>
<data name="SettingsPageThanksList.Header" xml:space="preserve">
<value>Daftar Ucapan Terima Kasih</value>
</data>
@@ -1524,12 +1431,6 @@
<data name="SettingsPageTitleBarAreaWhole.Content" xml:space="preserve">
<value>Seluruh Jendela</value>
</data>
<data name="SettingsPageTitleBarType.Header" xml:space="preserve">
<value>Ukuran Bilah Judul</value>
</data>
<data name="SettingsPageToggleHotKey.Header" xml:space="preserve">
<value>Pintasan Masuk dan Keluar (Cut-in/out)</value>
</data>
<data name="SettingsPageTranslatedText.Header" xml:space="preserve">
<value>Terjemahan</value>
</data>
@@ -1539,9 +1440,6 @@
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>Layanan Terjemahan LibreTranslate</value>
</data>
<data name="SettingsPageTranslationInfoLink.Text" xml:space="preserve">
<value>Kunjungi https://github.com/LibreTranslate/LibreTranslate untuk tutorial instalasi dan info lebih lanjut (perangkat lunak ini tidak memiliki afiliasi dengan layanan terjemahan tersebut)</value>
</data>
<data name="SettingsPageUserWhoPurchased.Text" xml:space="preserve">
<value>dan pengguna yang membeli serta mendukung BetterLyrics</value>
</data>
@@ -1554,9 +1452,6 @@
<data name="SettingsPageWidth.Header" xml:space="preserve">
<value>Lebar</value>
</data>
<data name="SettingsPageWindowBounds.Header" xml:space="preserve">
<value>Batas Jendela</value>
</data>
<data name="SettingsPageWorkArea.Description" xml:space="preserve">
<value>Sebagai ruang kerja terpisah, dipasang di tepi atas/bawah layar</value>
</data>
@@ -1572,6 +1467,69 @@
<data name="StandardMode" xml:space="preserve">
<value>Mode Standar</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktivitas per Jam</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Kustom</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Akhir</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Paling Tidak Aktif</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Mulai</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Bulan ini</value>
</data>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Kuartal ini</value>
</data>
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Minggu Ini</value>
</data>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Tahun ini</value>
</data>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Rentang Waktu</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Waktu</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hari ini</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artis Top</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Trek Teratas</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Sumber Teratas</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Total Durasi</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Waktu</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Waktu</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Lagu yang Dimainkan</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Keluar dari Program</value>
</data>
@@ -1581,9 +1539,6 @@
<data name="SystemTrayMusicGallery.Text" xml:space="preserve">
<value>Buka Galeri Musik</value>
</data>
<data name="SystemTrayPageTitle" xml:space="preserve">
<value>Baki Sistem - BetterLyrics</value>
</data>
<data name="SystemTrayResetWindowPosition.Text" xml:space="preserve">
<value>Atur Ulang Posisi Jendela</value>
</data>

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