Compare commits

...

306 Commits

Author SHA1 Message Date
Zhe Fang
2dc8b1283f Update README.CN.md 2026-01-01 12:38:31 -05:00
Zhe Fang
c482edea0f Update README.md 2026-01-01 12:36:39 -05:00
Zhe Fang
315722252c Update README.md 2026-01-01 12:32:01 -05:00
Zhe Fang
32ba453264 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 16:30:16 -05:00
Zhe Fang
d4902329bb chores: update readme 2025-12-31 16:30:14 -05:00
Zhe Fang
83aee8948b Update README.CN.md 2025-12-31 16:13:48 -05:00
Zhe Fang
1f9fab3228 Update README.CN.md 2025-12-31 16:10:42 -05:00
Zhe Fang
7a3a659dfc Update README.CN.md 2025-12-31 16:09:17 -05:00
Zhe Fang
a14afd3eb5 Update README.md 2025-12-31 16:01:37 -05:00
Zhe Fang
c2af7f3186 Update README.CN.md 2025-12-31 16:00:16 -05:00
Zhe Fang
cd026dd2bf Update README.CN.md 2025-12-31 15:56:08 -05:00
Zhe Fang
4bc1a9975d Update README.md 2025-12-31 15:52:05 -05:00
Zhe Fang
07eecf0930 Update README.md 2025-12-31 15:49:22 -05:00
Zhe Fang
35fba5abb0 chores 2025-12-31 15:44:58 -05:00
Zhe Fang
03ef231a3f chores 2025-12-31 15:39:44 -05:00
Zhe Fang
f41879f4e5 chores: add pic 2025-12-31 15:38:18 -05:00
Zhe Fang
bda7510ed6 chores: update readme 2025-12-31 14:28:54 -05:00
Zhe Fang
5ec8c7c61f chores: bump to 1.2.236 2025-12-31 13:32:45 -05:00
Zhe Fang
7e6bd9dade fix: _scrobbleStopwatch 2025-12-31 10:55:56 -05:00
Zhe Fang
56244cb793 fix: scrollable timer 2025-12-31 10:47:43 -05:00
Zhe Fang
cb5f70ab55 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 10:12:29 -05:00
Zhe Fang
3cc018bb1f chores: bump to 1.2.234.0 2025-12-31 10:12:28 -05:00
Zhe Fang
c517d2b008 Update README.md 2025-12-31 10:08:58 -05:00
Zhe Fang
e79f2a0223 Update README.CN.md 2025-12-31 10:07:53 -05:00
Zhe Fang
39122b9147 Update README.CN.md 2025-12-31 10:07:01 -05:00
Zhe Fang
accbdc1806 Update README.md 2025-12-31 10:06:01 -05:00
Zhe Fang
de014d1ad7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 09:52:12 -05:00
Zhe Fang
cc2ce5f8cf chores: i18n 2025-12-31 09:52:10 -05:00
Zhe Fang
2a2d80436e Update README.CN.md 2025-12-31 09:34:29 -05:00
Zhe Fang
ce3f79f35c Update README.md 2025-12-31 09:33:01 -05:00
Zhe Fang
12e6000cb3 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:43:44 -05:00
Zhe Fang
c1dc684411 fix: scrobble timer is still running when music is paused 2025-12-31 08:43:43 -05:00
Zhe Fang
69ea2cb495 Update README.md 2025-12-31 08:21:33 -05:00
Zhe Fang
e2ee03c4be Update README.CN.md 2025-12-31 08:21:03 -05:00
Zhe Fang
c6fe33d6ae Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:12:12 -05:00
Zhe Fang
7744e145fa chores: i18n 2025-12-31 08:12:10 -05:00
Zhe Fang
0284b1de81 Update README.CN.md 2025-12-30 21:46:32 -05:00
Zhe Fang
108c2cd34b Update README.md 2025-12-30 21:45:56 -05:00
Zhe Fang
390e30f7f5 chores: bump to v1.2.232.0 2025-12-30 20:54:40 -05:00
Zhe Fang
900774668d chores: i18n 2025-12-30 20:09:02 -05:00
Zhe Fang
6ca2d1f897 chores 2025-12-30 19:49:54 -05:00
Zhe Fang
164bd077b8 fix: app crash when audio device was not found 2025-12-30 14:30:10 -05:00
Zhe Fang
8ec71fcfb7 chores: update StatsDashboardControl 2025-12-30 14:04:08 -05:00
Zhe Fang
f39ad54df8 fix: record play history 2025-12-30 13:02:23 -05:00
Zhe Fang
9b809983df chores: i18n 2025-12-30 08:59:30 -05:00
Zhe Fang
8006b3a443 chores: i18n 2025-12-30 08:41:12 -05:00
Zhe Fang
26a7454de2 feat: play history 2025-12-30 08:02:48 -05:00
Zhe Fang
0793a074cf fix: add to playlists 2025-12-29 16:11:21 -05:00
Zhe Fang
125bf1682e chores 2025-12-29 12:15:39 -05:00
Zhe Fang
48bdffb2fe Merge pull request #179 from jayfunc/l10n_dev
New Crowdin updates
2025-12-28 21:37:57 -05:00
Zhe Fang
d324a7552f New translations resources.resw (Chinese Traditional) 2025-12-28 21:37:17 -05:00
Zhe Fang
78c308c393 New translations resources.resw (Japanese) 2025-12-28 21:37:15 -05:00
Zhe Fang
a1bba00db6 chores: i18n 2025-12-28 21:31:37 -05:00
Zhe Fang
0787f5b111 fix 2025-12-28 21:10:02 -05:00
Zhe Fang
884026594b fix: ignore system, hidden files and unsupported format files 2025-12-28 21:03:56 -05:00
Zhe Fang
b0a777db8d chores: adjust layout and fix bugs 2025-12-28 20:01:41 -05:00
Zhe Fang
83f3a3bd6d chores: fix SMB and local file system, add auto-sync, improve lyruics search, album art search, local music gallery load speed (after 1st time) 2025-12-27 15:25:49 -05:00
Zhe Fang
bfb2ed29e5 fix: window title is synced with config name 2025-12-25 13:44:43 -05:00
Zhe Fang
131a0f0eb1 Merge pull request #172 from jayfunc/l10n_dev
New Crowdin updates
2025-12-24 19:24:20 -05:00
Zhe Fang
ac2a7b3f7b New translations resources.resw (Malay) 2025-12-24 19:22:52 -05:00
Zhe Fang
36eea7f8f2 New translations resources.resw (Hindi) 2025-12-24 19:22:52 -05:00
Zhe Fang
6b338deb55 New translations resources.resw (Thai) 2025-12-24 19:22:51 -05:00
Zhe Fang
af323ecd00 New translations resources.resw (Indonesian) 2025-12-24 19:22:50 -05:00
Zhe Fang
c79d01c75b New translations resources.resw (Vietnamese) 2025-12-24 19:22:49 -05:00
Zhe Fang
b51ec1e60f New translations resources.resw (Chinese Traditional) 2025-12-24 19:22:48 -05:00
Zhe Fang
7fe925bcba New translations resources.resw (Chinese Simplified) 2025-12-24 19:22:47 -05:00
Zhe Fang
0626472d66 New translations resources.resw (Russian) 2025-12-24 19:22:46 -05:00
Zhe Fang
33099bc186 New translations resources.resw (Portuguese) 2025-12-24 19:22:45 -05:00
Zhe Fang
e653efc227 New translations resources.resw (Korean) 2025-12-24 19:22:44 -05:00
Zhe Fang
074fef3faf New translations resources.resw (Japanese) 2025-12-24 19:22:43 -05:00
Zhe Fang
029cbbd343 New translations resources.resw (German) 2025-12-24 19:22:42 -05:00
Zhe Fang
802b2a4c1c New translations resources.resw (Arabic) 2025-12-24 19:22:41 -05:00
Zhe Fang
eccc4d519c New translations resources.resw (Spanish) 2025-12-24 19:22:39 -05:00
Zhe Fang
5f274ea28a New translations resources.resw (French) 2025-12-24 19:22:37 -05:00
Zhe Fang
aa1a1f5d58 chores: i18n 2025-12-24 19:22:16 -05:00
Zhe Fang
3a56d53487 New translations resources.resw (Malay) 2025-12-24 19:15:10 -05:00
Zhe Fang
bbc5eb772c New translations resources.resw (Hindi) 2025-12-24 19:15:09 -05:00
Zhe Fang
05b491052b New translations resources.resw (Thai) 2025-12-24 19:15:08 -05:00
Zhe Fang
8accbf0431 New translations resources.resw (Indonesian) 2025-12-24 19:15:07 -05:00
Zhe Fang
1174209c2a New translations resources.resw (Vietnamese) 2025-12-24 19:15:06 -05:00
Zhe Fang
23ed719046 New translations resources.resw (Chinese Traditional) 2025-12-24 19:15:05 -05:00
Zhe Fang
a34f00662e New translations resources.resw (Chinese Simplified) 2025-12-24 19:15:04 -05:00
Zhe Fang
f783314258 New translations resources.resw (Russian) 2025-12-24 19:15:03 -05:00
Zhe Fang
215a39c5d5 New translations resources.resw (Portuguese) 2025-12-24 19:15:02 -05:00
Zhe Fang
16bcef5f64 New translations resources.resw (Korean) 2025-12-24 19:15:01 -05:00
Zhe Fang
fbba9a3c36 New translations resources.resw (Japanese) 2025-12-24 19:15:00 -05:00
Zhe Fang
f205ab0364 New translations resources.resw (German) 2025-12-24 19:14:59 -05:00
Zhe Fang
10314f3c2f New translations resources.resw (Arabic) 2025-12-24 19:14:58 -05:00
Zhe Fang
b4710e87d3 New translations resources.resw (Spanish) 2025-12-24 19:14:57 -05:00
Zhe Fang
282a934cd2 New translations resources.resw (French) 2025-12-24 19:14:56 -05:00
Zhe Fang
b4c4e394ef Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-24 18:36:28 -05:00
Zhe Fang
17cfdf37bd chores: i18n 2025-12-24 18:36:26 -05:00
Zhe Fang
900a8e1e7c Update credit wording in CONTRIBUTING.md 2025-12-24 11:55:18 -05:00
Zhe Fang
ea9a9c2f5f Update translation contribution instructions in README
Added a link for contributors to find more information.
2025-12-24 11:52:00 -05:00
Zhe Fang
0c4d02b337 Revise translation assistance information
Updated translation assistance section with a link for contributions.
2025-12-24 11:51:38 -05:00
Zhe Fang
d137d82ecf Update translation assistance section in README
Added a section for translation assistance and removed the previous translation section.
2025-12-24 11:51:07 -05:00
Zhe Fang
02551e2053 Add translation contribution section to README
Added a section encouraging users to help translate the project and contribute.
2025-12-24 11:50:10 -05:00
Zhe Fang
026926e9b8 Update CONTRIBUTING.md 2025-12-24 11:39:08 -05:00
Zhe Fang
4c811db16a Update README.CN.md with translation and donation info
Added a section for translation contributions and donation support.
2025-12-24 11:36:29 -05:00
Zhe Fang
6f83fa11db Revise contributing guidelines for translations
Updated the contributing guidelines to include translation information in both English and Chinese. Added a status table for languages and contributors.
2025-12-24 11:34:55 -05:00
Zhe Fang
bc8e15c144 Update translation section in README.md
Added a section for translation contributions and removed the previous translation details.
2025-12-24 11:32:16 -05:00
Zhe Fang
85de1eb2cd Add Chinese translation guidelines to CONTRIBUTING.md 2025-12-24 11:29:00 -05:00
Zhe Fang
d2bf19ed3d Add translation contribution guidelines
Added instructions for translating BetterLyrics using Crowdin.
2025-12-24 11:27:23 -05:00
Zhe Fang
43c205c839 Update Japanese language support status in README 2025-12-24 11:07:35 -05:00
Zhe Fang
9664b1ab78 Update Japanese language entry in README.CN.md 2025-12-24 11:07:00 -05:00
Zhe Fang
08c5f6b515 Add contributor link for Japanese language support 2025-12-24 11:06:31 -05:00
Zhe Fang
260de40f81 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-24 11:05:41 -05:00
Zhe Fang
c00d0eb005 fix: duration issue 2025-12-24 11:05:39 -05:00
Zhe Fang
32e761724c Merge pull request #170 from jayfunc/l10n_dev
New Crowdin updates
2025-12-24 11:05:05 -05:00
Zhe Fang
9fd08af582 New translations resources.resw (Japanese) 2025-12-24 11:03:57 -05:00
Zhe Fang
266dcfc930 New translations resources.resw (Chinese Simplified) 2025-12-24 10:43:52 -05:00
Zhe Fang
8764585f2c New translations resources.resw (Japanese) 2025-12-24 10:43:50 -05:00
Zhe Fang
91ab3a48c0 New translations resources.resw (Chinese Simplified) 2025-12-24 09:23:59 -05:00
Zhe Fang
80fa34d9e8 New translations resources.resw (Japanese) 2025-12-24 09:23:58 -05:00
Zhe Fang
b4ca4fd990 New translations resources.resw (Chinese Traditional) 2025-12-24 08:00:35 -05:00
Zhe Fang
86527f6b82 New translations resources.resw (Japanese) 2025-12-24 08:00:34 -05:00
Zhe Fang
d8066bc683 New translations resources.resw (Japanese) 2025-12-24 06:04:12 -05:00
Zhe Fang
b261a86791 New translations resources.resw (Japanese) 2025-12-24 04:35:20 -05:00
Zhe Fang
34f2a51b74 New translations resources.resw (Japanese) 2025-12-24 00:10:01 -05:00
Zhe Fang
b1e9c25e01 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-23 20:43:31 -05:00
Zhe Fang
346de93c3f fix: AlbumArtThemeColors is not updated when PaletteGeneratorType is changed 2025-12-23 20:43:29 -05:00
Zhe Fang
6f48cbcd16 Add Contributor Covenant Code of Conduct
This document outlines the Contributor Covenant Code of Conduct, detailing our pledge, standards, enforcement responsibilities, and consequences for violations.
2025-12-23 18:02:06 -05:00
Zhe Fang
85b3121479 chores 2025-12-23 15:48:39 -05:00
Zhe Fang
94f00d1a31 chores 2025-12-23 14:10:35 -05:00
Zhe Fang
be9e4bba0f chores: update readme 2025-12-23 14:04:48 -05:00
Zhe Fang
2454927582 Merge pull request #169 from jayfunc/l10n_dev
New Crowdin updates
2025-12-23 13:44:27 -05:00
Zhe Fang
aca5f8e00d New translations resources.resw (Malay) 2025-12-23 13:43:43 -05:00
Zhe Fang
09709e8e62 New translations resources.resw (Hindi) 2025-12-23 13:43:42 -05:00
Zhe Fang
98fd8b43c4 New translations resources.resw (Thai) 2025-12-23 13:43:41 -05:00
Zhe Fang
3051180eb9 New translations resources.resw (Indonesian) 2025-12-23 13:43:38 -05:00
Zhe Fang
d48c81cfa1 New translations resources.resw (Vietnamese) 2025-12-23 13:43:37 -05:00
Zhe Fang
695147be9b New translations resources.resw (Portuguese) 2025-12-23 13:43:36 -05:00
Zhe Fang
e782944a44 New translations resources.resw (Arabic) 2025-12-23 13:43:35 -05:00
Zhe Fang
01462d42ce New translations resources.resw (Russian) 2025-12-23 13:43:33 -05:00
Zhe Fang
65b7dfcc44 New translations resources.resw (Japanese) 2025-12-23 13:43:31 -05:00
Zhe Fang
ec3146d4a7 New translations resources.resw (German) 2025-12-23 13:43:30 -05:00
Zhe Fang
9ec0bf0b1a New translations resources.resw (Spanish) 2025-12-23 13:43:29 -05:00
Zhe Fang
47e4b93613 New translations resources.resw (French) 2025-12-23 13:43:28 -05:00
Zhe Fang
192ad4a503 fix: i18n 2025-12-23 13:08:42 -05:00
Zhe Fang
091e33ae08 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-23 13:04:44 -05:00
Zhe Fang
3b010ed674 add: more langs 2025-12-23 13:04:42 -05:00
Zhe Fang
a9f685d51b Merge pull request #168 from jayfunc/l10n_dev
New Crowdin updates
2025-12-23 13:04:04 -05:00
Zhe Fang
c6c31f8839 New translations resources.resw (Chinese Traditional) 2025-12-23 13:03:26 -05:00
Zhe Fang
78c53760cc New translations resources.resw (Chinese Simplified) 2025-12-23 13:03:25 -05:00
Zhe Fang
0bb6b5a204 New translations resources.resw (Chinese Traditional) 2025-12-23 12:34:51 -05:00
Zhe Fang
dff36a5e4d New translations resources.resw (Chinese Simplified) 2025-12-23 12:34:50 -05:00
Zhe Fang
0188e443db New translations resources.resw (Chinese Traditional) 2025-12-23 11:05:21 -05:00
Zhe Fang
5a9cdedc0c New translations resources.resw (Chinese Simplified) 2025-12-23 11:05:19 -05:00
Zhe Fang
31460fcc6d New translations resources.resw (Russian) 2025-12-23 11:05:18 -05:00
Zhe Fang
c12fc6f381 New translations resources.resw (Korean) 2025-12-23 11:05:16 -05:00
Zhe Fang
e5e0342994 New translations resources.resw (Japanese) 2025-12-23 11:05:15 -05:00
Zhe Fang
061958f20c New translations resources.resw (German) 2025-12-23 11:05:14 -05:00
Zhe Fang
95c73d0a34 New translations resources.resw (Spanish) 2025-12-23 11:05:13 -05:00
Zhe Fang
026a12ac87 New translations resources.resw (French) 2025-12-23 11:05:11 -05:00
Zhe Fang
da53f2166f Update contributors for Simplified Chinese section 2025-12-23 09:59:48 -05:00
Zhe Fang
717277e17c Update contributors for Simplified Chinese translation 2025-12-23 09:59:02 -05:00
Zhe Fang
1dc3ea57e9 chores: delete unused res items 2025-12-23 09:54:07 -05:00
Zhe Fang
4ec2ba8b59 chores: delete unused res item 2025-12-23 09:49:38 -05:00
Zhe Fang
91d9f253f0 Merge pull request #167 from jayfunc/l10n_dev
New Crowdin updates
2025-12-23 09:38:59 -05:00
Zhe Fang
90cf373e50 New translations resources.resw (Chinese Simplified) 2025-12-23 09:37:57 -05:00
Zhe Fang
cf2778da7a Add contributor table and translation invitation link 2025-12-23 09:27:10 -05:00
Zhe Fang
45ff7d7aa8 Update README with language and contributor information
Added a table listing supported languages and contributors.
2025-12-23 09:24:18 -05:00
Zhe Fang
eb37cb1b55 New translations resources.resw (Chinese Traditional) 2025-12-23 09:15:18 -05:00
Zhe Fang
45aa1d787d New translations resources.resw (Chinese Simplified) 2025-12-23 09:15:15 -05:00
Zhe Fang
0b28419ab5 New translations resources.resw (Russian) 2025-12-23 09:15:13 -05:00
Zhe Fang
258bf9220e New translations resources.resw (Korean) 2025-12-23 09:15:12 -05:00
Zhe Fang
9ece9f3edc New translations resources.resw (Japanese) 2025-12-23 09:15:11 -05:00
Zhe Fang
40c1f0a5ce New translations resources.resw (German) 2025-12-23 09:15:10 -05:00
Zhe Fang
5f75e6c63c New translations resources.resw (Spanish) 2025-12-23 09:15:09 -05:00
Zhe Fang
43387ce4c8 New translations resources.resw (French) 2025-12-23 09:15:08 -05:00
Zhe Fang
34eda9a262 fix: i18n 2025-12-23 08:51:38 -05:00
Zhe Fang
804673696f fix: i18n 2025-12-23 07:35:19 -05:00
Zhe Fang
b69e3bb24b chores: i18n 2025-12-23 07:12:36 -05:00
Zhe Fang
c028aa8e46 chores 2025-12-23 06:18:16 -05:00
Zhe Fang
fe3e257215 chores 2025-12-22 18:13:11 -05:00
Zhe Fang
eae2428d85 fix: lang 2025-12-22 17:19:09 -05:00
Zhe Fang
b078365136 fix: lang 2025-12-22 17:07:16 -05:00
Zhe Fang
1ede8dbef4 fix: song info fade 2025-12-22 16:45:41 -05:00
Zhe Fang
a66051b937 chores: i18n 2025-12-22 16:01:43 -05:00
Zhe Fang
1eca21c285 chores: i18n 2025-12-22 14:17:43 -05:00
Zhe Fang
2254a28e40 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-22 11:47:30 -05:00
Zhe Fang
812eca369d chores: improve i18n method 2025-12-22 11:47:29 -05:00
Zhe Fang
132d3d8ac8 Merge pull request #164 from jayfunc/l10n_dev
New Crowdin updates
2025-12-22 11:46:56 -05:00
Zhe Fang
641a23621f New translations resources.resw (Chinese Simplified) 2025-12-22 11:46:00 -05:00
Zhe Fang
6802d10142 Merge pull request #163 from jayfunc/l10n_dev
New Crowdin updates
2025-12-22 06:47:45 -05:00
Zhe Fang
36f43e6d54 New translations resources.resw (Chinese Simplified) 2025-12-22 06:46:42 -05:00
Zhe Fang
e8298ec7bd New translations resources.resw (Chinese Simplified) 2025-12-22 06:33:43 -05:00
Zhe Fang
99a21cb935 New translations resources.resw (German) 2025-12-21 19:17:28 -05:00
Zhe Fang
b6da7bea5d New translations resources.resw (Chinese Traditional) 2025-12-21 19:17:27 -05:00
Zhe Fang
cf5bf75346 New translations resources.resw (Russian) 2025-12-21 19:17:26 -05:00
Zhe Fang
7497d7014d New translations resources.resw (Korean) 2025-12-21 19:17:24 -05:00
Zhe Fang
dd8c62ffa5 New translations resources.resw (Japanese) 2025-12-21 19:17:24 -05:00
Zhe Fang
15b147ba06 New translations resources.resw (Spanish) 2025-12-21 19:17:23 -05:00
Zhe Fang
85146ffc95 New translations resources.resw (French) 2025-12-21 19:17:22 -05:00
Zhe Fang
e9dce765e4 fix: i18n 2025-12-21 18:18:25 -05:00
Zhe Fang
3b2c4477b5 Merge pull request #162 from jayfunc/l10n_dev
New Crowdin updates
2025-12-21 18:07:48 -05:00
Zhe Fang
9d71c4aecf New translations resources.resw (German) 2025-12-21 18:02:53 -05:00
Zhe Fang
7184c148c4 New translations resources.resw (Chinese Traditional) 2025-12-21 18:02:52 -05:00
Zhe Fang
85f928ce3b New translations resources.resw (Chinese Simplified) 2025-12-21 18:02:51 -05:00
Zhe Fang
7c5032b0c2 New translations resources.resw (Russian) 2025-12-21 18:02:50 -05:00
Zhe Fang
2c3bd056b7 New translations resources.resw (Korean) 2025-12-21 18:02:49 -05:00
Zhe Fang
9f2843b7a0 New translations resources.resw (Japanese) 2025-12-21 18:02:48 -05:00
Zhe Fang
7fb6d5346e New translations resources.resw (Spanish) 2025-12-21 18:02:47 -05:00
Zhe Fang
27125d9051 New translations resources.resw (French) 2025-12-21 18:02:46 -05:00
Zhe Fang
5b2fb8b345 fix: english 2025-12-21 17:06:10 -05:00
Zhe Fang
d558811cb4 Merge pull request #161 from jayfunc/l10n_dev
New Crowdin updates
2025-12-21 16:58:03 -05:00
Zhe Fang
6e30aa7ebd New translations resources.resw (Chinese Traditional) 2025-12-21 16:56:21 -05:00
Zhe Fang
15fc337944 Merge pull request #160 from jayfunc/l10n_dev
New Crowdin updates
2025-12-21 16:46:11 -05:00
Zhe Fang
b7ef159b9e New translations resources.resw (German) 2025-12-21 16:44:37 -05:00
Zhe Fang
393b33ed83 New translations resources.resw (Chinese Traditional) 2025-12-21 16:44:36 -05:00
Zhe Fang
23dfda4413 New translations resources.resw (Chinese Simplified) 2025-12-21 16:44:35 -05:00
Zhe Fang
fde7340f4d New translations resources.resw (Russian) 2025-12-21 16:44:34 -05:00
Zhe Fang
22330d7fe9 New translations resources.resw (Korean) 2025-12-21 16:44:33 -05:00
Zhe Fang
c64e5776e8 New translations resources.resw (Japanese) 2025-12-21 16:44:32 -05:00
Zhe Fang
ffa2cd75a0 New translations resources.resw (Spanish) 2025-12-21 16:44:31 -05:00
Zhe Fang
873e75a7e9 New translations resources.resw (French) 2025-12-21 16:44:30 -05:00
Zhe Fang
ffa4101d5f chores: i18n 2025-12-21 16:26:35 -05:00
Zhe Fang
1c12b582c2 chores: add help us translate message 2025-12-21 16:22:32 -05:00
Zhe Fang
c50d31ced7 chores 2025-12-21 16:03:10 -05:00
Zhe Fang
f8108151b6 Merge pull request #159 from jayfunc/l10n_dev
New Crowdin updates
2025-12-21 16:02:22 -05:00
Zhe Fang
2932366767 New translations resources.resw (German) 2025-12-21 16:01:50 -05:00
Zhe Fang
cbf643ca70 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-21 15:53:46 -05:00
Zhe Fang
a72d0f5c28 chores 2025-12-21 15:53:44 -05:00
Zhe Fang
3b4d98f9a3 Merge pull request #158 from jayfunc/l10n_dev
New Crowdin updates
2025-12-21 15:52:58 -05:00
Zhe Fang
d5828101d8 New translations resources.resw (Chinese Traditional) 2025-12-21 15:45:59 -05:00
Zhe Fang
56051537ea New translations resources.resw (Chinese Simplified) 2025-12-21 15:45:58 -05:00
Zhe Fang
6b465a09b1 New translations resources.resw (Russian) 2025-12-21 15:45:57 -05:00
Zhe Fang
450b86ebaf New translations resources.resw (Dutch) 2025-12-21 15:45:56 -05:00
Zhe Fang
c0078baa13 New translations resources.resw (Korean) 2025-12-21 15:45:55 -05:00
Zhe Fang
6b28212ec3 New translations resources.resw (Japanese) 2025-12-21 15:45:54 -05:00
Zhe Fang
9a3c2f5f70 New translations resources.resw (Spanish) 2025-12-21 15:45:54 -05:00
Zhe Fang
31be2bd8f7 New translations resources.resw (French) 2025-12-21 15:45:52 -05:00
Zhe Fang
47056e07a1 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-21 15:03:32 -05:00
Zhe Fang
f30673b9d3 chores: add langs 2025-12-21 15:03:31 -05:00
Zhe Fang
d8624c49d0 Update Crowdin configuration file 2025-12-21 14:45:18 -05:00
Zhe Fang
72810e7440 Update Crowdin configuration file 2025-12-21 14:39:11 -05:00
Zhe Fang
e881d36743 chores: i18n 2025-12-21 14:34:34 -05:00
Zhe Fang
aa3e79d3ff chores: i18n 2025-12-21 13:28:22 -05:00
Zhe Fang
9979474ce1 fix 2025-12-21 12:24:46 -05:00
Zhe Fang
2e7cd93cfe fix: renderer error 2025-12-21 08:04:26 -05:00
Zhe Fang
bdc31c3e0d fix: lyrics source search issue when id is not null; feat: support ftp, smb, webdav (still testing) 2025-12-21 06:34:11 -05:00
Zhe Fang
631d079aa2 chores: adjustment for album art info flyout 2025-12-19 16:29:36 -05:00
Zhe Fang
f76ef87167 feat: save album art to local 2025-12-19 16:10:14 -05:00
Zhe Fang
76aa5ee8d0 fix: search control result can not invoke play selected lyrics line 2025-12-19 14:46:23 -05:00
Zhe Fang
d7f4978a66 chores: update deps 2025-12-19 09:36:53 -05:00
Zhe Fang
0905c46e45 fix: scroll 2025-12-19 08:54:24 -05:00
Zhe Fang
d0991c5ddb fix: 3d lyrics effect incorrect y center 2025-12-17 20:22:26 -05:00
Zhe Fang
619a3ba196 chores: bump to v1.1.198.0 2025-12-17 15:55:33 -05:00
Zhe Fang
13526bb85c chores: i18n 2025-12-17 15:43:48 -05:00
Zhe Fang
61f4f608db fix 2025-12-17 15:06:22 -05:00
Zhe Fang
f690da8501 feat: add language info in playback settings 2025-12-17 14:52:01 -05:00
Zhe Fang
145c13a0e6 chores: improve lyrics window switch exp 2025-12-17 14:05:28 -05:00
Zhe Fang
cea4fbb54d fix: PasswordVaultHelper save issue 2025-12-17 10:54:44 -05:00
Zhe Fang
1d489c68e9 chores: bump to v1.1.194.0 2025-12-15 15:45:25 -05:00
Zhe Fang
90e7fa42d0 chores 2025-12-15 15:36:50 -05:00
Zhe Fang
29a6879e45 chores: remove csharp-kana 2025-12-15 14:58:51 -05:00
Zhe Fang
58499a2d09 chores: update i18n 2025-12-15 14:55:37 -05:00
Zhe Fang
580255699b chores: change romaji method 2025-12-15 14:42:11 -05:00
Zhe Fang
9cac7818f1 fix: system tray left click 2025-12-15 10:45:24 -05:00
Zhe Fang
118668a457 fix: timeline 2025-12-15 07:36:26 -05:00
Zhe Fang
37621dbf2a chores: adjust margin for NowPlayingBar 2025-12-15 07:29:26 -05:00
Zhe Fang
aa7d56f1cb chores: bump to v1.1.190.0 2025-12-14 11:56:24 -05:00
Zhe Fang
8dbe76e790 chores: change bg color for top title bar and now playing bar 2025-12-14 11:45:09 -05:00
Zhe Fang
de6410492e fix: Canvas_CreateResources 2025-12-14 11:20:04 -05:00
Zhe Fang
26df7c7f67 chores: bump to 1.1.188.0 2025-12-14 09:48:01 -05:00
Zhe Fang
3c411374bd chores: undo 2025-12-14 09:40:13 -05:00
Zhe Fang
99f0b9443b fix: spectrum count is not updating when chaning amount 2025-12-14 09:26:19 -05:00
Zhe Fang
a3bc148816 fix: prop clone 2025-12-14 09:18:49 -05:00
Zhe Fang
cea757702b feat: add settings item for always hiding unlock button 2025-12-14 09:17:27 -05:00
Zhe Fang
8938a5c798 feat: add settings item for stop music when closing music gallery window 2025-12-14 08:18:21 -05:00
Zhe Fang
46f4589b64 chores: dispose cover renderer when closed 2025-12-14 08:03:58 -05:00
Zhe Fang
adb02658f4 feat: add cover background 2025-12-14 07:58:41 -05:00
Zhe Fang
3d7e6061e9 fix: lyrics window settings config panel wont auto hide when open again 2025-12-14 06:52:08 -05:00
Zhe Fang
a51220c7b9 fix: playing line top offset out of bounds 2025-12-14 05:51:49 -05:00
Zhe Fang
22b813e687 chores: bump to v1.1.186.0 2025-12-13 17:02:55 -05:00
Zhe Fang
fda94d5020 fix: settings save issue 2025-12-13 16:54:08 -05:00
Zhe Fang
205cbe8fb6 fix: settings storage issue 2025-12-13 16:19:05 -05:00
Zhe Fang
816f7064db fix: fan style lyrics animation 2025-12-13 15:45:41 -05:00
Zhe Fang
132c5267b0 chores: bump to v1.1.184.0 2025-12-13 12:20:20 -05:00
Zhe Fang
4e866818df fix: font family issue 2025-12-13 12:11:50 -05:00
Zhe Fang
9b7b56a0ee Revise acknowledgments in README.CN.md
Updated the acknowledgments section to reflect ongoing support and gratitude towards contributors and users.
2025-12-13 09:46:47 -05:00
Zhe Fang
66f2da0e4c Revise acknowledgments and donation list in README
Updated the acknowledgments section to reflect user support and removed the manual donation list.
2025-12-13 09:46:04 -05:00
Zhe Fang
1735c6a7e6 Add sponsors list and gratitude section to SPONSORS.md 2025-12-13 09:44:10 -05:00
Zhe Fang
8c06c98068 Update README.CN.md to acknowledge supporters
Added a section to thank supporters and users.
2025-12-13 09:36:00 -05:00
Zhe Fang
e2ac4c166c Add acknowledgments for supporters in README
Added a section to acknowledge and thank supporters.
2025-12-13 09:34:22 -05:00
Zhe Fang
728397cafa chores: code cleanup 2025-12-13 08:39:48 -05:00
Zhe Fang
059787a28f chores: bump to v1.1.183.0 2025-12-13 08:16:23 -05:00
Zhe Fang
4c4231b48c fix: white line in fullscreen mode 2025-12-13 07:58:28 -05:00
Zhe Fang
2412927b29 Add Zread badge to README.CN.md 2025-12-12 16:53:45 -05:00
Zhe Fang
f3bdbba83e Add Zread badge to README 2025-12-12 16:53:30 -05:00
Zhe Fang
4c811b12ca Create initial wiki structure for BetterLyrics
Added comprehensive documentation for BetterLyrics, including sections on installation, configuration, architecture, user interface, media integration, and development.
2025-12-12 16:20:42 -05:00
Zhe Fang
933103c57f Update badge links in README.CN.md 2025-12-12 15:55:15 -05:00
Zhe Fang
718e7bdad3 Fix badge formatting in README.md 2025-12-12 15:52:48 -05:00
Zhe Fang
42284b4f45 add: CoverBackgroundRenderer 2025-12-11 15:20:51 -05:00
Zhe Fang
7da8af7c2a fix: lyrics window manager tab not showing 2025-12-11 08:24:51 -05:00
Zhe Fang
a4fc457065 chores: bump to v1.1.181.0 2025-12-11 07:44:20 -05:00
Zhe Fang
d3c2ee592c fix: selectorbar section disappear 2025-12-11 07:19:05 -05:00
Zhe Fang
2eef88523c Modify taskbar.json for layout and style changes
Updated taskbar settings including bounds, opacity, and effects.
2025-12-10 16:17:25 -05:00
Zhe Fang
220b1063ac fix: taskbar mode is not working in release mode 2025-12-10 16:15:58 -05:00
Zhe Fang
28bcd8ddfc chores: remove taskbar mode entry 2025-12-10 14:09:16 -05:00
Zhe Fang
5750bd3ad7 add: more info in thanks control 2025-12-10 11:54:03 -05:00
Zhe Fang
1667c701b0 feat: taskbar mode 2025-12-10 11:09:53 -05:00
Zhe Fang
160398f7ab fix: add null check for Window.AppWindow 2025-12-10 10:51:45 -05:00
Zhe Fang
c25ddf770f fix: when open app with window locked, then unlock the window, title bar and bottom bar will not be interactive 2025-12-10 10:42:06 -05:00
Zhe Fang
44fa3312b2 fix: docked mode ux exp 2025-12-10 07:09:08 -05:00
Zhe Fang
81f3d1f6bf fix: cannot exit app when docked mode is on 2025-12-10 06:54:21 -05:00
198 changed files with 27172 additions and 5121 deletions

