Compare commits

...

326 Commits

Author SHA1 Message Date
Zhe Fang
4e33444d8e Update README.md 2026-01-09 19:26:31 -05:00
Zhe Fang
8fc67711b6 Simplify language selection in README.CN.md
Removed redundant link from Chinese README.
2026-01-09 19:26:16 -05:00
Zhe Fang
28757d9880 chores: bump to 253 2026-01-09 18:09:45 -05:00
Zhe Fang
e5fb04f577 fix: ValueTransition init 2026-01-09 17:54:50 -05:00
Zhe Fang
9d03c8f688 chores: i18n 2026-01-09 17:39:35 -05:00
Zhe Fang
094fe7b7a1 fix: lyrics animation gpu usage 2026-01-09 17:31:07 -05:00
Zhe Fang
bc32a3f34c feat: add all time filter for stats 2026-01-09 15:27:00 -05:00
Zhe Fang
b23d3c280f fix: not found and loading lyrics placeholder was not displayed 2026-01-09 14:48:12 -05:00
Zhe Fang
2738d45b69 fix: fill Duration (via searching NCM) for amll-ttml-db lyrics source; improve matching system 2026-01-09 13:50:46 -05:00
Zhe Fang
77a9bb0a1b Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-08 21:41:57 -05:00
Zhe Fang
c07389acfb chores: add new sponsor to AboutControl 2026-01-08 21:41:56 -05:00
Zhe Fang
042229ae74 Add new sponsor to SPONSORS.md 2026-01-08 21:40:00 -05:00
Zhe Fang
caaf93cf27 chores: i18n 2026-01-08 21:33:37 -05:00
Zhe Fang
92e4b9468c feat: enhance ValueTransition (support keyframes) 2026-01-08 20:58:12 -05:00
Zhe Fang
6f60952d09 fix: lyrics cache was not updated when searching without enabling cache, lyrics animation issue 2026-01-08 16:21:44 -05:00
Zhe Fang
efc175668e chores: remove unused files 2026-01-07 21:03:16 -05:00
Zhe Fang
3bf0fbef5f chores: code cleanup 2026-01-07 20:49:20 -05:00
Zhe Fang
96b7835e8f chores: re-structure Models folder 2026-01-07 20:47:08 -05:00
Zhe Fang
a0b6511a53 chores: extra data update algo from draw method to update method 2026-01-07 20:08:28 -05:00
Zhe Fang
3947050d6f fix: lyrics cache dir was not created when first launch 2026-01-07 07:12:50 -05:00
Zhe Fang
707d85bc75 fix: database create issue 2026-01-07 06:58:02 -05:00
Zhe Fang
78bafb8508 fix: sponsor date 2026-01-06 22:45:09 -05:00
Zhe Fang
b4d24c5570 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-06 22:43:07 -05:00
Zhe Fang
83c9f9806d chores: update sponsors 2026-01-06 22:43:06 -05:00
Zhe Fang
adde74afb0 Update SPONSORS.md 2026-01-06 22:39:10 -05:00
Zhe Fang
67b4d4e409 chores: delete unused pre-defined path 2026-01-06 21:43:30 -05:00
Zhe Fang
8d7fbe63c5 fix: amll-ttml-db lyrics searching issue (due to metadata matching calculator error) 2026-01-06 16:43:16 -05:00
Zhe Fang
5037b92913 chores: change lyrics cache system from .json based to database based 2026-01-06 11:17:06 -05:00
Zhe Fang
c1ee7a6779 feat: add bg support for lrc (extended) 2026-01-05 19:43:41 -05:00
Zhe Fang
7ddfd1118b feat: support bg lyrics 2026-01-05 19:32:30 -05:00
Zhe Fang
97f20decf2 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-05 09:54:59 -05:00
Zhe Fang
81eb4e1c96 chores: update patron name 2026-01-05 09:54:57 -05:00
Zhe Fang
00ee4a051c Update SPONSORS.md 2026-01-05 09:17:36 -05:00
Zhe Fang
a13bb6e8e4 chores: bump to 1.2.243.0 2026-01-04 16:43:02 -05:00
Zhe Fang
0b436c1ea9 chores: rollback UpdateLyrics 2026-01-04 16:21:33 -05:00
Zhe Fang
5d332fdfc6 fix: media sessions record issue 2026-01-04 16:02:11 -05:00
Zhe Fang
572d2cd8ba fix 2026-01-04 14:54:15 -05:00
Zhe Fang
1e5a95c55e chores: bump to v1.2.240.0 2026-01-04 12:05:53 -05:00
Zhe Fang
18ce6d3a57 chores: improve thanks list 2026-01-04 11:39:47 -05:00
Zhe Fang
427aed6857 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-04 09:51:53 -05:00
Zhe Fang
ebfa484a2e fix: GSMTC 2026-01-04 09:51:51 -05:00
Zhe Fang
3ef9d81bea Update README.CN.md 2026-01-04 06:45:25 -05:00
Zhe Fang
e999d07834 Update README.md 2026-01-04 06:43:52 -05:00
Zhe Fang
838b8de94f Update README.md 2026-01-04 06:43:20 -05:00
Zhe Fang
b3059dbeb1 chores: improve GSMTC service 2026-01-03 22:02:08 -05:00
Zhe Fang
6fea88a6a1 fix: stats dashboard ui 2026-01-03 17:27:47 -05:00
Zhe Fang
abca9ae5fb fix: auto-play in music gallery wont show song title and artist when first play 2026-01-03 12:50:33 -05:00
Zhe Fang
a062897e1a fix: music gallery play issue (and improve ui/ux) 2026-01-03 12:14:54 -05:00
Zhe Fang
8b4748df1b fix: stats dashboard data selection issue 2026-01-02 12:05:10 -05:00
Zhe Fang
1df5ea6bab fix: playback realtime info encoded cache path 2026-01-02 08:27:03 -05:00
Zhe Fang
c576635af2 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-01 21:49:33 -05:00
Zhe Fang
c8590202ec chores: improve music stats 2026-01-01 21:49:31 -05:00
Zhe Fang
2dc8b1283f Update README.CN.md 2026-01-01 12:38:31 -05:00
Zhe Fang
c482edea0f Update README.md 2026-01-01 12:36:39 -05:00
Zhe Fang
315722252c Update README.md 2026-01-01 12:32:01 -05:00
Zhe Fang
32ba453264 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 16:30:16 -05:00
Zhe Fang
d4902329bb chores: update readme 2025-12-31 16:30:14 -05:00
Zhe Fang
83aee8948b Update README.CN.md 2025-12-31 16:13:48 -05:00
Zhe Fang
1f9fab3228 Update README.CN.md 2025-12-31 16:10:42 -05:00
Zhe Fang
7a3a659dfc Update README.CN.md 2025-12-31 16:09:17 -05:00
Zhe Fang
a14afd3eb5 Update README.md 2025-12-31 16:01:37 -05:00
Zhe Fang
c2af7f3186 Update README.CN.md 2025-12-31 16:00:16 -05:00
Zhe Fang
cd026dd2bf Update README.CN.md 2025-12-31 15:56:08 -05:00
Zhe Fang
4bc1a9975d Update README.md 2025-12-31 15:52:05 -05:00
Zhe Fang
07eecf0930 Update README.md 2025-12-31 15:49:22 -05:00
Zhe Fang
35fba5abb0 chores 2025-12-31 15:44:58 -05:00
Zhe Fang
03ef231a3f chores 2025-12-31 15:39:44 -05:00
Zhe Fang
f41879f4e5 chores: add pic 2025-12-31 15:38:18 -05:00
Zhe Fang
bda7510ed6 chores: update readme 2025-12-31 14:28:54 -05:00
Zhe Fang
5ec8c7c61f chores: bump to 1.2.236 2025-12-31 13:32:45 -05:00
Zhe Fang
7e6bd9dade fix: _scrobbleStopwatch 2025-12-31 10:55:56 -05:00
Zhe Fang
56244cb793 fix: scrollable timer 2025-12-31 10:47:43 -05:00
Zhe Fang
cb5f70ab55 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 10:12:29 -05:00
Zhe Fang
3cc018bb1f chores: bump to 1.2.234.0 2025-12-31 10:12:28 -05:00
Zhe Fang
c517d2b008 Update README.md 2025-12-31 10:08:58 -05:00
Zhe Fang
e79f2a0223 Update README.CN.md 2025-12-31 10:07:53 -05:00
Zhe Fang
39122b9147 Update README.CN.md 2025-12-31 10:07:01 -05:00
Zhe Fang
accbdc1806 Update README.md 2025-12-31 10:06:01 -05:00
Zhe Fang
de014d1ad7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 09:52:12 -05:00
Zhe Fang
cc2ce5f8cf chores: i18n 2025-12-31 09:52:10 -05:00
Zhe Fang
2a2d80436e Update README.CN.md 2025-12-31 09:34:29 -05:00
Zhe Fang
ce3f79f35c Update README.md 2025-12-31 09:33:01 -05:00
Zhe Fang
12e6000cb3 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:43:44 -05:00
Zhe Fang
c1dc684411 fix: scrobble timer is still running when music is paused 2025-12-31 08:43:43 -05:00
Zhe Fang
69ea2cb495 Update README.md 2025-12-31 08:21:33 -05:00
Zhe Fang
e2ee03c4be Update README.CN.md 2025-12-31 08:21:03 -05:00
Zhe Fang
c6fe33d6ae Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:12:12 -05:00
Zhe Fang
7744e145fa chores: i18n 2025-12-31 08:12:10 -05:00
Zhe Fang
0284b1de81 Update README.CN.md 2025-12-30 21:46:32 -05:00
Zhe Fang
108c2cd34b Update README.md 2025-12-30 21:45:56 -05:00
Zhe Fang
390e30f7f5 chores: bump to v1.2.232.0 2025-12-30 20:54:40 -05:00
Zhe Fang
900774668d chores: i18n 2025-12-30 20:09:02 -05:00
Zhe Fang
6ca2d1f897 chores 2025-12-30 19:49:54 -05:00
Zhe Fang
164bd077b8 fix: app crash when audio device was not found 2025-12-30 14:30:10 -05:00
Zhe Fang
8ec71fcfb7 chores: update StatsDashboardControl 2025-12-30 14:04:08 -05:00
Zhe Fang
f39ad54df8 fix: record play history 2025-12-30 13:02:23 -05:00
Zhe Fang
9b809983df chores: i18n 2025-12-30 08:59:30 -05:00
Zhe Fang
8006b3a443 chores: i18n 2025-12-30 08:41:12 -05:00
Zhe Fang
26a7454de2 feat: play history 2025-12-30 08:02:48 -05:00
Zhe Fang
0793a074cf fix: add to playlists 2025-12-29 16:11:21 -05:00
Zhe Fang
125bf1682e chores 2025-12-29 12:15:39 -05:00
Zhe Fang
48bdffb2fe Merge pull request #179 from jayfunc/l10n_dev
New Crowdin updates
2025-12-28 21:37:57 -05:00
Zhe Fang
d324a7552f New translations resources.resw (Chinese Traditional) 2025-12-28 21:37:17 -05:00
Zhe Fang
78c308c393 New translations resources.resw (Japanese) 2025-12-28 21:37:15 -05:00
Zhe Fang
a1bba00db6 chores: i18n 2025-12-28 21:31:37 -05:00
Zhe Fang
0787f5b111 fix 2025-12-28 21:10:02 -05:00
Zhe Fang
884026594b fix: ignore system, hidden files and unsupported format files 2025-12-28 21:03:56 -05:00
Zhe Fang
b0a777db8d chores: adjust layout and fix bugs 2025-12-28 20:01:41 -05:00
Zhe Fang
83f3a3bd6d chores: fix SMB and local file system, add auto-sync, improve lyruics search, album art search, local music gallery load speed (after 1st time) 2025-12-27 15:25:49 -05:00
Zhe Fang
bfb2ed29e5 fix: window title is synced with config name 2025-12-25 13:44:43 -05:00
Zhe Fang
131a0f0eb1 Merge pull request #172 from jayfunc/l10n_dev
New Crowdin updates
2025-12-24 19:24:20 -05:00
Zhe Fang
ac2a7b3f7b New translations resources.resw (Malay) 2025-12-24 19:22:52 -05:00
Zhe Fang
36eea7f8f2 New translations resources.resw (Hindi) 2025-12-24 19:22:52 -05:00
Zhe Fang
6b338deb55 New translations resources.resw (Thai) 2025-12-24 19:22:51 -05:00
Zhe Fang
af323ecd00 New translations resources.resw (Indonesian) 2025-12-24 19:22:50 -05:00
Zhe Fang
c79d01c75b New translations resources.resw (Vietnamese) 2025-12-24 19:22:49 -05:00
Zhe Fang
b51ec1e60f New translations resources.resw (Chinese Traditional) 2025-12-24 19:22:48 -05:00
Zhe Fang
7fe925bcba New translations resources.resw (Chinese Simplified) 2025-12-24 19:22:47 -05:00
Zhe Fang
0626472d66 New translations resources.resw (Russian) 2025-12-24 19:22:46 -05:00
Zhe Fang
33099bc186 New translations resources.resw (Portuguese) 2025-12-24 19:22:45 -05:00
Zhe Fang
e653efc227 New translations resources.resw (Korean) 2025-12-24 19:22:44 -05:00
Zhe Fang
074fef3faf New translations resources.resw (Japanese) 2025-12-24 19:22:43 -05:00
Zhe Fang
029cbbd343 New translations resources.resw (German) 2025-12-24 19:22:42 -05:00
Zhe Fang
802b2a4c1c New translations resources.resw (Arabic) 2025-12-24 19:22:41 -05:00
Zhe Fang
eccc4d519c New translations resources.resw (Spanish) 2025-12-24 19:22:39 -05:00
Zhe Fang
5f274ea28a New translations resources.resw (French) 2025-12-24 19:22:37 -05:00
Zhe Fang
aa1a1f5d58 chores: i18n 2025-12-24 19:22:16 -05:00
Zhe Fang
3a56d53487 New translations resources.resw (Malay) 2025-12-24 19:15:10 -05:00
Zhe Fang
bbc5eb772c New translations resources.resw (Hindi) 2025-12-24 19:15:09 -05:00
Zhe Fang
05b491052b New translations resources.resw (Thai) 2025-12-24 19:15:08 -05:00
Zhe Fang
8accbf0431 New translations resources.resw (Indonesian) 2025-12-24 19:15:07 -05:00
Zhe Fang
1174209c2a New translations resources.resw (Vietnamese) 2025-12-24 19:15:06 -05:00
Zhe Fang
23ed719046 New translations resources.resw (Chinese Traditional) 2025-12-24 19:15:05 -05:00
Zhe Fang
a34f00662e New translations resources.resw (Chinese Simplified) 2025-12-24 19:15:04 -05:00
Zhe Fang
f783314258 New translations resources.resw (Russian) 2025-12-24 19:15:03 -05:00
Zhe Fang
215a39c5d5 New translations resources.resw (Portuguese) 2025-12-24 19:15:02 -05:00
Zhe Fang
16bcef5f64 New translations resources.resw (Korean) 2025-12-24 19:15:01 -05:00
Zhe Fang
fbba9a3c36 New translations resources.resw (Japanese) 2025-12-24 19:15:00 -05:00
Zhe Fang
f205ab0364 New translations resources.resw (German) 2025-12-24 19:14:59 -05:00
Zhe Fang
10314f3c2f New translations resources.resw (Arabic) 2025-12-24 19:14:58 -05:00
Zhe Fang
b4710e87d3 New translations resources.resw (Spanish) 2025-12-24 19:14:57 -05:00
Zhe Fang
282a934cd2 New translations resources.resw (French) 2025-12-24 19:14:56 -05:00
Zhe Fang
b4c4e394ef Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-24 18:36:28 -05:00
Zhe Fang
17cfdf37bd chores: i18n 2025-12-24 18:36:26 -05:00
Zhe Fang
900a8e1e7c Update credit wording in CONTRIBUTING.md 2025-12-24 11:55:18 -05:00
Zhe Fang
ea9a9c2f5f Update translation contribution instructions in README
Added a link for contributors to find more information.
2025-12-24 11:52:00 -05:00
Zhe Fang
0c4d02b337 Revise translation assistance information
Updated translation assistance section with a link for contributions.
2025-12-24 11:51:38 -05:00
Zhe Fang
d137d82ecf Update translation assistance section in README
Added a section for translation assistance and removed the previous translation section.
2025-12-24 11:51:07 -05:00
Zhe Fang
02551e2053 Add translation contribution section to README
Added a section encouraging users to help translate the project and contribute.
2025-12-24 11:50:10 -05:00
Zhe Fang
026926e9b8 Update CONTRIBUTING.md 2025-12-24 11:39:08 -05:00
Zhe Fang
4c811db16a Update README.CN.md with translation and donation info
Added a section for translation contributions and donation support.
2025-12-24 11:36:29 -05:00
Zhe Fang
6f83fa11db Revise contributing guidelines for translations
Updated the contributing guidelines to include translation information in both English and Chinese. Added a status table for languages and contributors.
2025-12-24 11:34:55 -05:00
Zhe Fang
bc8e15c144 Update translation section in README.md
Added a section for translation contributions and removed the previous translation details.
2025-12-24 11:32:16 -05:00
Zhe Fang
85de1eb2cd Add Chinese translation guidelines to CONTRIBUTING.md 2025-12-24 11:29:00 -05:00
Zhe Fang
d2bf19ed3d Add translation contribution guidelines
Added instructions for translating BetterLyrics using Crowdin.
2025-12-24 11:27:23 -05:00
Zhe Fang
43c205c839 Update Japanese language support status in README 2025-12-24 11:07:35 -05:00
Zhe Fang
9664b1ab78 Update Japanese language entry in README.CN.md 2025-12-24 11:07:00 -05:00
Zhe Fang
08c5f6b515 Add contributor link for Japanese language support 2025-12-24 11:06:31 -05:00
Zhe Fang
260de40f81 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-24 11:05:41 -05:00
Zhe Fang
c00d0eb005 fix: duration issue 2025-12-24 11:05:39 -05:00
Zhe Fang
32e761724c Merge pull request #170 from jayfunc/l10n_dev
New Crowdin updates
2025-12-24 11:05:05 -05:00
Zhe Fang
9fd08af582 New translations resources.resw (Japanese) 2025-12-24 11:03:57 -05:00
Zhe Fang
266dcfc930 New translations resources.resw (Chinese Simplified) 2025-12-24 10:43:52 -05:00
Zhe Fang
8764585f2c New translations resources.resw (Japanese) 2025-12-24 10:43:50 -05:00
Zhe Fang
91ab3a48c0 New translations resources.resw (Chinese Simplified) 2025-12-24 09:23:59 -05:00
Zhe Fang
80fa34d9e8 New translations resources.resw (Japanese) 2025-12-24 09:23:58 -05:00
Zhe Fang
b4ca4fd990 New translations resources.resw (Chinese Traditional) 2025-12-24 08:00:35 -05:00
Zhe Fang
86527f6b82 New translations resources.resw (Japanese) 2025-12-24 08:00:34 -05:00
Zhe Fang
d8066bc683 New translations resources.resw (Japanese) 2025-12-24 06:04:12 -05:00
Zhe Fang
b261a86791 New translations resources.resw (Japanese) 2025-12-24 04:35:20 -05:00
Zhe Fang
34f2a51b74 New translations resources.resw (Japanese) 2025-12-24 00:10:01 -05:00
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
256 changed files with 30274 additions and 7417 deletions

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Interfaces\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
namespace BetterLyrics.Core
{
public class Class1
{
}
}

