mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 10:54:55 +08:00
Compare commits
275 Commits
v1.1.182.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 | ||
|
|
1d489c68e9 | ||
|
|
90e7fa42d0 | ||
|
|
29a6879e45 | ||
|
|
58499a2d09 | ||
|
|
580255699b | ||
|
|
9cac7818f1 | ||
|
|
118668a457 | ||
|
|
37621dbf2a | ||
|
|
aa7d56f1cb | ||
|
|
8dbe76e790 | ||
|
|
de6410492e | ||
|
|
26df7c7f67 | ||
|
|
3c411374bd | ||
|
|
99f0b9443b | ||
|
|
a3bc148816 | ||
|
|
cea757702b | ||
|
|
8938a5c798 | ||
|
|
46f4589b64 | ||
|
|
adb02658f4 | ||
|
|
3d7e6061e9 | ||
|
|
a51220c7b9 | ||
|
|
22b813e687 | ||
|
|
fda94d5020 | ||
|
|
205cbe8fb6 | ||
|
|
816f7064db | ||
|
|
132c5267b0 | ||
|
|
4e866818df | ||
|
|
9b7b56a0ee | ||
|
|
66f2da0e4c | ||
|
|
1735c6a7e6 | ||
|
|
8c06c98068 | ||
|
|
e2ac4c166c | ||
|
|
728397cafa | ||
|
|
059787a28f | ||
|
|
4c4231b48c | ||
|
|
2412927b29 | ||
|
|
f3bdbba83e | ||
|
|
4c811b12ca | ||
|
|
933103c57f | ||
|
|
718e7bdad3 |
291
.devin/wiki.json
Normal file
291
.devin/wiki.json
Normal file
@@ -0,0 +1,291 @@
|
||||
{
|
||||
"repo_notes": [
|
||||
{
|
||||
"content": "Always use the latest files in the repo to generate the wiki"
|
||||
}
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"title": "Overview",
|
||||
"purpose": "Introduce BetterLyrics, its purpose as a WinUI3 lyrics display application, key features, and supported music players",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Getting Started",
|
||||
"purpose": "Guide users through installation, initial setup, and basic usage of BetterLyrics",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Installation and Deployment",
|
||||
"purpose": "Explain how to install BetterLyrics from Microsoft Store or build from source, system requirements, and supported Windows versions",
|
||||
"parent": "Getting Started",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Initial Configuration",
|
||||
"purpose": "Walk through first-time setup including media player configuration, folder selection, and basic settings",
|
||||
"parent": "Getting Started",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Architecture",
|
||||
"purpose": "Provide technical overview of BetterLyrics' internal architecture, design patterns, and component organization",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Application Entry Point and Dependency Injection",
|
||||
"purpose": "Document the App.xaml.cs entry point, service registration, and dependency injection container configuration",
|
||||
"parent": "Architecture",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Service Layer Architecture",
|
||||
"purpose": "Explain the service-oriented architecture, service interfaces, and their implementations including MediaSessionsService, LyricsSearchService, and SettingsService",
|
||||
"parent": "Architecture",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Data Models",
|
||||
"purpose": "Document core data models including LyricsLine, LyricsData, SongInfo, and configuration models",
|
||||
"parent": "Architecture",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Multi-Window System",
|
||||
"purpose": "Explain how BetterLyrics manages multiple simultaneous lyrics windows, window lifecycle, and state management",
|
||||
"parent": "Architecture",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "User Interface",
|
||||
"purpose": "Document the UI components, windows, and user interaction patterns in BetterLyrics",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Now Playing Window",
|
||||
"purpose": "Detail the main lyrics display window, its components, and integration with the rendering system",
|
||||
"parent": "User Interface",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Display Modes and Window Configurations",
|
||||
"purpose": "Explain different display modes (Standard, Desktop, Docked, Fullscreen, Narrow, Taskbar) and how to configure them",
|
||||
"parent": "User Interface",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Settings and Customization",
|
||||
"purpose": "Document the settings interface, configuration options, and how to customize lyrics appearance and behavior",
|
||||
"parent": "User Interface",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Music Gallery",
|
||||
"purpose": "Explain the local music library management feature, including playback, playlist management, and integration with media controls",
|
||||
"parent": "User Interface",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "System Tray and Global Controls",
|
||||
"purpose": "Document the system tray integration, global hotkeys, and application-wide controls",
|
||||
"parent": "User Interface",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Lyrics System",
|
||||
"purpose": "Comprehensive documentation of the lyrics acquisition, processing, and display pipeline",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Lyrics Search and Providers",
|
||||
"purpose": "Document the lyrics search system, supported providers (QQ Music, Netease, Kugou, LrcLib, Apple Music, local files), and search strategies",
|
||||
"parent": "Lyrics System",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Parsing and Translation",
|
||||
"purpose": "Explain how lyrics are parsed from different formats (LRC, QRC, TTML), translation system using LibreTranslate, and metadata matching",
|
||||
"parent": "Lyrics System",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Rendering Pipeline",
|
||||
"purpose": "Document the Win2D-based rendering system, LyricsCanvas, PlayingLineRenderer, UnplayingLineRenderer, and LyricsLayoutManager",
|
||||
"parent": "Lyrics System",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Visual Effects and Animation",
|
||||
"purpose": "Explain character-level effects (glow, float, scale), background effects (fluid, snow, fog, spectrum), and the animation transition system",
|
||||
"parent": "Lyrics System",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Media Integration",
|
||||
"purpose": "Document how BetterLyrics integrates with music players and manages media sessions",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Windows Media Transport Controls Integration",
|
||||
"purpose": "Explain how BetterLyrics uses Windows SMTC to work universally with media players",
|
||||
"parent": "Media Integration",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Supported Players and Special Configurations",
|
||||
"purpose": "List supported media players, document special configurations (Apple Music token, LX Music SSE), and player-specific handling",
|
||||
"parent": "Media Integration",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Album Art and Theme Colors",
|
||||
"purpose": "Document album art retrieval from multiple sources, color palette generation (MedianCut, OctTree), and adaptive theming",
|
||||
"parent": "Media Integration",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Internationalization",
|
||||
"purpose": "Explain the localization system, supported languages, and how resources are managed",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Development",
|
||||
"purpose": "Technical documentation for developers contributing to or extending BetterLyrics",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Build Configuration and Deployment",
|
||||
"purpose": "Document the build process, publish profiles for different architectures, CI/CD pipeline, and packaging for Microsoft Store",
|
||||
"parent": "Development",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Helper Utilities and Extensions",
|
||||
"purpose": "Document utility classes including TaskbarHook, WindowHook, ColorHelper, ImageHelper, and various extension methods",
|
||||
"parent": "Development",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "External Dependencies and Libraries",
|
||||
"purpose": "List and explain third-party dependencies including Win2D, NAudio, ATL.NET, FlaUI, and their usage in the application",
|
||||
"parent": "Development",
|
||||
"page_notes": [
|
||||
{
|
||||
"content": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.182.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,15 +2,17 @@
|
||||
|
||||
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.TranslateService;
|
||||
using BetterLyrics.WinUI3.Services.TranslationService;
|
||||
using BetterLyrics.WinUI3.Services.TransliterationService;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
@@ -18,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;
|
||||
@@ -70,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)
|
||||
@@ -113,11 +120,12 @@ namespace BetterLyrics.WinUI3
|
||||
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
|
||||
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
|
||||
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
|
||||
.AddSingleton<ILibWatcherService, LibWatcherService>()
|
||||
.AddSingleton<ITranslateService, TranslateService>()
|
||||
.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,21 +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-kana" Version="1.0.2" />
|
||||
<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" />
|
||||
@@ -83,11 +91,11 @@
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
<PackageReference Include="NTextCat" Version="0.3.65" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="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" />
|
||||
@@ -98,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>
|
||||
@@ -161,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>
|
||||
@@ -335,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 -->
|
||||
@@ -71,6 +80,10 @@
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.AutoPlay, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageStopTrackOnGalleryWindowClosed" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.StopOnWindowClosed, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageExitOnGalleryWindowClosed" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.ExitOnWindowClosed, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
// FontFamilies = fontFamilies;
|
||||
// });
|
||||
//});
|
||||
FontFamilies = FontHelper.SystemFontFamilies.OrderBy(x => x).ToList();
|
||||
FontFamilies = FontHelper.GetSystemFontFamilies();
|
||||
}
|
||||
|
||||
private void AutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
|
||||
|
||||
@@ -47,6 +47,43 @@
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
|
||||
<dev:SettingsExpander
|
||||
x:Uid="SettingsPageAlbumArtLayer"
|
||||
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
Glyph=}"
|
||||
IsExpanded="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=TwoWay}" />
|
||||
<dev:SettingsExpander.Items>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageOpacity" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<uc:ExtendedSlider
|
||||
Default="100"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Unit="%"
|
||||
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayOpacity, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageSpeed" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<uc:ExtendedSlider
|
||||
Default="50"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Unit="%"
|
||||
Value="{x:Bind LyricsBackgroundSettings.CoverOverlaySpeed, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageBlurAmount" IsEnabled="{x:Bind LyricsBackgroundSettings.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<uc:ExtendedSlider
|
||||
Default="100"
|
||||
Maximum="200"
|
||||
Minimum="0"
|
||||
Value="{x:Bind LyricsBackgroundSettings.CoverOverlayBlurAmount, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
|
||||
<dev:SettingsExpander
|
||||
x:Uid="SettingsPageFluidLayer"
|
||||
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
|
||||
@@ -13,6 +13,7 @@ using BetterLyrics.WinUI3.Services.SettingsService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -20,7 +21,9 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.UI;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Controls
|
||||
@@ -34,7 +37,8 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
IRecipient<PropertyChangedMessage<bool>>,
|
||||
IRecipient<PropertyChangedMessage<TextAlignmentType>>,
|
||||
IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
|
||||
IRecipient<PropertyChangedMessage<string>>
|
||||
IRecipient<PropertyChangedMessage<string>>,
|
||||
IRecipient<PropertyChangedMessage<IRandomAccessStream?>>
|
||||
{
|
||||
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
|
||||
@@ -42,6 +46,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private readonly LyricsRenderer _lyricsRenderer = new();
|
||||
private readonly FluidBackgroundRenderer _fluidRenderer = new();
|
||||
private readonly CoverBackgroundRenderer _coverRenderer = new();
|
||||
private readonly PureColorBackgroundRenderer _pureColorRenderer = new();
|
||||
private readonly SnowRenderer _snowRenderer = new();
|
||||
private readonly FogRenderer _fogRenderer = new();
|
||||
@@ -368,8 +373,8 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
lyricsBg.IsPureColorOverlayEnabled
|
||||
);
|
||||
|
||||
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity / 100.0;
|
||||
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
|
||||
_coverRenderer.Draw(sender, args.DrawingSession);
|
||||
|
||||
_fluidRenderer.Draw(sender, args.DrawingSession);
|
||||
|
||||
_snowRenderer.Draw(sender, args.DrawingSession);
|
||||
@@ -540,25 +545,31 @@ 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;
|
||||
|
||||
if (_fluidRenderer.IsEnabled)
|
||||
{
|
||||
_fluidRenderer.UpdateColors(
|
||||
_accentColor1Transition.Value,
|
||||
_accentColor2Transition.Value,
|
||||
_accentColor3Transition.Value,
|
||||
_accentColor4Transition.Value
|
||||
);
|
||||
_fluidRenderer.Update(elapsedTime);
|
||||
}
|
||||
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
|
||||
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity / 100.0;
|
||||
_fluidRenderer.UpdateColors(
|
||||
_accentColor1Transition.Value,
|
||||
_accentColor2Transition.Value,
|
||||
_accentColor3Transition.Value,
|
||||
_accentColor4Transition.Value
|
||||
);
|
||||
_fluidRenderer.Update(elapsedTime);
|
||||
|
||||
_coverRenderer.IsEnabled = lyricsBg.IsCoverOverlayEnabled;
|
||||
_coverRenderer.Opacity = lyricsBg.CoverOverlayOpacity;
|
||||
_coverRenderer.BlurAmount = lyricsBg.CoverOverlayBlurAmount;
|
||||
_coverRenderer.Speed = lyricsBg.CoverOverlaySpeed;
|
||||
_coverRenderer.Update(elapsedTime);
|
||||
|
||||
_snowRenderer.IsEnabled = lyricsBg.IsSnowFlakeOverlayEnabled;
|
||||
_snowRenderer.Amount = lyricsBg.SnowFlakeOverlayAmount / 100f;
|
||||
@@ -586,6 +597,7 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
private void Canvas_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_fluidRenderer.Dispose();
|
||||
_coverRenderer.Dispose();
|
||||
_snowRenderer.Dispose();
|
||||
_fogRenderer.Dispose();
|
||||
_spectrumRenderer.Dispose();
|
||||
@@ -600,7 +612,13 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private async void Canvas_CreateResources(CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
|
||||
{
|
||||
args.TrackAsyncAction(_fluidRenderer.LoadResourcesAsync().AsAsyncAction());
|
||||
var tasks = new Task[]
|
||||
{
|
||||
_fluidRenderer.LoadResourcesAsync(),
|
||||
ReloadCoverBackgroundResourcesAsync()
|
||||
};
|
||||
args.TrackAsyncAction(Task.WhenAll(tasks).AsAsyncAction());
|
||||
|
||||
_snowRenderer.LoadResources();
|
||||
_fogRenderer.LoadResources();
|
||||
|
||||
@@ -682,6 +700,16 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task ReloadCoverBackgroundResourcesAsync()
|
||||
{
|
||||
if (_mediaSessionsService.AlbumArtBitmapStream is IRandomAccessStream stream)
|
||||
{
|
||||
stream.Seek(0);
|
||||
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
|
||||
_coverRenderer.SetCoverBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<TimeSpan> message)
|
||||
{
|
||||
if (message.Sender is IMediaSessionsService)
|
||||
@@ -827,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)
|
||||
{
|
||||
@@ -874,5 +910,15 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
|
||||
{
|
||||
if (message.Sender is IMediaSessionsService)
|
||||
{
|
||||
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
|
||||
{
|
||||
_ = ReloadCoverBackgroundResourcesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
<dev:SettingsCard x:Uid="SettingsPageLyricsCenterTopOffset" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<local:ExtendedSlider
|
||||
Default="50"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Maximum="99"
|
||||
Minimum="1"
|
||||
Unit="%"
|
||||
Value="{x:Bind LyricsStyleSettings.PlayingLineTopOffset, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:uc="using:BetterLyrics.WinUI3.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
Loaded="UserControl_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
@@ -99,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
|
||||
@@ -116,7 +114,6 @@
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
@@ -124,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>
|
||||
@@ -197,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>
|
||||
|
||||
@@ -26,14 +26,14 @@ namespace BetterLyrics.WinUI3.Controls
|
||||
|
||||
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
|
||||
|
||||
public LyricsWindowStatus LyricsWindowStatus
|
||||
public LyricsWindowStatus? LyricsWindowStatus
|
||||
{
|
||||
get { return (LyricsWindowStatus)GetValue(LyricsWindowStatusProperty); }
|
||||
get { return (LyricsWindowStatus?)GetValue(LyricsWindowStatusProperty); }
|
||||
set { SetValue(LyricsWindowStatusProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty LyricsWindowStatusProperty =
|
||||
DependencyProperty.Register(nameof(LyricsWindowStatus), typeof(LyricsWindowStatus), typeof(LyricsWindowSettingsControl), new PropertyMetadata(default));
|
||||
DependencyProperty.Register(nameof(LyricsWindowStatus), typeof(LyricsWindowStatus), typeof(LyricsWindowSettingsControl), new PropertyMetadata(null));
|
||||
|
||||
public LyricsWindowSettingsControl()
|
||||
{
|
||||
@@ -166,52 +166,14 @@ 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 ConfigSelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args)
|
||||
{
|
||||
if (sender is SelectorBar bar)
|
||||
{
|
||||
if (bar.SelectedItem is SelectorBarItem item)
|
||||
{
|
||||
ViewModel?.SelectorBarSelectedItemTag = item.Tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.CloseConfigPanelCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<Grid
|
||||
x:Name="BottomCommandGrid"
|
||||
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
|
||||
Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
|
||||
Opacity="{x:Bind ViewModel.BottomCommandGridOpacity, Mode=OneWay}"
|
||||
PointerEntered="BottomCommandGrid_PointerEntered"
|
||||
PointerExited="BottomCommandGrid_PointerExited">
|
||||
@@ -342,7 +342,7 @@
|
||||
|
||||
<Slider
|
||||
x:Name="TimelineSlider"
|
||||
Margin="0,-12,0,0"
|
||||
Margin="0,-14,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Maximum="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
|
||||
@@ -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)
|
||||
@@ -290,10 +290,7 @@ public sealed partial class NowPlayingBar : UserControl,
|
||||
{
|
||||
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
TimelineSlider.Value = message.NewValue.TotalSeconds;
|
||||
});
|
||||
TimelineSlider.Value = message.NewValue.TotalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
@@ -397,16 +392,23 @@
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
<dev:SettingsCard x:Uid="SettingsPageLibreTranslateServer" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Grid ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
x:Uid="LibreTranslateServerTextBox"
|
||||
Grid.Column="0"
|
||||
IsEnabled="{x:Bind ViewModel.IsLibreTranslateServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.AppSettings.TranslationSettings.LibreTranslateServer, Mode=TwoWay}" />
|
||||
Text="{x:Bind ViewModel.AppSettings.TranslationSettings.LibreTranslateServer, Mode=TwoWay}"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Uid="SettingsPageServerTestButton"
|
||||
Grid.Column="1"
|
||||
Command="{x:Bind ViewModel.LibreTranslateServerTestCommand}"
|
||||
IsEnabled="{x:Bind ViewModel.IsLibreTranslateServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</dev:SettingsCard>
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
@@ -424,9 +426,33 @@
|
||||
</dev:SettingsCard>
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
<dev:SettingsCard x:Uid="SettingsPageJapanese">
|
||||
<dev:SettingsExpander x:Uid="SettingsPageJapanese" IsExpanded="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
<dev:SettingsExpander.Items>
|
||||
<dev:SettingsCard x:Uid="SettingsPageCutletDockerServer" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
|
||||
<dev:SettingsCard.Description>
|
||||
<HyperlinkButton Content="https://github.com/jayfunc/cutlet-docker" NavigateUri="https://github.com/jayfunc/cutlet-docker" />
|
||||
</dev:SettingsCard.Description>
|
||||
<Grid ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
x:Uid="CutletServerTextBox"
|
||||
Grid.Column="0"
|
||||
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.AppSettings.TranslationSettings.CutletDockerServer, Mode=TwoWay}"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Uid="SettingsPageServerTestButton"
|
||||
Grid.Column="1"
|
||||
Command="{x:Bind ViewModel.CutletDockerServerTestCommand}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</dev:SettingsCard>
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
|
||||
<!-- 中文简体繁体偏好 -->
|
||||
<TextBlock x:Uid="SettingsPageChineseLyrics" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
@@ -435,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()
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
x:Name="TrayIcon"
|
||||
x:FieldModifier="public"
|
||||
ContextMenuMode="SecondWindow"
|
||||
DoubleClickCommand="{x:Bind ViewModel.OpenLyricsWindowSwitchCommand}"
|
||||
IconSource="ms-appx:///Assets/Logo.ico"
|
||||
LeftClickCommand="{x:Bind ViewModel.OpenLyricsWindowSwitchCommand}"
|
||||
NoLeftClickDelay="True"
|
||||
|
||||
@@ -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
|
||||
@@ -102,6 +96,10 @@
|
||||
</dev:SettingsExpander.Items>
|
||||
</dev:SettingsExpander>
|
||||
|
||||
<dev:SettingsCard x:Uid="SettingsPageAlwaysHideUnlockButton" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind LyricsWindowStatus.IsAlwaysHideUnlockButton, Mode=TwoWay}" />
|
||||
</dev:SettingsCard>
|
||||
|
||||
<dev:SettingsExpander
|
||||
x:Uid="SettingsPageAOT"
|
||||
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
|
||||
|
||||
@@ -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,11 +22,12 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
LocalLrcFile,
|
||||
LocalEslrcFile,
|
||||
LocalTtmlFile,
|
||||
BetterLyrics
|
||||
BetterLyrics,
|
||||
CutletDocker
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Markup;
|
||||
@@ -8,8 +8,6 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public static class FontHelper
|
||||
{
|
||||
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies().Order().ToArray();
|
||||
|
||||
public static string GetLocalizedFontFamilyName(string sourceName, string langCode)
|
||||
{
|
||||
if (langCode == "")
|
||||
@@ -33,5 +31,20 @@ namespace BetterLyrics.WinUI3.Helper
|
||||
|
||||
return sourceName;
|
||||
}
|
||||
|
||||
public static List<string> GetSystemFontFamilies()
|
||||
{
|
||||
List<string> fontFamilies = new();
|
||||
|
||||
foreach (var font in Fonts.SystemFontFamilies)
|
||||
{
|
||||
if (font.FamilyNames.TryGetValue(XmlLanguage.GetLanguage("en-us"), out string englishFamilyName))
|
||||
{
|
||||
fontFamilies.Add(englishFamilyName);
|
||||
}
|
||||
}
|
||||
|
||||
return fontFamilies.Order().ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 +22,16 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToRomaji(string text)
|
||||
{
|
||||
return Kana.Kana.KanaToRomaji(text, Kana.Error.Ignore).ToStr();
|
||||
}
|
||||
|
||||
public static string ToPinyin(string text, Pinyin.ManTone.Style style = Pinyin.ManTone.Style.TONE)
|
||||
{
|
||||
return Pinyin.Pinyin.Instance.HanziToPinyin(text, style).ToStr();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -224,10 +224,7 @@ namespace BetterLyrics.WinUI3.Hooks
|
||||
|
||||
private static void WindowHelper_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
if (_activeWindows.Contains(sender))
|
||||
{
|
||||
_activeWindows.Remove(sender);
|
||||
}
|
||||
_activeWindows.Remove(sender);
|
||||
}
|
||||
|
||||
public static void SetIsWorkArea(this NowPlayingWindow window, bool enable)
|
||||
@@ -282,23 +279,27 @@ namespace BetterLyrics.WinUI3.Hooks
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetIsFullscreen(this Window window, bool enable)
|
||||
public static bool SetIsFullscreen(this Window window, bool enable, bool defaultExtendsContentIntoTitleBar = true)
|
||||
{
|
||||
if (window.AppWindow == null) return;
|
||||
if (window.AppWindow == null) return false;
|
||||
|
||||
if (enable)
|
||||
{
|
||||
window.ExtendsContentIntoTitleBar = false;
|
||||
window.AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.ExtendsContentIntoTitleBar = defaultExtendsContentIntoTitleBar;
|
||||
window.AppWindow.SetPresenter(AppWindowPresenterKind.Overlapped);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetIsMaximized(this Window window, bool enable)
|
||||
public static bool SetIsMaximized(this Window window, bool enable)
|
||||
{
|
||||
if (window.AppWindow == null) return;
|
||||
if (window.AppWindow == null) return false;
|
||||
|
||||
if (enable)
|
||||
{
|
||||
@@ -308,6 +309,8 @@ namespace BetterLyrics.WinUI3.Hooks
|
||||
{
|
||||
window.Restore();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SetIsShowInSwitchers(this Window window, bool enable)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class CutletDockerRequest
|
||||
{
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class CutletDockerResponse
|
||||
{
|
||||
[JsonPropertyName("romaji")]
|
||||
public string RomajiText { get; set; }
|
||||
}
|
||||
}
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models
|
||||
{
|
||||
public class TranslateResponse
|
||||
public class LibreTranslateResponse
|
||||
{
|
||||
[JsonPropertyName("translatedText")]
|
||||
public string TranslatedText { 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,100 +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 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysOnTopPolling { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsShownInSwitchers { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLocked { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysHideUnlockButton { get; set; } = false;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPinToTaskbar { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TaskbarPlacement TaskbarPlacement { get; set; } = TaskbarPlacement.Right;
|
||||
@@ -52,15 +53,74 @@ namespace BetterLyrics.WinUI3.Models
|
||||
|
||||
public LyricsWindowStatus()
|
||||
{
|
||||
|
||||
LyricsStyleSettings.PropertyChanged += LyricsStyleSettings_PropertyChanged;
|
||||
LyricsEffectSettings.PropertyChanged += LyricsEffectSettings_PropertyChanged;
|
||||
LyricsBackgroundSettings.PropertyChanged += LyricsBackgroundSettings_PropertyChanged;
|
||||
AlbumArtLayoutSettings.PropertyChanged += AlbumArtLayoutSettings_PropertyChanged;
|
||||
AlbumArtAreaEffectSettings.PropertyChanged += AlbumArtAreaEffectSettings_PropertyChanged;
|
||||
}
|
||||
|
||||
public LyricsWindowStatus(Window? targetWindow = null)
|
||||
public LyricsWindowStatus(Window? targetWindow = null) : this()
|
||||
{
|
||||
UpdateMonitorNameAndBounds(targetWindow);
|
||||
UpdateDemoWindowAndMonitorBounds();
|
||||
}
|
||||
|
||||
partial void OnLyricsStyleSettingsChanged(LyricsStyleSettings oldValue, LyricsStyleSettings newValue)
|
||||
{
|
||||
oldValue.PropertyChanged -= LyricsStyleSettings_PropertyChanged;
|
||||
newValue.PropertyChanged += LyricsStyleSettings_PropertyChanged;
|
||||
}
|
||||
|
||||
private void LyricsStyleSettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(LyricsStyleSettings));
|
||||
}
|
||||
|
||||
partial void OnLyricsEffectSettingsChanged(LyricsEffectSettings oldValue, LyricsEffectSettings newValue)
|
||||
{
|
||||
oldValue.PropertyChanged -= LyricsEffectSettings_PropertyChanged;
|
||||
newValue.PropertyChanged += LyricsEffectSettings_PropertyChanged;
|
||||
}
|
||||
|
||||
private void LyricsEffectSettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(LyricsEffectSettings));
|
||||
}
|
||||
|
||||
partial void OnLyricsBackgroundSettingsChanged(LyricsBackgroundSettings oldValue, LyricsBackgroundSettings newValue)
|
||||
{
|
||||
oldValue.PropertyChanged -= LyricsBackgroundSettings_PropertyChanged;
|
||||
newValue.PropertyChanged += LyricsBackgroundSettings_PropertyChanged;
|
||||
}
|
||||
|
||||
private void LyricsBackgroundSettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(LyricsBackgroundSettings));
|
||||
}
|
||||
|
||||
partial void OnAlbumArtLayoutSettingsChanged(AlbumArtAreaStyleSettings oldValue, AlbumArtAreaStyleSettings newValue)
|
||||
{
|
||||
oldValue.PropertyChanged -= AlbumArtLayoutSettings_PropertyChanged;
|
||||
newValue.PropertyChanged += AlbumArtLayoutSettings_PropertyChanged;
|
||||
}
|
||||
|
||||
private void AlbumArtLayoutSettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(AlbumArtLayoutSettings));
|
||||
}
|
||||
|
||||
partial void OnAlbumArtAreaEffectSettingsChanged(AlbumArtAreaEffectSettings oldValue, AlbumArtAreaEffectSettings newValue)
|
||||
{
|
||||
oldValue.PropertyChanged -= AlbumArtAreaEffectSettings_PropertyChanged;
|
||||
newValue.PropertyChanged += AlbumArtAreaEffectSettings_PropertyChanged;
|
||||
}
|
||||
|
||||
private void AlbumArtAreaEffectSettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(AlbumArtAreaEffectSettings));
|
||||
}
|
||||
|
||||
partial void OnWindowBoundsChanged(Rect value)
|
||||
{
|
||||
UpdateMonitorNameAndBounds();
|
||||
@@ -141,6 +201,7 @@ namespace BetterLyrics.WinUI3.Models
|
||||
IsAlwaysOnTopPolling = this.IsAlwaysOnTopPolling,
|
||||
IsShownInSwitchers = this.IsShownInSwitchers,
|
||||
IsLocked = this.IsLocked,
|
||||
IsAlwaysHideUnlockButton = this.IsAlwaysHideUnlockButton,
|
||||
|
||||
IsPinToTaskbar = this.IsPinToTaskbar,
|
||||
TaskbarPlacement = this.TaskbarPlacement,
|
||||
|
||||
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; } = [];
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PureColorOverlayOpacity { get; set; } = 100; // 100 % = 1.0
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsCoverOverlayEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayOpacity { get; set; } = 100; // 100 % = 1.0
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlaySpeed { get; set; } = 50;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayBlurAmount { get; set; } = 100;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = true;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FluidOverlayOpacity { get; set; } = 100;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.MedianCut;
|
||||
@@ -39,14 +44,25 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
IsPureColorOverlayEnabled = this.IsPureColorOverlayEnabled,
|
||||
PureColorOverlayOpacity = this.PureColorOverlayOpacity,
|
||||
|
||||
IsCoverOverlayEnabled = this.IsCoverOverlayEnabled,
|
||||
CoverOverlayOpacity = this.CoverOverlayOpacity,
|
||||
CoverOverlaySpeed = this.CoverOverlaySpeed,
|
||||
CoverOverlayBlurAmount = this.CoverOverlayBlurAmount,
|
||||
|
||||
IsFluidOverlayEnabled = this.IsFluidOverlayEnabled,
|
||||
FluidOverlayOpacity = this.FluidOverlayOpacity,
|
||||
PaletteGeneratorType = this.PaletteGeneratorType,
|
||||
|
||||
IsSpectrumOverlayEnabled = this.IsSpectrumOverlayEnabled,
|
||||
SpectrumPlacement = this.SpectrumPlacement,
|
||||
SpectrumStyle = this.SpectrumStyle,
|
||||
SpectrumCount = this.SpectrumCount,
|
||||
|
||||
IsSnowFlakeOverlayEnabled = this.IsSnowFlakeOverlayEnabled,
|
||||
SnowFlakeOverlayAmount = this.SnowFlakeOverlayAmount,
|
||||
SnowFlakeOverlaySpeed = this.SnowFlakeOverlaySpeed,
|
||||
|
||||
IsFogOverlayEnabled = this.IsFogOverlayEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +54,14 @@ 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,
|
||||
IsLyricsGlowEffectAmountAutoAdjust = this.IsLyricsGlowEffectAmountAutoAdjust,
|
||||
LyricsGlowEffectAmount = this.LyricsGlowEffectAmount,
|
||||
LyricsGlowEffectScope = this.LyricsGlowEffectScope,
|
||||
|
||||
IsLyricsScaleEffectEnabled = this.IsLyricsScaleEffectEnabled,
|
||||
LyricsScaleEffectLongSyllableDuration = this.LyricsScaleEffectLongSyllableDuration,
|
||||
|
||||
@@ -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
|
||||
@@ -34,8 +32,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial double LyricsLineSpacingFactor { get; set; } = 0.5;
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsCJKFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsWesternFontFamily { get; set; } = FontHelper.SystemFontFamilies.FirstOrDefault() ?? "";
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsCJKFontFamily { get; set; } = "Arial";
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string LyricsWesternFontFamily { get; set; } = "Arial";
|
||||
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PlayingLineTopOffset { get; set; } = 50; // 50 %
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool AutoPlay { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsWindowStatus LyricsWindowStatus { get; set; } = new();
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnWindowClosed { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool StopOnWindowClosed { get; set; } = false;
|
||||
|
||||
public MusicGallerySettings() { }
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial ChineseRomanization ChineseRomanization { get; set; }
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsChineseRomanizationEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsJapaneseRomanizationEnabled { get; set; } = false;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string CutletDockerServer { get; set; } = string.Empty;
|
||||
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTraditionalChineseEnabled { get; set; } = false;
|
||||
|
||||
public TranslationSettings() { }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ using BetterLyrics.WinUI3.Extensions;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Models.Settings;
|
||||
using BetterLyrics.WinUI3.Services.TranslateService;
|
||||
using BetterLyrics.WinUI3.Services.TranslationService;
|
||||
using BetterLyrics.WinUI3.Services.TransliterationService;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Lyricify.Lyrics.Helpers.General;
|
||||
using Lyricify.Lyrics.Parsers;
|
||||
@@ -70,7 +71,13 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
return _lyricsDataArr;
|
||||
}
|
||||
|
||||
public async Task<(LyricsData, TransliterationSearchProvider?, TranslationSearchProvider?)> Parse(ITranslateService translateService, TranslationSettings settings, LyricsSearchResult? lyricsSearchResult, CancellationToken token)
|
||||
public async Task<(LyricsData, TransliterationSearchProvider?, TranslationSearchProvider?)> Parse(
|
||||
ITranslationService translationService,
|
||||
ITransliterationService transliterationService,
|
||||
TranslationSettings settings,
|
||||
LyricsSearchResult? lyricsSearchResult,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
TransliterationSearchProvider? transliterationSearchProvider = null;
|
||||
TranslationSearchProvider? translationSearchProvider = null;
|
||||
@@ -81,6 +88,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
|
||||
// 应用音译
|
||||
LyricsData? phoneticLyricsData = null;
|
||||
// 已解析歌词内寻找
|
||||
if (settings.IsChineseRomanizationEnabled && main.LanguageCode == LanguageHelper.ChineseCode)
|
||||
{
|
||||
phoneticLyricsData = settings.ChineseRomanization switch
|
||||
@@ -89,21 +97,41 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
ChineseRomanization.Jyutping => _lyricsDataArr.FirstOrDefault(x => x.LanguageCode == PhoneticHelper.JyutpingCode),
|
||||
_ => null,
|
||||
};
|
||||
if (phoneticLyricsData != null)
|
||||
{
|
||||
main.SetPhoneticText(phoneticLyricsData);
|
||||
if (phoneticLyricsData.AutoGenerated)
|
||||
{
|
||||
transliterationSearchProvider = TransliterationSearchProvider.BetterLyrics;
|
||||
}
|
||||
else
|
||||
{
|
||||
transliterationSearchProvider = lyricsSearchResult?.Provider.ToTransliterationSearchProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (settings.IsJapaneseRomanizationEnabled && main.LanguageCode == LanguageHelper.JapaneseCode)
|
||||
{
|
||||
phoneticLyricsData = _lyricsDataArr.FirstOrDefault(x => x.LanguageCode == PhoneticHelper.RomanCode);
|
||||
}
|
||||
if (phoneticLyricsData != null)
|
||||
{
|
||||
main.SetPhoneticText(phoneticLyricsData);
|
||||
if (phoneticLyricsData.AutoGenerated)
|
||||
if (phoneticLyricsData != null)
|
||||
{
|
||||
transliterationSearchProvider = TransliterationSearchProvider.BetterLyrics;
|
||||
main.SetPhoneticText(phoneticLyricsData);
|
||||
transliterationSearchProvider = lyricsSearchResult?.Provider.ToTransliterationSearchProvider();
|
||||
}
|
||||
else
|
||||
{
|
||||
transliterationSearchProvider = lyricsSearchResult?.Provider.ToTransliterationSearchProvider();
|
||||
string romaji = string.Empty;
|
||||
try
|
||||
{
|
||||
romaji = await transliterationService.TransliterateText(main.WrappedOriginalText, PhoneticHelper.RomanCode, token);
|
||||
_lyricsDataArr.FirstOrDefault()?.SetTransliteration(romaji);
|
||||
transliterationSearchProvider = TransliterationSearchProvider.CutletDocker;
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception)
|
||||
{
|
||||
ToastHelper.ShowToast("CutletDockerFailed", null, InfoBarSeverity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +149,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
string translated = string.Empty;
|
||||
try
|
||||
{
|
||||
translated = await translateService.TranslateTextAsync(main.WrappedOriginalText, settings.SelectedTargetLanguageCode, token);
|
||||
translated = await translationService.TranslateTextAsync(main.WrappedOriginalText, settings.SelectedTargetLanguageCode, token);
|
||||
_lyricsDataArr.FirstOrDefault()?.SetTranslation(translated);
|
||||
translationSearchProvider = TranslationSearchProvider.LibreTranslate;
|
||||
}
|
||||
@@ -239,30 +267,6 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (languageCode == LanguageHelper.JapaneseCode)
|
||||
{
|
||||
if (!_lyricsDataArr.Any(x => x.LanguageCode == PhoneticHelper.RomanCode))
|
||||
{
|
||||
_lyricsDataArr.Add(new LyricsData
|
||||
{
|
||||
LanguageCode = PhoneticHelper.RomanCode,
|
||||
AutoGenerated = true,
|
||||
LyricsLines = main.LyricsLines.Select(line => new LyricsLine
|
||||
{
|
||||
StartMs = line.StartMs,
|
||||
EndMs = line.EndMs,
|
||||
OriginalText = PhoneticHelper.ToRomaji(line.OriginalText),
|
||||
LyricsSyllables = line.LyricsSyllables.Select(c => new LyricsSyllable
|
||||
{
|
||||
StartMs = c.StartMs,
|
||||
EndMs = c.EndMs,
|
||||
Text = PhoneticHelper.ToRomaji(c.Text),
|
||||
StartIndex = c.StartIndex
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Renderer
|
||||
{
|
||||
@@ -13,17 +14,47 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
private CanvasBitmap? _currentBitmap;
|
||||
private CanvasBitmap? _previousBitmap;
|
||||
|
||||
private readonly ValueTransition<double> _crossfadeTransition;
|
||||
private CanvasRenderTarget? _currentTargetCache;
|
||||
private CanvasRenderTarget? _previousTargetCache;
|
||||
|
||||
private Size _lastScreenSize;
|
||||
private bool _lastWasRotating = false;
|
||||
|
||||
private readonly ValueTransition<double> _crossfadeTransition;
|
||||
private float _rotationAngle = 0f;
|
||||
|
||||
public bool IsEnabled { get; set; } = false;
|
||||
|
||||
public int Opacity { get; set; } = 100;
|
||||
|
||||
public int BlurAmount { get; set; } = 100;
|
||||
private bool _needsCacheUpdate = false;
|
||||
|
||||
public int Speed { get; set; } = 100;
|
||||
private int _blurAmount = 100;
|
||||
public int BlurAmount
|
||||
{
|
||||
get => _blurAmount;
|
||||
set
|
||||
{
|
||||
if (_blurAmount != value)
|
||||
{
|
||||
_blurAmount = value;
|
||||
_needsCacheUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _speed = 100;
|
||||
public int Speed
|
||||
{
|
||||
get => _speed;
|
||||
set
|
||||
{
|
||||
if (_speed != value)
|
||||
{
|
||||
_speed = value;
|
||||
_needsCacheUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CoverBackgroundRenderer()
|
||||
{
|
||||
@@ -34,26 +65,30 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
{
|
||||
if (_currentBitmap == newBitmap) return;
|
||||
|
||||
if (_currentBitmap == null)
|
||||
{
|
||||
_currentBitmap = newBitmap;
|
||||
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
|
||||
return;
|
||||
}
|
||||
|
||||
_previousBitmap = _currentBitmap;
|
||||
_previousTargetCache = _currentTargetCache;
|
||||
_currentTargetCache = null;
|
||||
|
||||
_currentBitmap = newBitmap;
|
||||
|
||||
if (newBitmap != null)
|
||||
if (_currentBitmap == null)
|
||||
{
|
||||
_crossfadeTransition.Reset(0.0);
|
||||
_crossfadeTransition.StartTransition(1.0);
|
||||
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_previousBitmap = null;
|
||||
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
|
||||
if (_previousBitmap == null)
|
||||
{
|
||||
_crossfadeTransition.StartTransition(1.0, jumpTo: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_crossfadeTransition.Reset(0.0);
|
||||
_crossfadeTransition.StartTransition(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
_needsCacheUpdate = true;
|
||||
}
|
||||
|
||||
public void Update(TimeSpan deltaTime)
|
||||
@@ -64,17 +99,17 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
|
||||
if (Speed > 0)
|
||||
{
|
||||
float baseSpeed = 0.6f; // 弧度/秒
|
||||
float baseSpeed = 0.6f;
|
||||
float currentSpeed = (Speed / 100.0f) * baseSpeed;
|
||||
|
||||
_rotationAngle += currentSpeed * (float)deltaTime.TotalSeconds;
|
||||
|
||||
_rotationAngle %= (float)(2 * Math.PI);
|
||||
}
|
||||
|
||||
if (_crossfadeTransition.Value >= 1.0 && _previousBitmap != null)
|
||||
{
|
||||
_previousBitmap = null;
|
||||
_previousTargetCache?.Dispose();
|
||||
_previousTargetCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,84 +117,133 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
{
|
||||
if (!IsEnabled || Opacity <= 0) return;
|
||||
|
||||
if (_lastScreenSize != control.Size)
|
||||
{
|
||||
_lastScreenSize = control.Size;
|
||||
_needsCacheUpdate = true;
|
||||
}
|
||||
|
||||
bool isRotating = Speed > 0;
|
||||
if (_lastWasRotating != isRotating)
|
||||
{
|
||||
_lastWasRotating = isRotating;
|
||||
_needsCacheUpdate = true;
|
||||
}
|
||||
|
||||
EnsureCachedLayer(control, _currentBitmap, ref _currentTargetCache);
|
||||
|
||||
float baseAlpha = Opacity / 100.0f;
|
||||
float currentBlur = BlurAmount;
|
||||
|
||||
float angle = Speed > 0 ? _rotationAngle : 0f;
|
||||
|
||||
float angle = isRotating ? _rotationAngle : 0f;
|
||||
double fadeProgress = _crossfadeTransition.Value;
|
||||
bool isCrossfading = fadeProgress < 1.0 && _previousBitmap != null;
|
||||
bool isCrossfading = fadeProgress < 1.0 && _previousTargetCache != null;
|
||||
|
||||
Vector2 screenCenter = new Vector2((float)control.Size.Width / 2f, (float)control.Size.Height / 2f);
|
||||
|
||||
if (isCrossfading)
|
||||
{
|
||||
DrawLayer(ds, control.Size, _previousBitmap, angle, currentBlur, baseAlpha);
|
||||
DrawCachedLayer(ds, _previousTargetCache, screenCenter, angle, baseAlpha);
|
||||
|
||||
float newLayerAlpha = baseAlpha * (float)fadeProgress;
|
||||
if (newLayerAlpha > 0.005f)
|
||||
{
|
||||
DrawLayer(ds, control.Size, _currentBitmap, angle, currentBlur, newLayerAlpha);
|
||||
}
|
||||
DrawCachedLayer(ds, _currentTargetCache, screenCenter, angle, newLayerAlpha);
|
||||
}
|
||||
else if (_currentBitmap != null)
|
||||
else if (_currentTargetCache != null)
|
||||
{
|
||||
DrawLayer(ds, control.Size, _currentBitmap, angle, currentBlur, baseAlpha);
|
||||
DrawCachedLayer(ds, _currentTargetCache, screenCenter, angle, baseAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLayer(CanvasDrawingSession ds, Windows.Foundation.Size screenSize, CanvasBitmap? bitmap, float rotationRadians, float blurAmount, float alpha)
|
||||
private void EnsureCachedLayer(ICanvasResourceCreator resourceCreator, CanvasBitmap? sourceBitmap, ref CanvasRenderTarget? targetCache)
|
||||
{
|
||||
if (bitmap == null) return;
|
||||
|
||||
float imgW = bitmap.SizeInPixels.Width;
|
||||
float imgH = bitmap.SizeInPixels.Height;
|
||||
Vector2 screenCenter = new Vector2((float)screenSize.Width / 2f, (float)screenSize.Height / 2f);
|
||||
|
||||
float scale;
|
||||
if (Speed > 0 && Math.Abs(rotationRadians) > 0.001f)
|
||||
if (sourceBitmap == null)
|
||||
{
|
||||
float screenDiagonal = (float)Math.Sqrt(screenSize.Width * screenSize.Width + screenSize.Height * screenSize.Height);
|
||||
|
||||
float scaleX = screenDiagonal / imgW;
|
||||
float scaleY = screenDiagonal / imgH;
|
||||
scale = Math.Max(scaleX, scaleY);
|
||||
}
|
||||
else
|
||||
{
|
||||
float scaleX = (float)screenSize.Width / imgW;
|
||||
float scaleY = (float)screenSize.Height / imgH;
|
||||
scale = Math.Max(scaleX, scaleY);
|
||||
targetCache?.Dispose();
|
||||
targetCache = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 缩放图片 -> 将图片中心移动到 (0,0) 以便旋转 -> 旋转 ->将图片移回屏幕中心
|
||||
Vector2 imgCenterOffset = new Vector2(
|
||||
((float)screenSize.Width - imgW * scale) / 2.0f,
|
||||
((float)screenSize.Height - imgH * scale) / 2.0f
|
||||
);
|
||||
bool deviceMismatch = targetCache != null && targetCache.Device != resourceCreator.Device;
|
||||
|
||||
if (_needsCacheUpdate || targetCache == null || deviceMismatch)
|
||||
{
|
||||
targetCache?.Dispose();
|
||||
|
||||
float imgW = sourceBitmap.SizeInPixels.Width;
|
||||
float imgH = sourceBitmap.SizeInPixels.Height;
|
||||
Size screenSize = _lastScreenSize;
|
||||
|
||||
float scale;
|
||||
if (_lastWasRotating) // Speed > 0
|
||||
{
|
||||
float screenDiagonal = (float)Math.Sqrt(screenSize.Width * screenSize.Width + screenSize.Height * screenSize.Height);
|
||||
scale = Math.Max(screenDiagonal / imgW, screenDiagonal / imgH);
|
||||
}
|
||||
else
|
||||
{
|
||||
float scaleX = (float)screenSize.Width / imgW;
|
||||
float scaleY = (float)screenSize.Height / imgH;
|
||||
scale = Math.Max(scaleX, scaleY);
|
||||
}
|
||||
|
||||
float targetW = imgW * scale;
|
||||
float targetH = imgH * scale;
|
||||
|
||||
targetCache = new CanvasRenderTarget(resourceCreator, targetW, targetH, sourceBitmap.Dpi);
|
||||
|
||||
using (var ds = targetCache.CreateDrawingSession())
|
||||
{
|
||||
ds.Clear(Windows.UI.Color.FromArgb(0, 0, 0, 0));
|
||||
|
||||
using (var transformEffect = new Transform2DEffect())
|
||||
using (var blurEffect = new GaussianBlurEffect())
|
||||
{
|
||||
transformEffect.Source = sourceBitmap;
|
||||
transformEffect.TransformMatrix = Matrix3x2.CreateScale(scale);
|
||||
transformEffect.InterpolationMode = CanvasImageInterpolation.Linear;
|
||||
|
||||
blurEffect.Source = transformEffect;
|
||||
blurEffect.BlurAmount = BlurAmount;
|
||||
blurEffect.BorderMode = EffectBorderMode.Hard;
|
||||
|
||||
ds.DrawImage(blurEffect);
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceBitmap == _currentBitmap)
|
||||
{
|
||||
_needsCacheUpdate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCachedLayer(CanvasDrawingSession ds, CanvasRenderTarget? cachedTexture, Vector2 screenCenter, float rotationRadians, float alpha)
|
||||
{
|
||||
if (cachedTexture == null) return;
|
||||
|
||||
Vector2 textureCenter = new Vector2((float)cachedTexture.Size.Width / 2f, (float)cachedTexture.Size.Height / 2f);
|
||||
|
||||
Matrix3x2 transform =
|
||||
Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(imgCenterOffset) * Matrix3x2.CreateRotation(rotationRadians, screenCenter);
|
||||
Matrix3x2.CreateTranslation(-textureCenter) * Matrix3x2.CreateRotation(rotationRadians) * Matrix3x2.CreateTranslation(screenCenter);
|
||||
|
||||
using (var transformEffect = new Transform2DEffect())
|
||||
using (var blurEffect = new GaussianBlurEffect())
|
||||
{
|
||||
transformEffect.Source = bitmap;
|
||||
transformEffect.TransformMatrix = transform;
|
||||
transformEffect.InterpolationMode = CanvasImageInterpolation.Linear;
|
||||
Matrix3x2 previousTransform = ds.Transform;
|
||||
|
||||
blurEffect.Source = transformEffect;
|
||||
blurEffect.BlurAmount = blurAmount > 0 ? (blurAmount / 2.0f) : 0f;
|
||||
blurEffect.BorderMode = EffectBorderMode.Hard;
|
||||
ds.Transform = transform * previousTransform;
|
||||
ds.DrawImage(cachedTexture, 0, 0, new Rect(0, 0, cachedTexture.Size.Width, cachedTexture.Size.Height), alpha);
|
||||
|
||||
ds.DrawImage(blurEffect, 0, 0, new Windows.Foundation.Rect(0, 0, screenSize.Width, screenSize.Height), alpha);
|
||||
}
|
||||
ds.Transform = previousTransform;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_currentBitmap?.Dispose();
|
||||
_currentBitmap = null;
|
||||
_previousBitmap?.Dispose();
|
||||
|
||||
_currentTargetCache?.Dispose();
|
||||
_previousTargetCache?.Dispose();
|
||||
|
||||
_currentBitmap = null;
|
||||
_previousBitmap = null;
|
||||
_currentTargetCache = null;
|
||||
_previousTargetCache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,8 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
var effectSettings = windowStatus.LyricsEffectSettings;
|
||||
var styleSettings = windowStatus.LyricsStyleSettings;
|
||||
|
||||
var rotationY = currentPlayingLine.OriginalPosition.WithX(effectSettings.FanLyricsAngle < 0 ? (float)lyricsWidth : 0);
|
||||
var rotationX = effectSettings.FanLyricsAngle < 0 ? lyricsWidth : 0;
|
||||
rotationX += lyricsWidth / 2 * (effectSettings.FanLyricsAngle < 0 ? 1 : -1);
|
||||
|
||||
for (int i = startVisibleIndex; i <= endVisibleIndex; i++)
|
||||
{
|
||||
@@ -145,14 +146,19 @@ namespace BetterLyrics.WinUI3.Renderer
|
||||
if (line.OriginalCanvasTextLayout == null) continue;
|
||||
if (line.OriginalCanvasTextLayout.LayoutBounds.Width <= 0) continue;
|
||||
|
||||
double xOffset = lyricsX;
|
||||
double yOffset = line.YOffsetTransition.Value + userScrollOffset + lyricsY + lyricsHeight * playingLineTopOffsetFactor;
|
||||
|
||||
var transform =
|
||||
Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition) *
|
||||
Matrix3x2.CreateRotation((float)line.AngleTransition.Value, rotationY) *
|
||||
Matrix3x2.CreateTranslation((float)lyricsX, (float)yOffset);
|
||||
ds.Transform = Matrix3x2.CreateScale((float)line.ScaleTransition.Value, line.CenterPosition);
|
||||
|
||||
ds.Transform = transform;
|
||||
if (effectSettings.IsFanLyricsEnabled)
|
||||
{
|
||||
xOffset += Math.Abs(line.AngleTransition.Value) / (Math.PI / 2) * lyricsWidth / 2 * (effectSettings.FanLyricsAngle < 0 ? 1 : -1);
|
||||
var rotationY = line.CenterPosition.Y;
|
||||
ds.Transform *= Matrix3x2.CreateRotation((float)line.AngleTransition.Value, new Vector2((float)rotationX, rotationY));
|
||||
}
|
||||
|
||||
ds.Transform *= Matrix3x2.CreateTranslation((float)xOffset, (float)yOffset);
|
||||
|
||||
using (var textOnlyLayer = RenderBaseTextLayer(control, line, styleSettings.LyricsFontStrokeWidth, strokeColor, line.ColorTransition.Value))
|
||||
{
|
||||
@@ -227,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(
|
||||
|
||||
@@ -7,7 +7,9 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Serialization
|
||||
{
|
||||
[JsonSerializable(typeof(TranslateResponse))]
|
||||
[JsonSerializable(typeof(LibreTranslateResponse))]
|
||||
[JsonSerializable(typeof(CutletDockerRequest))]
|
||||
[JsonSerializable(typeof(CutletDockerResponse))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(AppSettings))]
|
||||
[JsonSerializable(typeof(LyricsSearchResult))]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user