291
.devin/wiki.json Normal file
View 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": ""
}
]
}
]
}

View File

@@ -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">

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.1.175.0" />
Version="1.2.236.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>

View File

@@ -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,9 @@
<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" />
<converter:DoubleToDecimalConverter x:Key="DoubleToDecimalConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
@@ -96,7 +98,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">

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -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,7 +44,9 @@
<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\StatsDashboardControl.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Controls\WindowSettingsControl.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
@@ -58,36 +67,42 @@
<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.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
<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.0" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<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" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<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="System.Drawing.Common" Version="10.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.0" />
<PackageReference Include="SMBLibrary" Version="1.5.5.1" />
<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" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.2.1" />
@@ -96,6 +111,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>
@@ -115,6 +131,12 @@
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Abstractions" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Relational" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Sqlite" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
@@ -157,6 +179,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>
@@ -199,6 +224,9 @@
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -230,6 +258,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -331,12 +364,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">

View File

@@ -5,9 +5,11 @@
public const string MicrosoftStore = "https://apps.microsoft.com/detail/9p1wcd1p597r";
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";
public const string TermsOfService = $"{BetterLyricsGitHub}/blob/dev/TermsofService.md";
public const string PrivacyPolicy = $"{BetterLyricsGitHub}/blob/dev/PrivacyPolicy.md";