View File

@@ -53,27 +53,27 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle> <AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en</DefaultLanguage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle> <AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en</DefaultLanguage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<AppxBundle>Always</AppxBundle> <AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en</DefaultLanguage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AppxBundle>Always</AppxBundle> <AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en</DefaultLanguage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<AppxBundle>Always</AppxBundle> <AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en</DefaultLanguage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<AppxBundle>Always</AppxBundle> <AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en</DefaultLanguage>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<AppxManifest Include="Package.appxmanifest"> <AppxManifest Include="Package.appxmanifest">

View File

@@ -12,7 +12,7 @@
<Identity <Identity
Name="37412.BetterLyrics" Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9" Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.1.186.0" /> Version="1.2.253.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
@@ -28,11 +28,22 @@
</Dependencies> </Dependencies>
<Resources> <Resources>
<Resource Language="en-US"/> <Resource Language="ar"/>
<Resource Language="zh-CN"/> <Resource Language="de"/>
<Resource Language="zh-TW"/> <Resource Language="en"/>
<Resource Language="ja-JP"/> <Resource Language="es"/>
<Resource Language="ko-KR"/> <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> </Resources>
<Applications> <Applications>

View File

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

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.Hooks;
using BetterLyrics.WinUI3.Models.DbContext;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService; using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService; using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LastFMService; using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService; using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.LyricsSearchService; using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService; using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService; using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources; using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理
using Serilog; using Serilog;
using System; using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3 namespace BetterLyrics.WinUI3
{ {
public partial class App : Application public partial class App : Application
{ {
private Window? m_window;
private readonly ILogger<App> _logger; private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current; public static new App Current => (App)Application.Current;
private static Mutex? _instanceMutex; private readonly string _appKey = Windows.ApplicationModel.Package.Current.Id.FamilyName;
public App() public App()
{ {
this.InitializeComponent(); // Must be done before InitializeComponent
if (!TryHandleSingleInstance())
{
// 如果移交成功直接退出当前进程
Environment.Exit(0);
return;
}
EnsureSingleInstance(); this.InitializeComponent();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories(); PathHelper.EnsureDirectories();
@@ -49,29 +58,106 @@ namespace BetterLyrics.WinUI3
_logger = Ioc.Default.GetRequiredService<ILogger<App>>(); _logger = Ioc.Default.GetRequiredService<ILogger<App>>();
// 注册全局异常捕获
UnhandledException += App_UnhandledException; UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException; AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; 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); // 监听 "Activated" 事件。
Environment.Exit(0); // 当第二个实例启动并重定向过来时,这个事件会被触发。
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 InitDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>(); var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
WindowHook.OpenOrShowWindow<SystemTrayWindow>(); // Migrate MappedSongSearchQueries
var songSearchMapService = Ioc.Default.GetRequiredService<ISongSearchMapService>();
var obsoleteSongSearchMap = settingsService.AppSettings.MappedSongSearchQueries;
if (obsoleteSongSearchMap.Count > 0)
{
foreach (var item in obsoleteSongSearchMap)
{
await songSearchMapService.SaveMappingAsync(item);
}
obsoleteSongSearchMap.Clear();
}
// Start scan tasks in background
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
{
_ = Task.Run(async () => await fileSystemService.ScanMediaFolderAsync(item, CancellationToken.None));
}
}
fileSystemService.StartAllFolderTimers();
// Init system tray
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// Open lyrics window if set
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow) if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{ {
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault); var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -87,37 +173,82 @@ namespace BetterLyrics.WinUI3
} }
} }
} }
// Open music gallery if set
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen) if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{ {
WindowHook.OpenOrShowWindow<MusicGalleryWindow>(); WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
} }
} }
private async Task InitDatabasesAsync()
{
// Init databases
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var songSearchMapFactory = Ioc.Default.GetRequiredService<IDbContextFactory<SongSearchMapDbContext>>();
var filesIndexFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
var lyricsCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<LyricsCacheDbContext>>();
using (var playHistoryDb = await playHistoryFactory.CreateDbContextAsync())
{
await playHistoryDb.Database.EnsureCreatedAsync();
}
using (var songSearchMapDb = await songSearchMapFactory.CreateDbContextAsync())
{
await songSearchMapDb.Database.EnsureCreatedAsync();
}
using (var filesIndexDb = await filesIndexFactory.CreateDbContextAsync())
{
await filesIndexDb.Database.EnsureCreatedAsync();
}
using (var lyricsCacheDb = await lyricsCacheFactory.CreateDbContextAsync())
{
await lyricsCacheDb.Database.EnsureCreatedAsync();
}
}
private static void ConfigureServices() private static void ConfigureServices()
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose) .MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Error)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day) .WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger(); .CreateLogger();
// Register services
Ioc.Default.ConfigureServices( Ioc.Default.ConfigureServices(
new ServiceCollection() new ServiceCollection()
// 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
.AddDbContextFactory<LyricsCacheDbContext>(options => options.UseSqlite($"Data Source={PathHelper.LyricsCachePath}"))
.AddDbContextFactory<SongSearchMapDbContext>(options => options.UseSqlite($"Data Source={PathHelper.SongSearchMapPath}"))
// 日志
.AddLogging(loggingBuilder => .AddLogging(loggingBuilder =>
{ {
loggingBuilder.ClearProviders(); loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog(); loggingBuilder.AddSerilog();
}) })
// Services // Services
.AddSingleton<ISettingsService, SettingsService>() .AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>() .AddSingleton<ISMTCService, SMTCService>()
.AddSingleton<IGSMTCService, GSMTCService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>() .AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>() .AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>() .AddSingleton<ITranslationService, TranslationService>()
.AddSingleton<ITranslateService, TranslateService>() .AddSingleton<ITransliterationService, TransliterationService>()
.AddSingleton<ILastFMService, LastFMService>() .AddSingleton<ILastFMService, LastFMService>()
.AddSingleton<IResourceService, ResourceService>()
.AddSingleton<IDiscordService, DiscordService>() .AddSingleton<IDiscordService, DiscordService>()
.AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
// ViewModels // ViewModels
.AddSingleton<AppSettingsControlViewModel>() .AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>() .AddSingleton<PlaybackSettingsControlViewModel>()
@@ -132,6 +263,8 @@ namespace BetterLyrics.WinUI3
.AddSingleton<MusicGalleryPageViewModel>() .AddSingleton<MusicGalleryPageViewModel>()
.AddSingleton<AboutControlViewModel>() .AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>() .AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddSingleton<PlayQueueViewModel>()
.AddTransient<NowPlayingWindowViewModel>() .AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>() .AddTransient<NowPlayingPageViewModel>()
@@ -149,7 +282,8 @@ namespace BetterLyrics.WinUI3
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) 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) private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

View File

@@ -10,13 +10,20 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="TemplateSelector\**" />
<Compile Remove="ViewModels\Lyrics\**" /> <Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="TemplateSelector\**" />
<Content Remove="ViewModels\Lyrics\**" /> <Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="TemplateSelector\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" /> <EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="TemplateSelector\**" />
<None Remove="ViewModels\Lyrics\**" /> <None Remove="ViewModels\Lyrics\**" />
<Page Remove="TemplateSelector\**" />
<Page Remove="ViewModels\Lyrics\**" /> <Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="TemplateSelector\**" />
<PRIResource Remove="ViewModels\Lyrics\**" /> <PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -35,11 +42,17 @@
<None Remove="Controls\LyricsWindowSwitchControl.xaml" /> <None Remove="Controls\LyricsWindowSwitchControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" /> <None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\NowPlayingBar.xaml" /> <None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PatronControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" /> <None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PropertyRow.xaml" /> <None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" /> <None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\StatsDashboardControl.xaml" />
<None Remove="Controls\SystemTray.xaml" /> <None Remove="Controls\SystemTray.xaml" />
<None Remove="Controls\WindowSettingsControl.xaml" /> <None Remove="Controls\WindowSettingsControl.xaml" />
<None Remove="Styles\GhostSliderStyle.xaml" />
<None Remove="Styles\InteractiveListViewHeaderStyle.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" /> <None Remove="Views\LyricsSearchWindow.xaml" />
<None Remove="Views\LyricsWindowSwitchWindow.xaml" /> <None Remove="Views\LyricsWindowSwitchWindow.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" /> <None Remove="Views\MusicGalleryPage.xaml" />
@@ -58,36 +71,41 @@
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251021-build.2365" /> <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.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" /> <PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.250402" /> <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="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="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="DevWinUI.Controls" Version="9.7.1" /> <PackageReference Include="DevWinUI.Controls" Version="9.8.1" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" /> <PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" />
<PackageReference Include="F23.StringSimilarity" Version="7.0.1" /> <PackageReference Include="F23.StringSimilarity" Version="7.0.1" />
<PackageReference Include="FlaUI.UIA3" Version="5.0.0" /> <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="H.NotifyIcon.WinUI" Version="2.4.1" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" /> <PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" /> <PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc6.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" /> <PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" 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.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" /> <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="NTextCat" Version="0.3.65" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SMBLibrary" Version="1.5.5.1" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" /> <PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.1" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" /> <PackageReference Include="TagLibSharp" Version="2.3.0" />
@@ -98,6 +116,7 @@
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" /> <PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
<PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" /> <PackageReference Include="Vanara.Windows.Shell" Version="4.2.1" />
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" /> <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="WinUIEx" Version="2.9.0" />
<PackageReference Include="z440.atl.core" Version="7.9.0" /> <PackageReference Include="z440.atl.core" Version="7.9.0" />
</ItemGroup> </ItemGroup>
@@ -119,6 +138,10 @@
<ItemGroup> <ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" /> <TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" /> <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="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" /> <TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" /> <TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
@@ -161,6 +184,9 @@
<Content Update="Assets\EmptyState.png"> <Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png"> <Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
@@ -203,6 +229,9 @@
<Content Update="Assets\NetEaseCloudMusic.png"> <Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png"> <Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
@@ -218,6 +247,9 @@
<Content Update="Assets\Question.png"> <Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png"> <Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
@@ -234,6 +266,31 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml"> <Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -335,12 +392,9 @@
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PRIResource Update="Strings\en-US\Resources.resw"> <Page Update="Controls\RemoteServerConfigControl.xaml">
<Generator></Generator> <Generator>MSBuild:Compile</Generator>
</PRIResource> </Page>
</ItemGroup>
<ItemGroup>
<Folder Include="TemplateSelector\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\NowPlayingBar.xaml"> <Page Update="Controls\NowPlayingBar.xaml">
@@ -382,6 +436,11 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties --> <!-- Publish Properties -->
<PropertyGroup> <PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>

View File

@@ -6,6 +6,8 @@
public const string AuthorGitHub = "https://github.com/jayfunc"; 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 BetterLyricsGitHub = $"{AuthorGitHub}/BetterLyrics";
public const string ShareHub = $"{BetterLyricsGitHub}/blob/dev/ShareHub/index.md"; public const string ShareHub = $"{BetterLyricsGitHub}/blob/dev/ShareHub/index.md";

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Constants namespace BetterLyrics.WinUI3.Constants
{ {
public static class PlayerID public static class PlayerId
{ {
public const string LXMusic = "cn.toside.music.desktop"; public const string LXMusic = "cn.toside.music.desktop";
public const string LXMusicPortable = "lx-music-desktop.exe"; public const string LXMusicPortable = "lx-music-desktop.exe";
@@ -25,5 +25,6 @@
public const string MoeKoeMusic = "cn.MoeKoe.Music"; public const string MoeKoeMusic = "cn.MoeKoe.Music";
public const string MoeKoeMusicAlternative = "electron.app.MoeKoe Music"; public const string MoeKoeMusicAlternative = "electron.app.MoeKoe Music";
public const string Listen1 = "com.listen1.listen1"; 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 SaltPlayerForWindowsSteam = "Salt Player for Windows (Steam)";
public const string MoeKoeMusic = "MoeKoe Music"; public const string MoeKoeMusic = "MoeKoe Music";
public const string Listen1 = "Listen 1"; public const string Listen1 = "Listen 1";
public const string OriginalSoundHQPlayer = "Original Sound HQ Player";
} }
} }

View File

@@ -6,5 +6,7 @@ namespace BetterLyrics.WinUI3.Constants
{ {
public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250); public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250);
public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(350); public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(350);
public static readonly TimeSpan LongAnimationDuration = TimeSpan.FromMilliseconds(650);
public static readonly TimeSpan WaitingDuration = TimeSpan.FromMilliseconds(300);
} }
} }

View File

