mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 10:54:55 +08:00
Compare commits
235 Commits
v1.1.194.0
...
ded4cff1ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ded4cff1ba | ||
|
|
f0c790387b | ||
|
|
ee47045b13 | ||
|
|
5175249e92 | ||
|
|
4382a41d00 | ||
|
|
7725b54e41 | ||
|
|
4dca9cc4c2 | ||
|
|
fcf40d5a94 | ||
|
|
54f379dd83 | ||
|
|
106c6de18e | ||
|
|
a3c4cd241d | ||
|
|
d346fddb01 | ||
|
|
eb94c35063 | ||
|
|
7a9ea29d16 | ||
|
|
32cb8248aa | ||
|
|
6e53284863 | ||
|
|
1a043de37c | ||
|
|
199b1f3bbf | ||
|
|
af475c45a4 | ||
|
|
475b43126b | ||
|
|
5afda0f289 | ||
|
|
6eebbfcfcd | ||
|
|
9c2fe9a63a | ||
|
|
adba80ae06 | ||
|
|
c9dcae3ab9 | ||
|
|
d529ca87cf | ||
|
|
08ba66e8cc | ||
|
|
23c3bda10a | ||
|
|
06cdfce5d5 | ||
|
|
f010f3a380 | ||
|
|
aa0f56349b | ||
|
|
48bdffb2fe | ||
|
|
d324a7552f | ||
|
|
78c308c393 | ||
|
|
a1bba00db6 | ||
|
|
0787f5b111 | ||
|
|
884026594b | ||
|
|
b0a777db8d | ||
|
|
83f3a3bd6d | ||
|
|
bfb2ed29e5 | ||
|
|
131a0f0eb1 | ||
|
|
ac2a7b3f7b | ||
|
|
36eea7f8f2 | ||
|
|
6b338deb55 | ||
|
|
af323ecd00 | ||
|
|
c79d01c75b | ||
|
|
b51ec1e60f | ||
|
|
7fe925bcba | ||
|
|
0626472d66 | ||
|
|
33099bc186 | ||
|
|
e653efc227 | ||
|
|
074fef3faf | ||
|
|
029cbbd343 | ||
|
|
802b2a4c1c | ||
|
|
eccc4d519c | ||
|
|
5f274ea28a | ||
|
|
aa1a1f5d58 | ||
|
|
3a56d53487 | ||
|
|
bbc5eb772c | ||
|
|
05b491052b | ||
|
|
8accbf0431 | ||
|
|
1174209c2a | ||
|
|
23ed719046 | ||
|
|
a34f00662e | ||
|
|
f783314258 | ||
|
|
215a39c5d5 | ||
|
|
16bcef5f64 | ||
|
|
fbba9a3c36 | ||
|
|
f205ab0364 | ||
|
|
10314f3c2f | ||
|
|
b4710e87d3 | ||
|
|
282a934cd2 | ||
|
|
b4c4e394ef | ||
|
|
17cfdf37bd | ||
|
|
900a8e1e7c | ||
|
|
ea9a9c2f5f | ||
|
|
0c4d02b337 | ||
|
|
d137d82ecf | ||
|
|
02551e2053 | ||
|
|
026926e9b8 | ||
|
|
4c811db16a | ||
|
|
6f83fa11db | ||
|
|
bc8e15c144 | ||
|
|
85de1eb2cd | ||
|
|
d2bf19ed3d | ||
|
|
43c205c839 | ||
|
|
9664b1ab78 | ||
|
|
08c5f6b515 | ||
|
|
260de40f81 | ||
|
|
c00d0eb005 | ||
|
|
32e761724c | ||
|
|
9fd08af582 | ||
|
|
266dcfc930 | ||
|
|
8764585f2c | ||
|
|
91ab3a48c0 | ||
|
|
80fa34d9e8 | ||
|
|
b4ca4fd990 | ||
|
|
86527f6b82 | ||
|
|
d8066bc683 | ||
|
|
b261a86791 | ||
|
|
34f2a51b74 | ||
|
|
b1e9c25e01 | ||
|
|
346de93c3f | ||
|
|
6f48cbcd16 | ||
|
|
85b3121479 | ||
|
|
94f00d1a31 | ||
|
|
be9e4bba0f | ||
|
|
2454927582 | ||
|
|
aca5f8e00d | ||
|
|
09709e8e62 | ||
|
|
98fd8b43c4 | ||
|
|
3051180eb9 | ||
|
|
d48c81cfa1 | ||
|
|
695147be9b | ||
|
|
e782944a44 | ||
|
|
01462d42ce | ||
|
|
65b7dfcc44 | ||
|
|
ec3146d4a7 | ||
|
|
9ec0bf0b1a | ||
|
|
47e4b93613 | ||
|
|
192ad4a503 | ||
|
|
091e33ae08 | ||
|
|
3b010ed674 | ||
|
|
a9f685d51b | ||
|
|
c6c31f8839 | ||
|
|
78c53760cc | ||
|
|
0bb6b5a204 | ||
|
|
dff36a5e4d | ||
|
|
0188e443db | ||
|
|
5a9cdedc0c | ||
|
|
31460fcc6d | ||
|
|
c12fc6f381 | ||
|
|
e5e0342994 | ||
|
|
061958f20c | ||
|
|
95c73d0a34 | ||
|
|
026a12ac87 | ||
|
|
da53f2166f | ||
|
|
717277e17c | ||
|
|
1dc3ea57e9 | ||
|
|
4ec2ba8b59 | ||
|
|
91d9f253f0 | ||
|
|
90cf373e50 | ||
|
|
cf2778da7a | ||
|
|
45ff7d7aa8 | ||
|
|
eb37cb1b55 | ||
|
|
45aa1d787d | ||
|
|
0b28419ab5 | ||
|
|
258bf9220e | ||
|
|
9ece9f3edc | ||
|
|
40c1f0a5ce | ||
|
|
5f75e6c63c | ||
|
|
43387ce4c8 | ||
|
|
34eda9a262 | ||
|
|
804673696f | ||
|
|
b69e3bb24b | ||
|
|
c028aa8e46 | ||
|
|
fe3e257215 | ||
|
|
eae2428d85 | ||
|
|
b078365136 | ||
|
|
1ede8dbef4 | ||
|
|
a66051b937 | ||
|
|
1eca21c285 | ||
|
|
2254a28e40 | ||
|
|
812eca369d | ||
|
|
132d3d8ac8 | ||
|
|
641a23621f | ||
|
|
6802d10142 | ||
|
|
36f43e6d54 | ||
|
|
e8298ec7bd | ||
|
|
99a21cb935 | ||
|
|
b6da7bea5d | ||
|
|
cf5bf75346 | ||
|
|
7497d7014d | ||
|
|
dd8c62ffa5 | ||
|
|
15b147ba06 | ||
|
|
85146ffc95 | ||
|
|
e9dce765e4 | ||
|
|
3b2c4477b5 | ||
|
|
9d71c4aecf | ||
|
|
7184c148c4 | ||
|
|
85f928ce3b | ||
|
|
7c5032b0c2 | ||
|
|
2c3bd056b7 | ||
|
|
9f2843b7a0 | ||
|
|
7fb6d5346e | ||
|
|
27125d9051 | ||
|
|
5b2fb8b345 | ||
|
|
d558811cb4 | ||
|
|
6e30aa7ebd | ||
|
|
15fc337944 | ||
|
|
b7ef159b9e | ||
|
|
393b33ed83 | ||
|
|
23dfda4413 | ||
|
|
fde7340f4d | ||
|
|
22330d7fe9 | ||
|
|
c64e5776e8 | ||
|
|
ffa2cd75a0 | ||
|
|
873e75a7e9 | ||
|
|
ffa4101d5f | ||
|
|
1c12b582c2 | ||
|
|
c50d31ced7 | ||
|
|
f8108151b6 | ||
|
|
2932366767 | ||
|
|
cbf643ca70 | ||
|
|
a72d0f5c28 | ||
|
|
3b4d98f9a3 | ||
|
|
d5828101d8 | ||
|
|
56051537ea | ||
|
|
6b465a09b1 | ||
|
|
450b86ebaf | ||
|
|
c0078baa13 | ||
|
|
6b28212ec3 | ||
|
|
9a3c2f5f70 | ||
|
|
31be2bd8f7 | ||
|
|
47056e07a1 | ||
|
|
f30673b9d3 | ||
|
|
d8624c49d0 | ||
|
|
72810e7440 | ||
|
|
e881d36743 | ||
|
|
aa3e79d3ff | ||
|
|
9979474ce1 | ||
|
|
2e7cd93cfe | ||
|
|
bdc31c3e0d | ||
|
|
631d079aa2 | ||
|
|
f76ef87167 | ||
|
|
76aa5ee8d0 | ||
|
|
d7f4978a66 | ||
|
|
0905c46e45 | ||
|
|
d0991c5ddb | ||
|
|
619a3ba196 | ||
|
|
13526bb85c | ||
|
|
61f4f608db | ||
|
|
f690da8501 | ||
|
|
145c13a0e6 | ||
|
|
cea4fbb54d |
@@ -53,27 +53,27 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
<DefaultLanguage>en</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="37412.BetterLyrics"
|
||||
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
|
||||
Version="1.1.194.0" />
|
||||
Version="1.1.221.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
@@ -28,11 +28,22 @@
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="en-US"/>
|
||||
<Resource Language="zh-CN"/>
|
||||
<Resource Language="zh-TW"/>
|
||||
<Resource Language="ja-JP"/>
|
||||
<Resource Language="ko-KR"/>
|
||||
<Resource Language="ar"/>
|
||||
<Resource Language="de"/>
|
||||
<Resource Language="en"/>
|
||||
<Resource Language="es"/>
|
||||
<Resource Language="fr"/>
|
||||
<Resource Language="hi"/>
|
||||
<Resource Language="id"/>
|
||||
<Resource Language="ja"/>
|
||||
<Resource Language="ko"/>
|
||||
<Resource Language="ms"/>
|
||||
<Resource Language="pt"/>
|
||||
<Resource Language="ru"/>
|
||||
<Resource Language="th"/>
|
||||
<Resource Language="vi"/>
|
||||
<Resource Language="zh-Hans"/>
|
||||
<Resource Language="zh-Hant"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
<converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
|
||||
<converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
|
||||
<converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
|
||||
<converter:TrackToLyricsConverter x:Key="TrackToLyricsConverter" />
|
||||
<converter:IntToBoolConverter x:Key="IntToBoolConverter" />
|
||||
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
|
||||
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
|
||||
@@ -75,6 +74,8 @@
|
||||
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
|
||||
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
|
||||
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
|
||||
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
|
||||
<converter:PathToImageConverter x:Key="PathToImageConverter" />
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
@@ -96,7 +97,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">
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
|
||||
using BetterLyrics.WinUI3.Services.DiscordService;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
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.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Services.TranslationService;
|
||||
using BetterLyrics.WinUI3.Services.TransliterationService;
|
||||
@@ -19,8 +20,10 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
using Microsoft.Windows.Globalization;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -71,6 +74,9 @@ namespace BetterLyrics.WinUI3
|
||||
{
|
||||
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
|
||||
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
|
||||
fileSystemService.StartAllFolderTimers();
|
||||
|
||||
WindowHook.OpenOrShowWindow<SystemTrayWindow>();
|
||||
|
||||
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
|
||||
@@ -114,12 +120,12 @@ namespace BetterLyrics.WinUI3
|
||||
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
|
||||
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
|
||||
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
|
||||
.AddSingleton<ILibWatcherService, LibWatcherService>()
|
||||
.AddSingleton<ITranslationService, TranslationService>()
|
||||
.AddSingleton<ITransliterationService, TransliterationService>()
|
||||
.AddSingleton<ILastFMService, LastFMService>()
|
||||
.AddSingleton<IResourceService, ResourceService>()
|
||||
.AddSingleton<IDiscordService, DiscordService>()
|
||||
.AddSingleton<ILocalizationService, LocalizationService>()
|
||||
.AddSingleton<IFileSystemService, FileSystemService>()
|
||||
// ViewModels
|
||||
.AddSingleton<AppSettingsControlViewModel>()
|
||||
.AddSingleton<PlaybackSettingsControlViewModel>()
|
||||
|
||||
BIN
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Folder.png
Normal file
BIN
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -10,13 +10,20 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<AppxDefaultResourceQualifiers>Language=ar;de;en;es;fr;hi;id;ja;ko;ms;pt;ru;th;vi;zh-Hans;zh-Hant;</AppxDefaultResourceQualifiers>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="TemplateSelector\**" />
|
||||
<Compile Remove="ViewModels\Lyrics\**" />
|
||||
<Content Remove="TemplateSelector\**" />
|
||||
<Content Remove="ViewModels\Lyrics\**" />
|
||||
<EmbeddedResource Remove="TemplateSelector\**" />
|
||||
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
|
||||
<None Remove="TemplateSelector\**" />
|
||||
<None Remove="ViewModels\Lyrics\**" />
|
||||
<Page Remove="TemplateSelector\**" />
|
||||
<Page Remove="ViewModels\Lyrics\**" />
|
||||
<PRIResource Remove="TemplateSelector\**" />
|
||||
<PRIResource Remove="ViewModels\Lyrics\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -37,6 +44,7 @@
|
||||
<None Remove="Controls\NowPlayingBar.xaml" />
|
||||
<None Remove="Controls\PlaybackSettingsControl.xaml" />
|
||||
<None Remove="Controls\PropertyRow.xaml" />
|
||||
<None Remove="Controls\RemoteServerConfigControl.xaml" />
|
||||
<None Remove="Controls\ShortcutTextBox.xaml" />
|
||||
<None Remove="Controls\SystemTray.xaml" />
|
||||
<None Remove="Controls\WindowSettingsControl.xaml" />
|
||||
@@ -58,20 +66,21 @@
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251021-build.2365" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.250402" />
|
||||
<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.Converters" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.251219" />
|
||||
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
|
||||
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
|
||||
<PackageReference Include="DevWinUI.Controls" Version="9.7.1" />
|
||||
<PackageReference Include="DevWinUI.Controls" Version="9.8.1" />
|
||||
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" />
|
||||
<PackageReference Include="F23.StringSimilarity" Version="7.0.1" />
|
||||
<PackageReference Include="FlaUI.UIA3" Version="5.0.0" />
|
||||
<PackageReference Include="FluentFTP" Version="53.0.2" />
|
||||
<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" />
|
||||
@@ -85,6 +94,8 @@
|
||||
<PackageReference Include="NTextCat" Version="0.3.65" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SMBLibrary" Version="1.5.5.1" />
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.1" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
@@ -95,6 +106,7 @@
|
||||
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" />
|
||||
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
|
||||
<PackageReference Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="7.9.0" />
|
||||
</ItemGroup>
|
||||
@@ -158,6 +170,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>
|
||||
@@ -332,12 +347,9 @@
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PRIResource Update="Strings\en-US\Resources.resw">
|
||||
<Generator></Generator>
|
||||
</PRIResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="TemplateSelector\" />
|
||||
<Page Update="Controls\RemoteServerConfigControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\NowPlayingBar.xaml">
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
public const string AuthorGitHub = "https://github.com/jayfunc";
|
||||
|
||||
public const string Crowdin = "https://crowdin.com/project/betterlyrics/invite?h=413bb0df7afa420247a98fefdae5e12c2647410";
|
||||
|
||||
public const string BetterLyricsGitHub = $"{AuthorGitHub}/BetterLyrics";
|
||||
|
||||
public const string ShareHub = $"{BetterLyricsGitHub}/blob/dev/ShareHub/index.md";
|
||||
|
||||
@@ -71,9 +71,9 @@
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock x:Uid="SetingsPageFeedback" />
|
||||
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
|
||||
<HyperlinkButton x:Uid="SettingsPageQQGroup" NavigateUri="{x:Bind const:Link.QQGroup}" />
|
||||
<HyperlinkButton x:Uid="SettingsPageDiscord" NavigateUri="{x:Bind const:Link.Discord}" />
|
||||
<HyperlinkButton x:Uid="SettingsPageTelegram" NavigateUri="{x:Bind const:Link.Telegram}" />
|
||||
<HyperlinkButton Content="QQ 反馈交流群" NavigateUri="{x:Bind const:Link.QQGroup}" />
|
||||
<HyperlinkButton Content="Discord" NavigateUri="{x:Bind const:Link.Discord}" />
|
||||
<HyperlinkButton Content="Telegram" NavigateUri="{x:Bind const:Link.Telegram}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</dev:SettingsCard>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
x:Class="BetterLyrics.WinUI3.Controls.AppSettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:consts="using:BetterLyrics.WinUI3.Constants"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dev="using:DevWinUI"
|
||||
@@ -42,6 +43,14 @@
|
||||
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
|
||||
</dev:SettingsCard>
|
||||
</dev:SettingsExpander.Items>
|
||||
<dev:SettingsExpander.ItemsFooter>
|
||||
<InfoBar IsClosable="False" IsOpen="True">
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsPageHelpUsTranslate"
|
||||
Padding="0"
|
||||
NavigateUri="{x:Bind consts:Link.Crowdin}" />
|
||||
</InfoBar>
|
||||
</dev:SettingsExpander.ItemsFooter>
|
||||
</dev:SettingsExpander>
|
||||
|
||||
<!-- Startup -->
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
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
|
||||
@@ -91,6 +92,47 @@
|
||||
Text="{x:Bind LyricsWindowStatus.Name, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
|
||||
<Grid Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}" Opacity="0">
|
||||
<Grid.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</Grid.OpacityTransition>
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:EventTriggerBehavior EventName="PointerEntered">
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
<interactivity:EventTriggerBehavior EventName="PointerExited">
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Click="OpenButton_Click"
|
||||
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
Glyph=}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="SystemTrayLyrics" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Click="CloseButton_Click"
|
||||
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
Glyph=}"
|
||||
IsEnabled="{x:Bind LyricsWindowStatus.IsOpened, Mode=OneWay}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="SettingsPageCloseStatus" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Linq;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
@@ -9,6 +14,8 @@ namespace BetterLyrics.WinUI3.Controls;
|
||||
|
||||
public sealed partial class DemoWindowGrid : UserControl
|
||||
{
|
||||
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
|
||||
public DemoWindowGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -22,4 +29,32 @@ public sealed partial class DemoWindowGrid : UserControl
|
||||
get => (LyricsWindowStatus)GetValue(LyricsWindowStatusProperty);
|
||||
set => SetValue(LyricsWindowStatusProperty, value);
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var data = (LyricsWindowStatus)(((FrameworkElement)sender).DataContext);
|
||||
var window = WindowHook.GetWindows<NowPlayingWindow>().FirstOrDefault(x => x.LyricsWindowStatus == data);
|
||||
window?.CloseWindow();
|
||||
}
|
||||
|
||||
private void OpenButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var status = (LyricsWindowStatus)(((FrameworkElement)sender).DataContext);
|
||||
// <20>ģʽ
|
||||
if (_settingsService.AppSettings.GeneralSettings.MultiNowPlayingWindowMode)
|
||||
{
|
||||
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
|
||||
}
|
||||
// <20><><EFBFBD><EFBFBD>ģʽ
|
||||
else
|
||||
{
|
||||
var openedWindows = WindowHook.GetWindows<NowPlayingWindow>();
|
||||
foreach (var item in openedWindows.Where(x => x.LyricsWindowStatus != status))
|
||||
{
|
||||
item.CloseWindow();
|
||||
}
|
||||
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -81,14 +81,6 @@
|
||||
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayBlurAmount, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!--<dev:SettingsCard x:Uid="SettingsPageBackgroundAcrylicEffectAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<uc:ExtendedSlider
|
||||
Default="0"
|
||||
Maximum="10"
|
||||
Minimum="0"
|
||||
Value="{x:Bind LyricsBackgroundSettings.CoverAcrylicEffectAmount, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>-->
|
||||
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Lyricify.Lyrics.Providers.Web.Netease;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using Microsoft.UI;
|
||||
@@ -21,13 +20,11 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.UI;
|
||||
using static Vanara.PInvoke.Ole32;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
@@ -548,11 +545,12 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
_isMouseScrollingChanged = false;
|
||||
|
||||
_lyricsRenderer.CalculateLyrics3DMatrix(
|
||||
lyricsStyle: lyricsStyle,
|
||||
lyricsEffect: lyricsEffect,
|
||||
lyricsX: _renderLyricsStartX,
|
||||
lyricsY: _renderLyricsStartY,
|
||||
lyricsWidth: _renderLyricsWidth,
|
||||
canvasHeight: sender.Size.Height
|
||||
lyricsHeight: _renderLyricsHeight
|
||||
);
|
||||
|
||||
_isLayoutChanged = false;
|
||||
@@ -857,6 +855,14 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
_isLayoutChanged = true;
|
||||
}
|
||||
else if (message.PropertyName == nameof(LyricsEffectSettings.IsLyricsFadeOutEffectEnabled))
|
||||
{
|
||||
_isLayoutChanged = true;
|
||||
}
|
||||
else if (message.PropertyName == nameof(LyricsEffectSettings.IsLyricsOutOfSightEffectEnabled))
|
||||
{
|
||||
_isLayoutChanged = true;
|
||||
}
|
||||
}
|
||||
else if (message.Sender == LyricsWindowStatus?.LyricsStyleSettings)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!-- 淡出效果 -->
|
||||
<dev:SettingsCard x:Uid="SettingsPageLyricsFadeOutEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsFadeOutEffectEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!-- 远离视野 -->
|
||||
<dev:SettingsCard x:Uid="SettingsPageLyricsOutOfSightEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsOutOfSightEffectEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!-- 辉光效果 -->
|
||||
<dev:SettingsExpander x:Uid="SettingsPageLyricsGlowEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
|
||||
|
||||
@@ -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">
|
||||
@@ -278,18 +272,43 @@
|
||||
</Pivot.HeaderTemplate>
|
||||
<Pivot.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LyricsData">
|
||||
<ListView ItemsSource="{x:Bind LyricsLines, Mode=OneWay}" SelectionChanged="ListView_SelectionChanged">
|
||||
<ListView
|
||||
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
|
||||
ItemsSource="{x:Bind LyricsLines, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LyricsLine">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<TextBlock
|
||||
Margin="1,0"
|
||||
Foreground="{ThemeResource SystemFillColorNeutralBrush}"
|
||||
Text="-" />
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind EndMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<TextBlock Margin="6,0" Text="{x:Bind OriginalText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<Grid Margin="0,6" ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
|
||||
<Button
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Click="PlayLyricsLineButton_Click"
|
||||
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
FontSize=16,
|
||||
Glyph=}"
|
||||
Opacity="0"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<Button.OpacityTransition>
|
||||
<ScalarTransition />
|
||||
</Button.OpacityTransition>
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:EventTriggerBehavior EventName="PointerEntered">
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
<interactivity:EventTriggerBehavior EventName="PointerExited">
|
||||
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</Button>
|
||||
</Grid>
|
||||
<local:PropertyRow Grid.Column="1" Value="{x:Bind OriginalText, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
@@ -321,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>
|
||||
|
||||
@@ -18,10 +18,10 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
DataContext = Ioc.Default.GetRequiredService<LyricsSearchControlViewModel>();
|
||||
}
|
||||
|
||||
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
private void PlayLyricsLineButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.SelectedLyricsLine = e.OriginalSource as LyricsLine;
|
||||
var lyricsLine = (LyricsLine)((Button)sender).DataContext;
|
||||
ViewModel.PlayLyricsLine(lyricsLine);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,10 +100,7 @@
|
||||
BorderThickness="4"
|
||||
CornerRadius="4"
|
||||
Visibility="{Binding IsOpened, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<uc:DemoWindowGrid
|
||||
Margin="4"
|
||||
LyricsWindowStatus="{Binding}"
|
||||
Tapped="DemoWindowGrid_Tapped" />
|
||||
<uc:DemoWindowGrid Margin="4" LyricsWindowStatus="{Binding}" />
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ToggleButton
|
||||
@@ -117,7 +114,6 @@
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
@@ -125,13 +121,6 @@
|
||||
Click="ConfigButton_Click">
|
||||
<TextBlock x:Uid="LyricsWindowSettingsControlLyricsWindowConfig" />
|
||||
</Button>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="CloseStatusButton_Click"
|
||||
IsEnabled="{Binding IsOpened, Mode=OneWay}">
|
||||
<TextBlock x:Uid="SettingsPageCloseStatus" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
@@ -198,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>
|
||||
|
||||
@@ -10,7 +10,6 @@ using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -167,38 +166,6 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
ViewModel.OpenConfigPanel();
|
||||
}
|
||||
|
||||
private void DemoWindowGrid_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
var status = (LyricsWindowStatus)(((FrameworkElement)sender).DataContext);
|
||||
// <20>ģʽ
|
||||
if (_settingsService.AppSettings.GeneralSettings.MultiNowPlayingWindowMode)
|
||||
{
|
||||
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
|
||||
}
|
||||
// <20><><EFBFBD><EFBFBD>ģʽ
|
||||
else
|
||||
{
|
||||
var openedWindows = WindowHook.GetWindows<NowPlayingWindow>();
|
||||
foreach (var item in openedWindows.Where(x => x.LyricsWindowStatus != status))
|
||||
{
|
||||
item.CloseWindow();
|
||||
}
|
||||
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseStatusButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
if (element.DataContext is LyricsWindowStatus data)
|
||||
{
|
||||
var window = WindowHook.GetWindows<NowPlayingWindow>().FirstOrDefault(x => x.LyricsWindowStatus == data);
|
||||
window?.CloseWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigSegmented_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
ViewModel.SelectorBarSelectedItemTag = (string)((SegmentedItem)((Segmented)sender).SelectedItem).Tag;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
@@ -7,7 +6,6 @@ using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
@@ -29,22 +27,6 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private async void Grid_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
var status = (LyricsWindowStatus)(((FrameworkElement)sender).DataContext);
|
||||
// <20>ģʽ
|
||||
if (_settingsService.AppSettings.GeneralSettings.MultiNowPlayingWindowMode)
|
||||
{
|
||||
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
|
||||
}
|
||||
// <20><><EFBFBD><EFBFBD>ģʽ
|
||||
else
|
||||
{
|
||||
var openedWindows = WindowHook.GetWindows<NowPlayingWindow>();
|
||||
foreach (var item in openedWindows.Where(x => x.LyricsWindowStatus != status))
|
||||
{
|
||||
item.CloseWindow();
|
||||
}
|
||||
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
|
||||
}
|
||||
await HideAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="BetterLyrics.WinUI3.Controls.MediaSettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
@@ -9,6 +8,7 @@
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:BetterLyrics.WinUI3.Models"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@@ -49,42 +49,131 @@
|
||||
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<dev:SettingsExpander>
|
||||
<DataTemplate x:DataType="models:MediaFolder">
|
||||
<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
|
||||
Click="LocalFolderHyperlinkButton_Click"
|
||||
Content="{Binding Path, Mode=OneWay}"
|
||||
Tag="{Binding Path, Mode=OneWay}" />
|
||||
<TextBlock IsTextSelectionEnabled="True" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</dev:SettingsExpander.Header>
|
||||
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
|
||||
<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"
|
||||
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">
|
||||
<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.ItemsFooter>
|
||||
<StackPanel>
|
||||
<!-- Index info -->
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind IsIndexing, Mode=OneWay}"
|
||||
Message="{x:Bind IndexingStatusText, Mode=OneWay}" />
|
||||
<ProgressBar Visibility="{x:Bind IsIndexing, 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>
|
||||
<!-- Clean up info -->
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind IsCleaningUp, Mode=OneWay}"
|
||||
Message="{x:Bind CleaningUpStatusText, Mode=OneWay}" />
|
||||
<ProgressBar IsIndeterminate="True" Visibility="{x:Bind IsCleaningUp, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</dev:SettingsExpander.ItemsFooter>
|
||||
|
||||
</dev:SettingsExpander>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}">
|
||||
<Button
|
||||
x:Uid="SettingsPageAddFolderButton"
|
||||
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}"
|
||||
CommandParameter="{Binding ElementName=RootGrid}" />
|
||||
<dev:SettingsCard Style="{StaticResource DefaultSettingsExpanderItemStyle}">
|
||||
<DropDownButton x:Uid="SettingsPageAddFolderButton">
|
||||
<DropDownButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="SettingsPageLocalFolder"
|
||||
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
|
||||
CommandParameter="Local">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator />
|
||||
|
||||
<MenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
|
||||
CommandParameter="SMB"
|
||||
Text="SMB">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
|
||||
CommandParameter="FTP"
|
||||
Text="FTP">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem
|
||||
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
|
||||
CommandParameter="WebDAV"
|
||||
Text="WebDAV">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
</MenuFlyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
</dev:SettingsCard>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
@@ -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((LocalMediaFolder)(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
Padding="8,4"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
|
||||
Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
|
||||
CornerRadius="6"
|
||||
Opacity="{x:Bind ViewModel.TimelineSliderThumbOpacity, Mode=OneWay}">
|
||||
<Grid.OpacityTransition>
|
||||
|
||||
@@ -190,7 +190,7 @@ public sealed partial class NowPlayingBar : UserControl,
|
||||
|
||||
private void TimelineSliderOverlay_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
|
||||
{
|
||||
ViewModel.TimelineSliderThumbOpacity = 0.7f;
|
||||
ViewModel.TimelineSliderThumbOpacity = 1f;
|
||||
}
|
||||
|
||||
private void TimelineSliderOverlay_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<InfoBar
|
||||
x:Uid="SettingsPageMusicGalleryOpened"
|
||||
Grid.Row="0"
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind ViewModel.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened, Mode=OneWay}"
|
||||
Message="音乐库窗口已打开,将忽略对其他播放源的监听"
|
||||
Severity="Informational" />
|
||||
|
||||
<!-- 播放源列表 -->
|
||||
@@ -328,10 +328,7 @@
|
||||
<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="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
@@ -344,10 +341,8 @@
|
||||
<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="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
|
||||
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
|
||||
<local:PropertyRow
|
||||
x:Uid="LyricsPageLyricsProviderPrefix"
|
||||
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
|
||||
@@ -466,9 +461,9 @@
|
||||
</dev:SettingsCard>
|
||||
|
||||
<!-- Last.fm -->
|
||||
<TextBlock x:Uid="SettingsPageLastFM" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Last.fm" />
|
||||
<dev:SettingsExpander
|
||||
x:Uid="SettingsPageLastFMManager"
|
||||
Header="Last.fm"
|
||||
HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/LastFM.png}"
|
||||
IsExpanded="{x:Bind ViewModel.IsLastFMAuthenticated, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="BetterLyrics.WinUI3.Controls.RemoteServerConfigControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<ScrollViewer>
|
||||
<StackPanel Width="400" Spacing="16">
|
||||
<ProgressBar
|
||||
x:Name="ProgressBar"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Collapsed" />
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
IsClosable="True"
|
||||
IsOpen="False"
|
||||
Severity="Error" />
|
||||
|
||||
<TextBox
|
||||
x:Name="NameBox"
|
||||
x:Uid="RemoteServerConfigControlName"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<StackPanel x:Name="RemoteFieldsPanel" Spacing="16">
|
||||
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="12">
|
||||
<TextBox
|
||||
x:Name="HostBox"
|
||||
x:Uid="RemoteServerConfigControlServerAddress"
|
||||
Grid.Column="0"
|
||||
InputScope="Url"
|
||||
PlaceholderText="192.168.1.x"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<NumberBox
|
||||
x:Name="PortBox"
|
||||
x:Uid="RemoteServerConfigControlPort"
|
||||
Grid.Column="1"
|
||||
MinWidth="100"
|
||||
LargeChange="10"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
ToolTipService.ToolTip="80"
|
||||
Value="80" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*, Auto" ColumnSpacing="8">
|
||||
<TextBox
|
||||
x:Name="PathBox"
|
||||
x:Uid="RemoteServerConfigControlPath"
|
||||
Grid.Column="0"
|
||||
TextChanged="PathBox_TextChanged"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<Button
|
||||
x:Name="BrowseButton"
|
||||
x:Uid="RemoteServerConfigControlBrowse"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Bottom"
|
||||
Click="BrowseButton_Click"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
|
||||
<InfoBar
|
||||
x:Name="PathWarningBar"
|
||||
IsClosable="False"
|
||||
IsOpen="False"
|
||||
Severity="Warning" />
|
||||
|
||||
<StackPanel x:Name="AuthFieldsPanel" Spacing="16">
|
||||
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
|
||||
<TextBox
|
||||
x:Name="UserBox"
|
||||
x:Uid="RemoteServerConfigControlUsername"
|
||||
Grid.Column="0"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<PasswordBox
|
||||
x:Name="PwdBox"
|
||||
x:Uid="RemoteServerConfigControlPassword"
|
||||
Grid.Column="1"
|
||||
PasswordRevealMode="Peek" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
public sealed partial class RemoteServerConfigControl : UserControl
|
||||
{
|
||||
private readonly string _protocolType;
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public RemoteServerConfigControl(string protocolType)
|
||||
{
|
||||
this.InitializeComponent();
|
||||
_protocolType = protocolType;
|
||||
|
||||
SetupDefaults();
|
||||
CheckPathForWarning();
|
||||
}
|
||||
|
||||
private void SetupDefaults()
|
||||
{
|
||||
if (_protocolType.Equals("Local", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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 (_protocolType.ToUpper())
|
||||
{
|
||||
case "SMB":
|
||||
PortBox.Value = 445;
|
||||
PathBox.PlaceholderText = "SharedMusic";
|
||||
break;
|
||||
case "FTP":
|
||||
PortBox.Value = 21;
|
||||
PathBox.PlaceholderText = "/pub/music";
|
||||
break;
|
||||
case "WEBDAV":
|
||||
PortBox.Value = 80;
|
||||
PathBox.PlaceholderText = "/dav/music";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetScheme()
|
||||
{
|
||||
string scheme = string.Empty;
|
||||
switch (_protocolType.ToUpper())
|
||||
{
|
||||
case "SMB":
|
||||
scheme = "smb";
|
||||
break;
|
||||
case "FTP":
|
||||
scheme = "ftp";
|
||||
break;
|
||||
case "WEBDAV":
|
||||
scheme = "https";
|
||||
break;
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
public MediaFolder GetConfig()
|
||||
{
|
||||
string finalName = HostBox.Text.Trim();
|
||||
|
||||
if (_protocolType.Equals("Local", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PathBox.Text))
|
||||
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathRequired"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(NameBox.Text))
|
||||
finalName = NameBox.Text.Trim();
|
||||
else
|
||||
finalName = PathBox.Text.TrimEnd(System.IO.Path.DirectorySeparatorChar);
|
||||
|
||||
return new MediaFolder
|
||||
{
|
||||
Name = finalName,
|
||||
SourceType = FileSourceType.Local,
|
||||
UriScheme = "file",
|
||||
UriPath = PathBox.Text.Trim(),
|
||||
};
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(HostBox.Text))
|
||||
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlServerAddressRequired"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(NameBox.Text))
|
||||
{
|
||||
finalName = NameBox.Text.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
finalName = $"{_protocolType} - {HostBox.Text}";
|
||||
}
|
||||
|
||||
Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
|
||||
|
||||
string scheme = GetScheme();
|
||||
|
||||
var folder = new MediaFolder
|
||||
{
|
||||
Name = finalName,
|
||||
SourceType = sourceType,
|
||||
|
||||
UriScheme = scheme,
|
||||
UriHost = HostBox.Text.Trim(), // ȥ<><C8A5><EFBFBD><EFBFBD>β<EFBFBD>ո<EFBFBD>
|
||||
UriPort = (int)PortBox.Value,
|
||||
|
||||
UriPath = PathBox.Text.Trim(),
|
||||
|
||||
UserName = UserBox.Text.Trim(),
|
||||
Password = PwdBox.Password,
|
||||
};
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
public void ShowError(string? message)
|
||||
{
|
||||
ErrorInfoBar.Message = message;
|
||||
ErrorInfoBar.IsOpen = !string.IsNullOrWhiteSpace(message);
|
||||
}
|
||||
|
||||
public void SetProgressBarVisibility(Visibility visibility)
|
||||
{
|
||||
ProgressBar.Visibility = visibility;
|
||||
}
|
||||
|
||||
private void PathBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
CheckPathForWarning();
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -16,7 +16,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
{
|
||||
public sealed partial class ShortcutTextBox : UserControl
|
||||
{
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public ShortcutTextBox()
|
||||
{
|
||||
|
||||
@@ -18,13 +18,7 @@
|
||||
<TextBlock x:Uid="AppSettingsControlGeneral" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageConfigName" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<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=}" Style="{StaticResource GhostButtonStyle}" />
|
||||
</StackPanel>
|
||||
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsExpander
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
@@ -8,7 +8,7 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class AlbumArtSearchProviderToDisplayNameConverter : IValueConverter
|
||||
{
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
@@ -16,8 +16,8 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
AlbumArtSearchProvider.Local => _resourceService.GetLocalizedString("AlbumArtSearchLocalProvider"),
|
||||
AlbumArtSearchProvider.SMTC => _resourceService.GetLocalizedString("AlbumArtSearchSMTCProvider"),
|
||||
AlbumArtSearchProvider.Local => _localizationService.GetLocalizedString("AlbumArtSearchLocalProvider"),
|
||||
AlbumArtSearchProvider.SMTC => _localizationService.GetLocalizedString("AlbumArtSearchSMTCProvider"),
|
||||
AlbumArtSearchProvider.iTunes => "iTunes",
|
||||
_ => throw new Exception($"Unknown AlbumArtSearchProvider: {provider}"),
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
if (value is string langCode)
|
||||
{
|
||||
return LanguageHelper.SupportedDisplayLanguages.FindIndex(x => x.LanguageCode == langCode);
|
||||
var found = LanguageHelper.SupportedDisplayLanguages.FindIndex(x => x.LanguageCode == langCode);
|
||||
return found == -1 ? 0 : found;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class FileSourceTypeToIconConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is FileSourceType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
FileSourceType.Local => "\uE8B7", // Folder
|
||||
FileSourceType.SMB => "\uE839", // Network
|
||||
FileSourceType.FTP => "\uE838", // Globe
|
||||
FileSourceType.WebDav => "\uE753", // Cloud
|
||||
_ => "\uE8B7"
|
||||
};
|
||||
}
|
||||
return "\uE8B7";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
if (value is string langCode)
|
||||
{
|
||||
if (PhoneticHelper.IsPhoneticCode(langCode))
|
||||
if (langCode == "N/A")
|
||||
{
|
||||
return langCode;
|
||||
}
|
||||
else if (PhoneticHelper.IsPhoneticCode(langCode))
|
||||
{
|
||||
return PhoneticHelper.GetDisplayName(langCode);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
@@ -10,7 +10,7 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class LyricsSearchProviderToDisplayNameConverter : IValueConverter
|
||||
{
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
@@ -24,10 +24,10 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
LyricsSearchProvider.Kugou => "酷狗音乐",
|
||||
LyricsSearchProvider.AmllTtmlDb => "amll-ttml-db",
|
||||
LyricsSearchProvider.AppleMusic => "Apple Music",
|
||||
LyricsSearchProvider.LocalLrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
|
||||
LyricsSearchProvider.LocalMusicFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
|
||||
LyricsSearchProvider.LocalEslrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
|
||||
LyricsSearchProvider.LocalTtmlFile => _resourceService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
|
||||
LyricsSearchProvider.LocalLrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
|
||||
LyricsSearchProvider.LocalMusicFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
|
||||
LyricsSearchProvider.LocalEslrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
|
||||
LyricsSearchProvider.LocalTtmlFile => _localizationService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
|
||||
_ => "N/A",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
@@ -10,13 +11,13 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
|
||||
{
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is string path)
|
||||
{
|
||||
if (path == _resourceService.GetLocalizedString("MainPageNoLocalFilesMatched"))
|
||||
if (path == _localizationService.GetLocalizedString("MainPageNoLocalFilesMatched"))
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class TrackToLyricsConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Track track)
|
||||
{
|
||||
return track.GetRawLyrics();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
@@ -10,7 +10,7 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class TranslationSearchProviderToDisplayNameConverter : IValueConverter
|
||||
{
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
@@ -24,10 +24,10 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
TranslationSearchProvider.Kugou => "酷狗音乐",
|
||||
TranslationSearchProvider.AmllTtmlDb => "amll-ttml-db",
|
||||
TranslationSearchProvider.AppleMusic => "Apple Music",
|
||||
TranslationSearchProvider.LocalLrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
|
||||
TranslationSearchProvider.LocalMusicFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
|
||||
TranslationSearchProvider.LocalEslrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
|
||||
TranslationSearchProvider.LocalTtmlFile => _resourceService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
|
||||
TranslationSearchProvider.LocalLrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
|
||||
TranslationSearchProvider.LocalMusicFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
|
||||
TranslationSearchProvider.LocalEslrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
|
||||
TranslationSearchProvider.LocalTtmlFile => _localizationService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
|
||||
TranslationSearchProvider.LibreTranslate => "LibreTranslate",
|
||||
_ => "N/A",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
@@ -8,7 +8,7 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
{
|
||||
public partial class TransliterationSearchProviderToDisplayNameConverter : IValueConverter
|
||||
{
|
||||
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
@@ -22,10 +22,10 @@ namespace BetterLyrics.WinUI3.Converter
|
||||
TransliterationSearchProvider.Kugou => "酷狗音乐",
|
||||
TransliterationSearchProvider.AmllTtmlDb => "amll-ttml-db",
|
||||
TransliterationSearchProvider.AppleMusic => "Apple Music",
|
||||
TransliterationSearchProvider.LocalLrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
|
||||
TransliterationSearchProvider.LocalMusicFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
|
||||
TransliterationSearchProvider.LocalEslrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
|
||||
TransliterationSearchProvider.LocalTtmlFile => _resourceService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
|
||||
TransliterationSearchProvider.LocalLrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
|
||||
TransliterationSearchProvider.LocalMusicFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
|
||||
TransliterationSearchProvider.LocalEslrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
|
||||
TransliterationSearchProvider.LocalTtmlFile => _localizationService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
|
||||
TransliterationSearchProvider.BetterLyrics => "BetterLyrics",
|
||||
TransliterationSearchProvider.CutletDocker => "cutlet-docker",
|
||||
_ => "N/A",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace BetterLyrics.WinUI3.Enums
|
||||
{
|
||||
public enum FileSourceType
|
||||
{
|
||||
Local,
|
||||
SMB,
|
||||
FTP,
|
||||
WebDav
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -12,14 +12,14 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class LyricsWindowStatusExtensions
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public static LyricsWindowStatus DesktopMode(Window? window = null)
|
||||
{
|
||||
window ??= WindowHook.GetWindow<SystemTrayWindow>();
|
||||
return new LyricsWindowStatus(window)
|
||||
{
|
||||
Name = _resourceService.GetLocalizedString("DesktopMode"),
|
||||
Name = _localizationService.GetLocalizedString("DesktopMode"),
|
||||
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
|
||||
WindowBounds = new Rect(100, 100, 600, 250),
|
||||
IsLocked = true,
|
||||
@@ -44,7 +44,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
window ??= WindowHook.GetWindow<SystemTrayWindow>();
|
||||
var status = new LyricsWindowStatus(window)
|
||||
{
|
||||
Name = _resourceService.GetLocalizedString("DockedMode"),
|
||||
Name = _localizationService.GetLocalizedString("DockedMode"),
|
||||
IsWorkArea = true,
|
||||
IsAlwaysOnTop = true,
|
||||
IsAlwaysOnTopPolling = true,
|
||||
@@ -71,7 +71,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
window ??= WindowHook.GetWindow<SystemTrayWindow>();
|
||||
var status = new LyricsWindowStatus(window)
|
||||
{
|
||||
Name = _resourceService.GetLocalizedString("FullscreenMode"),
|
||||
Name = _localizationService.GetLocalizedString("FullscreenMode"),
|
||||
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
|
||||
LyricsStyleSettings = new LyricsStyleSettings
|
||||
{
|
||||
@@ -93,7 +93,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
window ??= WindowHook.GetWindow<SystemTrayWindow>();
|
||||
return new LyricsWindowStatus(window)
|
||||
{
|
||||
Name = _resourceService.GetLocalizedString("StandardMode"),
|
||||
Name = _localizationService.GetLocalizedString("StandardMode"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
window ??= WindowHook.GetWindow<SystemTrayWindow>();
|
||||
return new LyricsWindowStatus(window)
|
||||
{
|
||||
Name = _resourceService.GetLocalizedString("NarrowMode"),
|
||||
Name = _localizationService.GetLocalizedString("NarrowMode"),
|
||||
WindowBounds = new Rect(100, 100, 400, 800),
|
||||
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
|
||||
};
|
||||
@@ -113,7 +113,7 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
window ??= WindowHook.GetWindow<SystemTrayWindow>();
|
||||
return new LyricsWindowStatus(window)
|
||||
{
|
||||
Name = _resourceService.GetLocalizedString("TaskbarMode"),
|
||||
Name = _localizationService.GetLocalizedString("TaskbarMode"),
|
||||
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
|
||||
IsPinToTaskbar = true,
|
||||
IsLocked = true,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using ATL;
|
||||
using System.IO;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class TrackExtensions
|
||||
{
|
||||
extension(Track track)
|
||||
{
|
||||
public string GetParentFolderName() => Directory.GetParent(track.Path)?.Name ?? "";
|
||||
|
||||
public string GetParentFolderPath() => Directory.GetParent(track.Path)?.FullName ?? "";
|
||||
|
||||
public string GetRawLyrics()
|
||||
{
|
||||
if (track.Path is string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return TagLib.File.Create(path).Tag.Lyrics;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public string GetFileName() => Path.GetFileName(track.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -9,16 +9,24 @@ namespace BetterLyrics.WinUI3.Extensions
|
||||
{
|
||||
public static class WindowExtensions
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
extension(Window window)
|
||||
{
|
||||
public void Init(
|
||||
string titleKey,
|
||||
string titleKey = "",
|
||||
string title = "",
|
||||
TitleBarHeightOption titleBarHeightOption = TitleBarHeightOption.Standard,
|
||||
BackdropType backdropType = BackdropType.DesktopAcrylic)
|
||||
{
|
||||
window.Title = _resourceService.GetLocalizedString(titleKey);
|
||||
if (titleKey != "")
|
||||
{
|
||||
window.Title = _localizationService.GetLocalizedString(titleKey);
|
||||
}
|
||||
if (title != "")
|
||||
{
|
||||
window.Title = title;
|
||||
}
|
||||
window.AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
|
||||
window.AppWindow.SetIcons();
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ 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 Ude;
|
||||
|
||||
@@ -86,5 +88,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using NTextCat;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Windows.Globalization;
|
||||
|
||||
@@ -10,9 +12,9 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public class LanguageHelper
|
||||
{
|
||||
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
private static readonly RankedLanguageIdentifierFactory _factory = new();
|
||||
private static readonly RankedLanguageIdentifier _identifier;
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
|
||||
public const string ChineseCode = "zh";
|
||||
public const string JapaneseCode = "ja";
|
||||
@@ -92,12 +94,23 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static List<ExtendedLanguage> SupportedDisplayLanguages { get; set; } =
|
||||
[
|
||||
new ExtendedLanguage("", _resourceService.GetLocalizedString("SettingsPageSystemLanguage")),
|
||||
new ExtendedLanguage("en-US", "English"),
|
||||
new ExtendedLanguage("ja-JP"),
|
||||
new ExtendedLanguage("ko-KR"),
|
||||
new ExtendedLanguage("zh-CN", "简体中文"),
|
||||
new ExtendedLanguage("zh-TW", "繁體中文"),
|
||||
new ExtendedLanguage(CultureInfo.CurrentUICulture.Name, _localizationService.GetLocalizedString("SettingsPageSystemLanguage")),
|
||||
new ExtendedLanguage("ar"),
|
||||
new ExtendedLanguage("de"),
|
||||
new ExtendedLanguage("en"),
|
||||
new ExtendedLanguage("es"),
|
||||
new ExtendedLanguage("fr"),
|
||||
new ExtendedLanguage("hi"),
|
||||
new ExtendedLanguage("id"),
|
||||
new ExtendedLanguage("ja"),
|
||||
new ExtendedLanguage("ko"),
|
||||
new ExtendedLanguage("ms"),
|
||||
new ExtendedLanguage("pt"),
|
||||
new ExtendedLanguage("ru"),
|
||||
new ExtendedLanguage("th"),
|
||||
new ExtendedLanguage("vi"),
|
||||
new ExtendedLanguage("zh-Hans"),
|
||||
new ExtendedLanguage("zh-Hant"),
|
||||
];
|
||||
|
||||
static LanguageHelper()
|
||||
@@ -107,7 +120,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
public static string? DetectLanguageCode(string? text)
|
||||
{
|
||||
if (text == null) return null;
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
var guessList = _identifier.Identify(text);
|
||||
string? code = guessList?.FirstOrDefault()?.Item1.Iso639_2T;
|
||||
code = code switch
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Windows.Security.Credentials;
|
||||
using System;
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
@@ -12,23 +13,13 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
/// <param name="value">要保存的值</param>
|
||||
public static void Save(string resource, string key, string value)
|
||||
{
|
||||
// 删除旧值(避免重复存储)
|
||||
try
|
||||
{
|
||||
var vault = new PasswordVault();
|
||||
|
||||
var oldCredential = vault.Retrieve(resource, key);
|
||||
if (oldCredential != null)
|
||||
{
|
||||
vault.Remove(oldCredential);
|
||||
}
|
||||
|
||||
vault.Add(new PasswordCredential(resource, key, value));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 没有旧值就忽略
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +38,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
credential.RetrievePassword();
|
||||
return credential.Password;
|
||||
}
|
||||
catch
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -65,10 +56,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
var credential = vault.Retrieve(resource, key);
|
||||
vault.Remove(credential);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 不存在就忽略
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,10 @@ 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 FilesCachePath => Path.Combine(CacheFolder, "files_cache.db");
|
||||
|
||||
public static void EnsureDirectories()
|
||||
{
|
||||
@@ -75,6 +77,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
Directory.CreateDirectory(LocalTtmlCacheDirectory);
|
||||
|
||||
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
|
||||
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using System;
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public static class PhoneticHelper
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public const string PinyinCode = "zh-cmn-pinyin";
|
||||
public const string JyutpingCode = "zh-yue-jyutping";
|
||||
@@ -22,11 +22,11 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
switch (code)
|
||||
{
|
||||
case PinyinCode:
|
||||
return _resourceService.GetLocalizedString("Pinyin");
|
||||
return _localizationService.GetLocalizedString("Pinyin");
|
||||
case JyutpingCode:
|
||||
return _resourceService.GetLocalizedString("Jyutping");
|
||||
return _localizationService.GetLocalizedString("Jyutping");
|
||||
case RomanCode:
|
||||
return _resourceService.GetLocalizedString("Romaji");
|
||||
return _localizationService.GetLocalizedString("Romaji");
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(code));
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices)
|
||||
{
|
||||
var window = WindowHook.GetWindow<T>();
|
||||
|
||||
return await PickSaveFileAsync(window, fileTypeChoices);
|
||||
}
|
||||
|
||||
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices)
|
||||
{
|
||||
if (window == null) return null;
|
||||
|
||||
var picker = new Windows.Storage.Pickers.FileSavePicker();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public class StreamFileAbstraction : TagLib.File.IFileAbstraction
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly Stream _stream;
|
||||
private readonly bool _closeStreamOnDispose;
|
||||
|
||||
public StreamFileAbstraction(string path, Stream? stream, bool closeStreamOnDispose = false)
|
||||
{
|
||||
_name = Path.GetFileName(path);
|
||||
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
_closeStreamOnDispose = closeStreamOnDispose;
|
||||
}
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public Stream ReadStream => _stream;
|
||||
|
||||
public Stream WriteStream
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stream.CanWrite)
|
||||
{
|
||||
return _stream;
|
||||
}
|
||||
throw new InvalidOperationException("The underlying stream is read-only. Tag saving is not supported for this source.");
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseStream(Stream stream)
|
||||
{
|
||||
if (_closeStreamOnDispose)
|
||||
{
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
@@ -8,12 +8,12 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public class ToastHelper
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public static void ShowToast(string localizedTitleKey, string? description, InfoBarSeverity severity)
|
||||
{
|
||||
AppNotification notification = new AppNotificationBuilder()
|
||||
.AddText(_resourceService.GetLocalizedString(localizedTitleKey))
|
||||
.AddText(_localizationService.GetLocalizedString(localizedTitleKey))
|
||||
.AddText(description)
|
||||
.BuildNotification();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -92,27 +92,30 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
|
||||
line.ScaleTransition.SetDuration(yScrollDuration);
|
||||
line.ScaleTransition.SetDelay(yScrollDelay);
|
||||
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale));
|
||||
line.ScaleTransition.StartTransition(
|
||||
lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
|
||||
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
|
||||
_highlightedScale);
|
||||
|
||||
line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.PhoneticOpacityTransition.StartTransition(
|
||||
absLineCountDelta == 0 ? phoneticOpacity : (isMouseScrolling ? phoneticOpacity : (1 - distanceFactor) * phoneticOpacity));
|
||||
CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.PlayedOriginalOpacityTransition.StartTransition(
|
||||
absLineCountDelta == 0 ? 1 : (isMouseScrolling ? 1.0 : (1 - distanceFactor) * originalOpacity));
|
||||
CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.UnplayedOriginalOpacityTransition.StartTransition(
|
||||
absLineCountDelta == 0 ? originalOpacity : (isMouseScrolling ? originalOpacity : (1 - distanceFactor) * originalOpacity));
|
||||
CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
|
||||
line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
|
||||
line.TranslatedOpacityTransition.StartTransition(
|
||||
absLineCountDelta == 0 ? translatedOpacity : (isMouseScrolling ? translatedOpacity : (1 - distanceFactor) * translatedOpacity));
|
||||
CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
|
||||
|
||||
line.ColorTransition.SetDuration(yScrollDuration);
|
||||
line.ColorTransition.SetDelay(yScrollDelay);
|
||||
@@ -121,8 +124,10 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
|
||||
line.AngleTransition.SetDuration(yScrollDuration);
|
||||
line.AngleTransition.SetDelay(yScrollDelay);
|
||||
line.AngleTransition.StartTransition(lyricsEffect.IsFanLyricsEnabled ?
|
||||
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) : 0);
|
||||
line.AngleTransition.StartTransition(
|
||||
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
|
||||
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) :
|
||||
0);
|
||||
|
||||
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
|
||||
line.YOffsetTransition.SetDuration(yScrollDuration);
|
||||
@@ -143,5 +148,33 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
line.ColorTransition.Update(elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
private static double CalculateTargetOpacity(double baseOpacity, double baseOpacityWhenZeroDistanceFactor, double distanceFactor, bool isMouseScrolling, LyricsEffectSettings lyricsEffect)
|
||||
{
|
||||
double targetOpacity;
|
||||
if (distanceFactor == 0)
|
||||
{
|
||||
targetOpacity = baseOpacityWhenZeroDistanceFactor;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isMouseScrolling)
|
||||
{
|
||||
targetOpacity = baseOpacity;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lyricsEffect.IsLyricsFadeOutEffectEnabled)
|
||||
{
|
||||
targetOpacity = (1 - distanceFactor) * baseOpacity;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetOpacity = baseOpacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
return targetOpacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +213,17 @@ namespace BetterLyrics.WinUI3.Logic
|
||||
if (value >= mousePosition.Y) { result = mid; right = mid - 1; }
|
||||
else { left = mid + 1; }
|
||||
}
|
||||
|
||||
if (result != -1)
|
||||
{
|
||||
var line = lines[result];
|
||||
double lineTopY = offset + line.TopLeftPosition.Y;
|
||||
if (mousePosition.Y < lineTopY)
|
||||
{
|
||||
result = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
|
||||
224
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/ExtendedTrack.cs
Normal file
224
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/ExtendedTrack.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class ExtendedTrack
|
||||
{
|
||||
public string Uri { get; private set; } = "";
|
||||
|
||||
public string DecodedAbsoluteUri
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Uri)) return "";
|
||||
try
|
||||
{
|
||||
var u = new Uri(Uri);
|
||||
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsoluteUri);
|
||||
}
|
||||
catch { return Uri; }
|
||||
}
|
||||
}
|
||||
|
||||
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 uriString) : base()
|
||||
{
|
||||
Uri = uriString;
|
||||
|
||||
string atlPath = uriString;
|
||||
try
|
||||
{
|
||||
var u = new Uri(uriString);
|
||||
if (u.IsFile) atlPath = u.LocalPath;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 用于本地文件
|
||||
var track = new Track(atlPath);
|
||||
SetFromTrack(track);
|
||||
}
|
||||
|
||||
public ExtendedTrack(FileCacheEntity? entity, Stream? stream = null) : base()
|
||||
{
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
RawLyrics = TagLib.File.Create(streamFileAbstraction).Tag.Lyrics;
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using SQLite;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
[Table("FileCache")]
|
||||
public class FileCacheEntity
|
||||
{
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public int Id { get; set; }
|
||||
|
||||
// 【新增】关键字段!
|
||||
// 关联到 MediaFolder.Id。
|
||||
// 作用:
|
||||
// 1. 区分不同配置(即使两个配置连的是同一个 SMB,但在 APP 里视为不同源)。
|
||||
// 2. 删除配置时,可以由 MediaFolderId 快速级联删除所有缓存。
|
||||
[Indexed]
|
||||
public string MediaFolderId { get; set; }
|
||||
|
||||
// 【修改】从 ParentPath 改为 ParentUri
|
||||
// 存储父文件夹的标准 URI (smb://host/share/parent)
|
||||
// 根目录文件的 ParentUri 可以为空,或者等于 MediaFolder 的 Base Uri
|
||||
[Indexed]
|
||||
public string? ParentUri { get; set; }
|
||||
|
||||
// 【核心】标准化的完整 URI (smb://host/share/folder/file.ext)
|
||||
// 确保它是 URL 编码过且格式统一的
|
||||
[Indexed(Unique = true)]
|
||||
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; }
|
||||
|
||||
// ------ 元数据部分 (保持不变) ------
|
||||
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; }
|
||||
public string AudioFormatName { get; set; } = "";
|
||||
public string AudioFormatShortName { get; set; } = "";
|
||||
public string Encoder { get; set; } = "";
|
||||
public string? EmbeddedLyrics { get; set; }
|
||||
public string? LocalAlbumArtPath { get; set; }
|
||||
public bool IsMetadataParsed { get; set; }
|
||||
}
|
||||
}
|
||||
24
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FolderNode.cs
Normal file
24
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FolderNode.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public partial class LocalMediaFolder : ObservableRecipient
|
||||
{
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Path { get; set; }
|
||||
|
||||
public LocalMediaFolder() { }
|
||||
|
||||
public LocalMediaFolder(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -9,9 +9,9 @@ namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class LyricsData
|
||||
{
|
||||
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
|
||||
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
|
||||
|
||||
public List<LyricsLine> LyricsLines { get; set; }
|
||||
public List<LyricsLine> LyricsLines { get; set; } = [];
|
||||
public string? LanguageCode
|
||||
{
|
||||
get => field ?? LanguageHelper.DetectLanguageCode(WrappedOriginalText);
|
||||
@@ -22,7 +22,6 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
public LyricsData()
|
||||
{
|
||||
LyricsLines = [];
|
||||
}
|
||||
|
||||
public LyricsData(List<LyricsLine> lyricsLines)
|
||||
@@ -30,118 +29,15 @@ 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
|
||||
{
|
||||
StartMs = 0,
|
||||
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
|
||||
OriginalText = _resourceService.GetLocalizedString("LyricsNotFound"),
|
||||
OriginalText = _localizationService.GetLocalizedString("LyricsNotFound"),
|
||||
}]);
|
||||
}
|
||||
|
||||
public static LyricsData GetLoadingPlaceholder()
|
||||
{
|
||||
return new LyricsData([
|
||||
new LyricsLine
|
||||
{
|
||||
StartMs = 0,
|
||||
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
|
||||
OriginalText = "● ● ●",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
127
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MediaFolder.cs
Normal file
127
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MediaFolder.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public partial class MediaFolder : ObservableRecipient
|
||||
{
|
||||
[ObservableProperty] public partial string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
|
||||
|
||||
[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][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]
|
||||
[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;
|
||||
|
||||
[JsonIgnore][ObservableProperty] public partial bool IsIndexing { get; set; } = false;
|
||||
[JsonIgnore][ObservableProperty] public partial double IndexingProgress { get; set; } = 0;
|
||||
[JsonIgnore][ObservableProperty] public partial string IndexingStatusText { get; set; } = "";
|
||||
|
||||
[JsonIgnore][ObservableProperty] public partial bool IsCleaningUp { get; set; } = false;
|
||||
[JsonIgnore][ObservableProperty] public partial string CleaningUpStatusText { get; set; } = "";
|
||||
|
||||
[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 UriPath;
|
||||
return $"{UriScheme}://{UriHost}{(UriPort > 0 ? ":" + UriPort : "")}/{UriPath?.TrimStart('/', '\\')} {(string.IsNullOrEmpty(UserName) ? "" : $"({UserName})")}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore] public string VaultKey => $"{Id}-{UserName}";
|
||||
|
||||
public MediaFolder() { }
|
||||
|
||||
public MediaFolder(string path)
|
||||
{
|
||||
UriPath = path;
|
||||
SourceType = FileSourceType.Local;
|
||||
}
|
||||
|
||||
public IUnifiedFileSystem? CreateFileSystem()
|
||||
{
|
||||
if (!IsEnabled) return null;
|
||||
if (string.IsNullOrEmpty(Password) && !IsLocal)
|
||||
{
|
||||
Password = PasswordVaultHelper.Get(Constants.App.AppName, VaultKey) ?? "";
|
||||
}
|
||||
|
||||
return SourceType switch
|
||||
{
|
||||
FileSourceType.Local => new LocalFileSystem(this),
|
||||
FileSourceType.SMB => new SMBFileSystem(this),
|
||||
FileSourceType.FTP => new FTPFileSystem(this),
|
||||
FileSourceType.WebDav => new WebDavFileSystem(this),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
using ATL;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class PlayQueueItem
|
||||
{
|
||||
public Track Track { get; set; }
|
||||
public ExtendedTrack Track { get; set; }
|
||||
|
||||
public PlayQueueItem(Track track)
|
||||
public PlayQueueItem(ExtendedTrack track)
|
||||
{
|
||||
Track = track;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial MusicGallerySettings MusicGallerySettings { get; set; } = new MusicGallerySettings();
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AdvancedSettings AdvancedSettings { get; set; } = new AdvancedSettings();
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaFolder> LocalMediaFolders { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsWindowStatus> WindowBoundsRecords { get; set; } = [];
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
public partial class LyricsEffectSettings : ObservableRecipient, ICloneable
|
||||
{
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsBlurEffectEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsFadeOutEffectEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsOutOfSightEffectEnabled { get; set; } = true;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsGlowEffectEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectScope LyricsGlowEffectScope { get; set; } = LyricsEffectScope.LongDurationSyllable;
|
||||
@@ -52,6 +54,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
return new LyricsEffectSettings(this.LyricsScrollTopDuration, this.LyricsScrollDuration, this.LyricsScrollBottomDuration, this.LyricsScrollEasingType)
|
||||
{
|
||||
IsLyricsBlurEffectEnabled = this.IsLyricsBlurEffectEnabled,
|
||||
IsLyricsFadeOutEffectEnabled = this.IsLyricsFadeOutEffectEnabled,
|
||||
IsLyricsOutOfSightEffectEnabled = this.IsLyricsOutOfSightEffectEnabled,
|
||||
|
||||
IsLyricsGlowEffectEnabled = this.IsLyricsGlowEffectEnabled,
|
||||
LyricsGlowEffectLongSyllableDuration = this.LyricsGlowEffectLongSyllableDuration,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Windows.UI;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models.Settings
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -233,13 +233,15 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
public void CalculateLyrics3DMatrix(LyricsEffectSettings lyricsEffect, double lyricsX, double lyricsY, double lyricsWidth, double canvasHeight)
|
||||
public void CalculateLyrics3DMatrix(LyricsStyleSettings lyricsStyle, LyricsEffectSettings lyricsEffect, double lyricsX, double lyricsY, double lyricsWidth, double lyricsHeight)
|
||||
{
|
||||
if (!lyricsEffect.Is3DLyricsEnabled) return;
|
||||
|
||||
var playingLineTopOffsetFactor = lyricsStyle.PlayingLineTopOffset / 100.0;
|
||||
|
||||
Vector3 center = new(
|
||||
(float)(lyricsX + lyricsWidth / 2),
|
||||
(float)(lyricsY + canvasHeight / 2),
|
||||
(float)(lyricsY + lyricsHeight * playingLineTopOffsetFactor / 2),
|
||||
0);
|
||||
|
||||
float rotationX = (float)(Math.PI * lyricsEffect.Lyrics3DXAngle / 180.0);
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
float blur,
|
||||
float opacity)
|
||||
{
|
||||
if (opacity <= 0) return;
|
||||
if (float.IsNaN(opacity) || opacity <= 0) return;
|
||||
|
||||
var bounds = layout.LayoutBounds;
|
||||
var destRect = new Rect(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Helper.BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.FileSystemService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
@@ -24,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();
|
||||
}
|
||||
@@ -49,7 +50,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
switch (provider.Provider)
|
||||
{
|
||||
case AlbumArtSearchProvider.Local:
|
||||
result = SearchFile(songInfo)?.AsBuffer();
|
||||
result = (await SearchFile(songInfo))?.AsBuffer();
|
||||
break;
|
||||
case AlbumArtSearchProvider.SMTC:
|
||||
result = bufferFromSMTC;
|
||||
@@ -77,29 +78,57 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
|
||||
return null;
|
||||
}
|
||||
|
||||
private byte[]? SearchFile(SongInfo songInfo)
|
||||
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();
|
||||
|
||||
FileCacheEntity? bestMatch = null;
|
||||
|
||||
foreach (var item in allFiles)
|
||||
{
|
||||
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
var ext = Path.GetExtension(item.FileName).ToLower();
|
||||
if (!FileHelper.MusicExtensions.Contains(ext)) continue;
|
||||
|
||||
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)
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
|
||||
{
|
||||
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
|
||||
{
|
||||
Track track = new(file);
|
||||
if ((track.Title == songInfo.Title && track.Artist == songInfo.DisplayArtists) || StringHelper.IsSwitchableNormalizedMatch(Path.GetFileNameWithoutExtension(file), songInfo.DisplayArtists, songInfo.Title))
|
||||
{
|
||||
var bytes = track.EmbeddedPictures.FirstOrDefault()?.PictureData;
|
||||
if (bytes != null)
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bestMatch = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
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.Extensions.Logging;
|
||||
using SQLite;
|
||||
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 SQLiteAsyncConnection _db;
|
||||
private bool _isInitialized = false;
|
||||
|
||||
// 定时器字典
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _folderTimerTokens = new();
|
||||
// 当前正在执行的扫描任务字典
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeScanTokens = new();
|
||||
|
||||
private static readonly SemaphoreSlim _dbLock = new(1, 1);
|
||||
private static readonly SemaphoreSlim _folderScanLock = new(1, 1);
|
||||
|
||||
public FileSystemService(ISettingsService settingsService, ILocalizationService localizationService, ILogger<FileSystemService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_localizationService = localizationService;
|
||||
_settingsService = settingsService;
|
||||
_db = new SQLiteAsyncConnection(PathHelper.FilesCachePath);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
await _db.CreateTableAsync<FileCacheEntity>();
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false)
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
string queryParentUri;
|
||||
if (parentFolder == null)
|
||||
{
|
||||
if (!forceRefresh) forceRefresh = true;
|
||||
queryParentUri = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
queryParentUri = parentFolder.Uri;
|
||||
}
|
||||
|
||||
List<FileCacheEntity> cachedEntities = new List<FileCacheEntity>();
|
||||
|
||||
if (parentFolder != null)
|
||||
{
|
||||
cachedEntities = await _db.Table<FileCacheEntity>()
|
||||
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
bool needSync = forceRefresh || cachedEntities.Count == 0;
|
||||
|
||||
if (needSync)
|
||||
{
|
||||
cachedEntities = await SyncAsync(provider, parentFolder, configId);
|
||||
}
|
||||
|
||||
return cachedEntities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从远端/本地同步文件至数据库,该阶段不会解析文件全部元数据。
|
||||
/// <para/>
|
||||
/// 如果某个已有文件被修改或有新文件被添加,会预留空位,等待后续填充(通常交给 <see cref="ScanMediaFolderAsync"/> 完成)
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="parentFolder"></param>
|
||||
/// <param name="configId"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<List<FileCacheEntity>> SyncAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId)
|
||||
{
|
||||
List<FileCacheEntity> 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
|
||||
{
|
||||
await _db.RunInTransactionAsync(conn =>
|
||||
{
|
||||
var dbItems = conn.Table<FileCacheEntity>()
|
||||
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
|
||||
.ToList();
|
||||
|
||||
var dbMap = dbItems.ToDictionary(x => x.Uri, x => x);
|
||||
|
||||
var remoteMap = remoteItems
|
||||
.GroupBy(x => x.Uri)
|
||||
.Select(g => g.First())
|
||||
.ToDictionary(x => x.Uri, x => x);
|
||||
|
||||
var toInsert = new List<FileCacheEntity>();
|
||||
var toUpdate = new List<FileCacheEntity>();
|
||||
var toDelete = new List<FileCacheEntity>();
|
||||
|
||||
foreach (var remote in remoteItems)
|
||||
{
|
||||
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; // 标记为未解析,下次会重新读取元数据
|
||||
|
||||
toUpdate.Add(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 数据库里原有的 Title, Artist, LocalAlbumArtPath 都会被完美保留
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
toInsert.Add(remote);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dbItem in dbItems)
|
||||
{
|
||||
if (!remoteMap.ContainsKey(dbItem.Uri))
|
||||
{
|
||||
toDelete.Add(dbItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (toInsert.Count > 0) conn.InsertAll(toInsert);
|
||||
if (toUpdate.Count > 0) conn.UpdateAll(toUpdate);
|
||||
if (toDelete.Count > 0)
|
||||
{
|
||||
foreach (var item in toDelete) conn.Delete(item);
|
||||
}
|
||||
});
|
||||
|
||||
var finalItems = await _db.Table<FileCacheEntity>()
|
||||
.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(FileCacheEntity entity)
|
||||
{
|
||||
// 现在的实体已经包含了完整信息,直接 Update 即可
|
||||
// 我们只需要确保 Where 子句用的是主键或者 Uri
|
||||
|
||||
// 简化版 SQL,直接用 ORM 的 Update
|
||||
// 但因为 entity 对象可能包含一些不应该被覆盖的旧数据(如果多线程操作),
|
||||
// 手写 SQL 只更新 Metadata 字段更安全。
|
||||
|
||||
string sql = @"
|
||||
UPDATE FileCache
|
||||
SET
|
||||
Title = ?, Artists = ?, Album = ?,
|
||||
Year = ?, Bitrate = ?, SampleRate = ?, BitDepth = ?,
|
||||
Duration = ?, AudioFormatName = ?, AudioFormatShortName = ?, Encoder = ?,
|
||||
EmbeddedLyrics = ?, LocalAlbumArtPath = ?,
|
||||
IsMetadataParsed = 1
|
||||
WHERE Id = ?"; // 推荐用 Id (主键) 最快,如果没有 Id 则用 Uri
|
||||
|
||||
await _db.ExecuteAsync(sql,
|
||||
entity.Title, entity.Artists, entity.Album,
|
||||
entity.Year, entity.Bitrate, entity.SampleRate, entity.BitDepth,
|
||||
entity.Duration, entity.AudioFormatName, entity.AudioFormatShortName, entity.Encoder,
|
||||
entity.EmbeddedLyrics, entity.LocalAlbumArtPath,
|
||||
entity.Id // WHERE Id = ?
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity)
|
||||
{
|
||||
// 直接传递实体给 Provider
|
||||
return await provider.OpenReadAsync(entity);
|
||||
}
|
||||
|
||||
public async Task DeleteCacheForMediaFolderAsync(MediaFolder folder)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServicePrepareToClean");
|
||||
folder.IsCleaningUp = 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.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
|
||||
});
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
await _dbLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await _db.ExecuteAsync("DELETE FROM FileCache WHERE MediaFolderId = ?", folder.Id);
|
||||
await _db.ExecuteAsync("VACUUM");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dbLock.Release();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_folderScanLock.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("DeleteCacheForMediaFolderAsync: {}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.CleaningUpStatusText = "";
|
||||
folder.IsCleaningUp = 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.IsIndexing = true;
|
||||
folder.IndexingProgress = 0;
|
||||
folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceWaitingForScan");
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await _folderScanLock.WaitAsync(scanCts.Token);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
using var fs = folder.CreateFileSystem();
|
||||
if (fs == null || !await fs.ConnectAsync())
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnectFailed"));
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceFetchingFileList"));
|
||||
|
||||
var filesToProcess = new List<FileCacheEntity>();
|
||||
var foldersToScan = new Queue<FileCacheEntity?>();
|
||||
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(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
folder.IndexingProgress = progress;
|
||||
folder.IndexingStatusText = $"{_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)
|
||||
{
|
||||
await _dbLock.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
await UpdateMetadataAsync(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dbLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("ScanMediaFolderAsync: {}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.LastSyncTime = DateTime.Now;
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_folderScanLock.Release();
|
||||
|
||||
_activeScanTokens.TryRemove(folder.Id, out _);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
folder.IsIndexing = false;
|
||||
folder.IndexingStatusText = "";
|
||||
folder.IndexingProgress = 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (enabledConfigIds == null || !enabledConfigIds.Any())
|
||||
{
|
||||
return new List<FileCacheEntity>();
|
||||
}
|
||||
|
||||
var idList = enabledConfigIds.ToList();
|
||||
|
||||
// SQL 逻辑: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
|
||||
var results = await _db.Table<FileCacheEntity>()
|
||||
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
|
||||
.ToListAsync();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 参数为 string parentUri,表示哪个文件夹的内容变了
|
||||
public event EventHandler<string>? FolderUpdated;
|
||||
|
||||
private async Task<string?> SaveAlbumArtToDiskAsync(ExtendedTrack track)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// <returns></returns>
|
||||
Task InitializeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="parentFolder"></param>
|
||||
/// <param name="configId"></param>
|
||||
/// <param name="forceRefresh">强制需要从远端/本地同步至数据库</param>
|
||||
/// <returns></returns>
|
||||
Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false);
|
||||
|
||||
/// <summary>
|
||||
/// 打开文件(通过远端/本地流)
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// 更新数据库(单个文件)
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <returns></returns>
|
||||
Task UpdateMetadataAsync(FileCacheEntity 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>
|
||||
/// <param name="enabledConfigIds"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
|
||||
|
||||
void StartAllFolderTimers();
|
||||
|
||||
event EventHandler<string> FolderUpdated;
|
||||
}
|
||||
}
|
||||
@@ -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<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null);
|
||||
/// <summary>
|
||||
/// 打开流
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <returns></returns>
|
||||
Task<Stream?> OpenReadAsync(FileCacheEntity file);
|
||||
Task DisconnectAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_client.IsConnected) return true;
|
||||
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
|
||||
return _client.IsConnected;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"FTP连接失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var result = new List<FileCacheEntity>();
|
||||
|
||||
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 FileCacheEntity
|
||||
{
|
||||
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(FileCacheEntity file)
|
||||
{
|
||||
if (file == null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 还原服务器路径
|
||||
string serverPath = GetServerPathFromUri(file.Uri);
|
||||
|
||||
// 2. 解码 (Uri 里的空格是 %20,FTP 需要真实空格)
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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()
|
||||
{
|
||||
return Task.FromResult(Directory.Exists(_rootLocalPath));
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var result = new List<FileCacheEntity>();
|
||||
|
||||
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 FileCacheEntity
|
||||
{
|
||||
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(FileCacheEntity 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() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
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()
|
||||
{
|
||||
try
|
||||
{
|
||||
_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;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件列表
|
||||
/// </summary>
|
||||
/// <param name="parentFolder">
|
||||
/// 传入要列出的文件夹实体。
|
||||
/// 如果传入 null,则默认列出 MediaFolder 配置的根目录。
|
||||
/// </param>
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var result = new List<FileCacheEntity>();
|
||||
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 FileCacheEntity
|
||||
{
|
||||
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(FileCacheEntity 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(FileCacheEntity? 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("/", "\\");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using SMBLibrary;
|
||||
using SMBLibrary.Client;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
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;
|
||||
|
||||
// SMB 协议建议的最大读取块大小 (64KB 是最安全的通用值)
|
||||
private const int MaxReadChunkSize = 65536;
|
||||
|
||||
public SMBReadOnlyStream(ISMBFileStore store, object handle)
|
||||
{
|
||||
_store = store;
|
||||
_handle = handle;
|
||||
_position = 0;
|
||||
|
||||
var status = _store.GetFileInformation(out FileInformation result, handle, FileInformationClass.FileStandardInformation);
|
||||
if (status == NTStatus.STATUS_SUCCESS && result is FileStandardInformation info)
|
||||
{
|
||||
_length = info.EndOfFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
_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;
|
||||
set => _position = value;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (_position >= _length) return 0;
|
||||
|
||||
int totalBytesRead = 0;
|
||||
int remainingRequest = count;
|
||||
|
||||
// 循环读取,直到读完请求的数量,或者文件结束
|
||||
while (remainingRequest > 0)
|
||||
{
|
||||
// 计算剩余文件长度
|
||||
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;
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPos = _position;
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin:
|
||||
newPos = offset;
|
||||
break;
|
||||
case SeekOrigin.Current:
|
||||
newPos = _position + offset;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
newPos = _length + offset;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newPos < 0)
|
||||
{
|
||||
throw new IOException("Seek before beginning.");
|
||||
}
|
||||
|
||||
_position = newPos;
|
||||
return _position;
|
||||
}
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
public override void Flush() { }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
try { _store.CloseFile(_handle); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 测试连接:Propfind 请求配置的根路径
|
||||
// GetStandardUri 已经包含了用户设置的路径
|
||||
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
|
||||
return result.IsSuccessful;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
|
||||
{
|
||||
var list = new List<FileCacheEntity>();
|
||||
|
||||
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 FileCacheEntity
|
||||
{
|
||||
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(FileCacheEntity 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();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Hooks;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.ResourceService;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using Hqub.Lastfm;
|
||||
@@ -16,7 +16,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
|
||||
public partial class LastFMService : ILastFMService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IResourceService _resourceService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
private readonly LastfmClient _client;
|
||||
|
||||
@@ -27,10 +27,10 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
|
||||
|
||||
public bool IsAuthenticated { get; private set; }
|
||||
|
||||
public LastFMService(ISettingsService settingsService, IResourceService resourceService)
|
||||
public LastFMService(ISettingsService settingsService, ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
_settingsService = settingsService;
|
||||
_resourceService = resourceService;
|
||||
|
||||
_client = new LastfmClient(Constants.LastFM.ApiKey, Constants.LastFM.SharedSecret);
|
||||
_client.Session.SessionKey = PasswordVaultHelper.Get(Constants.App.AppName, Constants.LastFM.SessionKeyCredentialKey) ?? string.Empty;
|
||||
@@ -68,10 +68,10 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = _resourceService.GetLocalizedString("LastFMRequestAuthTitle") ?? "",
|
||||
Content = _resourceService.GetLocalizedString("LastFMRequestAuthDesc") ?? "",
|
||||
PrimaryButtonText = _resourceService.GetLocalizedString("LastFMRequestAuthConfirm") ?? "",
|
||||
CloseButtonText = _resourceService.GetLocalizedString("Cancel") ?? "",
|
||||
Title = _localizationService.GetLocalizedString("LastFMRequestAuthTitle") ?? "",
|
||||
Content = _localizationService.GetLocalizedString("LastFMRequestAuthDesc") ?? "",
|
||||
PrimaryButtonText = _localizationService.GetLocalizedString("LastFMRequestAuthConfirm") ?? "",
|
||||
CloseButtonText = _localizationService.GetLocalizedString("Cancel") ?? "",
|
||||
DefaultButton = ContentDialogButton.Close,
|
||||
XamlRoot = dialogXamlRoot,
|
||||
};
|
||||
@@ -95,10 +95,10 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = _resourceService.GetLocalizedString("LastFMRequestUnAuthTitle") ?? "",
|
||||
Content = _resourceService.GetLocalizedString("LastFMRequestUnAuthDesc") ?? "",
|
||||
PrimaryButtonText = _resourceService.GetLocalizedString("LastFMRequestUnAuthConfirm") ?? "",
|
||||
CloseButtonText = _resourceService.GetLocalizedString("Cancel") ?? "",
|
||||
Title = _localizationService.GetLocalizedString("LastFMRequestUnAuthTitle") ?? "",
|
||||
Content = _localizationService.GetLocalizedString("LastFMRequestUnAuthDesc") ?? "",
|
||||
PrimaryButtonText = _localizationService.GetLocalizedString("LastFMRequestUnAuthConfirm") ?? "",
|
||||
CloseButtonText = _localizationService.GetLocalizedString("Cancel") ?? "",
|
||||
DefaultButton = ContentDialogButton.Close,
|
||||
XamlRoot = dialogXamlRoot,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace BetterLyrics.WinUI3.Services.LocalizationService
|
||||
{
|
||||
public interface ILocalizationService
|
||||
{
|
||||
string GetLocalizedString(string id);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.ResourceService
|
||||
namespace BetterLyrics.WinUI3.Services.LocalizationService
|
||||
{
|
||||
public class ResourceService : IResourceService
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly ResourceLoader _resourceLoader = new();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// 2025/6/23 by Zhe Fang
|
||||
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Enums;
|
||||
using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Helper.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;
|
||||
@@ -30,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();
|
||||
@@ -242,7 +243,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
lyricsSearchResult = await SearchAmllTtmlDbAsync(songInfo);
|
||||
break;
|
||||
case LyricsSearchProvider.LocalMusicFile:
|
||||
lyricsSearchResult = SearchEmbedded(songInfo);
|
||||
lyricsSearchResult = await SearchEmbedded(songInfo);
|
||||
break;
|
||||
case LyricsSearchProvider.LocalLrcFile:
|
||||
case LyricsSearchProvider.LocalEslrcFile:
|
||||
@@ -277,109 +278,105 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
private async Task<LyricsSearchResult> SearchFile(SongInfo songInfo, LyricsFormat format)
|
||||
{
|
||||
int maxScore = 0;
|
||||
string? bestFile = null;
|
||||
|
||||
FileCacheEntity? 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 (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
if (item.FileName.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path, $"*{format.ToFileExtension()}"))
|
||||
{
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = file });
|
||||
if (score > maxScore)
|
||||
{
|
||||
bestFile = file;
|
||||
maxScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FileName });
|
||||
|
||||
if (score > maxScore)
|
||||
{
|
||||
maxScore = score;
|
||||
bestFileEntity = item;
|
||||
|
||||
bestFolderConfig = enabledFolders.FirstOrDefault(f => f.Id == item.MediaFolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestFile != null)
|
||||
if (bestFileEntity != null)
|
||||
{
|
||||
lyricsSearchResult.Reference = bestFile;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
lyricsSearchResult.Raw = bestFileEntity.EmbeddedLyrics;
|
||||
|
||||
string? raw = await File.ReadAllTextAsync(bestFile, FileHelper.GetEncoding(bestFile));
|
||||
if (raw != null)
|
||||
{
|
||||
lyricsSearchResult.Raw = raw;
|
||||
}
|
||||
lyricsSearchResult.Reference = bestFileEntity.Uri;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
}
|
||||
|
||||
return lyricsSearchResult;
|
||||
}
|
||||
|
||||
private LyricsSearchResult SearchEmbedded(SongInfo songInfo)
|
||||
private async Task<LyricsSearchResult> SearchEmbedded(SongInfo songInfo)
|
||||
{
|
||||
int bestScore = 0;
|
||||
string? bestFile = null;
|
||||
string? bestRaw = null;
|
||||
|
||||
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();
|
||||
|
||||
FileCacheEntity? bestFile = null;
|
||||
int maxScore = 0;
|
||||
|
||||
foreach (var item in allFiles)
|
||||
{
|
||||
if (Directory.Exists(folder.Path) && folder.IsEnabled)
|
||||
if (string.IsNullOrEmpty(item.EmbeddedLyrics)) continue;
|
||||
|
||||
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
|
||||
{
|
||||
foreach (var file in DirectoryHelper.GetAllFiles(folder.Path))
|
||||
{
|
||||
if (FileHelper.MusicExtensions.Contains(Path.GetExtension(file)))
|
||||
{
|
||||
var track = new Track(file);
|
||||
var raw = track.GetRawLyrics();
|
||||
Title = item.Title,
|
||||
Artists = item.Artists?.Split(ATL.Settings.DisplayValueSeparator),
|
||||
Album = item.Album,
|
||||
Duration = item.Duration
|
||||
});
|
||||
|
||||
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 = file,
|
||||
});
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestFile = file;
|
||||
bestRaw = raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (score > maxScore)
|
||||
{
|
||||
maxScore = score;
|
||||
bestFile = item;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestFile != null)
|
||||
if (bestFile != null && maxScore > 0)
|
||||
{
|
||||
var track = new Track(bestFile);
|
||||
lyricsSearchResult.Title = bestFile.Title;
|
||||
lyricsSearchResult.Artists = bestFile.Artists?.Split(ATL.Settings.DisplayValueSeparator);
|
||||
lyricsSearchResult.Album = bestFile.Album;
|
||||
lyricsSearchResult.Duration = bestFile.Duration;
|
||||
|
||||
lyricsSearchResult.Title = track.Title;
|
||||
lyricsSearchResult.Artists = track.Artist.Split(ATL.Settings.DisplayValueSeparator);
|
||||
lyricsSearchResult.Album = track.Album;
|
||||
lyricsSearchResult.Duration = track.Duration;
|
||||
|
||||
lyricsSearchResult.Raw = bestRaw;
|
||||
lyricsSearchResult.Reference = bestFile;
|
||||
lyricsSearchResult.MatchPercentage = bestScore;
|
||||
lyricsSearchResult.Raw = bestFile.EmbeddedLyrics;
|
||||
lyricsSearchResult.Reference = bestFile.Uri;
|
||||
lyricsSearchResult.MatchPercentage = maxScore;
|
||||
}
|
||||
|
||||
return lyricsSearchResult;
|
||||
@@ -560,13 +557,14 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
|
||||
}
|
||||
|
||||
ISearchResult? result;
|
||||
if (searcher == Searchers.Netease && songInfo.SongId != null)
|
||||
|
||||
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIDHelper.IsNeteaseFamily(songInfo.PlayerId))
|
||||
{
|
||||
result = new NeteaseSearchResult("", [], "", [], 0, songInfo.SongId);
|
||||
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
|
||||
}
|
||||
else if (searcher == Searchers.QQMusic && songInfo.SongId != null)
|
||||
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerID.QQMusic)
|
||||
{
|
||||
result = new QQMusicSearchResult("", [], "", [], 0, songInfo.SongId, "");
|
||||
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +11,6 @@ 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.LyricsSearchService;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.Services.TranslationService;
|
||||
@@ -27,6 +26,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;
|
||||
@@ -41,7 +41,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
|
||||
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,7 +53,6 @@ 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;
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
|
||||
[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; }
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo? CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
|
||||
|
||||
[ObservableProperty] public partial MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; set; }
|
||||
|
||||
@@ -71,7 +71,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
ISettingsService settingsService,
|
||||
IAlbumArtSearchService albumArtSearchService,
|
||||
ILyricsSearchService lyricsSearchService,
|
||||
ILibWatcherService libWatcherService,
|
||||
IDiscordService discordService,
|
||||
ITranslationService libreTranslateService,
|
||||
ITransliterationService transliterationService,
|
||||
@@ -80,7 +79,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
_settingsService = settingsService;
|
||||
_albumArtSearchService = albumArtSearchService;
|
||||
_lyrcsSearchService = lyricsSearchService;
|
||||
_libWatcherService = libWatcherService;
|
||||
_translationService = libreTranslateService;
|
||||
_transliterationService = transliterationService;
|
||||
_discordService = discordService;
|
||||
@@ -91,13 +89,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 +106,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 +133,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
}
|
||||
}
|
||||
|
||||
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
|
||||
{
|
||||
UpdateAlbumArt();
|
||||
UpdateLyrics();
|
||||
}
|
||||
|
||||
private MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
|
||||
{
|
||||
var desiredSession = GetCurrentSession();
|
||||
@@ -331,9 +314,9 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
|
||||
|
||||
CurrentSongInfo = new SongInfo
|
||||
{
|
||||
Title = mediaProperties?.Title ?? "",
|
||||
Artists = fixedArtist?.SplitByCommonSplitter() ?? [],
|
||||
Album = fixedAlbum ?? "",
|
||||
Title = mediaProperties?.Title ?? "N/A",
|
||||
Artists = fixedArtist?.SplitByCommonSplitter() ?? ["N/A"],
|
||||
Album = fixedAlbum ?? "N/A",
|
||||
DurationMs = mediaSession?.ControlSession?.GetTimelineProperties().EndTime.TotalMilliseconds ?? 0,
|
||||
PlayerId = sessionId,
|
||||
SongId = songId,
|
||||
@@ -693,6 +676,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 +717,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace BetterLyrics.WinUI3.Services.ResourceService
|
||||
{
|
||||
public interface IResourceService
|
||||
{
|
||||
string GetLocalizedString(string id);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
|
||||
public interface ISettingsService
|
||||
{
|
||||
AppSettings AppSettings { get; set; }
|
||||
// App behavior
|
||||
|
||||
bool ImportSettings(string importPath);
|
||||
void ExportSettings(string exportPath);
|
||||
|
||||
@@ -7,12 +7,15 @@ using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Serialization;
|
||||
using BetterLyrics.WinUI3.Services.LocalizationService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Windows.ApplicationModel.Resources;
|
||||
using Windows.Globalization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.SettingsService
|
||||
@@ -21,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();
|
||||
@@ -57,6 +62,7 @@ namespace BetterLyrics.WinUI3.Services.SettingsService
|
||||
AppSettings.Version = MetadataHelper.AppVersion;
|
||||
|
||||
EnsureMediaSourceProvidersInfo();
|
||||
EnsureStarredPlaylists();
|
||||
}
|
||||
|
||||
private void EnsureMediaSourceProvidersInfo()
|
||||
@@ -99,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();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.TranslationService
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Serialization;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.TransliterationService
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Serialization;
|
||||
using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Vanara.PInvoke;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.TransliterationService
|
||||
{
|
||||
|
||||
1536
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ar/Resources.resw
Normal file
1536
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/ar/Resources.resw
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user