View File

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

View File

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

View File

@@ -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>
@@ -118,13 +118,21 @@
</HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="*" />
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="*" />
<TextBlock
x:Uid="SetingsPageThanks"
Grid.Column="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</StackPanel>
</dev:SettingsCard>
@@ -182,6 +190,12 @@
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard>
@@ -248,7 +262,7 @@
</Hyperlink>
</Paragraph>
<!-- 打赏者 -->
<!-- 赞助 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SettingsPagePatrons" />
</Paragraph>
@@ -277,231 +291,94 @@
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Aug 28, 2025" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageYouNowUsing" FontStyle="Italic" />
<Run x:Uid="SettingsPageUserWhoPurchased" />
</Paragraph>
<!-- 依赖包 -->
<!-- 特别鸣谢 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageSpecialThanks" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageYouNowUsing" />
</Paragraph>
<!-- 代码参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageDeps" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/3v.EvtSource">
<Run Text="3v.EvtSource" />
<Hyperlink NavigateUri="https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce">
<Run Text="Get album artwork from ITunes (with Python3 or C#)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView">
<Run Text="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" />
<Hyperlink NavigateUri="https://stackoverflow.com/a/32013610/11048731">
<Run Text="FullyObservableCollection" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.Labs.WinUI.Shimmer">
<Run Text="CommunityToolkit.Labs.WinUI.Shimmer" />
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/Impressionist">
<Run Text="Impressionist" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.Mvvm">
<Run Text="CommunityToolkit.Mvvm" />
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/ColorThief.WinUI3">
<Run Text="ColorThief.WinUI3" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Behaviors">
<Run Text="CommunityToolkit.WinUI.Behaviors" />
<Hyperlink NavigateUri="https://github.com/Johnwikix/SpectrumVisualization">
<Run Text="SpectrumVisualization" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.Primitives">
<Run Text="CommunityToolkit.WinUI.Controls.Primitives" />
<Hyperlink NavigateUri="https://www.shadertoy.com/view/Mdt3Df">
<Run Text="Snow (as shown in sweden)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Controls.Segmented">
<Run Text="CommunityToolkit.WinUI.Controls.Segmented" />
<Hyperlink NavigateUri="https://www.shadertoy.com/view/lllSR2">
<Run Text="w10" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Converters">
<Run Text="CommunityToolkit.WinUI.Converters" />
<Hyperlink NavigateUri="https://github.com/mo-jinran/Taskbar-Lyrics">
<Run Text="Taskbar-Lyrics" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Extensions">
<Run Text="CommunityToolkit.WinUI.Extensions" />
<Hyperlink NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<Run Text="..." />
</Hyperlink>
</Paragraph>
<!-- UI/UX 设计参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageUIUXRef" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/solstice23/refined-now-playing-netease">
<Run Text="refined-now-playing-netease" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Helpers">
<Run Text="CommunityToolkit.WinUI.Helpers" />
<Hyperlink NavigateUri="https://github.com/WXRIW/Lyricify-App">
<Run Text="Lyricify" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Media">
<Run Text="CommunityToolkit.WinUI.Media" />
<Hyperlink NavigateUri="https://moriafly.com/program/salt-player">
<Run Text="椒盐音乐 Salt Player" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/CommunityToolkit.WinUI.Triggers">
<Run Text="CommunityToolkit.WinUI.Triggers" />
<Hyperlink NavigateUri="https://github.com/TwilightLemon/MyToolBar">
<Run Text="MyToolBar" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/ComputeSharp.D2D1.WinUI">
<Run Text="ComputeSharp.D2D1.WinUI" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/csharp-kana">
<Run Text="csharp-kana" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/csharp-pinyin">
<Run Text="csharp-pinyin" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/DevWinUI.Controls">
<Run Text="DevWinUI.Controls" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Dubya.WindowsMediaController">
<Run Text="Dubya.WindowsMediaController" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/F23.StringSimilarity">
<Run Text="F23.StringSimilarity" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/H.NotifyIcon.WinUI">
<Run Text="H.NotifyIcon.WinUI" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Hqub.Last.fm">
<Run Text="Hqub.Last.fm" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Lyricify.Lyrics.Helper-NativeAot">
<Run Text="Lyricify.Lyrics.Helper-NativeAot" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection">
<Run Text="Microsoft.Extensions.DependencyInjection" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Microsoft.Extensions.Logging">
<Run Text="Microsoft.Extensions.Logging" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Microsoft.Graphics.Win2D">
<Run Text="Microsoft.Graphics.Win2D" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools">
<Run Text="Microsoft.Windows.SDK.BuildTools" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Microsoft.WindowsAppSDK">
<Run Text="Microsoft.WindowsAppSDK" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/NAudio.Wasapi">
<Run Text="NAudio.Wasapi" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Nito.AsyncEx">
<Run Text="Nito.AsyncEx" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Nito.AsyncEx.Tasks">
<Run Text="Nito.AsyncEx.Tasks" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/NTextCat">
<Run Text="NTextCat" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Serilog.Extensions.Logging">
<Run Text="Serilog.Extensions.Logging" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Serilog.Sinks.File">
<Run Text="Serilog.Sinks.File" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/System.Drawing.Common">
<Run Text="System.Drawing.Common" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/System.Text.Encoding.CodePages">
<Run Text="System.Text.Encoding.CodePages" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/TagLibSharp">
<Run Text="TagLibSharp" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Ude.NetStandard">
<Run Text="Ude.NetStandard" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Vanara.PInvoke.DwmApi">
<Run Text="Vanara.PInvoke.DwmApi" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Vanara.PInvoke.Gdi32">
<Run Text="Vanara.PInvoke.Gdi32" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Vanara.PInvoke.Shell32">
<Run Text="Vanara.PInvoke.Shell32" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Vanara.PInvoke.User32">
<Run Text="Vanara.PInvoke.User32" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/Vanara.Windows.Shell">
<Run Text="Vanara.Windows.Shell" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/VCollab.DiscordRichPresence">
<Run Text="VCollab.DiscordRichPresence" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/WinUIEx">
<Run Text="WinUIEx" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.nuget.org/packages/z440.atl.core">
<Run Text="z440.atl.core" />
<Hyperlink NavigateUri="">
<Run Text="" />
</Hyperlink>
</Paragraph>