@@ -57,12 +57,12 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left"> <dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal"> <dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="GitHub" NavigateUri="{x:Bind const:Link.BetterLyricsGitHub}" /> <HyperlinkButton Content="GitHub" NavigateUri="{x:Bind const:Link.BetterLyricsGitHub}" />
<HyperlinkButton x:Uid="UserGuide" NavigateUri="{x:Bind const:Link.UserGuide}" /> <HyperlinkButton x:Uid="UserGuide" NavigateUri="{x:Bind const:Link.UserGuide}" />
<HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" /> <HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfService}" /> <HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfService}" />
</StackPanel> </dev:WrapPanel>
</StackPanel> </StackPanel>
</dev:SettingsCard> </dev:SettingsCard>
@@ -70,18 +70,18 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left"> <dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageFeedback" /> <TextBlock x:Uid="SetingsPageFeedback" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal"> <dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton x:Uid="SettingsPageQQGroup" NavigateUri="{x:Bind const:Link.QQGroup}" /> <HyperlinkButton Content="QQ 反馈交流群" NavigateUri="{x:Bind const:Link.QQGroup}" />
<HyperlinkButton x:Uid="SettingsPageDiscord" NavigateUri="{x:Bind const:Link.Discord}" /> <HyperlinkButton Content="Discord" NavigateUri="{x:Bind const:Link.Discord}" />
<HyperlinkButton x:Uid="SettingsPageTelegram" NavigateUri="{x:Bind const:Link.Telegram}" /> <HyperlinkButton Content="Telegram" NavigateUri="{x:Bind const:Link.Telegram}" />
</StackPanel> </dev:WrapPanel>
</StackPanel> </StackPanel>
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left"> <dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDonation" /> <TextBlock x:Uid="SetingsPageDonation" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal"> <dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="Buy Me a Coffee" NavigateUri="{x:Bind const:Link.BuyMeACoffee}" /> <HyperlinkButton Content="Buy Me a Coffee" NavigateUri="{x:Bind const:Link.BuyMeACoffee}" />
<HyperlinkButton Content="PayPal" NavigateUri="{x:Bind const:Link.PayPal}" /> <HyperlinkButton Content="PayPal" NavigateUri="{x:Bind const:Link.PayPal}" />
<HyperlinkButton <HyperlinkButton
@@ -117,24 +117,23 @@
</HyperlinkButton.ContextFlyout> </HyperlinkButton.ContextFlyout>
</HyperlinkButton> </HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" /> <HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel> </dev:WrapPanel>
<StackPanel Orientation="Horizontal" Spacing="6"> <Grid ColumnSpacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="*" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="*" />
<TextBlock <TextBlock
x:Uid="SetingsPageThanks" x:Uid="SetingsPageThanks"
Grid.Column="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</Grid>
</StackPanel> </StackPanel>
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageThanksList">
<Button
Click="Patron_Click"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7FD;}"
Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard> </dev:SettingsCard>
</dev:SettingsExpander.Items> </dev:SettingsExpander.Items>
@@ -149,6 +148,108 @@
</dev:SettingsExpander.ItemsFooter> </dev:SettingsExpander.ItemsFooter>
</dev:SettingsExpander> </dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageThanksList">
<dev:SettingsExpander.HeaderIcon>
<ImageIcon Source="ms-appx:///Assets/RevolvingHearts.gif" />
</dev:SettingsExpander.HeaderIcon>
<dev:SettingsExpander.Items>
<!-- 贡献者 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Code" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="jayfunc" NavigateUri="https://github.com/jayfunc" />
<HyperlinkButton Content="Raspberry-Monster" NavigateUri="https://github.com/Raspberry-Monster" />
<HyperlinkButton Content="ZHider" NavigateUri="https://github.com/ZHider" />
<HyperlinkButton Content="kusutori" NavigateUri="https://github.com/kusutori" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 贡献者 (Translator) -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Translator" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="borcolasky" NavigateUri="https://crowdin.com/profile/borcolasky" />
<HyperlinkButton Content="SuHeAndZl" NavigateUri="https://crowdin.com/profile/SuHeAndZl" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 赞助 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SettingsPagePatrons" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<uc:PatronControl Date="Jan 8, 2026" PatronName="Eureka-K_K" />
<uc:PatronControl Date="Jan 3, 2026" PatronName="**轩" />
<uc:PatronControl Date="Dec 13, 2025" PatronName="&lt;Anonymous&gt;" />
<uc:PatronControl Date="Dec 3, 2025" PatronName="YE" />
<uc:PatronControl Date="Dec 2, 2025" PatronName="&lt;Anonymous&gt;" />
<uc:PatronControl Date="Nov 23, 2025" PatronName="**玄" />
<uc:PatronControl Date="Nov 21, 2025" PatronName="**智" />
<uc:PatronControl Date="Nov 17, 2025" PatronName="SuHeAndZl" />
<uc:PatronControl Date="Nov 2, 2025" PatronName="借过" />
<uc:PatronControl Date="Aug 28, 2025" PatronName="**华" />
<TextBlock
x:Uid="SettingsPageUserWhoPurchased"
Margin="12,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 特别鸣谢 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageSpecialThanks" />
<TextBlock
x:Uid="SettingsPageYouNowUsing"
Margin="0,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</dev:SettingsCard>
<!-- 代码参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDeps" />
<HyperlinkButton Margin="-12,0,0,0" NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<TextBlock x:Uid="SetingsPageDeps" />
</HyperlinkButton>
</StackPanel>
</dev:SettingsCard>
<!-- UI/UX 参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageUIUXRef" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="refined-now-playing-netease" NavigateUri="https://github.com/solstice23/refined-now-playing-netease" />
<HyperlinkButton Content="Lyricify" NavigateUri="https://github.com/WXRIW/Lyricify-App" />
<HyperlinkButton Content="椒盐音乐 Salt Player" NavigateUri="https://moriafly.com/program/salt-player" />
<HyperlinkButton Content="MyToolBar" NavigateUri="https://github.com/TwilightLemon/MyToolBar" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageMockMusicPlaying"> <dev:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<HyperlinkButton x:Uid="SettingsPagePlayingMockMusicButton" NavigateUri="https://soundcloud.com/carlyraejepsen/cut-to-the-feeling" /> <HyperlinkButton x:Uid="SettingsPagePlayingMockMusicButton" NavigateUri="https://soundcloud.com/carlyraejepsen/cut-to-the-feeling" />
</dev:SettingsCard> </dev:SettingsCard>
@@ -195,194 +296,28 @@
Value="{x:Bind ViewModel.AppSettings.AdvancedSettings.FPS, Mode=TwoWay}" /> Value="{x:Bind ViewModel.AppSettings.AdvancedSettings.FPS, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
<Grid
x:Name="CreditsReel"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
Opacity="0"
SizeChanged="CreditsReel_SizeChanged"
Tapped="CreditsReel_Tapped"
Visibility="Collapsed">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<ScrollViewer
x:Name="CreditsReelScrollViewer"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollMode="Disabled">
<RichTextBlock <RichTextBlock
Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalTextAlignment="Center" HorizontalTextAlignment="Center"
LineHeight="28" LineHeight="28">
PointerEntered="RichTextBlock_PointerEntered" <Paragraph FontWeight="Bold">
PointerExited="RichTextBlock_PointerExited">
<Paragraph x:Name="CreditsReelHeader" />
<!-- 贡献者 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageContributors" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc">
<Run Text="jayfunc" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Raspberry-Monster">
<Run Text="Raspberry-Monster" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/ZHider">
<Run Text="ZHider" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/kusutori">
<Run Text="kusutori" />
</Hyperlink>
</Paragraph>
<!-- 赞助 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SettingsPagePatrons" />
</Paragraph>
<Paragraph>
<Run Text="YE" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Dec 3, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**玄" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 23, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**智" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 21, 2025" />
</Paragraph>
<Paragraph>
<Run Text="*鹤" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 17, 2025" />
</Paragraph>
<Paragraph>
<Run Text="借过" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 2, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**华" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Aug 28, 2025" />
</Paragraph>
<Paragraph>
<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://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce">
<Run Text="Get album artwork from ITunes (with Python3 or C#)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://stackoverflow.com/a/32013610/11048731">
<Run Text="FullyObservableCollection" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/Impressionist">
<Run Text="Impressionist" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/ColorThief.WinUI3">
<Run Text="ColorThief.WinUI3" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Johnwikix/SpectrumVisualization">
<Run Text="SpectrumVisualization" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/Mdt3Df">
<Run Text="Snow (as shown in sweden)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/lllSR2">
<Run Text="w10" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/mo-jinran/Taskbar-Lyrics">
<Run Text="Taskbar-Lyrics" />
</Hyperlink>
</Paragraph>
<Paragraph>
<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://github.com/WXRIW/Lyricify-App">
<Run Text="Lyricify" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://moriafly.com/program/salt-player">
<Run Text="椒盐音乐 Salt Player" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/TwilightLemon/MyToolBar">
<Run Text="MyToolBar" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="">
<Run Text="" />
</Hyperlink>
</Paragraph>
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" /> <Run Text="{x:Bind const:App.AppName}" />
</Paragraph> </Paragraph>
<Paragraph>
<Run Text="An elegant and deeply customizable lyrics visualizer &amp; versatile music player" />
</Paragraph>
<Paragraph> <Paragraph>
<Run Text="Proudly built by" /> <Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}"> <Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" /> <Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink> </Hyperlink>
</Paragraph> </Paragraph>
<Paragraph x:Name="CreditsReelFooter" />
</RichTextBlock> </RichTextBlock>
</ScrollViewer>
</StackPanel>
</Grid> </Grid>
</ScrollViewer>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,8 +1,6 @@
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Threading.Tasks;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -11,7 +9,6 @@ namespace BetterLyrics.WinUI3.Controls
{ {
public sealed partial class AboutControl : UserControl public sealed partial class AboutControl : UserControl
{ {
private bool _isCreditsScrolling = false;
public AboutControlViewModel ViewModel => (AboutControlViewModel)DataContext; public AboutControlViewModel ViewModel => (AboutControlViewModel)DataContext;
public AboutControl() public AboutControl()
@@ -20,47 +17,6 @@ namespace BetterLyrics.WinUI3.Controls
DataContext = Ioc.Default.GetRequiredService<AboutControlViewModel>(); DataContext = Ioc.Default.GetRequiredService<AboutControlViewModel>();
} }
private async void Patron_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
CreditsReel.Opacity = 1;
_isCreditsScrolling = true;
}
private void CompositionTarget_Rendering(object? sender, object e)
{
if (_isCreditsScrolling)
{
CreditsReelScrollViewer.ChangeView(null, CreditsReelScrollViewer.VerticalOffset + 0.5, null);
}
}
private async void CreditsReel_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
CreditsReel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
CompositionTarget.Rendering -= CompositionTarget_Rendering;
CreditsReelScrollViewer.ChangeView(null, 0, null);
}
private void CreditsReel_SizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
CreditsReelHeader.LineHeight = e.NewSize.Height;
CreditsReelFooter.LineHeight = e.NewSize.Height / 2;
}
private void RichTextBlock_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = false;
}
private void RichTextBlock_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = true;
}
private void WeChat_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) private void WeChat_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{ {
WeChatFlyout.ShowAt(WeChatButton); WeChatFlyout.ShowAt(WeChatButton);

View File

@@ -3,6 +3,7 @@
x:Class="BetterLyrics.WinUI3.Controls.AppSettingsControl" x:Class="BetterLyrics.WinUI3.Controls.AppSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:consts="using:BetterLyrics.WinUI3.Constants"
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI" xmlns:dev="using:DevWinUI"
@@ -42,6 +43,14 @@
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" /> <Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
</dev:SettingsCard> </dev:SettingsCard>
</dev:SettingsExpander.Items> </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> </dev:SettingsExpander>
<!-- Startup --> <!-- Startup -->
@@ -71,6 +80,10 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.AutoPlay, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.AutoPlay, Mode=TwoWay}" />
</dev:SettingsCard> </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;}"> <dev:SettingsCard x:Uid="SettingsPageExitOnGalleryWindowClosed" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE711;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.ExitOnWindowClosed, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.MusicGallerySettings.ExitOnWindowClosed, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>

View File

@@ -7,6 +7,7 @@
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid <Grid
@@ -91,6 +92,47 @@
Text="{x:Bind LyricsWindowStatus.Name, Mode=OneWay}" Text="{x:Bind LyricsWindowStatus.Name, Mode=OneWay}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</Grid> </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> </Grid>
</UserControl> </UserControl>

View File

@@ -1,6 +1,11 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System.Linq;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // 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 public sealed partial class DemoWindowGrid : UserControl
{ {
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public DemoWindowGrid() public DemoWindowGrid()
{ {
InitializeComponent(); InitializeComponent();
@@ -22,4 +29,32 @@ public sealed partial class DemoWindowGrid : UserControl
get => (LyricsWindowStatus)GetValue(LyricsWindowStatusProperty); get => (LyricsWindowStatus)GetValue(LyricsWindowStatusProperty);
set => SetValue(LyricsWindowStatusProperty, value); 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

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

View File

@@ -5,14 +5,15 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Logic; using BetterLyrics.WinUI3.Logic;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Renderer; using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.LastFMService; using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages; using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI; using Microsoft.UI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -20,7 +21,9 @@ using Microsoft.UI.Xaml.Controls;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Windows.Foundation; using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.UI; using Windows.UI;
namespace BetterLyrics.WinUI3.Controls namespace BetterLyrics.WinUI3.Controls
@@ -28,20 +31,21 @@ namespace BetterLyrics.WinUI3.Controls
public sealed partial class LyricsCanvas : UserControl, public sealed partial class LyricsCanvas : UserControl,
IRecipient<PropertyChangedMessage<TimeSpan>>, IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsData?>>, IRecipient<PropertyChangedMessage<LyricsData?>>,
IRecipient<PropertyChangedMessage<SongInfo?>>, IRecipient<PropertyChangedMessage<SongInfo>>,
IRecipient<PropertyChangedMessage<int>>, IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<double>>, IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>, IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<TextAlignmentType>>, IRecipient<PropertyChangedMessage<TextAlignmentType>>,
IRecipient<PropertyChangedMessage<LyricsFontWeight>>, IRecipient<PropertyChangedMessage<LyricsFontWeight>>,
IRecipient<PropertyChangedMessage<string>> IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<IRandomAccessStream?>>
{ {
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>(); private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>(); private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly LyricsRenderer _lyricsRenderer = new(); private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new(); private readonly FluidBackgroundRenderer _fluidRenderer = new();
private readonly CoverBackgroundRenderer _coverRenderer = new();
private readonly PureColorBackgroundRenderer _pureColorRenderer = new(); private readonly PureColorBackgroundRenderer _pureColorRenderer = new();
private readonly SnowRenderer _snowRenderer = new(); private readonly SnowRenderer _snowRenderer = new();
private readonly FogRenderer _fogRenderer = new(); private readonly FogRenderer _fogRenderer = new();
@@ -54,48 +58,46 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ValueTransition<Color> _immersiveBgColorTransition = new( private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new( private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f, initialValue: 1f,
durationSeconds: 0.3f defaultTotalDuration: 0.3f
); );
private readonly ValueTransition<Color> _accentColor1Transition = new( private readonly ValueTransition<Color> _accentColor1Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor2Transition = new( private readonly ValueTransition<Color> _accentColor2Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor3Transition = new( private readonly ValueTransition<Color> _accentColor3Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor4Transition = new( private readonly ValueTransition<Color> _accentColor4Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<double> _canvasYScrollTransition = new( private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f, initialValue: 0f,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
private readonly ValueTransition<double> _mouseYScrollTransition = new( private readonly ValueTransition<double> _mouseYScrollTransition = new(
initialValue: 0f, initialValue: 0f,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
easingType: EasingType.EaseInOutSine defaultEasingType: EasingType.EaseInOutSine
); );
private TimeSpan _songPositionWithOffset; private TimeSpan _songPositionWithOffset;
private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1> private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1>
private TimeSpan _totalPlayedTime; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD>ŵ<EFBFBD>ʱ<EFBFBD>
private bool _isLastFMTracked = false;
private double _renderLyricsStartX = 0; private double _renderLyricsStartX = 0;
private double _renderLyricsStartY = 0; private double _renderLyricsStartY = 0;
@@ -117,7 +119,7 @@ namespace BetterLyrics.WinUI3.Controls
private bool _isLayoutChanged = true; private bool _isLayoutChanged = true;
private bool _isMouseScrollingChanged = false; private bool _isMouseScrollingChanged = false;
private int _playingLineIndex; private int _primaryPlayingLineIndex;
private (int Start, int End) _visibleRange; private (int Start, int End) _visibleRange;
private double _canvasTargetScrollOffset; private double _canvasTargetScrollOffset;
@@ -290,7 +292,7 @@ namespace BetterLyrics.WinUI3.Controls
} }
else if (e.Property == MouseScrollOffsetProperty) else if (e.Property == MouseScrollOffsetProperty)
{ {
canvas._mouseYScrollTransition.StartTransition(Convert.ToDouble(e.NewValue)); canvas._mouseYScrollTransition.Start(Convert.ToDouble(e.NewValue));
} }
else if (e.Property == MousePositionProperty) else if (e.Property == MousePositionProperty)
{ {
@@ -316,11 +318,11 @@ namespace BetterLyrics.WinUI3.Controls
else if (e.Property == AlbumArtThemeColorsProperty) else if (e.Property == AlbumArtThemeColorsProperty)
{ {
var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue; var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue;
canvas._immersiveBgColorTransition.StartTransition(albumArtThemeColors.EnvColor); canvas._immersiveBgColorTransition.Start(albumArtThemeColors.EnvColor);
canvas._accentColor1Transition.StartTransition(albumArtThemeColors.AccentColor1); canvas._accentColor1Transition.Start(albumArtThemeColors.AccentColor1);
canvas._accentColor2Transition.StartTransition(albumArtThemeColors.AccentColor2); canvas._accentColor2Transition.Start(albumArtThemeColors.AccentColor2);
canvas._accentColor3Transition.StartTransition(albumArtThemeColors.AccentColor3); canvas._accentColor3Transition.Start(albumArtThemeColors.AccentColor3);
canvas._accentColor4Transition.StartTransition(albumArtThemeColors.AccentColor4); canvas._accentColor4Transition.Start(albumArtThemeColors.AccentColor4);
canvas._albumArtThemeColors = albumArtThemeColors; canvas._albumArtThemeColors = albumArtThemeColors;
canvas._isLayoutChanged = true; canvas._isLayoutChanged = true;
@@ -341,8 +343,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings; var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings; var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0; double songDuration = _gsmtcService.CurrentSongInfo.DurationMs;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor; Color overlayColor;
double finalOpacity; double finalOpacity;
@@ -368,8 +369,8 @@ namespace BetterLyrics.WinUI3.Controls
lyricsBg.IsPureColorOverlayEnabled lyricsBg.IsPureColorOverlayEnabled
); );
_fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity / 100.0; _coverRenderer.Draw(sender, args.DrawingSession);
_fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
_fluidRenderer.Draw(sender, args.DrawingSession); _fluidRenderer.Draw(sender, args.DrawingSession);
_snowRenderer.Draw(sender, args.DrawingSession); _snowRenderer.Draw(sender, args.DrawingSession);
@@ -380,7 +381,6 @@ namespace BetterLyrics.WinUI3.Controls
control: sender, control: sender,
ds: args.DrawingSession, ds: args.DrawingSession,
lines: _renderLyricsLines, lines: _renderLyricsLines,
playingLineIndex: _playingLineIndex,
mouseHoverLineIndex: _mouseHoverLineIndex, mouseHoverLineIndex: _mouseHoverLineIndex,
isMousePressing: _isMousePressing, isMousePressing: _isMousePressing,
startVisibleIndex: _visibleRange.Start, startVisibleIndex: _visibleRange.Start,
@@ -396,6 +396,7 @@ namespace BetterLyrics.WinUI3.Controls
strokeColor: _albumArtThemeColors.StrokeFontColor, strokeColor: _albumArtThemeColors.StrokeFontColor,
bgColor: _albumArtThemeColors.BgFontColor, bgColor: _albumArtThemeColors.BgFontColor,
fgColor: _albumArtThemeColors.FgFontColor, fgColor: _albumArtThemeColors.FgFontColor,
currentProgressMs: _songPositionWithOffset.TotalMilliseconds,
getPlaybackState: (lineIndex) => getPlaybackState: (lineIndex) =>
{ {
if (_renderLyricsLines == null) return new LinePlaybackState(); if (_renderLyricsLines == null) return new LinePlaybackState();
@@ -408,9 +409,7 @@ namespace BetterLyrics.WinUI3.Controls
return _synchronizer.GetLinePlayingProgress( return _synchronizer.GetLinePlayingProgress(
_songPositionWithOffset.TotalMilliseconds, _songPositionWithOffset.TotalMilliseconds,
line, line,
nextLine, lyricsEffect.WordByWordEffectMode
songDuration,
isForceWordByWord
); );
} }
); );
@@ -431,19 +430,19 @@ namespace BetterLyrics.WinUI3.Controls
); );
} }
#if DEBUG #if DEBUG && false
//args.DrawingSession.DrawText( args.DrawingSession.DrawText(
// $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" + $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
// $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" + $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
// $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" + $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
// $"Playing line (idx): {_playingLineIndex}\n" + $"Playing line (idx): {_playingLineIndex}\n" +
// $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" + $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
// $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" + $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
// $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" + $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
// $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" + $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_gsmtcService.CurrentSongInfo.DurationMs)}\n" +
// $"Y offset: {_canvasYScrollTransition.Value}\n" + $"Y offset: {_canvasYScrollTransition.Value}\n" +
// $"User scroll offset: {_mouseYScrollTransition.Value}", $"User scroll offset: {_mouseYScrollTransition.Value}",
// new Vector2(0, 0), Colors.Red); new Vector2(0, 0), Colors.Red);
#endif #endif
} }
@@ -455,7 +454,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsBg = _lyricsWindowStatus.LyricsBackgroundSettings; var lyricsBg = _lyricsWindowStatus.LyricsBackgroundSettings;
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings; var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings; var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
var lyricsData = _mediaSessionsService.CurrentLyricsData; var lyricsData = _gsmtcService.CurrentLyricsData;
TimeSpan elapsedTime = args.Timing.ElapsedTime; TimeSpan elapsedTime = args.Timing.ElapsedTime;
@@ -473,22 +472,29 @@ namespace BetterLyrics.WinUI3.Controls
#region UpdatePlayingLineIndex #region UpdatePlayingLineIndex
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, lyricsData); int primaryPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, _renderLyricsLines);
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex; bool isPrimaryPlayingLineChanged = primaryPlayingIndex != _primaryPlayingLineIndex;
_playingLineIndex = newPlayingIndex; _primaryPlayingLineIndex = primaryPlayingIndex;
#endregion #endregion
#region UpdateTargetScrollOffset #region UpdateTargetScrollOffset
if (isPlayingLineChanged || _isLayoutChanged) if (isPrimaryPlayingLineChanged || _isLayoutChanged)
{ {
var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _playingLineIndex); var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _primaryPlayingLineIndex);
if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value; if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value;
if (_isLayoutChanged)
{
_canvasYScrollTransition.JumpTo(_canvasTargetScrollOffset);
}
else
{
_canvasYScrollTransition.SetDurationMs(lyricsEffect.LyricsScrollDuration);
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType); _canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType);
_canvasYScrollTransition.SetDuration(lyricsEffect.LyricsScrollDuration / 1000.0); _canvasYScrollTransition.Start(_canvasTargetScrollOffset);
_canvasYScrollTransition.StartTransition(_canvasTargetScrollOffset, _isLayoutChanged); }
} }
_canvasYScrollTransition.Update(elapsedTime); _canvasYScrollTransition.Update(elapsedTime);
@@ -521,7 +527,7 @@ namespace BetterLyrics.WinUI3.Controls
_renderLyricsLines, _renderLyricsLines,
_isMouseScrolling ? maxRange.Start : _visibleRange.Start, _isMouseScrolling ? maxRange.Start : _visibleRange.Start,
_isMouseScrolling ? maxRange.End : _visibleRange.End, _isMouseScrolling ? maxRange.End : _visibleRange.End,
_playingLineIndex, _primaryPlayingLineIndex,
sender.Size.Height, sender.Size.Height,
_canvasTargetScrollOffset, _canvasTargetScrollOffset,
lyricsStyle.PlayingLineTopOffset / 100.0, lyricsStyle.PlayingLineTopOffset / 100.0,
@@ -533,24 +539,26 @@ namespace BetterLyrics.WinUI3.Controls
elapsedTime, elapsedTime,
_isMouseScrolling, _isMouseScrolling,
_isLayoutChanged, _isLayoutChanged,
isPlayingLineChanged, isPrimaryPlayingLineChanged,
_isMouseScrollingChanged _isMouseScrollingChanged,
_songPositionWithOffset.TotalMilliseconds
); );
_isMouseScrollingChanged = false; _isMouseScrollingChanged = false;
_lyricsRenderer.CalculateLyrics3DMatrix( _lyricsRenderer.CalculateLyrics3DMatrix(
lyricsStyle: lyricsStyle,
lyricsEffect: lyricsEffect, lyricsEffect: lyricsEffect,
lyricsX: _renderLyricsStartX, lyricsX: _renderLyricsStartX,
lyricsY: _renderLyricsStartY, lyricsY: _renderLyricsStartY,
lyricsWidth: _renderLyricsWidth, lyricsWidth: _renderLyricsWidth,
canvasHeight: sender.Size.Height lyricsHeight: _renderLyricsHeight
); );
_isLayoutChanged = false; _isLayoutChanged = false;
if (_fluidRenderer.IsEnabled) _fluidRenderer.IsEnabled = lyricsBg.IsFluidOverlayEnabled;
{ _fluidRenderer.Opacity = lyricsBg.FluidOverlayOpacity / 100.0;
_fluidRenderer.UpdateColors( _fluidRenderer.UpdateColors(
_accentColor1Transition.Value, _accentColor1Transition.Value,
_accentColor2Transition.Value, _accentColor2Transition.Value,
@@ -558,7 +566,12 @@ namespace BetterLyrics.WinUI3.Controls
_accentColor4Transition.Value _accentColor4Transition.Value
); );
_fluidRenderer.Update(elapsedTime); _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.IsEnabled = lyricsBg.IsSnowFlakeOverlayEnabled;
_snowRenderer.Amount = lyricsBg.SnowFlakeOverlayAmount / 100f; _snowRenderer.Amount = lyricsBg.SnowFlakeOverlayAmount / 100f;
@@ -586,6 +599,7 @@ namespace BetterLyrics.WinUI3.Controls
private void Canvas_Unloaded(object sender, RoutedEventArgs e) private void Canvas_Unloaded(object sender, RoutedEventArgs e)
{ {
_fluidRenderer.Dispose(); _fluidRenderer.Dispose();
_coverRenderer.Dispose();
_snowRenderer.Dispose(); _snowRenderer.Dispose();
_fogRenderer.Dispose(); _fogRenderer.Dispose();
_spectrumRenderer.Dispose(); _spectrumRenderer.Dispose();
@@ -600,7 +614,13 @@ namespace BetterLyrics.WinUI3.Controls
private async void Canvas_CreateResources(CanvasAnimatedControl sender, Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args) 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(); _snowRenderer.LoadResources();
_fogRenderer.LoadResources(); _fogRenderer.LoadResources();
@@ -637,73 +657,54 @@ namespace BetterLyrics.WinUI3.Controls
private void UpdatePlaybackState(TimeSpan elapsedTime) private void UpdatePlaybackState(TimeSpan elapsedTime)
{ {
if (_mediaSessionsService.CurrentIsPlaying) if (_gsmtcService.CurrentIsPlaying)
{ {
_songPosition += elapsedTime; _songPosition += elapsedTime;
_totalPlayedTime += elapsedTime; _songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_gsmtcService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
_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);
} }
} }
private void ResetPlaybackState() private void ResetPlaybackState()
{ {
_songPosition = TimeSpan.Zero; _songPosition = TimeSpan.Zero;
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
} }
private void UpdateRenderLyricsLines() private void UpdateRenderLyricsLines()
{ {
_renderLyricsLines = null; _renderLyricsLines = null;
_renderLyricsLines = _mediaSessionsService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine() var lines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine(x)).ToList();
if (lines != null)
{ {
LyricsSyllables = x.LyricsSyllables, LyricsLayoutManager.CalculateLanes(lines);
StartMs = x.StartMs, }
EndMs = x.EndMs, _renderLyricsLines = lines;
PhoneticText = x.PhoneticText, }
OriginalText = x.OriginalText,
TranslatedText = x.TranslatedText private async Task ReloadCoverBackgroundResourcesAsync()
}).ToList(); {
if (_gsmtcService.AlbumArtBitmapStream is IRandomAccessStream stream)
{
stream.Seek(0);
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
_coverRenderer.SetCoverBitmap(bitmap);
}
} }
public void Receive(PropertyChangedMessage<TimeSpan> message) public void Receive(PropertyChangedMessage<TimeSpan> message)
{ {
if (message.Sender is IMediaSessionsService) if (message.Sender is IGSMTCService)
{ {
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition)) if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
{ {
var realPosition = message.NewValue; var realPosition = message.NewValue;
var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds); var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds);
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0; var timelineSyncThreshold = _gsmtcService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
// ƫ<><C6AB> or seek // ƫ<><C6AB> or seek
if (diff >= timelineSyncThreshold) if (diff >= timelineSyncThreshold)
{ {
_songPosition = realPosition; _songPosition = realPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˿<EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD> LastFM ͳ<><CDB3>״̬
if (_songPosition.TotalSeconds <= 1)
{
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
} }
// <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD> // <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>
@@ -717,9 +718,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<LyricsData?> message) public void Receive(PropertyChangedMessage<LyricsData?> message)
{ {
if (message.Sender is IMediaSessionsService) if (message.Sender is IGSMTCService)
{ {
if (message.PropertyName == nameof(IMediaSessionsService.CurrentLyricsData)) if (message.PropertyName == nameof(IGSMTCService.CurrentLyricsData))
{ {
UpdateRenderLyricsLines(); UpdateRenderLyricsLines();
_isLayoutChanged = true; _isLayoutChanged = true;
@@ -727,11 +728,11 @@ namespace BetterLyrics.WinUI3.Controls
} }
} }
public void Receive(PropertyChangedMessage<SongInfo?> message) public void Receive(PropertyChangedMessage<SongInfo> message)
{ {
if (message.Sender is IMediaSessionsService) if (message.Sender is IGSMTCService)
{ {
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo)) if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{ {
ResetPlaybackState(); ResetPlaybackState();
} }
@@ -827,6 +828,14 @@ namespace BetterLyrics.WinUI3.Controls
{ {
_isLayoutChanged = true; _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) else if (message.Sender == LyricsWindowStatus?.LyricsStyleSettings)
{ {
@@ -874,5 +883,16 @@ namespace BetterLyrics.WinUI3.Controls
} }
} }
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}
}
}
} }
} }