View File

@@ -3,7 +3,6 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Threading.Tasks;
using Windows.UI.ApplicationSettings;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -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=&#xE71A;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.StopOnWindowClosed, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageExitOnGalleryWindowClosed" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE711;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.ExitOnWindowClosed, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -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=&#xE78B;}">
<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=&#xE711;}"
IsEnabled="{x:Bind LyricsWindowStatus.IsOpened, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SettingsPageCloseStatus" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</UserControl>

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -47,6 +47,43 @@
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPageAlbumArtLayer"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE93C;}"
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},

View File

@@ -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,14 +37,15 @@ 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>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
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 +372,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 +544,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 +596,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 +611,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();
@@ -642,22 +659,6 @@ namespace BetterLyrics.WinUI3.Controls
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
}
}
@@ -682,6 +683,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 +838,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 +893,15 @@ namespace BetterLyrics.WinUI3.Controls
}
}
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}
}
}
}
}

View File

@@ -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=&#xE89F;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsFadeOutEffectEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- 远离视野 -->
<dev:SettingsCard x:Uid="SettingsPageLyricsOutOfSightEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF19D;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsOutOfSightEffectEnabled, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- 辉光效果 -->
<dev:SettingsExpander x:Uid="SettingsPageLyricsGlowEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE9A9;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />

View File

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

View File

@@ -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);
}
}
}

View File

@@ -32,8 +32,8 @@
<dev:SettingsCard x:Uid="SettingsPageLyricsCenterTopOffset" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE78A;}">
<local:ExtendedSlider
Default="50"
Maximum="100"
Minimum="0"
Maximum="99"
Minimum="1"
Unit="%"
Value="{x:Bind LyricsStyleSettings.PlayingLineTopOffset, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -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>
@@ -37,6 +38,7 @@
<MenuFlyoutItem x:Uid="SettingsPageDockedMode" Command="{x:Bind ViewModel.CreateDockedLyricsWindowStatusCommand}" />
<MenuFlyoutItem x:Uid="SettingsPageFullscreenMode" Command="{x:Bind ViewModel.CreateFullLyricsWindowStatusCommand}" />
<MenuFlyoutItem x:Uid="SettingsPageNarrowMode" Command="{x:Bind ViewModel.CreateNarrowLyricsWindowStatusCommand}" />
<MenuFlyoutItem x:Uid="SettingsPageTaskbarMode" Command="{x:Bind ViewModel.CreateTaskbarLyricsWindowStatusCommand}" />
</MenuFlyout>
</Button.Flyout>
</Button>
@@ -98,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
@@ -115,7 +114,6 @@
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="0"
@@ -123,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>
@@ -194,26 +185,56 @@
Style="{StaticResource AccentButtonStyle}" />
</Grid>
<SelectorBar x:Name="ConfigSelectorBar" SelectionChanged="ConfigSelectorBar_SelectionChanged">
<controls:Segmented
x:Name="ConfigSegmented"
HorizontalAlignment="Stretch"
SelectionChanged="ConfigSegmented_SelectionChanged"
Style="{StaticResource PivotSegmentedStyle}">
<SelectorBarItem
x:Name="WindowSelectorBarItem"
x:Uid="AppSettingsControlGeneral"
Tag="Window" />
<SelectorBarItem
x:Name="LayoutSelectorBarItem"
x:Uid="SettingsPageLayout"
Tag="Layout" />
<SelectorBarItem
x:Name="AlbumArtStyleSelectorBarItem"
x:Uid="SettingsPageAlbumStyle"
Tag="AlbumArtStyle" />
<SelectorBarItem x:Uid="SettingsPageAlbumEffect" Tag="AlbumArtEffect" />
<SelectorBarItem x:Uid="SettingsPageLyricsStyle" Tag="LyricsStyle" />
<SelectorBarItem x:Uid="SettingsPageLyricsEffect" Tag="LyricsEffect" />
<SelectorBarItem x:Uid="SettingsPageBackgroundOverlay" Tag="LyricsBackground" />
<controls:SegmentedItem x:Name="WindowSegmentedItem" Tag="Window">
<TextBlock
x:Uid="AppSettingsControlGeneral"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
<controls:SegmentedItem x:Name="LayoutSegmentedItem" Tag="Layout">
<TextBlock
x:Uid="SettingsPageLayout"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
<controls:SegmentedItem x:Name="AlbumArtStyleSegmentedItem" Tag="AlbumArtStyle">
<TextBlock
x:Uid="SettingsPageAlbumStyle"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="AlbumArtEffect">
<TextBlock
x:Uid="SettingsPageAlbumEffect"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsStyle">
<TextBlock
x:Uid="SettingsPageLyricsStyle"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsEffect">
<TextBlock
x:Uid="SettingsPageLyricsEffect"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsBackground">
<TextBlock
x:Uid="SettingsPageBackgroundOverlay"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem>
</SelectorBar>
</controls:Segmented>
</StackPanel>
</Grid>

View File

@@ -6,6 +6,7 @@ using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
@@ -25,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()
{
@@ -151,61 +152,28 @@ namespace BetterLyrics.WinUI3.Controls
private void ConfigButton_Click(object sender, RoutedEventArgs e)
{
WindowSelectorBarItem.Visibility = LayoutSelectorBarItem.Visibility = Visibility.Visible;
ConfigSelectorBar.SelectedItem = WindowSelectorBarItem;
WindowSegmentedItem.IsEnabled = LayoutSegmentedItem.IsEnabled = true;
ConfigSegmented.SelectedItem = WindowSegmentedItem;
LyricsWindowStatus = (LyricsWindowStatus)((Button)sender).DataContext;
ViewModel.OpenConfigPanel();
}
private void EmbeddedConfigButton_Click(object sender, RoutedEventArgs e)
{
WindowSelectorBarItem.Visibility = LayoutSelectorBarItem.Visibility = Visibility.Collapsed;
ConfigSelectorBar.SelectedItem = AlbumArtStyleSelectorBarItem;
WindowSegmentedItem.IsEnabled = LayoutSegmentedItem.IsEnabled = false;
ConfigSegmented.SelectedItem = AlbumArtStyleSegmentedItem;
LyricsWindowStatus = _settingsService.AppSettings.MusicGallerySettings.LyricsWindowStatus;
ViewModel.OpenConfigPanel();
}
private void DemoWindowGrid_Tapped(object sender, TappedRoutedEventArgs e)
private void ConfigSegmented_SelectionChanged(object sender, SelectionChangedEventArgs 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);
}
ViewModel.SelectorBarSelectedItemTag = (string)((SegmentedItem)((Segmented)sender).SelectedItem).Tag;
}
private void ConfigSelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args)
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
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();
}
}
ViewModel.CloseConfigPanelCommand.Execute(null);
}
}
}