View File

@@ -23,11 +23,29 @@
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Effect" /> Text="Effect" />
<dev:SettingsCard x:Uid="SettingsPageLyricsWordByWordEffectMode" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF714;}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.WordByWordEffectMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAuto" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeNever" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAlways" />
</ComboBox>
</dev:SettingsCard>
<!-- 模糊效果 --> <!-- 模糊效果 -->
<dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}"> <dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" />
</dev:SettingsCard> </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;}"> <dev:SettingsExpander x:Uid="SettingsPageLyricsGlowEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE9A9;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
@@ -98,6 +116,14 @@
Minimum="0" Minimum="0"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationAmount, Mode=TwoWay}" /> Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationAmount, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsEffectSettingsControlAnimationDuration">
<local:ExtendedSlider
Default="450"
Maximum="2000"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationDuration, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items> </dev:SettingsExpander.Items>
</dev:SettingsExpander> </dev:SettingsExpander>

View File

@@ -8,6 +8,7 @@
xmlns:dev="using:DevWinUI" xmlns:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:lyricsmodels="using:BetterLyrics.WinUI3.Models.Lyrics"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models" xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
@@ -25,7 +26,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0"> <Grid Grid.Column="0">
<ScrollViewer> <ScrollViewer Padding="8,0">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}"> <StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="LyricsSearchControlSongInfoMapping" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="LyricsSearchControlSongInfoMapping" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -97,19 +98,6 @@
Style="{StaticResource GhostButtonStyle}" /> Style="{StaticResource GhostButtonStyle}" />
</Grid> </Grid>
<RichTextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
<Paragraph>
<Run Text="*" />
<Run x:Uid="ArtistsSplitHint" />
</Paragraph>
<Paragraph>
<Run Text="; , / " />
</Paragraph>
</RichTextBlock>
</StackPanel> </StackPanel>
</Grid> </Grid>
@@ -153,12 +141,11 @@
<CheckBox x:Uid="LyricsSearchControlMarkAsPureMusic" IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}" /> <CheckBox x:Uid="LyricsSearchControlMarkAsPureMusic" IsChecked="{x:Bind ViewModel.MappedSongSearchQuery.IsMarkedAsPureMusic, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsSearchControlTargetSearchProvider">
<Button <Button
x:Uid="LyricsSearchControlSearch" x:Uid="LyricsSearchControlSearch"
HorizontalAlignment="Stretch"
Command="{x:Bind ViewModel.SearchCommand}" Command="{x:Bind ViewModel.SearchCommand}"
Style="{StaticResource AccentButtonStyle}" /> Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache"> <dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreCacheWhenSearching, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IgnoreCacheWhenSearching, Mode=TwoWay}" />
@@ -170,7 +157,7 @@
<Grid Grid.Column="1"> <Grid Grid.Column="1">
<ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}"> <ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchResult"> <DataTemplate x:DataType="models:LyricsCacheItem">
<ListViewItem IsEnabled="{x:Bind IsFound}"> <ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}"> <StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow <local:PropertyRow
@@ -181,20 +168,13 @@
<!-- Lyrics search result --> <!-- Lyrics search result -->
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}"> <StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" /> <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="SettingsPageArtist" Value="{x:Bind Artist, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind Duration, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow
x:Uid="LyricsPageMatchPercentage" x:Uid="LyricsPageMatchPercentage"
Unit="%" Unit="%"
Value="{x:Bind MatchPercentage, Mode=OneWay}" /> Value="{x:Bind MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel> </StackPanel>
<!-- NOT FOUND --> <!-- NOT FOUND -->
<TextBlock <TextBlock
@@ -244,8 +224,6 @@
<ProgressBar <ProgressBar
VerticalAlignment="Top" VerticalAlignment="Top"
IsIndeterminate="True" IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.IsSearching, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsSearching, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</Grid> </Grid>
<Grid Grid.Column="2"> <Grid Grid.Column="2">
@@ -265,7 +243,7 @@
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
<Pivot.HeaderTemplate> <Pivot.HeaderTemplate>
<DataTemplate x:DataType="models:LyricsData"> <DataTemplate x:DataType="lyricsmodels:LyricsData">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" /> <TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<InfoBadge <InfoBadge
@@ -277,19 +255,44 @@
</DataTemplate> </DataTemplate>
</Pivot.HeaderTemplate> </Pivot.HeaderTemplate>
<Pivot.ItemTemplate> <Pivot.ItemTemplate>
<DataTemplate x:DataType="models:LyricsData"> <DataTemplate x:DataType="lyricsmodels: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> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsLine"> <DataTemplate x:DataType="lyricsmodels:LyricsLine">
<StackPanel Orientation="Horizontal"> <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}}" /> <TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock <Button
Margin="1,0" HorizontalAlignment="Center"
Foreground="{ThemeResource SystemFillColorNeutralBrush}" VerticalAlignment="Center"
Text="-" /> Click="PlayLyricsLineButton_Click"
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind EndMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" /> Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
<TextBlock Margin="6,0" Text="{x:Bind OriginalText, Mode=OneWay}" /> FontSize=16,
</StackPanel> 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 PrimaryText, Mode=OneWay}" />
</Grid>
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
@@ -321,8 +324,8 @@
</Grid> </Grid>
<Grid Grid.Row="1" ColumnSpacing="6"> <Grid Grid.Row="1" ColumnSpacing="6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>

View File

@@ -1,4 +1,4 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -18,10 +18,10 @@ namespace BetterLyrics.WinUI3.Controls
DataContext = Ioc.Default.GetRequiredService<LyricsSearchControlViewModel>(); 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;}"> <dev:SettingsCard x:Uid="SettingsPageLyricsCenterTopOffset" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE78A;}">
<local:ExtendedSlider <local:ExtendedSlider
Default="50" Default="50"
Maximum="100" Maximum="99"
Minimum="0" Minimum="1"
Unit="%" Unit="%"
Value="{x:Bind LyricsStyleSettings.PlayingLineTopOffset, Mode=TwoWay}" /> Value="{x:Bind LyricsStyleSettings.PlayingLineTopOffset, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>

View File

@@ -11,6 +11,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:BetterLyrics.WinUI3.Controls" xmlns:uc="using:BetterLyrics.WinUI3.Controls"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="UserControl_Loaded"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
@@ -99,10 +100,7 @@
BorderThickness="4" BorderThickness="4"
CornerRadius="4" CornerRadius="4"
Visibility="{Binding IsOpened, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" /> Visibility="{Binding IsOpened, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<uc:DemoWindowGrid <uc:DemoWindowGrid Margin="4" LyricsWindowStatus="{Binding}" />
Margin="4"
LyricsWindowStatus="{Binding}"
Tapped="DemoWindowGrid_Tapped" />
</Grid> </Grid>
<Grid> <Grid>
<ToggleButton <ToggleButton
@@ -116,7 +114,6 @@
<Grid ColumnSpacing="4"> <Grid ColumnSpacing="4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button <Button
Grid.Column="0" Grid.Column="0"
@@ -124,13 +121,6 @@
Click="ConfigButton_Click"> Click="ConfigButton_Click">
<TextBlock x:Uid="LyricsWindowSettingsControlLyricsWindowConfig" /> <TextBlock x:Uid="LyricsWindowSettingsControlLyricsWindowConfig" />
</Button> </Button>
<Button
Grid.Column="1"
HorizontalAlignment="Stretch"
Click="CloseStatusButton_Click"
IsEnabled="{Binding IsOpened, Mode=OneWay}">
<TextBlock x:Uid="SettingsPageCloseStatus" />
</Button>
</Grid> </Grid>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
@@ -197,29 +187,51 @@
<controls:Segmented <controls:Segmented
x:Name="ConfigSegmented" x:Name="ConfigSegmented"
HorizontalAlignment="Stretch"
SelectionChanged="ConfigSegmented_SelectionChanged" SelectionChanged="ConfigSegmented_SelectionChanged"
Style="{StaticResource PivotSegmentedStyle}"> Style="{StaticResource PivotSegmentedStyle}">
<controls:SegmentedItem x:Name="WindowSegmentedItem" Tag="Window"> <controls:SegmentedItem x:Name="WindowSegmentedItem" Tag="Window">
<TextBlock x:Uid="AppSettingsControlGeneral" /> <TextBlock
x:Uid="AppSettingsControlGeneral"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem x:Name="LayoutSegmentedItem" Tag="Layout"> <controls:SegmentedItem x:Name="LayoutSegmentedItem" Tag="Layout">
<TextBlock x:Uid="SettingsPageLayout" /> <TextBlock
x:Uid="SettingsPageLayout"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem x:Name="AlbumArtStyleSegmentedItem" Tag="AlbumArtStyle"> <controls:SegmentedItem x:Name="AlbumArtStyleSegmentedItem" Tag="AlbumArtStyle">
<TextBlock x:Uid="SettingsPageAlbumStyle" /> <TextBlock
x:Uid="SettingsPageAlbumStyle"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem Tag="AlbumArtEffect"> <controls:SegmentedItem Tag="AlbumArtEffect">
<TextBlock x:Uid="SettingsPageAlbumEffect" /> <TextBlock
x:Uid="SettingsPageAlbumEffect"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsStyle"> <controls:SegmentedItem Tag="LyricsStyle">
<TextBlock x:Uid="SettingsPageLyricsStyle" /> <TextBlock
x:Uid="SettingsPageLyricsStyle"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsEffect"> <controls:SegmentedItem Tag="LyricsEffect">
<TextBlock x:Uid="SettingsPageLyricsEffect" /> <TextBlock
x:Uid="SettingsPageLyricsEffect"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
<controls:SegmentedItem Tag="LyricsBackground"> <controls:SegmentedItem Tag="LyricsBackground">
<TextBlock x:Uid="SettingsPageBackgroundOverlay" /> <TextBlock
x:Uid="SettingsPageBackgroundOverlay"
MaxWidth="120"
TextWrapping="Wrap" />
</controls:SegmentedItem> </controls:SegmentedItem>
</controls:Segmented> </controls:Segmented>

View File

@@ -1,6 +1,6 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Serialization; using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
@@ -26,14 +26,14 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>(); 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); } set { SetValue(LyricsWindowStatusProperty, value); }
} }
public static readonly DependencyProperty LyricsWindowStatusProperty = 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() public LyricsWindowSettingsControl()
{ {
@@ -166,52 +166,14 @@ namespace BetterLyrics.WinUI3.Controls
ViewModel.OpenConfigPanel(); ViewModel.OpenConfigPanel();
} }
private void DemoWindowGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var status = (LyricsWindowStatus)(((FrameworkElement)sender).DataContext);
// <20>࿪ģʽ
if (_settingsService.AppSettings.GeneralSettings.MultiNowPlayingWindowMode)
{
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
}
// <20><><EFBFBD><EFBFBD>ģʽ
else
{
var openedWindows = WindowHook.GetWindows<NowPlayingWindow>();
foreach (var item in openedWindows.Where(x => x.LyricsWindowStatus != status))
{
item.CloseWindow();
}
WindowHook.OpenOrShowWindow<NowPlayingWindow>(status);
}
}
private void ConfigSelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args)
{
if (sender is SelectorBar bar)
{
if (bar.SelectedItem is SelectorBarItem item)
{
ViewModel?.SelectorBarSelectedItemTag = item.Tag;
}
}
}
private void CloseStatusButton_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
if (element.DataContext is LyricsWindowStatus data)
{
var window = WindowHook.GetWindows<NowPlayingWindow>().FirstOrDefault(x => x.LyricsWindowStatus == data);
window?.CloseWindow();
}
}
}
private void ConfigSegmented_SelectionChanged(object sender, SelectionChangedEventArgs e) private void ConfigSegmented_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
ViewModel.SelectorBarSelectedItemTag = (string)((SegmentedItem)((Segmented)sender).SelectedItem).Tag; ViewModel.SelectorBarSelectedItemTag = (string)((SegmentedItem)((Segmented)sender).SelectedItem).Tag;
} }
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
ViewModel.CloseConfigPanelCommand.Execute(null);
}
} }
} }