View File

@@ -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();
}

View File

@@ -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"
@@ -6,9 +5,11 @@
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:enums="using:BetterLyrics.WinUI3.Enums"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -49,42 +50,129 @@
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.ItemsHeader>
<StackPanel>
<!-- Index info -->
<InfoBar
IsClosable="False"
IsOpen="True"
Message="{x:Bind StatusText, Mode=OneWay}"
Severity="{x:Bind StatusSeverity, Mode=OneWay}" />
<ProgressBar
Background="Transparent"
Visibility="{x:Bind IsProcessing, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{x:Bind IndexingProgress, Mode=OneWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="IsIndeterminate" Value="True" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="IsIndeterminate" Value="False" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ProgressBar>
</StackPanel>
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<dev:SettingsCard 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="&#xE8B7;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="SMB"
Text="SMB">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE839;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="FTP"
Text="FTP">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE838;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="WebDAV"
Text="WebDAV">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE774;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</DropDownButton.Flyout>
</DropDownButton>
</dev:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
</UserControl>

View File

@@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;
using Windows.System;
// To learn more about WinUI, the WinUI project structure,
@@ -22,18 +23,14 @@ namespace BetterLyrics.WinUI3.Controls
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ViewModel.RemoveFolderAsync((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);
}
}
}

View File

@@ -11,10 +11,9 @@
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid
x:Name="BottomCommandGrid"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
Opacity="{x:Bind ViewModel.BottomCommandGridOpacity, Mode=OneWay}"
PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited">
@@ -113,7 +112,58 @@
x:Name="BottomCenterCommandStackPanel"
Padding="16"
Orientation="Horizontal"
Spacing="3">
Spacing="12">
<!-- Playback order -->
<Button
Grid.Column="2"
Click="PlaybackOrderButton_Click"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlaybackOrderButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<!-- 上一曲目 -->
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -164,6 +214,13 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<ToggleButton
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostToggleButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</Grid>
@@ -342,7 +399,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 +413,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>
@@ -427,7 +484,58 @@
</Grid.ContextFlyout>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PlaybackOrderState">
<VisualState x:Name="RepeatAll">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="1" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Visible" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="RepeatOne">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="1" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Visible" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Shuffle">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="1" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -1,5 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
@@ -12,6 +14,7 @@ using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Numerics;
using BetterLyrics.WinUI3.Extensions;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -46,6 +49,42 @@ public sealed partial class NowPlayingBar : UserControl,
public static readonly DependencyProperty ShowSongInfoProperty =
DependencyProperty.Register(nameof(ShowSongInfo), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlayingQueueButton
{
get { return (bool)GetValue(ShowPlayingQueueButtonProperty); }
set { SetValue(ShowPlayingQueueButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlayingQueueButtonProperty =
DependencyProperty.Register(nameof(ShowPlayingQueueButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlaybackOrderButton
{
get { return (bool)GetValue(ShowPlaybackOrderButtonProperty); }
set { SetValue(ShowPlaybackOrderButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public PlaybackOrder PlaybackOrder
{
get { return (PlaybackOrder)GetValue(PlaybackOrderProperty); }
set { SetValue(PlaybackOrderProperty, value); }
}
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsPlayingQueueOpened
{
get { return (bool)GetValue(IsPlayingQueueOpenedProperty); }
set { SetValue(IsPlayingQueueOpenedProperty, value); }
}
public static readonly DependencyProperty IsPlayingQueueOpenedProperty =
DependencyProperty.Register(nameof(IsPlayingQueueOpened), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool IsCompactMode
{
get { return (bool)GetValue(IsCompactModeProperty); }
@@ -190,7 +229,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)
@@ -210,12 +249,12 @@ public sealed partial class NowPlayingBar : UserControl,
private void SongInfoStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
SongInfoTapped?.Invoke(this, EventArgs.Empty);
SongInfoTapped?.Invoke(sender, EventArgs.Empty);
}
private void TimeStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
TimeTapped?.Invoke(this, EventArgs.Empty);
TimeTapped?.Invoke(sender, EventArgs.Empty);
}
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -262,6 +301,16 @@ public sealed partial class NowPlayingBar : UserControl,
}
}
private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
IsPlayingQueueOpened = !IsPlayingQueueOpened;
}
private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
{
PlaybackOrder = PlaybackOrder.GetNext();
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
@@ -290,10 +339,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;
}
}
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -0,0 +1,195 @@
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 FileSourceType _fileSourceType;
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public RemoteServerConfigControl(FileSourceType fileSourceType)
{
this.InitializeComponent();
_fileSourceType = fileSourceType;
SetupDefaults();
CheckPathForWarning();
}
private void SetupDefaults()
{
if (_fileSourceType == FileSourceType.Local)
{
RemoteFieldsPanel.Visibility = Visibility.Collapsed;
AuthFieldsPanel.Visibility = Visibility.Collapsed;
BrowseButton.Visibility = Visibility.Visible;
PathBox.PlaceholderText = @"D:\Music";
}
else
{
BrowseButton.Visibility = Visibility.Collapsed;
RemoteFieldsPanel.Visibility = Visibility.Visible;
AuthFieldsPanel.Visibility = Visibility.Visible;
switch (_fileSourceType)
{
case FileSourceType.SMB:
PortBox.Value = 445;
PathBox.PlaceholderText = "SharedMusic";
break;
case FileSourceType.FTP:
PortBox.Value = 21;
PathBox.PlaceholderText = "/pub/music";
break;
case FileSourceType.WebDAV:
PortBox.Value = 80;
PathBox.PlaceholderText = "/dav/music";
break;
}
}
}
private string GetScheme()
{
string scheme = string.Empty;
switch (_fileSourceType)
{
case FileSourceType.SMB:
scheme = "smb";
break;
case FileSourceType.FTP:
scheme = "ftp";
break;
case FileSourceType.WebDAV:
scheme = "https";
break;
}
return scheme;
}
public MediaFolder GetConfig()
{
string finalName = HostBox.Text.Trim();
if (_fileSourceType == FileSourceType.Local)
{
if (string.IsNullOrWhiteSpace(PathBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathRequired"));
if (!string.IsNullOrWhiteSpace(NameBox.Text))
finalName = NameBox.Text.Trim();
else
finalName = PathBox.Text.TrimEnd(System.IO.Path.DirectorySeparatorChar);
return new MediaFolder
{
Name = finalName,
SourceType = FileSourceType.Local,
UriScheme = "file",
UriPath = PathBox.Text.Trim(),
};
}
if (string.IsNullOrWhiteSpace(HostBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlServerAddressRequired"));
if (!string.IsNullOrWhiteSpace(NameBox.Text))
{
finalName = NameBox.Text.Trim();
}
else
{
finalName = $"{_fileSourceType} - {HostBox.Text}";
}
string scheme = GetScheme();
var folder = new MediaFolder
{
Name = finalName,
SourceType = _fileSourceType,
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 CheckPathForWarning()
{
string? path = PathBox.Text?.Trim();
bool isSymbolRoot = string.IsNullOrEmpty(path) ||
path == "/" ||
path == "\\";
bool isDriveRoot = false;
if (!string.IsNullOrEmpty(path))
{
var normalized = path.TrimEnd('\\', '/');
isDriveRoot = normalized.EndsWith(":") && normalized.Length == 2;
}
bool isRoot = isSymbolRoot || isDriveRoot;
if (isRoot)
{
PathWarningBar.Message = _localizationService.GetLocalizedString("FileSystemServiceRootDirectoryWarning");
PathWarningBar.IsOpen = true;
}
else
{
PathWarningBar.IsOpen = false;
}
}
private void PathBox_TextChanged(object sender, TextChangedEventArgs e)
{
CheckPathForWarning();
}
private async void BrowseButton_Click(object sender, RoutedEventArgs e)
{
try
{
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
if (folder != null)
{
PathBox.Text = folder.Path;
}
}
catch (Exception ex)
{
ShowError(ex.Message);
}
}
}
}

View File

@@ -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()
{

View File

@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.StatsDashboardControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:BetterLyrics.WinUI3.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:statsmodels="using:BetterLyrics.WinUI3.Models.Stats"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="StatsCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,12,12" />
</Style>
</UserControl.Resources>
<Grid Margin="0,24,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="20,0">
<Pivot x:Name="TimeRangePivot" SelectionChanged="Pivot_SelectionChanged">
<PivotItem x:Uid="StatsDashboardControlToday" Tag="Day" />
<PivotItem x:Uid="StatsDashboardControlThisWeek" Tag="Week" />
<PivotItem x:Uid="StatsDashboardControlThisMonth" Tag="Month" />
<PivotItem x:Uid="StatsDashboardControlThisQuarter" Tag="Quarter" />
<PivotItem x:Uid="StatsDashboardControlThisYear" Tag="Year" />
</Pivot>
</Grid>
<ScrollViewer Grid.Row="1" Padding="20,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 总播放时长 -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE916;" />
<TextBlock x:Uid="StatsDashboardControlTotalDuration" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<StackPanel
Margin="0,8,0,0"
Orientation="Horizontal"
Spacing="4">
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay, Converter={StaticResource DoubleToDecimalConverter}}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="Hrs" />
</StackPanel>
</StackPanel>
</Border>
<!-- 总播放歌曲次数 -->
<Border Grid.Column="1" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE8D6;" />
<TextBlock x:Uid="StatsDashboardControlTracksPlayed" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalTracksPlayed, Mode=OneWay}" />
</StackPanel>
</Border>
<!-- Top source -->
<Border
Grid.Column="2"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xEC4A;" />
<TextBlock x:Uid="StatsDashboardControlTopSource" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TopPlayerName, Mode=OneWay}" />
</StackPanel>
</Border>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopArtists"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10">
<PersonPicture
Width="32"
Height="32"
DisplayName="{x:Bind Artist}" />
<TextBlock
VerticalAlignment="Center"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
<!-- Top sources -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.PlayerStats, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PlayerStatDisplayItem">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</Grid>
<!-- Top song -->
<Border
Grid.Row="2"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ListView
Grid.Row="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Width="40"
Height="40"
Margin="0,0,12,0"
Background="{ThemeResource LayerFillColorAltBrush}"
CornerRadius="4">
<FontIcon
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xE8D6;" />
</Grid>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Style="{ThemeResource BodyStrongTextBlockStyle}" Text="{x:Bind Title}" />
<TextBlock
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Border>
</Grid>
</ScrollViewer>
<Button
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data"
Visibility="Collapsed" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using BetterLyrics.WinUI3.Enums;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls;
public sealed partial class StatsDashboardControl : UserControl
{
public StatsDashboardControlViewModel ViewModel => (StatsDashboardControlViewModel)DataContext;
public StatsDashboardControl()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<StatsDashboardControlViewModel>();
this.Loaded += StatsDashboardControl_Loaded;
}
private async void StatsDashboardControl_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
await ViewModel.LoadDataAsync(StatsRange.Day);
}
private async void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ViewModel == null) return;
if (TimeRangePivot.SelectedItem is PivotItem item && item.Tag is string tag)
{
var range = tag switch
{
"Day" => StatsRange.Day,
"Week" => StatsRange.Week,
"Month" => StatsRange.Month,
"Quarter" => StatsRange.Quarter,
"Year" => StatsRange.Year,
_ => StatsRange.Day
};
await ViewModel.LoadDataAsync(range);
}
}
}

View File

@@ -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"

View File

@@ -18,13 +18,7 @@
<TextBlock x:Uid="AppSettingsControlGeneral" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageConfigName" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8AC;}">
<StackPanel
Margin="0,6,0,0"
Orientation="Horizontal"
Spacing="6">
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay}" TextWrapping="Wrap" />
<Button Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, FontSize=12, Glyph=&#xE8FB;}" Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
<TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
</dev:SettingsCard>
<dev:SettingsExpander
@@ -86,6 +80,26 @@
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsExpander
x:Uid="SettingsPagePinToTaskbar"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7C4;}"
IsExpanded="{x:Bind LyricsWindowStatus.IsPinToTaskbar, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind LyricsWindowStatus.IsPinToTaskbar, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageTaskbarPlacement">
<ComboBox SelectedIndex="{x:Bind LyricsWindowStatus.TaskbarPlacement, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLeft" />
<ComboBoxItem x:Uid="SettingsPageRight" />
</ComboBox>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageAlwaysHideUnlockButton" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE785;}">
<ToggleSwitch IsOn="{x:Bind LyricsWindowStatus.IsAlwaysHideUnlockButton, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsExpander
x:Uid="SettingsPageAOT"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},

View File

@@ -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}"),
};

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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",
};
}

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

@@ -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();
}
}
}

View File

@@ -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",
};

View File

@@ -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",
};
}

View File

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

View File

@@ -0,0 +1,10 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum FileSourceType
{
Local,
SMB,
FTP,
WebDAV
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum StatsRange
{
Day,
Week,
Month,
Quarter,
Year
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum TaskbarPlacement
{
Left,
Right,
}
}

View File

@@ -12,6 +12,7 @@
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
BetterLyrics
BetterLyrics,
CutletDocker
}
}

View File

@@ -0,0 +1,14 @@
using System;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Events
{
public class TaskbarFreeBoundsChangedEventArgs : EventArgs
{
public Rect TaskbarFreeBounds { get; }
public TaskbarFreeBoundsChangedEventArgs(Rect taskbarBounds)
{
TaskbarFreeBounds = taskbarBounds;
}
}
}

View File

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

View File

@@ -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,19 +12,19 @@ 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,
IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true,
IsLocked = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
EnvironmentSampleMode = WindowPixelSampleMode.WindowEdge,
@@ -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,
@@ -52,7 +52,6 @@ namespace BetterLyrics.WinUI3.Extensions
IsShownInSwitchers = false,
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
EnvironmentSampleMode = WindowPixelSampleMode.BelowWindow,
TitleBarArea = TitleBarArea.None,
LyricsStyleSettings = new LyricsStyleSettings
{
LyricsAlignmentType = TextAlignmentType.Center,
@@ -72,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
{
@@ -94,7 +93,7 @@ namespace BetterLyrics.WinUI3.Extensions
window ??= WindowHook.GetWindow<SystemTrayWindow>();
return new LyricsWindowStatus(window)
{
Name = _resourceService.GetLocalizedString("StandardMode"),
Name = _localizationService.GetLocalizedString("StandardMode"),
};
}
@@ -103,10 +102,35 @@ 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,
};
}
public static LyricsWindowStatus TaskbarMode(Window? window = null)
{
window ??= WindowHook.GetWindow<SystemTrayWindow>();
return new LyricsWindowStatus(window)
{
Name = _localizationService.GetLocalizedString("TaskbarMode"),
LyricsDisplayType = LyricsDisplayType.LyricsOnly,
IsPinToTaskbar = true,
IsLocked = true,
IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true,
IsAdaptToEnvironment = true,
IsShownInSwitchers = false,
EnvironmentSampleMode = WindowPixelSampleMode.WindowEdge,
LyricsStyleSettings = new()
{
LyricsAlignmentType = TextAlignmentType.Center,
},
LyricsBackgroundSettings = new LyricsBackgroundSettings
{
IsFluidOverlayEnabled = false,
}
};
}
}
}

View File

@@ -1,4 +1,5 @@
using Windows.Foundation;
using Vanara.PInvoke;
using Windows.Foundation;
using Windows.Graphics;
namespace BetterLyrics.WinUI3.Extensions
@@ -77,5 +78,15 @@ namespace BetterLyrics.WinUI3.Extensions
);
}
}
extension(RECT rect)
{
public Rect ToRect() => new(
rect.Left,
rect.Top,
rect.Right - rect.Left,
rect.Bottom - rect.Top
);
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Drawing;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Extensions
{
public static class RectangleExtensions
{
extension(Rectangle rect)
{
public Rect ToRect() => new(
rect.Left,
rect.Top,
rect.Right - rect.Left,
rect.Bottom - rect.Top
);
}
}
}

View File

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

View File

@@ -1,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);
}
}
}

View File

@@ -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();

View File

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

View File

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

View File

@@ -1,4 +1,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();
}
}
}

View File

@@ -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

View File

@@ -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) { }
}
}
}

View File

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

View File

@@ -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();

View File

@@ -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();

View File

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

View File

@@ -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();
}
}
}
}

View File

@@ -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();

View File

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

View File

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

View File

@@ -1,24 +1,213 @@
using Microsoft.UI.Xaml;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Extensions;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Definitions;
using FlaUI.Core.EventHandlers;
using FlaUI.UIA3;
using Microsoft.UI.Dispatching;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
namespace BetterLyrics.WinUI3.Hooks
{
public class TaskbarHook
public partial class TaskbarHook : IDisposable
{
private readonly DispatcherTimer _timer;
private readonly UIA3Automation _automation;
private AutomationElement? _taskbar;
public Action<EventArgs> OnTaskbarBoundsChanged;
private StructureChangedEventHandlerBase? _structureHandler;
private PropertyChangedEventHandlerBase? _propertyHandler;
public TaskbarHook()
private TaskbarPlacement _currentPlacement;
private readonly DispatcherQueue _dispatcherQueue;
private readonly Action<TaskbarFreeBoundsChangedEventArgs> _onLayoutChanged;
private Timer? _debounceTimer;
private const int DebounceDelay = 150;
private bool _isDisposed;
public TaskbarHook(TaskbarPlacement placement, Action<TaskbarFreeBoundsChangedEventArgs> onLayoutChanged)
{
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromMilliseconds(50);
_timer.Tick += Timer_Tick;
_automation = new UIA3Automation();
_onLayoutChanged = onLayoutChanged;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_currentPlacement = placement;
StartHook();
}
private void Timer_Tick(object? sender, object e)
public void UpdatePlacement(TaskbarPlacement newPlacement)
{
_currentPlacement = newPlacement;
RequestUpdate(); // 立即刷新位置
}
private void StartHook()
{
try
{
var desktop = _automation.GetDesktop();
_taskbar = desktop.FindFirstChild(cf => cf.ByClassName("Shell_TrayWnd"));
if (_taskbar == null) return;
// 监听结构变化
// 这里的返回值就是一个可以 Dispose 的对象
_structureHandler = _taskbar.RegisterStructureChangedEvent(
TreeScope.Descendants,
(element, type, id) => RequestUpdate());
// 监听属性变化
_propertyHandler = _taskbar.RegisterPropertyChangedEvent(
TreeScope.Element,
(element, id, val) => RequestUpdate(),
_automation.PropertyLibrary.Element.BoundingRectangle);
RequestUpdate();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Hook Init Failed: {ex.Message}");
}
}
private void RequestUpdate()
{
if (_isDisposed) return;
_debounceTimer?.Dispose();
_debounceTimer = new Timer(_ =>
{
Rectangle voidRect = CalculateVoidRect(_currentPlacement);
_dispatcherQueue.TryEnqueue(() =>
{
if (!_isDisposed && voidRect != Rectangle.Empty)
{
_onLayoutChanged?.Invoke(new TaskbarFreeBoundsChangedEventArgs(voidRect.ToRect()));
}
});
}, null, DebounceDelay, Timeout.Infinite);
}
private Rectangle CalculateVoidRect(TaskbarPlacement placement)
{
try
{
if (_taskbar == null) return Rectangle.Empty;
try { var _ = _taskbar.BoundingRectangle; }
catch
{
var desktop = _automation.GetDesktop();
_taskbar = desktop.FindFirstChild(cf => cf.ByClassName("Shell_TrayWnd"));
if (_taskbar == null) return Rectangle.Empty;
}
Rectangle taskbarRect = _taskbar.BoundingRectangle;
// 绝对右边界:托盘
int barrierRight = taskbarRect.Right;
var tray = _taskbar.FindFirstDescendant(cf => cf.ByAutomationId("SystemTrayIcon")); // Win11
if (tray == null) tray = _taskbar.FindFirstDescendant(cf => cf.ByClassName("TrayNotifyWnd")); // Win10
if (tray != null) barrierRight = tray.BoundingRectangle.Left;
// 绝对左边界:任务栏左边缘 或 小组件(Win11)
int barrierLeft = taskbarRect.Left;
var widgets = _taskbar.FindFirstDescendant(cf => cf.ByAutomationId("WidgetsButton"));
// 只有当小组件确实在最左侧时 (Win11默认),它才构成左边界
// 如果用户把任务栏设为靠左对齐,小组件会在开始按钮右边,这时候不把它当做左边界
if (widgets != null && widgets.BoundingRectangle.Left < taskbarRect.Left + 100)
{
barrierLeft = (int)widgets.BoundingRectangle.Right;
}
// 寻找 中间内容区域 (Start + Search + Apps) 的 左右极值
int contentMinLeft = barrierRight;
int contentMaxRight = barrierLeft;
// 定义所有中间元素
string[] systemButtonIds = new[] {
"StartButton", "SearchButton", "TaskViewButton", "ChatButton"
};
// 系统按钮
foreach (var id in systemButtonIds)
{
var btn = _taskbar.FindFirstDescendant(cf => cf.ByAutomationId(id));
if (btn != null)
{
var rect = btn.BoundingRectangle;
// 排除不可见的
if (rect.Width <= 0) continue;
// 更新极值
if (rect.Left < contentMinLeft) contentMinLeft = (int)rect.Left;
if (rect.Right > contentMaxRight) contentMaxRight = (int)rect.Right;
}
}
// App 图标
var appIcons = _taskbar.FindAllDescendants(cf => cf.ByClassName("Taskbar.TaskListButtonAutomationPeer"));
foreach (var icon in appIcons)
{
var rect = icon.BoundingRectangle;
if (rect.Width <= 0) continue;
if (rect.Left < contentMinLeft) contentMinLeft = (int)rect.Left;
if (rect.Right > contentMaxRight) contentMaxRight = (int)rect.Right;
}
// 如果完全没找到内容,重置为中间
if (contentMinLeft == barrierRight) contentMinLeft = taskbarRect.Left;
if (contentMaxRight == barrierLeft) contentMaxRight = taskbarRect.Left;
int finalLeft, finalRight;
int padding = 10;
if (placement == TaskbarPlacement.Left)
{
// 【小组件】... [空隙] ...【开始按钮】
// 如果是 Win10 或 Win11左对齐contentMinLeft 几乎等于 barrierLeft空隙为0
finalLeft = barrierLeft + padding;
finalRight = contentMinLeft - padding;
}
else // Right
{
// 【最后一个图标】... [空隙] ...【托盘】
finalLeft = contentMaxRight + padding;
finalRight = barrierRight - padding;
}
int width = finalRight - finalLeft;
if (width < 20) return Rectangle.Empty;
var finalRect = new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height);
return finalRect;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Calc Rect Error: {ex.Message}");
return Rectangle.Empty;
}
}
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
_debounceTimer?.Dispose();
_structureHandler?.Dispose();
_propertyHandler?.Dispose();
_automation?.Dispose();
}
}
}
}