View File

@@ -1,5 +1,4 @@
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
@@ -7,7 +6,6 @@ using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
// To learn more about WinUI, the WinUI project structure, // 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) 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(); await HideAsync();
} }

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl <UserControl
x:Class="BetterLyrics.WinUI3.Controls.MediaSettingsControl" x:Class="BetterLyrics.WinUI3.Controls.MediaSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@@ -6,9 +5,12 @@
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI" xmlns:dev="using:DevWinUI"
xmlns:enums="using:BetterLyrics.WinUI3.Enums"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:settingsmodels="using:BetterLyrics.WinUI3.Models.Settings"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -49,39 +51,133 @@
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
SelectionMode="None"> SelectionMode="None">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate> <DataTemplate x:DataType="settingsmodels:MediaFolder">
<dev:SettingsExpander> <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> <dev:SettingsExpander.Header>
<HyperlinkButton <TextBlock IsTextSelectionEnabled="True" Text="{x:Bind Name, Mode=OneWay}" />
Click="LocalFolderHyperlinkButton_Click"
Content="{Binding Path, Mode=OneWay}"
Tag="{Binding Path, Mode=OneWay}" />
</dev:SettingsExpander.Header> </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:SettingsExpander.Items>
<dev:SettingsCard> <dev:SettingsCard x:Uid="MediaSettingsControlNameSetting">
<dev:SettingsCard.Header> <TextBox VerticalAlignment="Center" Text="{x:Bind Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Click="SettingsPageRemovePathButton_Click"
Tag="{Binding}" />
</dev:SettingsCard.Header>
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageMusicLibRealTimeWatch"> <dev:SettingsCard x:Uid="MediaSettingsControlLastSyncTime" Description="{x:Bind LastSyncTime.ToString(), Mode=OneWay, TargetNullValue=N/A}">
<ToggleSwitch IsOn="{Binding IsRealTimeWatchEnabled, Mode=TwoWay}" /> <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:SettingsCard>
</dev:SettingsExpander.Items> </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> </dev:SettingsExpander>
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
<dev:SettingsCard x:Uid="SettingsPageAddFolder" Style="{StaticResource DefaultSettingsExpanderItemStyle}"> <StackPanel
<Button Margin="0,6,0,0"
x:Uid="SettingsPageAddFolderButton" HorizontalAlignment="Right"
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}" Orientation="Horizontal"
CommandParameter="{Binding ElementName=RootGrid}" /> Spacing="6">
</dev:SettingsCard> <Button Command="{x:Bind ViewModel.OpenMusicGalleryWindowCommand}">
<TextBlock x:Uid="SystemTrayMusicGallery" />
</Button>
<DropDownButton x:Uid="SettingsPageAddFolderButton">
<DropDownButton.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="SettingsPageLocalFolder"
Command="{x:Bind ViewModel.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>
</StackPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -1,10 +1,8 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System;
using Windows.System;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -22,18 +20,14 @@ namespace BetterLyrics.WinUI3.Controls
private void SettingsPageRemovePathButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) 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) var folder = (MediaFolder)((FrameworkElement)sender).DataContext;
{ ViewModel.SyncFolder(folder);
if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri))
{
await Launcher.LaunchUriAsync(uri);
}
}
} }
} }
} }

View File

@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,10 +12,9 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Grid x:Name="RootGrid"> <Grid x:Name="RootGrid">
<Grid <Grid
x:Name="BottomCommandGrid" x:Name="BottomCommandGrid"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}" Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
Opacity="{x:Bind ViewModel.BottomCommandGridOpacity, Mode=OneWay}" Opacity="{x:Bind ViewModel.BottomCommandGridOpacity, Mode=OneWay}"
PointerEntered="BottomCommandGrid_PointerEntered" PointerEntered="BottomCommandGrid_PointerEntered"
PointerExited="BottomCommandGrid_PointerExited"> PointerExited="BottomCommandGrid_PointerExited">
@@ -60,16 +60,16 @@
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
<Grid VerticalAlignment="Center" CornerRadius="4"> <Grid VerticalAlignment="Center" CornerRadius="4">
<local:ImageSwitcher <local:ImageSwitcher
x:Name="AlbumArtImageSwitcher"
Width="36" Width="36"
Height="36" /> Height="36"
Source="{x:Bind ViewModel.GSMTCService.AlbumArtBitmapImage, Mode=OneWay}" />
</Grid> </Grid>
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">
<TextBlock x:Name="TitleTextBlock" /> <TextBlock Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<TextBlock <TextBlock
x:Name="ArtistsTextBlock"
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, Mode=OneWay}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
@@ -113,7 +113,58 @@
x:Name="BottomCenterCommandStackPanel" x:Name="BottomCenterCommandStackPanel"
Padding="16" Padding="16"
Orientation="Horizontal" 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 <Button
Command="{x:Bind ViewModel.PreviousSongCommand}" Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -126,13 +177,13 @@
Style="{StaticResource GhostButtonStyle}"> Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}" Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="True"> Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" /> <interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}" Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="False"> Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" /> <interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
@@ -146,13 +197,13 @@
Style="{StaticResource GhostButtonStyle}"> Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors> <interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}" Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="True"> Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" /> <interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior <interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}" Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal" ComparisonCondition="Equal"
Value="False"> Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" /> <interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -164,6 +215,17 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}" Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" /> Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<Button
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPagePlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
@@ -174,6 +236,19 @@
Orientation="Horizontal" Orientation="Horizontal"
Spacing="3"> Spacing="3">
<!-- Stop media session -->
<Button
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowStopButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Volume --> <!-- Volume -->
<Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}"> <Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}">
<Grid> <Grid>
@@ -342,13 +417,14 @@
<Slider <Slider
x:Name="TimelineSlider" x:Name="TimelineSlider"
Margin="0,-12,0,0" Margin="0,-14,0,0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}" Maximum="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Minimum="0" Minimum="0"
Style="{StaticResource GhostSliderStyle}" Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" /> ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.GSMTCService.CurrentPosition.TotalSeconds, Mode=OneWay}" />
<Grid <Grid
x:Name="TimelineSliderLyricsLineInfo" x:Name="TimelineSliderLyricsLineInfo"
@@ -356,7 +432,7 @@
Padding="8,4" Padding="8,4"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Top" VerticalAlignment="Top"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}" Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
CornerRadius="6" CornerRadius="6"
Opacity="{x:Bind ViewModel.TimelineSliderThumbOpacity, Mode=OneWay}"> Opacity="{x:Bind ViewModel.TimelineSliderThumbOpacity, Mode=OneWay}">
<Grid.OpacityTransition> <Grid.OpacityTransition>
@@ -371,7 +447,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" /> Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
<!-- TODO 原文翻译共同显示 --> <!-- TODO 原文翻译共同显示 -->
<TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.OriginalText, Mode=OneWay}" /> <TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.PrimaryText, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid <Grid
@@ -427,7 +503,58 @@
</Grid.ContextFlyout> </Grid.ContextFlyout>
</Grid> </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> </Grid>
</UserControl> </UserControl>

View File

@@ -1,15 +1,12 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
using System.Numerics; using System.Numerics;
@@ -18,15 +15,13 @@ using System.Numerics;
namespace BetterLyrics.WinUI3.Controls; namespace BetterLyrics.WinUI3.Controls;
public sealed partial class NowPlayingBar : UserControl, public sealed partial class NowPlayingBar : UserControl
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<BitmapImage?>>,
IRecipient<PropertyChangedMessage<TimeSpan>>
{ {
public NowPlayingBarViewModel ViewModel => (NowPlayingBarViewModel)DataContext; public NowPlayingBarViewModel ViewModel => (NowPlayingBarViewModel)DataContext;
public event EventHandler? SongInfoTapped; public event EventHandler? SongInfoTapped;
public event EventHandler? TimeTapped; public event EventHandler? TimeTapped;
public event EventHandler? PlayQueueButtonClick;
public bool ShowTime public bool ShowTime
{ {
@@ -46,6 +41,42 @@ public sealed partial class NowPlayingBar : UserControl,
public static readonly DependencyProperty ShowSongInfoProperty = public static readonly DependencyProperty ShowSongInfoProperty =
DependencyProperty.Register(nameof(ShowSongInfo), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false)); DependencyProperty.Register(nameof(ShowSongInfo), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlayingQueueButton
{
get { return (bool)GetValue(ShowPlayingQueueButtonProperty); }
set { SetValue(ShowPlayingQueueButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlayingQueueButtonProperty =
DependencyProperty.Register(nameof(ShowPlayingQueueButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlaybackOrderButton
{
get { return (bool)GetValue(ShowPlaybackOrderButtonProperty); }
set { SetValue(ShowPlaybackOrderButtonProperty, value); }
}
public static readonly DependencyProperty ShowStopButtonProperty =
DependencyProperty.Register(nameof(ShowStopButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowStopButton
{
get { return (bool)GetValue(ShowStopButtonProperty); }
set { SetValue(ShowStopButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public PlaybackOrder PlaybackOrder
{
get { return (PlaybackOrder)GetValue(PlaybackOrderProperty); }
set { SetValue(PlaybackOrderProperty, value); }
}
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsCompactMode public bool IsCompactMode
{ {
get { return (bool)GetValue(IsCompactModeProperty); } get { return (bool)GetValue(IsCompactModeProperty); }
@@ -70,8 +101,6 @@ public sealed partial class NowPlayingBar : UserControl,
{ {
InitializeComponent(); InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<NowPlayingBarViewModel>(); DataContext = Ioc.Default.GetRequiredService<NowPlayingBarViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
} }
private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -167,7 +196,7 @@ public sealed partial class NowPlayingBar : UserControl,
var grid = (Grid)sender; var grid = (Grid)sender;
var pos = e.GetCurrentPoint(grid).Position; var pos = e.GetCurrentPoint(grid).Position;
var ratio = pos.X / grid.ActualWidth; var ratio = pos.X / grid.ActualWidth;
ViewModel.MediaSessionsService.ChangePosition(TimelineSlider.Maximum * ratio); ViewModel.GSMTCService.ChangePosition(TimelineSlider.Maximum * ratio);
} }
private void TimelineSliderOverlay_PointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) private void TimelineSliderOverlay_PointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -190,7 +219,7 @@ public sealed partial class NowPlayingBar : UserControl,
private void TimelineSliderOverlay_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) 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) private void TimelineSliderOverlay_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -210,12 +239,12 @@ public sealed partial class NowPlayingBar : UserControl,
private void SongInfoStackPanel_Tapped(object sender, TappedRoutedEventArgs e) 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) 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) private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -262,40 +291,13 @@ public sealed partial class NowPlayingBar : UserControl,
} }
} }
public void Receive(PropertyChangedMessage<SongInfo?> message) private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
{ {
if (message.Sender is IMediaSessionsService) PlayQueueButtonClick?.Invoke(sender, EventArgs.Empty);
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
TitleTextBlock.Text = message.NewValue?.Title;
ArtistsTextBlock.Text = message.NewValue?.DisplayArtists;
}
}
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
{
AlbumArtImageSwitcher.Source = message.NewValue;
}
}
} }
public void Receive(PropertyChangedMessage<TimeSpan> message) private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
{ {
if (message.Sender is IMediaSessionsService) PlaybackOrder = PlaybackOrder.GetNext();
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
DispatcherQueue.TryEnqueue(() =>
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
});
} }
} }
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PatronControl"
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 Margin="12,8">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind PatronName, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorTertiaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,34 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
// 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 PatronControl : UserControl
{
public string PatronName
{
get { return (string)GetValue(PatronNameProperty); }
set { SetValue(PatronNameProperty, value); }
}
public static readonly DependencyProperty PatronNameProperty =
DependencyProperty.Register(nameof(PatronName), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public string Date
{
get { return (string)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register(nameof(Date), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public PatronControl()
{
InitializeComponent();
}
}
}

View File

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

View File

@@ -0,0 +1,94 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System;
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.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class PlayQueue : UserControl, IRecipient<PropertyChangedMessage<int>>
{
public PlayQueueViewModel ViewModel => (PlayQueueViewModel)DataContext;
public PlayQueue()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<PlayQueueViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
}
private void ScrollToPlayingItem()
{
if (PlayingQueueListView == null) return;
var targetItem = ViewModel.SMTCService.TrackPlayingQueue
.ElementAtOrDefault(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
if (targetItem == null) return;
PlayingQueueListView.ScrollIntoView(targetItem);
}
private void ScrollToPlayingItemButton_Click(object sender, RoutedEventArgs e)
{
ScrollToPlayingItem();
}
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
await ViewModel.SMTCService.PlayTrackAsync(item);
}
private async void RemoveFromPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
bool playNext = false;
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
int index = ViewModel.SMTCService.TrackPlayingQueue.IndexOf(item);
if (item == PlayingQueueListView.SelectedItem)
{
playNext = true;
}
ViewModel.SMTCService.TrackPlayingQueue.Remove(item);
if (playNext)
{
if (ViewModel.SMTCService.TrackPlayingQueue.Count == 0)
{
index = -1;
}
else if (index >= ViewModel.SMTCService.TrackPlayingQueue.Count)
{
index = ViewModel.SMTCService.TrackPlayingQueue.Count - 1;
}
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = index;
await ViewModel.SMTCService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
}
private async void EmptyPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.SMTCService.TrackPlayingQueue.Clear();
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = -1;
await ViewModel.SMTCService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is MusicGallerySettings)
{
if (message.PropertyName == nameof(MusicGallerySettings.PlayQueueIndex))
{
ScrollToPlayingItem();
}
}
}
}
}

View File

@@ -13,6 +13,7 @@
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models" xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:settingsmodels="using:BetterLyrics.WinUI3.Models.Settings"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -44,10 +45,10 @@
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
<InfoBar <InfoBar
x:Uid="SettingsPageMusicGalleryOpened"
Grid.Row="0" Grid.Row="0"
IsClosable="False" IsClosable="False"
IsOpen="{x:Bind ViewModel.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened, Mode=OneWay}" IsOpen="{x:Bind ViewModel.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened, Mode=OneWay}"
Message="音乐库窗口已打开,将忽略对其他播放源的监听"
Severity="Informational" /> Severity="Informational" />
<!-- 播放源列表 --> <!-- 播放源列表 -->
@@ -60,7 +61,7 @@
ItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}"> SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:MediaSourceProviderInfo"> <DataTemplate x:DataType="settingsmodels:MediaSourceProviderInfo">
<Grid Padding="2,4" ColumnSpacing="12"> <Grid Padding="2,4" ColumnSpacing="12">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -172,7 +173,7 @@
<ScalarTransition /> <ScalarTransition />
</ListView.OpacityTransition> </ListView.OpacityTransition>
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:AlbumArtSearchProviderInfo"> <DataTemplate x:DataType="settingsmodels:AlbumArtSearchProviderInfo">
<dev:SettingsCard Header="{Binding Provider, Converter={StaticResource AlbumArtSearchProviderToDisplayNameConverter}, Mode=OneWay}"> <dev:SettingsCard Header="{Binding Provider, Converter={StaticResource AlbumArtSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<dev:SettingsCard.HeaderIcon> <dev:SettingsCard.HeaderIcon>
<FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" /> <FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" />
@@ -219,7 +220,7 @@
</Style> </Style>
</ListView.ItemContainerStyle> </ListView.ItemContainerStyle>
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchProviderInfo"> <DataTemplate x:DataType="settingsmodels:LyricsSearchProviderInfo">
<Grid> <Grid>
<dev:SettingsExpander Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}" IsExpanded="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}"> <dev:SettingsExpander Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}" IsExpanded="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}">
<dev:SettingsExpander.HeaderIcon> <dev:SettingsExpander.HeaderIcon>
@@ -227,6 +228,9 @@
</dev:SettingsExpander.HeaderIcon> </dev:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items> <dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache">
<CheckBox IsChecked="{Binding IgnoreCacheWhenSearching, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageOverwriteMatchingThreshold"> <dev:SettingsCard x:Uid="SettingsPageOverwriteMatchingThreshold">
<ToggleSwitch IsOn="{Binding IsMatchingThresholdOverwritten, Mode=TwoWay}" /> <ToggleSwitch IsOn="{Binding IsMatchingThresholdOverwritten, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
@@ -314,8 +318,8 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"> HorizontalContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.GSMTCService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Expander> </Expander>
@@ -325,13 +329,10 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"> HorizontalContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, 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="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
x:Uid="LyricsSearchControlDurauion"
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Duration, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Expander> </Expander>
@@ -341,28 +342,22 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"> HorizontalContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Artist, 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="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
x:Uid="LyricsSearchControlDurauion" <local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
Unit="s"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow
x:Uid="LyricsPageLyricsProviderPrefix" x:Uid="LyricsPageLyricsProviderPrefix"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}" Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay}" ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" /> Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" /> <local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" /> <local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow <local:PropertyRow
x:Uid="LyricsPageMatchPercentage" x:Uid="LyricsPageMatchPercentage"
Unit="%" Unit="%"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" /> Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Expander> </Expander>
@@ -370,10 +365,6 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- Lyrics translation --> <!-- Lyrics translation -->
<TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True"> <dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True">
@@ -397,16 +388,23 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageLibreTranslateServer" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsLibreTranslateEnabled, Mode=OneWay}"> <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 <TextBox
x:Uid="LibreTranslateServerTextBox" x:Uid="LibreTranslateServerTextBox"
Grid.Column="0"
IsEnabled="{x:Bind ViewModel.IsLibreTranslateServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" 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 <Button
x:Uid="SettingsPageServerTestButton" x:Uid="SettingsPageServerTestButton"
Grid.Column="1"
Command="{x:Bind ViewModel.LibreTranslateServerTestCommand}" Command="{x:Bind ViewModel.LibreTranslateServerTestCommand}"
IsEnabled="{x:Bind ViewModel.IsLibreTranslateServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" /> IsEnabled="{x:Bind ViewModel.IsLibreTranslateServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
</StackPanel> </Grid>
</dev:SettingsCard> </dev:SettingsCard>
</dev:SettingsExpander.Items> </dev:SettingsExpander.Items>
</dev:SettingsExpander> </dev:SettingsExpander>
@@ -424,9 +422,33 @@
</dev:SettingsCard> </dev:SettingsCard>
</dev:SettingsExpander.Items> </dev:SettingsExpander.Items>
</dev:SettingsExpander> </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}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=TwoWay}" />
<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:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<!-- 中文简体繁体偏好 --> <!-- 中文简体繁体偏好 -->
<TextBlock x:Uid="SettingsPageChineseLyrics" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="SettingsPageChineseLyrics" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -435,9 +457,9 @@
</dev:SettingsCard> </dev:SettingsCard>
<!-- Last.fm --> <!-- Last.fm -->
<TextBlock x:Uid="SettingsPageLastFM" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Last.fm" />
<dev:SettingsExpander <dev:SettingsExpander
x:Uid="SettingsPageLastFMManager" Header="Last.fm"
HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/LastFM.png}" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/LastFM.png}"
IsExpanded="{x:Bind ViewModel.IsLastFMAuthenticated, Mode=OneWay}"> IsExpanded="{x:Bind ViewModel.IsLastFMAuthenticated, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6"> <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.Settings;
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.Helper;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Input; using Microsoft.UI.Input;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -16,7 +16,7 @@ namespace BetterLyrics.WinUI3.Controls
{ {
public sealed partial class ShortcutTextBox : UserControl public sealed partial class ShortcutTextBox : UserControl
{ {
private readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>(); private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public ShortcutTextBox() public ShortcutTextBox()
{ {

View File

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

View File

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

View File

@@ -14,7 +14,6 @@
x:Name="TrayIcon" x:Name="TrayIcon"
x:FieldModifier="public" x:FieldModifier="public"
ContextMenuMode="SecondWindow" ContextMenuMode="SecondWindow"
DoubleClickCommand="{x:Bind ViewModel.OpenLyricsWindowSwitchCommand}"
IconSource="ms-appx:///Assets/Logo.ico" IconSource="ms-appx:///Assets/Logo.ico"
LeftClickCommand="{x:Bind ViewModel.OpenLyricsWindowSwitchCommand}" LeftClickCommand="{x:Bind ViewModel.OpenLyricsWindowSwitchCommand}"
NoLeftClickDelay="True" NoLeftClickDelay="True"

View File

@@ -18,13 +18,7 @@
<TextBlock x:Uid="AppSettingsControlGeneral" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="AppSettingsControlGeneral" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsCard x:Uid="SettingsPageConfigName" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8AC;}"> <dev:SettingsCard x:Uid="SettingsPageConfigName" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8AC;}">
<StackPanel <TextBox Text="{x:Bind LyricsWindowStatus.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" />
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>
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsExpander <dev:SettingsExpander
@@ -102,6 +96,10 @@
</dev:SettingsExpander.Items> </dev:SettingsExpander.Items>
</dev:SettingsExpander> </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 <dev:SettingsExpander
x:Uid="SettingsPageAOT" x:Uid="SettingsPageAOT"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},

View File

@@ -1,5 +1,4 @@
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
@@ -8,7 +8,7 @@ namespace BetterLyrics.WinUI3.Converter
{ {
public partial class AlbumArtSearchProviderToDisplayNameConverter : IValueConverter 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
@@ -16,8 +16,8 @@ namespace BetterLyrics.WinUI3.Converter
{ {
return provider switch return provider switch
{ {
AlbumArtSearchProvider.Local => _resourceService.GetLocalizedString("AlbumArtSearchLocalProvider"), AlbumArtSearchProvider.Local => _localizationService.GetLocalizedString("AlbumArtSearchLocalProvider"),
AlbumArtSearchProvider.SMTC => _resourceService.GetLocalizedString("AlbumArtSearchSMTCProvider"), AlbumArtSearchProvider.SMTC => _localizationService.GetLocalizedString("AlbumArtSearchSMTCProvider"),
AlbumArtSearchProvider.iTunes => "iTunes", AlbumArtSearchProvider.iTunes => "iTunes",
_ => throw new Exception($"Unknown AlbumArtSearchProvider: {provider}"), _ => throw new Exception($"Unknown AlbumArtSearchProvider: {provider}"),
}; };

View File

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

View File

@@ -0,0 +1,31 @@
using Microsoft.UI.Xaml.Data;
using System;
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 (value is string langCode)
{ {
if (PhoneticHelper.IsPhoneticCode(langCode)) if (langCode == "N/A")
{
return langCode;
}
else if (PhoneticHelper.IsPhoneticCode(langCode))
{ {
return PhoneticHelper.GetDisplayName(langCode); return PhoneticHelper.GetDisplayName(langCode);
} }

View File

@@ -1,7 +1,7 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
@@ -10,7 +10,7 @@ namespace BetterLyrics.WinUI3.Converter
{ {
public partial class LyricsSearchProviderToDisplayNameConverter : IValueConverter 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
@@ -24,10 +24,10 @@ namespace BetterLyrics.WinUI3.Converter
LyricsSearchProvider.Kugou => "酷狗音乐", LyricsSearchProvider.Kugou => "酷狗音乐",
LyricsSearchProvider.AmllTtmlDb => "amll-ttml-db", LyricsSearchProvider.AmllTtmlDb => "amll-ttml-db",
LyricsSearchProvider.AppleMusic => "Apple Music", LyricsSearchProvider.AppleMusic => "Apple Music",
LyricsSearchProvider.LocalLrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"), LyricsSearchProvider.LocalLrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
LyricsSearchProvider.LocalMusicFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"), LyricsSearchProvider.LocalMusicFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
LyricsSearchProvider.LocalEslrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderEslrcFile"), LyricsSearchProvider.LocalEslrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
LyricsSearchProvider.LocalTtmlFile => _resourceService.GetLocalizedString("LyricsSearchProviderTtmlFile"), LyricsSearchProvider.LocalTtmlFile => _localizationService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
_ => "N/A", _ => "N/A",
}; };
} }

View File

@@ -1,6 +1,7 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Services.ResourceService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
@@ -10,13 +11,13 @@ namespace BetterLyrics.WinUI3.Converter
{ {
public partial class MatchedLocalFilesPathToVisibilityConverter : IValueConverter 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
if (value is string path) if (value is string path)
{ {
if (path == _resourceService.GetLocalizedString("MainPageNoLocalFilesMatched")) if (path == _localizationService.GetLocalizedString("MainPageNoLocalFilesMatched"))
{ {
return Visibility.Collapsed; return Visibility.Collapsed;
} }

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.IO;
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,7 +1,7 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
@@ -10,7 +10,7 @@ namespace BetterLyrics.WinUI3.Converter
{ {
public partial class TranslationSearchProviderToDisplayNameConverter : IValueConverter 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
@@ -24,10 +24,10 @@ namespace BetterLyrics.WinUI3.Converter
TranslationSearchProvider.Kugou => "酷狗音乐", TranslationSearchProvider.Kugou => "酷狗音乐",
TranslationSearchProvider.AmllTtmlDb => "amll-ttml-db", TranslationSearchProvider.AmllTtmlDb => "amll-ttml-db",
TranslationSearchProvider.AppleMusic => "Apple Music", TranslationSearchProvider.AppleMusic => "Apple Music",
TranslationSearchProvider.LocalLrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"), TranslationSearchProvider.LocalLrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
TranslationSearchProvider.LocalMusicFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"), TranslationSearchProvider.LocalMusicFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
TranslationSearchProvider.LocalEslrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderEslrcFile"), TranslationSearchProvider.LocalEslrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
TranslationSearchProvider.LocalTtmlFile => _resourceService.GetLocalizedString("LyricsSearchProviderTtmlFile"), TranslationSearchProvider.LocalTtmlFile => _localizationService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
TranslationSearchProvider.LibreTranslate => "LibreTranslate", TranslationSearchProvider.LibreTranslate => "LibreTranslate",
_ => "N/A", _ => "N/A",
}; };

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
@@ -8,7 +8,7 @@ namespace BetterLyrics.WinUI3.Converter
{ {
public partial class TransliterationSearchProviderToDisplayNameConverter : IValueConverter 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) public object Convert(object value, Type targetType, object parameter, string language)
{ {
@@ -22,11 +22,12 @@ namespace BetterLyrics.WinUI3.Converter
TransliterationSearchProvider.Kugou => "酷狗音乐", TransliterationSearchProvider.Kugou => "酷狗音乐",
TransliterationSearchProvider.AmllTtmlDb => "amll-ttml-db", TransliterationSearchProvider.AmllTtmlDb => "amll-ttml-db",
TransliterationSearchProvider.AppleMusic => "Apple Music", TransliterationSearchProvider.AppleMusic => "Apple Music",
TransliterationSearchProvider.LocalLrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"), TransliterationSearchProvider.LocalLrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalLrcFile"),
TransliterationSearchProvider.LocalMusicFile => _resourceService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"), TransliterationSearchProvider.LocalMusicFile => _localizationService.GetLocalizedString("LyricsSearchProviderLocalMusicFile"),
TransliterationSearchProvider.LocalEslrcFile => _resourceService.GetLocalizedString("LyricsSearchProviderEslrcFile"), TransliterationSearchProvider.LocalEslrcFile => _localizationService.GetLocalizedString("LyricsSearchProviderEslrcFile"),
TransliterationSearchProvider.LocalTtmlFile => _resourceService.GetLocalizedString("LyricsSearchProviderTtmlFile"), TransliterationSearchProvider.LocalTtmlFile => _localizationService.GetLocalizedString("LyricsSearchProviderTtmlFile"),
TransliterationSearchProvider.BetterLyrics => "BetterLyrics", TransliterationSearchProvider.BetterLyrics => "BetterLyrics",
TransliterationSearchProvider.CutletDocker => "cutlet-docker",
_ => "N/A", _ => "N/A",
}; };
} }

View File

@@ -1,17 +1,16 @@
using ATL; using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
namespace BetterLyrics.WinUI3.Converter namespace BetterLyrics.WinUI3.Converter
{ {
public partial class TrackToLyricsConverter : IValueConverter public partial class UriStringToDecodedAbsoluteUri : IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, string language) public object Convert(object value, Type targetType, object parameter, string language)
{ {
if (value is Track track) if (value is string uriString)
{ {
return track.GetRawLyrics(); return uriString.ToDecodedAbsoluteUri();
} }
return ""; return "";
} }

View File

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

@@ -1,14 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum Language
{
FollowSystem,
English,
SimplifiedChinese,
TraditionalChinese,
Japanese,
Korean,
}
}

View File

@@ -1,10 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LocalSearchTargetProps
{
LyricsOnly,
LyricsAndAlbumArt,
}
}

View File

@@ -1,8 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum SettingsStoreType
{
Container,
JSON
}
}

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Enums namespace BetterLyrics.WinUI3.Enums
{ {
public enum ShortcutID public enum ShortcutId
{ {
LyricsWindowShowOrHide, LyricsWindowShowOrHide,
LyricsWindowSwitch, LyricsWindowSwitch,

View File

@@ -0,0 +1,13 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum StatsRange
{
Today,
ThisWeek,
ThisMonth,
ThisQuarter,
ThisYear,
AllTime,
Custom
}
}

View File

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

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum WordByWordEffectMode
{
Auto,
Never,
Always,
}
}

View File

@@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, List<Color> albumArtLightAccentColors, List<Color> albumArtDarkAccentColors) : EventArgs
{
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public List<Color> AlbumArtLightAccentColors { get; set; } = albumArtLightAccentColors;
public List<Color> AlbumArtDarkAccentColors { get; set; } = albumArtDarkAccentColors;
}
}

View File

@@ -1,14 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
using System.IO;
namespace BetterLyrics.WinUI3.Events
{
public class LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType) : EventArgs
{
public WatcherChangeTypes ChangeType { get; } = changeType;
public string FilePath { get; } = filePath;
public string Folder { get; } = folder;
}
}

View File

@@ -1,10 +0,0 @@
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Events
{
public class LyricsChangedEventArgs(LyricsData? lyricsData) : EventArgs
{
public LyricsData? LyricsData { get; } = lyricsData;
}
}

View File

@@ -1,11 +0,0 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Events
{
public class MediaSourceProvidersInfoEventArgs(List<MediaSourceProviderInfo> sessionIds) : EventArgs
{
public List<MediaSourceProviderInfo> MediaSourceProviersInfo { get; set; } = sessionIds;
}
}

View File

@@ -0,0 +1,135 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Services.LocalizationService;
using System;
using System.Collections.Generic;
using System.Linq;
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,
PrimaryText = "● ● ●",
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }],
},
],
LanguageCode = "N/A",
};
}
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PrimaryText = "N/A",
PrimarySyllables = [new BaseLyrics { Text = "N/A", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }],
}]);
}
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.SecondaryText = transLine.PrimaryText;
}
else
{
// 没有匹配的翻译
line.SecondaryText = "";
}
}
}
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.TertiaryText = transLine.PrimaryText;
}
else
{
// 没有匹配的音译
line.TertiaryText = "";
}
}
}
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.SecondaryText = ""; // No translation available, keep empty
}
else
{
line.SecondaryText = 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.TertiaryText = ""; // No transliteration available, keep empty
}
else
{
line.TertiaryText = transliterationArr[i];
}
i++;
}
}
public LyricsLine? GetLyricsLine(double sec)
{
for (int i = 0; i < lyricsData.LyricsLines.Count; i++)
{
var line = lyricsData.LyricsLines[i];
if (line.StartMs > sec * 1000)
{
return lyricsData.LyricsLines.ElementAtOrDefault(i - 1);
}
}
return null;
}
}
}
}

View File

@@ -1,6 +1,4 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
namespace BetterLyrics.WinUI3.Extensions namespace BetterLyrics.WinUI3.Extensions
{ {
@@ -8,21 +6,6 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
extension(LyricsSearchProvider provider) extension(LyricsSearchProvider provider)
{ {
public string GetCacheDirectory() => provider switch
{
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
LyricsSearchProvider.LocalMusicFile => PathHelper.LocalMusicCacheDirectory,
LyricsSearchProvider.LocalLrcFile => PathHelper.LocalLrcCacheDirectory,
LyricsSearchProvider.LocalEslrcFile => PathHelper.LocalEslrcCacheDirectory,
LyricsSearchProvider.LocalTtmlFile => PathHelper.LocalTtmlCacheDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
};
public LyricsFormat GetLyricsFormat() => provider switch public LyricsFormat GetLyricsFormat() => provider switch
{ {
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc, LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,

View File

@@ -1,8 +1,7 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -12,14 +11,14 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
public static class LyricsWindowStatusExtensions 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) public static LyricsWindowStatus DesktopMode(Window? window = null)
{ {
window ??= WindowHook.GetWindow<SystemTrayWindow>(); window ??= WindowHook.GetWindow<SystemTrayWindow>();
return new LyricsWindowStatus(window) return new LyricsWindowStatus(window)
{ {
Name = _resourceService.GetLocalizedString("DesktopMode"), Name = _localizationService.GetLocalizedString("DesktopMode"),
LyricsDisplayType = LyricsDisplayType.LyricsOnly, LyricsDisplayType = LyricsDisplayType.LyricsOnly,
WindowBounds = new Rect(100, 100, 600, 250), WindowBounds = new Rect(100, 100, 600, 250),
IsLocked = true, IsLocked = true,
@@ -44,7 +43,7 @@ namespace BetterLyrics.WinUI3.Extensions
window ??= WindowHook.GetWindow<SystemTrayWindow>(); window ??= WindowHook.GetWindow<SystemTrayWindow>();
var status = new LyricsWindowStatus(window) var status = new LyricsWindowStatus(window)
{ {
Name = _resourceService.GetLocalizedString("DockedMode"), Name = _localizationService.GetLocalizedString("DockedMode"),
IsWorkArea = true, IsWorkArea = true,
IsAlwaysOnTop = true, IsAlwaysOnTop = true,
IsAlwaysOnTopPolling = true, IsAlwaysOnTopPolling = true,
@@ -71,7 +70,7 @@ namespace BetterLyrics.WinUI3.Extensions
window ??= WindowHook.GetWindow<SystemTrayWindow>(); window ??= WindowHook.GetWindow<SystemTrayWindow>();
var status = new LyricsWindowStatus(window) var status = new LyricsWindowStatus(window)
{ {
Name = _resourceService.GetLocalizedString("FullscreenMode"), Name = _localizationService.GetLocalizedString("FullscreenMode"),
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical, LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
LyricsStyleSettings = new LyricsStyleSettings LyricsStyleSettings = new LyricsStyleSettings
{ {
@@ -93,7 +92,7 @@ namespace BetterLyrics.WinUI3.Extensions
window ??= WindowHook.GetWindow<SystemTrayWindow>(); window ??= WindowHook.GetWindow<SystemTrayWindow>();
return new LyricsWindowStatus(window) return new LyricsWindowStatus(window)
{ {
Name = _resourceService.GetLocalizedString("StandardMode"), Name = _localizationService.GetLocalizedString("StandardMode"),
}; };
} }
@@ -102,7 +101,7 @@ namespace BetterLyrics.WinUI3.Extensions
window ??= WindowHook.GetWindow<SystemTrayWindow>(); window ??= WindowHook.GetWindow<SystemTrayWindow>();
return new LyricsWindowStatus(window) return new LyricsWindowStatus(window)
{ {
Name = _resourceService.GetLocalizedString("NarrowMode"), Name = _localizationService.GetLocalizedString("NarrowMode"),
WindowBounds = new Rect(100, 100, 400, 800), WindowBounds = new Rect(100, 100, 400, 800),
LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical, LyricsLayoutOrientation = LyricsLayoutOrientation.Vertical,
}; };
@@ -113,7 +112,7 @@ namespace BetterLyrics.WinUI3.Extensions
window ??= WindowHook.GetWindow<SystemTrayWindow>(); window ??= WindowHook.GetWindow<SystemTrayWindow>();
return new LyricsWindowStatus(window) return new LyricsWindowStatus(window)
{ {
Name = _resourceService.GetLocalizedString("TaskbarMode"), Name = _localizationService.GetLocalizedString("TaskbarMode"),
LyricsDisplayType = LyricsDisplayType.LyricsOnly, LyricsDisplayType = LyricsDisplayType.LyricsOnly,
IsPinToTaskbar = true, IsPinToTaskbar = true,
IsLocked = true, IsLocked = true,

View File

@@ -1,14 +1,19 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Entities;
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions namespace BetterLyrics.WinUI3.Extensions
{ {
public static class SongInfoExtensions public static class SongInfoExtensions
{ {
public static SongInfo Placeholder => new SongInfo public static SongInfo Placeholder => new()
{ {
Title = "N/A", Title = "N/A",
Album = "N/A", Album = "N/A",
Artists = ["N/A"], Artist = "N/A",
}; };
extension(SongInfo songInfo) extension(SongInfo songInfo)
@@ -19,9 +24,9 @@ namespace BetterLyrics.WinUI3.Extensions
return songInfo; return songInfo;
} }
public SongInfo WithArtist(string[] value) public SongInfo WithArtist(string value)
{ {
songInfo.Artists = value; songInfo.Artist = value;
return songInfo; return songInfo;
} }
@@ -30,6 +35,45 @@ namespace BetterLyrics.WinUI3.Extensions
songInfo.Album = value; songInfo.Album = value;
return songInfo; return songInfo;
} }
public SongInfo WithSongId(string value)
{
songInfo.SongId = value;
return songInfo;
}
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{
if (songInfo == null) return null;
return new PlayHistoryItem
{
Title = songInfo.Title,
Artist = songInfo.Artist,
Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.FromBinary(songInfo.StartedAt)
};
}
public string GetCacheKey()
{
string title = songInfo.Title?.Trim() ?? "";
string album = songInfo.Album?.Trim() ?? "";
string artists = songInfo.Artist?.Trim() ?? "";
long seconds = (long)Math.Round(songInfo.Duration);
string durationPart = seconds.ToString(CultureInfo.InvariantCulture);
string rawKey = $"{title}|{artists}|{album}|{durationPart}";
using var sha256 = SHA256.Create();
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawKey));
return Convert.ToHexString(bytes);
}
} }
} }
} }

View File

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

View File

@@ -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.Enums;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Windowing; using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -9,16 +9,24 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
public static class WindowExtensions 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) extension(Window window)
{ {
public void Init( public void Init(
string titleKey, string titleKey = "",
string title = "",
TitleBarHeightOption titleBarHeightOption = TitleBarHeightOption.Standard, TitleBarHeightOption titleBarHeightOption = TitleBarHeightOption.Standard,
BackdropType backdropType = BackdropType.DesktopAcrylic) 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.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
window.AppWindow.SetIcons(); window.AppWindow.SetIcons();

View File

@@ -15,6 +15,16 @@ namespace BetterLyrics.WinUI3.Helper
{ {
public static class ColorHelper public static class ColorHelper
{ {
public static Color GetSystemAccentColor()
{
if (Application.Current.Resources.TryGetValue("SystemAccentColor", out var resource) &&
resource is Color uiColor)
{
return uiColor;
}
return Color.FromArgb(255, 0, 120, 215);
}
public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor) public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor)
{ {
// 计算亮度YIQ公式 // 计算亮度YIQ公式

View File

@@ -56,6 +56,57 @@
} }
catch (Exception) { } catch (Exception) { }
} }
/// <summary>
/// https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories
/// </summary>
/// <param name="sourceDir"></param>
/// <param name="destinationDir"></param>
/// <param name="recursive"></param>
/// <exception cref="DirectoryNotFoundException"></exception>
public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive)
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
// Check if the source directory exists
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
// Cache directories before we start copying
DirectoryInfo[] dirs = dir.GetDirectories();
// Create the destination directory
Directory.CreateDirectory(destinationDir);
// Get the files in the source directory and copy to the destination directory
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
CopyLockedFile(file.FullName, targetFilePath);
}
// If recursive and copying subdirectories, recursively call this method
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}
private static void CopyLockedFile(string sourcePath, string targetPath)
{
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var destStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
{
sourceStream.CopyTo(destStream);
}
}
} }
} }
} }