View File

@@ -23,8 +23,8 @@ namespace BetterLyrics.WinUI3.Hooks
private static List<object> _activeWindows = [];
private static List<object> _workAreas = [];
private static readonly Dictionary<HWND, WindowStyle> _defaultWindowStyle = [];
private static readonly Dictionary<HWND, ExtendedWindowStyle> _defaultExtendedWindowStyle = [];
private static WindowStyle? _defaultWindowStyle;
private static ExtendedWindowStyle? _defaultExtendedWindowStyle;
public static void HideWindow(this Window window)
{
@@ -81,9 +81,9 @@ namespace BetterLyrics.WinUI3.Hooks
{
return frameworkElement.XamlRoot.ContentIslandEnvironment.AppWindowId.GetWindowHandle();
}
else if (obj != null)
else if (obj is Window window)
{
return WindowNative.GetWindowHandle(obj);
return WindowNative.GetWindowHandle(window);
}
else
{
@@ -141,17 +141,19 @@ namespace BetterLyrics.WinUI3.Hooks
TrackWindow(window);
var castedWindow = (Window)window;
if (typeof(T) != typeof(SystemTrayWindow))
castedWindow.Restore();
castedWindow.Activate();
if (typeof(T) == typeof(SystemTrayWindow))
{
castedWindow.Restore();
castedWindow.Activate();
_defaultWindowStyle = castedWindow.GetWindowStyle();
_defaultExtendedWindowStyle = castedWindow.GetExtendedWindowStyle();
castedWindow.HideWindow();
}
if (typeof(T) == typeof(NowPlayingWindow))
{
var hwnd = WindowNative.GetWindowHandle(castedWindow);
_defaultWindowStyle.Add(hwnd, castedWindow.GetWindowStyle());
_defaultExtendedWindowStyle.Add(hwnd, castedWindow.GetExtendedWindowStyle());
var lyricsWindow = (NowPlayingWindow)window;
lyricsWindow.InitStatus();
@@ -222,14 +224,7 @@ namespace BetterLyrics.WinUI3.Hooks
private static void WindowHelper_Closed(object sender, WindowEventArgs args)
{
if (_activeWindows.Contains(sender))
{
_activeWindows.Remove(sender);
var hwnd = WindowNative.GetWindowHandle(sender);
_defaultWindowStyle.Remove(hwnd);
_defaultExtendedWindowStyle.Remove(hwnd);
}
_activeWindows.Remove(sender);
}
public static void SetIsWorkArea(this NowPlayingWindow window, bool enable)
@@ -256,9 +251,7 @@ namespace BetterLyrics.WinUI3.Hooks
public static void SetIsClickThrough(this Window window, bool enable)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (_defaultExtendedWindowStyle.TryGetValue(hwnd, out var style))
if (_defaultExtendedWindowStyle is ExtendedWindowStyle style)
{
if (enable)
{
@@ -273,9 +266,7 @@ namespace BetterLyrics.WinUI3.Hooks
public static void SetIsBorderless(this Window window, bool enable)
{
var hwnd = WindowNative.GetWindowHandle(window);
if (_defaultWindowStyle.TryGetValue(hwnd, out var style))
if (_defaultWindowStyle is WindowStyle style)
{
if (enable)
{
@@ -288,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)
{
@@ -314,10 +309,14 @@ namespace BetterLyrics.WinUI3.Hooks
{
window.Restore();
}
return true;
}
public static void SetIsShowInSwitchers(this Window window, bool enable)
{
if (window.AppWindow == null) return;
window.AppWindow.IsShownInSwitchers = enable;
}
@@ -333,6 +332,8 @@ namespace BetterLyrics.WinUI3.Hooks
public static void MoveAndResize(this Window window, Rect rect)
{
if (window.AppWindow == null) return;
window.AppWindow.Move(new Windows.Graphics.PointInt32((int)rect.X, (int)rect.Y));
window.AppWindow.Resize(new Windows.Graphics.SizeInt32((int)rect.Width, (int)rect.Height));
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models
{
public class CutletDockerRequest
{
[JsonPropertyName("text")]
public string Text { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models
{
public class CutletDockerResponse
{
[JsonPropertyName("romaji")]
public string RomajiText { get; set; }
}
}

View File

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

View File

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

View File

@@ -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(FilesIndexItem? 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) { }
}
}
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
namespace BetterLyrics.WinUI3.Models
{
public class TranslateResponse
public class LibreTranslateResponse
{
[JsonPropertyName("translatedText")]
public string TranslatedText { get; set; }

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -22,6 +22,11 @@ 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;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsMaximized { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFullscreen { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsLayoutOrientation LyricsLayoutOrientation { get; set; } = LyricsLayoutOrientation.Horizontal;
@@ -48,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();
@@ -137,6 +201,11 @@ namespace BetterLyrics.WinUI3.Models
IsAlwaysOnTopPolling = this.IsAlwaysOnTopPolling,
IsShownInSwitchers = this.IsShownInSwitchers,
IsLocked = this.IsLocked,
IsAlwaysHideUnlockButton = this.IsAlwaysHideUnlockButton,
IsPinToTaskbar = this.IsPinToTaskbar,
TaskbarPlacement = this.TaskbarPlacement,
IsMaximized = this.IsMaximized,
IsFullscreen = this.IsFullscreen,

View 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 Microsoft.UI.Xaml.Controls;
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;
[JsonPropertyName("Path")]
[ObservableProperty]
[NotifyPropertyChangedRecipients]
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
[NotifyPropertyChangedFor(nameof(UriString))]
public partial string UriPath { get; set; }
[JsonIgnore] public string Password { get; set; }
[JsonIgnore] public bool IsLocal => SourceType == FileSourceType.Local;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsProcessing { get; set; } = false;
[ObservableProperty] public partial double IndexingProgress { get; set; } = 0;
[ObservableProperty] public partial string StatusText { get; set; } = "";
[ObservableProperty] public partial InfoBarSeverity StatusSeverity { get; set; } = InfoBarSeverity.Informational;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DateTime? LastSyncTime { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AutoScanInterval ScanInterval { get; set; } = AutoScanInterval.Disabled;
public Uri GetStandardUri()
{
try
{
if (IsLocal)
{
return new Uri(UriPath);
}
var builder = new UriBuilder
{
Scheme = UriScheme ?? "file",
Host = UriHost,
Port = UriPort,
};
if (!string.IsNullOrEmpty(UriPath))
{
string cleanPath = UriPath.Replace("\\", "/");
if (!cleanPath.StartsWith("/")) cleanPath = "/" + cleanPath;
builder.Path = cleanPath;
}
return builder.Uri;
}
catch (Exception)
{
return new Uri("about:blank");
}
}
// 例smb://user@host:445/share/path
[JsonIgnore]
public string UriString => GetStandardUri().AbsoluteUri;
[JsonIgnore]
public string ConnectionSummary
{
get
{
if (IsLocal) return 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()
};
}
}
}

View File

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

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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; } = [];

View File

@@ -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,
};
}
}

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