View File

@@ -3,10 +3,12 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions; using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Ude; using Ude;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
@@ -27,6 +29,18 @@ namespace BetterLyrics.WinUI3.Helper
return Encoding.GetEncoding(encoding); 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 = '_') public static string SanitizeFileName(string fileName, char replacement = '_')
{ {
var invalidChars = Path.GetInvalidFileNameChars(); var invalidChars = Path.GetInvalidFileNameChars();
@@ -38,22 +52,6 @@ namespace BetterLyrics.WinUI3.Helper
return sb.ToString(); return sb.ToString();
} }
public static LyricsSearchResult? ReadLyricsCache(SongInfo songInfo, LyricsSearchProvider lyricsSearchProvider)
{
var cacheFilePath = Path.Combine(
lyricsSearchProvider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
if (File.Exists(cacheFilePath))
{
var json = File.ReadAllText(cacheFilePath);
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
data?.SelfPath = cacheFilePath;
return data;
}
return null;
}
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath) public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{ {
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}")); var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
@@ -64,19 +62,9 @@ namespace BetterLyrics.WinUI3.Helper
return null; return null;
} }
public static void WriteLyricsCache(SongInfo songInfo, LyricsSearchResult lyricsSearchResult)
{
var cacheFilePath = Path.Combine(
lyricsSearchResult.Provider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
lyricsSearchResult.SelfPath = cacheFilePath;
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
File.WriteAllText(cacheFilePath, json);
}
public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath) public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath)
{ {
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.DisplayArtists} - {songInfo.Album}{format}")); var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.Artist} - {songInfo.Album}{format}"));
File.WriteAllBytes(cacheFilePath, img); File.WriteAllBytes(cacheFilePath, img);
} }
@@ -86,5 +74,15 @@ namespace BetterLyrics.WinUI3.Helper
".wav", ".aiff", ".aif", ".pcm", ".cda", ".dsf", ".dff", ".au", ".snd", ".wav", ".aiff", ".aif", ".pcm", ".cda", ".dsf", ".dff", ".au", ".snd",
".mid", ".midi", ".mod", ".xm", ".it", ".s3m" ".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,90 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
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,8 +1,6 @@
using Microsoft.Graphics.Canvas.Text; using System.Collections.Generic;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Windows.Documents;
using System.Windows.Markup; using System.Windows.Markup;
using System.Windows.Media; using System.Windows.Media;

View File

@@ -1,8 +1,9 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using NTextCat; using NTextCat;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using Windows.Globalization; using Windows.Globalization;
@@ -10,9 +11,9 @@ namespace BetterLyrics.WinUI3.Helper
{ {
public class LanguageHelper public class LanguageHelper
{ {
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
private static readonly RankedLanguageIdentifierFactory _factory = new(); private static readonly RankedLanguageIdentifierFactory _factory = new();
private static readonly RankedLanguageIdentifier _identifier; private static readonly RankedLanguageIdentifier _identifier;
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>();
public const string ChineseCode = "zh"; public const string ChineseCode = "zh";
public const string JapaneseCode = "ja"; public const string JapaneseCode = "ja";
@@ -92,12 +93,23 @@ namespace BetterLyrics.WinUI3.Helper
public static List<ExtendedLanguage> SupportedDisplayLanguages { get; set; } = public static List<ExtendedLanguage> SupportedDisplayLanguages { get; set; } =
[ [
new ExtendedLanguage("", _resourceService.GetLocalizedString("SettingsPageSystemLanguage")), new ExtendedLanguage(CultureInfo.CurrentUICulture.Name, _localizationService.GetLocalizedString("SettingsPageSystemLanguage")),
new ExtendedLanguage("en-US", "English"), new ExtendedLanguage("ar"),
new ExtendedLanguage("ja-JP"), new ExtendedLanguage("de"),
new ExtendedLanguage("ko-KR"), new ExtendedLanguage("en"),
new ExtendedLanguage("zh-CN", "简体中文"), new ExtendedLanguage("es"),
new ExtendedLanguage("zh-TW", "繁體中文"), 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() static LanguageHelper()
@@ -107,7 +119,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string? DetectLanguageCode(string? text) public static string? DetectLanguageCode(string? text)
{ {
if (text == null) return null; if (string.IsNullOrWhiteSpace(text)) return null;
var guessList = _identifier.Identify(text); var guessList = _identifier.Identify(text);
string? code = guessList?.FirstOrDefault()?.Item1.Iso639_2T; string? code = guessList?.FirstOrDefault()?.Item1.Iso639_2T;
code = code switch code = code switch

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Entities;
using F23.StringSimilarity; using F23.StringSimilarity;
using System; using System;
using System.IO; using System.IO;
@@ -9,29 +10,39 @@ namespace BetterLyrics.WinUI3.Helper
{ {
public static partial class MetadataComparer public static partial class MetadataComparer
{ {
private const double WeightTitle = 0.40; private const double WeightTitle = 0.30;
private const double WeightArtist = 0.40; private const double WeightArtist = 0.30;
private const double WeightAlbum = 0.10; private const double WeightAlbum = 0.10;
private const double WeightDuration = 0.10; private const double WeightDuration = 0.30;
// JaroWinkler 适合短字符串匹配 // JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new(); private static readonly JaroWinkler _algo = new();
public static int CalculateScore(SongInfo local, LyricsSearchResult remote) public static int CalculateScore(SongInfo songInfo, LyricsCacheItem remote)
{ {
if (local == null || remote == null) return 0; return CalculateScore(songInfo, remote.Title, remote.Artist, remote.Album, remote.Duration);
}
public static int CalculateScore(SongInfo songInfo, FilesIndexItem local)
{
return CalculateScore(songInfo, local.Title, local.Artist, local.Album, local.Duration, local.FileName);
}
public static int CalculateScore(
SongInfo songInfo,
string? compareTitle, string? compareArtist, string? compareAlbum, double? compareDuration, string? compareFileName = null)
{
double totalScore = 0; double totalScore = 0;
bool localHasMetadata = !string.IsNullOrWhiteSpace(local.Title); bool localHasMetadata = !string.IsNullOrWhiteSpace(songInfo.Title);
bool remoteHasMetadata = !string.IsNullOrWhiteSpace(remote.Title); bool remoteHasMetadata = !string.IsNullOrWhiteSpace(compareTitle);
if (localHasMetadata && remoteHasMetadata) if (localHasMetadata && remoteHasMetadata)
{ {
double titleScore = GetStringSimilarity(local.Title, remote.Title); double titleScore = GetStringSimilarity(songInfo.Title, compareTitle);
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists); double artistScore = GetStringSimilarity(songInfo.Artist, compareArtist);
double albumScore = GetStringSimilarity(local.Album, remote.Album); double albumScore = GetStringSimilarity(songInfo.Album, compareAlbum);
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration); double durationScore = GetDurationSimilarity(songInfo.Duration, compareDuration);
totalScore = (titleScore * WeightTitle) + totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) + (artistScore * WeightArtist) +
@@ -41,12 +52,12 @@ namespace BetterLyrics.WinUI3.Helper
else else
{ {
string? localQuery = localHasMetadata string? localQuery = localHasMetadata
? $"{local.Title} {string.Join(" ", local.Artists ?? [])}" ? $"{songInfo.Title} {songInfo.Artist}"
: Path.GetFileNameWithoutExtension(local.LinkedFileName); : Path.GetFileNameWithoutExtension(songInfo.LinkedFileName);
string remoteQuery = remoteHasMetadata string? remoteQuery = remoteHasMetadata
? $"{remote.Title} {string.Join(" ", remote.Artists ?? [])}" ? $"{compareTitle} {compareArtist}"
: Path.GetFileNameWithoutExtension(remote.Reference); : Path.GetFileNameWithoutExtension(compareFileName);
string fp1 = CreateSortedFingerprint(localQuery); string fp1 = CreateSortedFingerprint(localQuery);
string fp2 = CreateSortedFingerprint(remoteQuery); string fp2 = CreateSortedFingerprint(remoteQuery);
@@ -83,19 +94,18 @@ namespace BetterLyrics.WinUI3.Helper
return _algo.Similarity(s1, s2); return _algo.Similarity(s1, s2);
} }
private static double GetDurationSimilarity(double localMs, double? remoteSeconds) private static double GetDurationSimilarity(double localSeconds, double? remoteSeconds)
{ {
if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配 if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配
double localSeconds = localMs / 1000.0;
double diff = Math.Abs(localSeconds - remoteSeconds.Value); double diff = Math.Abs(localSeconds - remoteSeconds.Value);
// 差距 <= 3100% 相似 // 差距 <= 1 100 % 相似
// 差距 >= 200% 相似 // 差距 >= 10 0 % 相似
// 中间线性插值 // 中间线性插值
const double PerfectTolerance = 3.0; const double PerfectTolerance = 1.0;
const double MaxTolerance = 20.0; const double MaxTolerance = 10.0;
if (diff <= PerfectTolerance) return 1.0; if (diff <= PerfectTolerance) return 1.0;
if (diff >= MaxTolerance) return 0.0; if (diff >= MaxTolerance) return 0.0;

View File

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

View File

@@ -1,4 +1,5 @@
using Windows.Security.Credentials; using System;
using Windows.Security.Credentials;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
{ {
@@ -12,23 +13,13 @@ namespace BetterLyrics.WinUI3.Helper
/// <param name="value">要保存的值</param> /// <param name="value">要保存的值</param>
public static void Save(string resource, string key, string value) public static void Save(string resource, string key, string value)
{ {
// 删除旧值(避免重复存储)
try try
{ {
var vault = new PasswordVault(); var vault = new PasswordVault();
var oldCredential = vault.Retrieve(resource, key);
if (oldCredential != null)
{
vault.Remove(oldCredential);
}
vault.Add(new PasswordCredential(resource, key, value)); vault.Add(new PasswordCredential(resource, key, value));
} }
catch catch (Exception) { }
{
// 没有旧值就忽略
}
} }
/// <summary> /// <summary>
@@ -47,7 +38,7 @@ namespace BetterLyrics.WinUI3.Helper
credential.RetrievePassword(); credential.RetrievePassword();
return credential.Password; return credential.Password;
} }
catch catch (Exception)
{ {
return null; return null;
} }
@@ -65,10 +56,7 @@ namespace BetterLyrics.WinUI3.Helper
var credential = vault.Retrieve(resource, key); var credential = vault.Retrieve(resource, key);
vault.Remove(credential); vault.Remove(credential);
} }
catch catch (Exception) { }
{
// 不存在就忽略
}
} }
} }
} }

View File

@@ -33,48 +33,34 @@ namespace BetterLyrics.WinUI3.Helper
public static string SaltPlayerForWindowsLogoPath => Path.Combine(AssetsFolder, "SaltPlayerForWindows.png"); public static string SaltPlayerForWindowsLogoPath => Path.Combine(AssetsFolder, "SaltPlayerForWindows.png");
public static string MoeKoeMusicLogoPath => Path.Combine(AssetsFolder, "MoeKoeMusic.png"); public static string MoeKoeMusicLogoPath => Path.Combine(AssetsFolder, "MoeKoeMusic.png");
public static string Listen1LogoPath => Path.Combine(AssetsFolder, "Listen1.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 UnknownPlayerLogoPath => Path.Combine(AssetsFolder, "Question.png");
public static string LogDirectory => Path.Combine(CacheFolder, "logs"); public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt"); public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics"); public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics");
public static string LrcLibLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "lrclib");
public static string NeteaseLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "netease");
public static string QQLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "qq");
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
public static string AppleMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "apple-music");
public static string LocalMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-music");
public static string LocalLrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-lrc");
public static string LocalEslrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-eslrc");
public static string LocalTtmlCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-ttml");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl"); public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt"); public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art"); public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes"); 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 string SongSearchMapPath => Path.Combine(LocalFolder, "song-search-map.db");
public static string LyricsCachePath => Path.Combine(LyricsCacheDirectory, "lyrics-cache.db");
public static void EnsureDirectories() public static void EnsureDirectories()
{ {
Directory.CreateDirectory(SettingsDirectory); Directory.CreateDirectory(SettingsDirectory);
Directory.CreateDirectory(LogDirectory); Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LyricsCacheDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(AppleMusicCacheDirectory);
Directory.CreateDirectory(LocalMusicCacheDirectory);
Directory.CreateDirectory(LocalLrcCacheDirectory);
Directory.CreateDirectory(LocalEslrcCacheDirectory);
Directory.CreateDirectory(LocalTtmlCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory); 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 CommunityToolkit.Mvvm.DependencyInjection;
using System; using System;
@@ -6,7 +6,7 @@ namespace BetterLyrics.WinUI3.Helper
{ {
public static class PhoneticHelper 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 PinyinCode = "zh-cmn-pinyin";
public const string JyutpingCode = "zh-yue-jyutping"; public const string JyutpingCode = "zh-yue-jyutping";
@@ -22,21 +22,16 @@ namespace BetterLyrics.WinUI3.Helper
switch (code) switch (code)
{ {
case PinyinCode: case PinyinCode:
return _resourceService.GetLocalizedString("Pinyin"); return _localizationService.GetLocalizedString("Pinyin");
case JyutpingCode: case JyutpingCode:
return _resourceService.GetLocalizedString("Jyutping"); return _localizationService.GetLocalizedString("Jyutping");
case RomanCode: case RomanCode:
return _resourceService.GetLocalizedString("Romaji"); return _localizationService.GetLocalizedString("Romaji");
default: default:
throw new ArgumentOutOfRangeException(nameof(code)); 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) public static string ToPinyin(string text, Pinyin.ManTone.Style style = Pinyin.ManTone.Style.TONE)
{ {
return Pinyin.Pinyin.Instance.HanziToPinyin(text, style).ToStr(); return Pinyin.Pinyin.Instance.HanziToPinyin(text, style).ToStr();

View File

@@ -42,13 +42,23 @@ namespace BetterLyrics.WinUI3.Helper
return file; return file;
} }
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices) public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
{ {
var window = WindowHook.GetWindow<T>(); var window = WindowHook.GetWindow<T>();
return await PickSaveFileAsync(window, fileTypeChoices, suggestedFileName);
}
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
{
if (window == null) return null; if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileSavePicker(); var picker = new Windows.Storage.Pickers.FileSavePicker();
picker.FileTypeChoices.AddRange(fileTypeChoices); picker.FileTypeChoices.AddRange(fileTypeChoices);
if (suggestedFileName != null)
{
picker.SuggestedFileName = suggestedFileName;
}
var hwnd = WindowNative.GetWindowHandle(window); var hwnd = WindowNative.GetWindowHandle(window);
InitializeWithWindow.Initialize(picker, hwnd); InitializeWithWindow.Initialize(picker, hwnd);

View File

@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
{ {
public static class PlayerIDHelper public static class PlayerIdHelper
{ {
private static readonly List<string> neteaseFamilyRegex = private static readonly List<string> neteaseFamilyRegex =
[ [
@@ -25,64 +25,66 @@ namespace BetterLyrics.WinUI3.Helper
return false; 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 public static string? GetDisplayName(string? id) => id switch
{ {
PlayerID.Spotify => PlayerName.Spotify, PlayerId.Spotify => PlayerName.Spotify,
PlayerID.AppleMusic => PlayerName.AppleMusic, PlayerId.AppleMusic => PlayerName.AppleMusic,
PlayerID.iTunes => PlayerName.iTunes, PlayerId.iTunes => PlayerName.iTunes,
PlayerID.KugouMusic => PlayerName.KugouMusic, PlayerId.KugouMusic => PlayerName.KugouMusic,
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic, PlayerId.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerID.QQMusic => PlayerName.QQMusic, PlayerId.QQMusic => PlayerName.QQMusic,
PlayerID.LXMusic => PlayerName.LXMusic, PlayerId.LXMusic => PlayerName.LXMusic,
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable, PlayerId.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11, PlayerId.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerID.AIMP => PlayerName.AIMP, PlayerId.AIMP => PlayerName.AIMP,
PlayerID.Foobar2000 => PlayerName.Foobar2000, PlayerId.Foobar2000 => PlayerName.Foobar2000,
PlayerID.MusicBee => PlayerName.MusicBee, PlayerId.MusicBee => PlayerName.MusicBee,
PlayerID.PotPlayer => PlayerName.PotPlayer, PlayerId.PotPlayer => PlayerName.PotPlayer,
PlayerID.Chrome => PlayerName.Chrome, PlayerId.Chrome => PlayerName.Chrome,
PlayerID.Edge => PlayerName.Edge, PlayerId.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics, PlayerId.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug, PlayerId.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS, PlayerId.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerID.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam, PlayerId.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic, PlayerId.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic, PlayerId.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1, PlayerId.Listen1 => PlayerName.Listen1,
PlayerId.OriginalSoundHQPlayer => PlayerName.OriginalSoundHQPlayer,
_ => id, _ => id,
}; };
public static string GetLogoPath(string? id) => id switch public static string GetLogoPath(string? id) => id switch
{ {
PlayerID.Spotify => PathHelper.SpotifyLogoPath, PlayerId.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath, PlayerId.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath, PlayerId.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath, PlayerId.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath, PlayerId.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath, PlayerId.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerID.QQMusic => PathHelper.QQMusicLogoPath, PlayerId.QQMusic => PathHelper.QQMusicLogoPath,
PlayerID.LXMusic => PathHelper.LXMusicLogoPath, PlayerId.LXMusic => PathHelper.LXMusicLogoPath,
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath, PlayerId.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath, PlayerId.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerID.AIMP => PathHelper.AIMPLogoPath, PlayerId.AIMP => PathHelper.AIMPLogoPath,
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath, PlayerId.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath, PlayerId.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath, PlayerId.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerID.Chrome => PathHelper.ChromeLogoPath, PlayerId.Chrome => PathHelper.ChromeLogoPath,
PlayerID.Edge => PathHelper.EdgeLogoPath, PlayerId.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath, PlayerId.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath, PlayerId.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath, PlayerId.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath, PlayerId.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath, PlayerId.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath, PlayerId.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath, PlayerId.Listen1 => PathHelper.Listen1LogoPath,
PlayerId.OriginalSoundHQPlayer => PathHelper.OriginalSoundHQPlayerLogoPath,
_ => PathHelper.UnknownPlayerLogoPath, _ => 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 CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppNotifications; using Microsoft.Windows.AppNotifications;
@@ -8,12 +8,12 @@ namespace BetterLyrics.WinUI3.Helper
{ {
public class ToastHelper 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) public static void ShowToast(string localizedTitleKey, string? description, InfoBarSeverity severity)
{ {
AppNotification notification = new AppNotificationBuilder() AppNotification notification = new AppNotificationBuilder()
.AddText(_resourceService.GetLocalizedString(localizedTitleKey)) .AddText(_localizationService.GetLocalizedString(localizedTitleKey))
.AddText(description) .AddText(description)
.BuildNotification(); .BuildNotification();

View File

@@ -1,153 +1,241 @@
// 2025/6/23 by Zhe Fang using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Enums;
using System; using System;
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
{ {
public class ValueTransition<T> public class ValueTransition<T> where T : struct
where T : struct
{ {
// 状态变量
private T _currentValue; private T _currentValue;
private double _durationSeconds;
private double _delaySeconds;
private double _delayRemaining;
private EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress;
private T _startValue; private T _startValue;
private T _targetValue; private T _targetValue;
public double DurationSeconds => _durationSeconds; // 核心队列
public double DelaySeconds => _delaySeconds; private readonly Queue<Keyframe<T>> _keyframeQueue = new Queue<Keyframe<T>>();
public bool IsTransitioning => _isTransitioning; // 时间控制
private double _stepDuration; // 当前这一段的时长 (动态变化)
private double _totalDurationForAutoSplit; // 自动均分模式的总时长
private double _configuredDelaySeconds; // 配置的延迟时长
// 动画状态
private Enums.EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress; // 当前段的进度 (0.0 ~ 1.0)
// 公开属性
public T Value => _currentValue; public T Value => _currentValue;
public T StartValue => _startValue; public bool IsTransitioning => _isTransitioning;
public T TargetValue => _targetValue; public T TargetValue => _targetValue; // 获取当前段的目标值
public EasingType? EasingType => _easingType; public Enums.EasingType? EasingType => _easingType;
public double Progress => _progress; public double DurationSeconds => _totalDurationForAutoSplit;
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null, double delaySeconds = 0) public ValueTransition(T initialValue, double defaultTotalDuration = 0.3, EasingType? defaultEasingType = null, Func<T, T, double, T>? interpolator = null)
{ {
_currentValue = initialValue; _currentValue = initialValue;
_startValue = initialValue; _startValue = initialValue;
_targetValue = initialValue; _targetValue = initialValue;
_durationSeconds = durationSeconds; _totalDurationForAutoSplit = defaultTotalDuration;
_delaySeconds = delaySeconds;
_delayRemaining = 0;
_progress = 1f;
_isTransitioning = false;
if (interpolator != null) if (interpolator != null)
{ {
_interpolator = interpolator; _interpolator = interpolator;
_easingType = null; _easingType = null;
} }
else if (easingType.HasValue) else if (defaultEasingType != null)
{ {
_easingType = easingType; SetEasingType(defaultEasingType);
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
} }
else else
{ {
_easingType = Enums.EasingType.EaseInOutQuad; SetEasingType(Enums.EasingType.EaseInOutQuad);
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
} }
} }
#region Configuration
public void SetDuration(double seconds) public void SetDuration(double seconds)
{ {
if (seconds < 0) if (seconds < 0) throw new ArgumentOutOfRangeException(nameof(seconds));
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive."); _totalDurationForAutoSplit = seconds;
_durationSeconds = seconds;
} }
public void SetDurationMs(double millionSeconds) => SetDuration(millionSeconds / 1000.0);
/// <summary>
/// 设置启动延迟。
/// 原理:在动画队列最前方插入一个“数值不变”的关键帧。
/// </summary>
public void SetDelay(double seconds) public void SetDelay(double seconds)
{ {
_delaySeconds = seconds; _configuredDelaySeconds = seconds;
} }
private void JumpTo(T value) public void SetEasingType(Enums.EasingType? easingType)
{ {
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
#endregion
#region Control Methods
/// <summary>
/// 立即跳转到指定值(停止动画)
/// </summary>
public void JumpTo(T value)
{
_keyframeQueue.Clear();
_currentValue = value; _currentValue = value;
_startValue = value; _startValue = value;
_targetValue = value; _targetValue = value;
_progress = 1f;
_delayRemaining = 0;
_isTransitioning = false; _isTransitioning = false;
_progress = 0;
} }
public void Reset(T value) /// <summary>
/// 模式 A: 精确控制模式
/// 显式指定每一段的目标值和时长。
/// </summary>
public void Start(params Keyframe<T>[] keyframes)
{ {
_currentValue = value; if (keyframes == null || keyframes.Length == 0) return;
_startValue = value;
_targetValue = value; PrepareStart();
_progress = 0f;
_delayRemaining = 0; // 1. 处理延迟 (插入静止帧)
_isTransitioning = false; if (_configuredDelaySeconds > 0)
{
_keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
} }
public void StartTransition(T targetValue, bool jumpTo = false) // 2. 入队用户帧
foreach (var kf in keyframes)
{ {
if (jumpTo) _keyframeQueue.Enqueue(kf);
{
JumpTo(targetValue);
return;
} }
if (!targetValue.Equals(_currentValue)) MoveToNextSegment(firstStart: true);
}
/// <summary>
/// 模式 B: 自动均分模式 (兼容旧写法)
/// 指定一串目标值,系统根据 SetDuration 的总时长平均分配。
/// </summary>
public void Start(params T[] values)
{ {
_startValue = _currentValue; if (values == null || values.Length == 0) return;
_targetValue = targetValue;
_progress = 0f; // 如果目标就是当前值且只有1帧直接跳过以省性能
_delayRemaining = _delaySeconds; if (values.Length == 1 && values[0].Equals(_currentValue) && _configuredDelaySeconds <= 0) return;
PrepareStart();
// 1. 处理延迟
if (_configuredDelaySeconds > 0)
{
_keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
}
// 2. 计算均分时长
double autoStepDuration = _totalDurationForAutoSplit / values.Length;
// 3. 入队生成帧
foreach (var val in values)
{
_keyframeQueue.Enqueue(new Keyframe<T>(val, autoStepDuration));
}
MoveToNextSegment(firstStart: true);
}
#endregion
#region Core Logic
private void PrepareStart()
{
_keyframeQueue.Clear();
_isTransitioning = true; _isTransitioning = true;
} }
}
public static bool Equals(double x, double y, double tolerance) private void MoveToNextSegment(bool firstStart = false)
{ {
var diff = Math.Abs(x - y); if (_keyframeQueue.Count > 0)
return diff <= tolerance || diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance; {
var kf = _keyframeQueue.Dequeue();
// 起点逻辑:如果是刚开始,起点是当前值;如果是中间切换,起点是上一段的终点
_startValue = firstStart ? _currentValue : _targetValue;
_targetValue = kf.Value;
_stepDuration = kf.Duration;
if (firstStart) _progress = 0f;
// 注意:非 firstStart 时不重置 _progress保留溢出值以平滑过渡
}
else
{
// 队列耗尽,动画结束
_currentValue = _targetValue;
_isTransitioning = false;
_progress = 1f;
}
} }
public void Update(TimeSpan elapsedTime) public void Update(TimeSpan elapsedTime)
{ {
if (!_isTransitioning) return; if (!_isTransitioning) return;
if (_delayRemaining > 0) double timeStep = elapsedTime.TotalSeconds;
{
double consume = Math.Min(_delayRemaining, elapsedTime.TotalSeconds);
_delayRemaining -= consume;
if (_delayRemaining > 0)
return;
elapsedTime = TimeSpan.FromSeconds(elapsedTime.TotalSeconds - consume);
}
if (_durationSeconds <= 0) // 使用 while 处理单帧时间过长跨越多段的情况
while (timeStep > 0 && _isTransitioning)
{ {
_progress = 1f; // 计算当前帧的步进比例
} // 极小值保护防止除以0
else double progressDelta = (_stepDuration > 0.000001) ? (timeStep / _stepDuration) : 1.0;
{
_progress += elapsedTime.TotalSeconds / _durationSeconds;
}
if (_progress >= 1f) if (_progress + progressDelta >= 1.0)
{ {
_progress = 1f; // === 当前段结束 ===
// 1. 计算这一段实际消耗的时间
double timeConsumed = (1.0 - _progress) * _stepDuration;
// 2. 剩余时间留给下一段
timeStep -= timeConsumed;
// 3. 修正当前值到目标值
_progress = 1.0;
_currentValue = _targetValue; _currentValue = _targetValue;
_isTransitioning = false;
// 4. 切换到下一段
MoveToNextSegment();
// 5. 如果还有下一段,进度归零
if (_isTransitioning) _progress = 0f;
} }
else else
{ {
// === 当前段进行中 ===
_progress += progressDelta;
timeStep = 0; // 时间耗尽
// 插值计算
_currentValue = _interpolator(_startValue, _targetValue, _progress); _currentValue = _interpolator(_startValue, _targetValue, _progress);
} }
} }
}
private Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type) #endregion
#region Interpolators
private Func<T, T, double, T> GetInterpolatorByEasingType(Enums.EasingType? type)
{ {
if (typeof(T) == typeof(double)) if (typeof(T) == typeof(double))
{ {
@@ -156,58 +244,32 @@ namespace BetterLyrics.WinUI3.Helper
double s = (double)(object)start; double s = (double)(object)start;
double e = (double)(object)end; double e = (double)(object)end;
double t = progress; double t = progress;
// 使用 EasingHelper (假设您的项目中已有此辅助类)
switch (type) switch (type)
{ {
case Enums.EasingType.EaseInOutSine: case Enums.EasingType.EaseInOutSine: t = EasingHelper.EaseInOutSine(t); break;
t = EasingHelper.EaseInOutSine(t); case Enums.EasingType.EaseInOutQuad: t = EasingHelper.EaseInOutQuad(t); break;
break; case Enums.EasingType.EaseInOutCubic: t = EasingHelper.EaseInOutCubic(t); break;
case Enums.EasingType.EaseInOutQuad: case Enums.EasingType.EaseInOutQuart: t = EasingHelper.EaseInOutQuart(t); break;
t = EasingHelper.EaseInOutQuad(t); case Enums.EasingType.EaseInOutQuint: t = EasingHelper.EaseInOutQuint(t); break;
break; case Enums.EasingType.EaseInOutExpo: t = EasingHelper.EaseInOutExpo(t); break;
case Enums.EasingType.EaseInOutCubic: case Enums.EasingType.EaseInOutCirc: t = EasingHelper.EaseInOutCirc(t); break;
t = EasingHelper.EaseInOutCubic(t); case Enums.EasingType.EaseInOutBack: t = EasingHelper.EaseInOutBack(t); break;
break; case Enums.EasingType.EaseInOutElastic: t = EasingHelper.EaseInOutElastic(t); break;
case Enums.EasingType.EaseInOutQuart: case Enums.EasingType.EaseInOutBounce: t = EasingHelper.EaseInOutBounce(t); break;
t = EasingHelper.EaseInOutQuart(t); case Enums.EasingType.SmoothStep: t = EasingHelper.SmoothStep(t); break;
break; case Enums.EasingType.Linear: t = EasingHelper.Linear(t); break;
case Enums.EasingType.EaseInOutQuint: default: t = EasingHelper.EaseInOutQuad(t); break;
t = EasingHelper.EaseInOutQuint(t);
break;
case Enums.EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case Enums.EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case Enums.EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case Enums.EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case Enums.EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case Enums.EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case Enums.EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
t = EasingHelper.EaseInOutQuad(t);
break;
} }
return (T)(object)(s + (e - s) * t); return (T)(object)(s + (e - s) * t);
}; };
} }
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
throw new NotSupportedException($"Type {typeof(T)} is not supported.");
} }
public void SetEasingType(EasingType? easingType) #endregion
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
} }
} }

View File

@@ -0,0 +1,70 @@
using System;
using System.Net;
using System.Net.Http;
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

@@ -20,7 +20,7 @@ namespace BetterLyrics.WinUI3.Hooks
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="keys"></param> /// <param name="keys"></param>
/// <param name="action"></param> /// <param name="action"></param>
private static void RegisterHotKey(Window window, ShortcutID id, List<string> keys, Action action) private static void RegisterHotKey(Window window, ShortcutId id, List<string> keys, Action action)
{ {
if (keys.Count == 0) return; if (keys.Count == 0) return;
@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Hooks
} }
} }
private static void UnregisterHotKey(Window window, ShortcutID id) private static void UnregisterHotKey(Window window, ShortcutId id)
{ {
HWND hwnd = WindowNative.GetWindowHandle(window); HWND hwnd = WindowNative.GetWindowHandle(window);
User32.UnregisterHotKey(hwnd, (int)id); User32.UnregisterHotKey(hwnd, (int)id);
@@ -66,13 +66,13 @@ namespace BetterLyrics.WinUI3.Hooks
_keys.Remove((int)id); _keys.Remove((int)id);
} }
public static void UpdateHotKey(Window window, ShortcutID id, List<string> keys, Action action) public static void UpdateHotKey(Window window, ShortcutId id, List<string> keys, Action action)
{ {
UnregisterHotKey(window, id); UnregisterHotKey(window, id);
RegisterHotKey(window, id, keys, action); RegisterHotKey(window, id, keys, action);
} }
public static bool IsHotKeyRegistered(ShortcutID id) public static bool IsHotKeyRegistered(ShortcutId id)
{ {
return _actions.ContainsKey((int)id); return _actions.ContainsKey((int)id);
} }
@@ -82,7 +82,7 @@ namespace BetterLyrics.WinUI3.Hooks
return _keys.ContainsValue(keys); return _keys.ContainsValue(keys);
} }
public static bool TryInvokeAction(ShortcutID id) public static bool TryInvokeAction(ShortcutId id)
{ {
return TryInvokeAction((int)id); return TryInvokeAction((int)id);
} }

View File

@@ -16,8 +16,11 @@ namespace BetterLyrics.WinUI3.Hooks
public static event EventHandler<int>? VolumeNotification; public static event EventHandler<int>? VolumeNotification;
static SystemVolumeHook() static SystemVolumeHook()
{
try
{ {
_deviceEnumerator = new MMDeviceEnumerator(); _deviceEnumerator = new MMDeviceEnumerator();
// 找不到设备会抛出异常,在这里截获它
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); _defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (_defaultDevice != null) if (_defaultDevice != null)
@@ -25,6 +28,11 @@ namespace BetterLyrics.WinUI3.Hooks
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification; _defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
} }
} }
catch (Exception ex)
{
_defaultDevice = null;
}
}
private static void AudioEndpointVolume_OnVolumeNotification(AudioVolumeNotificationData data) private static void AudioEndpointVolume_OnVolumeNotification(AudioVolumeNotificationData data)
{ {

View File

@@ -186,7 +186,8 @@ namespace BetterLyrics.WinUI3.Hooks
if (width < 20) return Rectangle.Empty; if (width < 20) return Rectangle.Empty;
return new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height); var finalRect = new Rectangle(finalLeft, taskbarRect.Top, width, taskbarRect.Height);
return finalRect;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -1,7 +1,7 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
@@ -223,12 +223,9 @@ namespace BetterLyrics.WinUI3.Hooks
} }
private static void WindowHelper_Closed(object sender, WindowEventArgs args) private static void WindowHelper_Closed(object sender, WindowEventArgs args)
{
if (_activeWindows.Contains(sender))
{ {
_activeWindows.Remove(sender); _activeWindows.Remove(sender);
} }
}
public static void SetIsWorkArea(this NowPlayingWindow window, bool enable) public static void SetIsWorkArea(this NowPlayingWindow window, bool enable)
{ {

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