Compare commits

...

238 Commits

Author SHA1 Message Date
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
176 changed files with 25794 additions and 4583 deletions

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.232.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

@@ -65,7 +65,6 @@
<converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" /> <converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
<converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" /> <converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
<converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" /> <converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
<converter:TrackToLyricsConverter x:Key="TrackToLyricsConverter" />
<converter:IntToBoolConverter x:Key="IntToBoolConverter" /> <converter:IntToBoolConverter x:Key="IntToBoolConverter" />
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" /> <converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" /> <converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
@@ -75,6 +74,8 @@
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" /> <converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" /> <converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" /> <converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<converter:PathToImageConverter x:Key="PathToImageConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" /> <converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" /> <converters:BoolNegationConverter x:Key="BoolNegationConverter" />
@@ -96,7 +97,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">

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.Db;
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.LastFMService; using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService; using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsSearchService; using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService; using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService; using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching; // 关键:用于线程调度
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.IO;
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,94 @@ 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 EnsureDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>(); var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
WindowHook.OpenOrShowWindow<SystemTrayWindow>(); // 开始后台扫描任务
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
{
_ = Task.Run(async () => await fileSystemService.ScanMediaFolderAsync(item, CancellationToken.None));
}
}
fileSystemService.StartAllFolderTimers();
// 初始化托盘
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 根据设置打开歌词窗口
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow) if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{ {
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault); var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -87,12 +161,101 @@ namespace BetterLyrics.WinUI3
} }
} }
} }
// 根据设置自动打开主界面
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen) if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{ {
WindowHook.OpenOrShowWindow<MusicGalleryWindow>(); WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
} }
} }
private async Task EnsureDatabasesAsync()
{
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var fileCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
await SafeInitDatabaseAsync(
"PlayHistory",
PathHelper.PlayHistoryPath,
async () =>
{
using var db = await playHistoryFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: true
);
await SafeInitDatabaseAsync(
"FileCache",
PathHelper.FilesIndexPath,
async () =>
{
using var db = await fileCacheFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: false
);
}
private async Task SafeInitDatabaseAsync(string dbName, string dbPath, Func<Task> initAction, bool isCritical)
{
try
{
await initAction();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DB Error] {dbName} init failed: {ex.Message}");
try
{
if (File.Exists(dbPath))
{
// 尝试清理连接池
SqliteConnection.ClearAllPools();
if (isCritical)
{
var backupPath = dbPath + ".bak_" + DateTime.Now.ToString("yyyyMMddHHmmss");
File.Move(dbPath, backupPath, true);
await ShowErrorDialogAsync("Database Recovery", $"Database {dbName} is damaged, the old database has been backed up to {backupPath}, and the program will create a new database.");
}
else
{
File.Delete(dbPath);
}
}
await initAction();
System.Diagnostics.Debug.WriteLine($"[DB Info] {dbName} recovered successfully.");
}
catch (Exception fatalEx)
{
System.Diagnostics.Debug.WriteLine($"[] : {fatalEx.Message}");
await ShowErrorDialogAsync("Fatal Error", $"{dbName} recovery failed, please delete the file at {dbPath} and try again by restarting the program. ({fatalEx.Message})");
}
}
}
private async Task ShowErrorDialogAsync(string title, string content)
{
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
if (m_window != null)
{
m_window.DispatcherQueue.TryEnqueue(async () =>
{
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{
Title = title,
Content = content,
CloseButtonText = "OK",
XamlRoot = m_window.Content?.XamlRoot // 确保 Content 不为空
};
if (dialog.XamlRoot != null) await dialog.ShowAsync();
});
}
}
private static void ConfigureServices() private static void ConfigureServices()
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
@@ -100,24 +263,32 @@ namespace BetterLyrics.WinUI3
.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}"))
// 日志
.AddLogging(loggingBuilder => .AddLogging(loggingBuilder =>
{ {
loggingBuilder.ClearProviders(); loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog(); loggingBuilder.AddSerilog();
}) })
// Services // Services
.AddSingleton<ISettingsService, SettingsService>() .AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>() .AddSingleton<IMediaSessionsService, MediaSessionsService>()
.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>()
// ViewModels // ViewModels
.AddSingleton<AppSettingsControlViewModel>() .AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>() .AddSingleton<PlaybackSettingsControlViewModel>()
@@ -132,6 +303,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<MusicGalleryPageViewModel>() .AddSingleton<MusicGalleryPageViewModel>()
.AddSingleton<AboutControlViewModel>() .AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>() .AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddTransient<NowPlayingWindowViewModel>() .AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>() .AddTransient<NowPlayingPageViewModel>()
@@ -149,7 +321,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)
@@ -162,4 +335,4 @@ namespace BetterLyrics.WinUI3
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException"); _logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -10,13 +10,20 @@
<Nullable>enable</Nullable> <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>
@@ -37,7 +44,9 @@
<None Remove="Controls\NowPlayingBar.xaml" /> <None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" /> <None Remove="Controls\PlaybackSettingsControl.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="Views\LyricsSearchWindow.xaml" /> <None Remove="Views\LyricsSearchWindow.xaml" />
@@ -58,36 +67,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="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.Data.SqlClient" Version="6.1.3" />
<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 +112,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 +134,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 +180,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 +225,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>
@@ -234,6 +259,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </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 +365,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">

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

@@ -71,9 +71,9 @@
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageFeedback" /> <TextBlock x:Uid="SetingsPageFeedback" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal"> <StackPanel 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> </StackPanel>
</StackPanel> </StackPanel>
</dev:SettingsCard> </dev:SettingsCard>
@@ -118,13 +118,21 @@
</HyperlinkButton> </HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" /> <HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel> </StackPanel>
<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" />
</StackPanel> </Grid>
</StackPanel> </StackPanel>
</dev:SettingsCard> </dev:SettingsCard>
@@ -182,6 +190,12 @@
</dev:SettingsExpander.ItemsHeader> </dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander> </dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed"> <dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>

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.Hooks;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
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

@@ -13,6 +13,7 @@ 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
@@ -34,14 +37,15 @@ namespace BetterLyrics.WinUI3.Controls
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 IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
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();
@@ -368,8 +372,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);
@@ -540,25 +544,31 @@ namespace BetterLyrics.WinUI3.Controls
_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,
_accentColor3Transition.Value, _accentColor3Transition.Value,
_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 +596,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 +611,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();
@@ -642,22 +659,6 @@ namespace BetterLyrics.WinUI3.Controls
_songPosition += elapsedTime; _songPosition += elapsedTime;
_totalPlayedTime += elapsedTime; _totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.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);
} }
} }
@@ -682,6 +683,16 @@ namespace BetterLyrics.WinUI3.Controls
}).ToList(); }).ToList();
} }
private async Task ReloadCoverBackgroundResourcesAsync()
{
if (_mediaSessionsService.AlbumArtBitmapStream is IRandomAccessStream stream)
{
stream.Seek(0);
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
_coverRenderer.SetCoverBitmap(bitmap);
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message) public void Receive(PropertyChangedMessage<TimeSpan> message)
{ {
if (message.Sender is IMediaSessionsService) if (message.Sender is IMediaSessionsService)
@@ -827,6 +838,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 +893,15 @@ namespace BetterLyrics.WinUI3.Controls
} }
} }
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}
}
}
} }
} }

View File

@@ -28,6 +28,16 @@
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" /> <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}" />

View File

@@ -25,7 +25,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}" />
@@ -153,12 +153,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}" />
@@ -183,10 +182,7 @@
<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 DisplayArtists, 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="%"
@@ -244,8 +240,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">
@@ -278,18 +272,43 @@
</Pivot.HeaderTemplate> </Pivot.HeaderTemplate>
<Pivot.ItemTemplate> <Pivot.ItemTemplate>
<DataTemplate x:DataType="models:LyricsData"> <DataTemplate x:DataType="models:LyricsData">
<ListView ItemsSource="{x:Bind LyricsLines, Mode=OneWay}" SelectionChanged="ListView_SelectionChanged"> <ListView
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind LyricsLines, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsLine"> <DataTemplate x:DataType="models:LyricsLine">
<StackPanel Orientation="Horizontal"> <Grid Margin="0,6" ColumnSpacing="6">
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" /> <Grid.ColumnDefinitions>
<TextBlock <ColumnDefinition Width="Auto" />
Margin="1,0" <ColumnDefinition Width="*" />
Foreground="{ThemeResource SystemFillColorNeutralBrush}" </Grid.ColumnDefinitions>
Text="-" /> <Grid Grid.Column="0">
<TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind EndMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" /> <TextBlock Foreground="{ThemeResource SystemFillColorNeutralBrush}" Text="{x:Bind StartMs, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
<TextBlock Margin="6,0" Text="{x:Bind OriginalText, Mode=OneWay}" /> <Button
</StackPanel> HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="PlayLyricsLineButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE768;}"
Opacity="0"
Style="{StaticResource AccentButtonStyle}">
<Button.OpacityTransition>
<ScalarTransition />
</Button.OpacityTransition>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="1" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ChangePropertyAction PropertyName="Opacity" Value="0" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Button>
</Grid>
<local:PropertyRow Grid.Column="1" Value="{x:Bind OriginalText, Mode=OneWay}" />
</Grid>
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
@@ -321,8 +340,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

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

@@ -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,11 @@
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:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -49,42 +50,129 @@
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="models: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}"> <dev:SettingsCard Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button <DropDownButton x:Uid="SettingsPageAddFolderButton">
x:Uid="SettingsPageAddFolderButton" <DropDownButton.Flyout>
Command="{x:Bind ViewModel.SelectAndAddFolderCommand}" <MenuFlyout>
CommandParameter="{Binding ElementName=RootGrid}" /> <MenuFlyoutItem
x:Uid="SettingsPageLocalFolder"
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="Local">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE8B7;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="SMB"
Text="SMB">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE839;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="FTP"
Text="FTP">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE838;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem
Command="{x:Bind ViewModel.AddMediaSourceCommand}"
CommandParameter="WebDAV"
Text="WebDAV">
<MenuFlyoutItem.Icon>
<FontIcon FontFamily="{StaticResource IconFontFamily}" Glyph="&#xE774;" />
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
</MenuFlyout>
</DropDownButton.Flyout>
</DropDownButton>
</dev:SettingsCard> </dev:SettingsCard>
</StackPanel> </StackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>
</UserControl> </UserControl>

View File

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

@@ -11,10 +11,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">
@@ -113,7 +112,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},
@@ -164,6 +214,13 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}" Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" /> Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<ToggleButton
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostToggleButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
@@ -342,7 +399,7 @@
<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.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
@@ -356,7 +413,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>
@@ -427,7 +484,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,5 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService; using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
@@ -12,6 +14,7 @@ using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
using System.Numerics; using System.Numerics;
using BetterLyrics.WinUI3.Extensions;
// 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.
@@ -46,6 +49,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 ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public PlaybackOrder PlaybackOrder
{
get { return (PlaybackOrder)GetValue(PlaybackOrderProperty); }
set { SetValue(PlaybackOrderProperty, value); }
}
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsPlayingQueueOpened
{
get { return (bool)GetValue(IsPlayingQueueOpenedProperty); }
set { SetValue(IsPlayingQueueOpenedProperty, value); }
}
public static readonly DependencyProperty IsPlayingQueueOpenedProperty =
DependencyProperty.Register(nameof(IsPlayingQueueOpened), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool IsCompactMode public bool IsCompactMode
{ {
get { return (bool)GetValue(IsCompactModeProperty); } get { return (bool)GetValue(IsCompactModeProperty); }
@@ -190,7 +229,7 @@ public sealed partial class NowPlayingBar : UserControl,
private void TimelineSliderOverlay_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) 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 +249,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,6 +301,16 @@ public sealed partial class NowPlayingBar : UserControl,
} }
} }
private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
IsPlayingQueueOpened = !IsPlayingQueueOpened;
}
private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
{
PlaybackOrder = PlaybackOrder.GetNext();
}
public void Receive(PropertyChangedMessage<SongInfo?> message) public void Receive(PropertyChangedMessage<SongInfo?> message)
{ {
if (message.Sender is IMediaSessionsService) if (message.Sender is IMediaSessionsService)
@@ -290,10 +339,7 @@ public sealed partial class NowPlayingBar : UserControl,
{ {
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition)) if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{ {
DispatcherQueue.TryEnqueue(() => TimelineSlider.Value = message.NewValue.TotalSeconds;
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
});
} }
} }
} }

View File

@@ -44,10 +44,10 @@
</interactivity:Interaction.Behaviors> </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" />
<!-- 播放源列表 --> <!-- 播放源列表 -->
@@ -328,10 +328,7 @@
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.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>
@@ -344,10 +341,8 @@
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
x:Uid="LyricsSearchControlDurauion" <local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.MediaSessionsService.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.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
@@ -397,16 +392,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 +426,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:SettingsCard> <dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageCutletDockerServer" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
<dev:SettingsCard.Description>
<HyperlinkButton Content="https://github.com/jayfunc/cutlet-docker" NavigateUri="https://github.com/jayfunc/cutlet-docker" />
</dev:SettingsCard.Description>
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Uid="CutletServerTextBox"
Grid.Column="0"
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
Text="{x:Bind ViewModel.AppSettings.TranslationSettings.CutletDockerServer, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
x:Uid="SettingsPageServerTestButton"
Grid.Column="1"
Command="{x:Bind ViewModel.CutletDockerServerTestCommand}"
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
</Grid>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<!-- 中文简体繁体偏好 --> <!-- 中文简体繁体偏好 -->
<TextBlock x:Uid="SettingsPageChineseLyrics" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="SettingsPageChineseLyrics" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
@@ -435,9 +461,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;
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,302 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.StatsDashboardControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:BetterLyrics.WinUI3.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:statsmodels="using:BetterLyrics.WinUI3.Models.Stats"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="StatsCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,12,12" />
</Style>
</UserControl.Resources>
<Grid Margin="0,24,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="20,0">
<Pivot x:Name="TimeRangePivot" SelectionChanged="Pivot_SelectionChanged">
<PivotItem x:Uid="StatsDashboardControlToday" Tag="Day" />
<PivotItem x:Uid="StatsDashboardControlThisWeek" Tag="Week" />
<PivotItem x:Uid="StatsDashboardControlThisMonth" Tag="Month" />
<PivotItem x:Uid="StatsDashboardControlThisQuarter" Tag="Quarter" />
<PivotItem x:Uid="StatsDashboardControlThisYear" Tag="Year" />
</Pivot>
</Grid>
<ScrollViewer Grid.Row="1" Padding="20,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 总播放时长 -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE916;" />
<TextBlock x:Uid="StatsDashboardControlTotalDuration" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<StackPanel
Margin="0,8,0,0"
Orientation="Horizontal"
Spacing="4">
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="Hrs" />
</StackPanel>
</StackPanel>
</Border>
<!-- 总播放歌曲次数 -->
<Border Grid.Column="1" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE8D6;" />
<TextBlock x:Uid="StatsDashboardControlTracksPlayed" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalTracksPlayed, Mode=OneWay}" />
</StackPanel>
</Border>
<!-- Top source -->
<Border
Grid.Column="2"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xEC4A;" />
<TextBlock x:Uid="StatsDashboardControlTopSource" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TopPlayerName, Mode=OneWay}" />
</StackPanel>
</Border>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopArtists"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10">
<PersonPicture
Width="32"
Height="32"
DisplayName="{x:Bind Artist}" />
<TextBlock
VerticalAlignment="Center"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
<!-- Top sources -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.PlayerStats, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PlayerStatDisplayItem">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</Grid>
<!-- Top song -->
<Border
Grid.Row="2"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ListView
Grid.Row="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Width="40"
Height="40"
Margin="0,0,12,0"
Background="{ThemeResource LayerFillColorAltBrush}"
CornerRadius="4">
<FontIcon
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xE8D6;" />
</Grid>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Style="{ThemeResource BodyStrongTextBlockStyle}" Text="{x:Bind Title}" />
<TextBlock
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Border>
</Grid>
</ScrollViewer>
<Button
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data"
Visibility="Collapsed" />
</Grid>
</UserControl>

View File

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

View File

@@ -14,7 +14,6 @@
x:Name="TrayIcon" x: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,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,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();
{
return TimeSpan.FromMilliseconds(doubleMilliseconds).ToString(@"mm\:ss\.fff"); if (string.IsNullOrEmpty(format))
{
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,31 @@
using BetterLyrics.WinUI3.Helper;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace BetterLyrics.WinUI3.Converter
{
public partial class PathToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
string targetPath = PathHelper.AlbumArtPlaceholderPath;
if (value is string path)
{
if (File.Exists(path))
{
targetPath = path;
}
}
return new BitmapImage(new Uri(targetPath));
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
// 2025/6/23 by Zhe Fang // 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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models; 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 +12,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 +44,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 +71,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 +93,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 +102,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 +113,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,4 +1,5 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Extensions namespace BetterLyrics.WinUI3.Extensions
{ {
@@ -30,6 +31,22 @@ namespace BetterLyrics.WinUI3.Extensions
songInfo.Album = value; songInfo.Album = value;
return songInfo; return songInfo;
} }
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{
if (songInfo == null) return null;
return new PlayHistoryItem
{
Title = songInfo.Title,
Artist = songInfo.DisplayArtists,
Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.Now.AddMilliseconds(-actualPlayedMs)
};
}
} }
} }
} }

View File

@@ -1,33 +0,0 @@
using ATL;
using System.IO;
namespace BetterLyrics.WinUI3.Extensions
{
public static class TrackExtensions
{
extension(Track track)
{
public string GetParentFolderName() => Directory.GetParent(track.Path)?.Name ?? "";
public string GetParentFolderPath() => Directory.GetParent(track.Path)?.FullName ?? "";
public string GetRawLyrics()
{
if (track.Path is string path)
{
try
{
return TagLib.File.Create(path).Tag.Lyrics;
}
catch (System.Exception)
{
return "";
}
}
return "";
}
public string GetFileName() => Path.GetFileName(track.Path);
}
}
}

View File

@@ -1,6 +1,6 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.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

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

View File

@@ -1,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,10 @@
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.ComponentModel.Design;
using System.Globalization;
using System.Linq; using System.Linq;
using Windows.Globalization; using Windows.Globalization;
@@ -10,9 +12,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 +94,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 +120,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 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,6 +33,7 @@ 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");
@@ -54,8 +55,11 @@ namespace BetterLyrics.WinUI3.Helper
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 void EnsureDirectories() public static void EnsureDirectories()
{ {
@@ -75,6 +79,7 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(LocalTtmlCacheDirectory); 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

@@ -45,6 +45,12 @@ namespace BetterLyrics.WinUI3.Helper
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices) public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices)
{ {
var window = WindowHook.GetWindow<T>(); var window = WindowHook.GetWindow<T>();
return await PickSaveFileAsync(window, fileTypeChoices);
}
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices)
{
if (window == null) return null; if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileSavePicker(); var picker = new Windows.Storage.Pickers.FileSavePicker();

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

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

View File

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

View File

@@ -7,6 +7,7 @@ using FlaUI.Core.EventHandlers;
using FlaUI.UIA3; using FlaUI.UIA3;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using System; using System;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Threading; using System.Threading;
@@ -186,7 +187,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

@@ -224,10 +224,7 @@ 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)

View File

@@ -92,27 +92,30 @@ namespace BetterLyrics.WinUI3.Logic
line.ScaleTransition.SetDuration(yScrollDuration); line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay); line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.StartTransition(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)); line.ScaleTransition.StartTransition(
lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
_highlightedScale);
line.PhoneticOpacityTransition.SetDuration(yScrollDuration); line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
line.PhoneticOpacityTransition.SetDelay(yScrollDelay); line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
line.PhoneticOpacityTransition.StartTransition( line.PhoneticOpacityTransition.StartTransition(
absLineCountDelta == 0 ? phoneticOpacity : (isMouseScrolling ? phoneticOpacity : (1 - distanceFactor) * phoneticOpacity)); CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration); line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay); line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.PlayedOriginalOpacityTransition.StartTransition( line.PlayedOriginalOpacityTransition.StartTransition(
absLineCountDelta == 0 ? 1 : (isMouseScrolling ? 1.0 : (1 - distanceFactor) * originalOpacity)); CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration); line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay); line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.UnplayedOriginalOpacityTransition.StartTransition( line.UnplayedOriginalOpacityTransition.StartTransition(
absLineCountDelta == 0 ? originalOpacity : (isMouseScrolling ? originalOpacity : (1 - distanceFactor) * originalOpacity)); CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.TranslatedOpacityTransition.SetDuration(yScrollDuration); line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
line.TranslatedOpacityTransition.SetDelay(yScrollDelay); line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
line.TranslatedOpacityTransition.StartTransition( line.TranslatedOpacityTransition.StartTransition(
absLineCountDelta == 0 ? translatedOpacity : (isMouseScrolling ? translatedOpacity : (1 - distanceFactor) * translatedOpacity)); CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.ColorTransition.SetDuration(yScrollDuration); line.ColorTransition.SetDuration(yScrollDuration);
line.ColorTransition.SetDelay(yScrollDelay); line.ColorTransition.SetDelay(yScrollDelay);
@@ -121,8 +124,10 @@ namespace BetterLyrics.WinUI3.Logic
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType); line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration); line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay); line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.StartTransition(lyricsEffect.IsFanLyricsEnabled ? line.AngleTransition.StartTransition(
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) : 0); (lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) :
0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType); line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.YOffsetTransition.SetDuration(yScrollDuration); line.YOffsetTransition.SetDuration(yScrollDuration);
@@ -143,5 +148,33 @@ namespace BetterLyrics.WinUI3.Logic
line.ColorTransition.Update(elapsedTime); line.ColorTransition.Update(elapsedTime);
} }
} }
private static double CalculateTargetOpacity(double baseOpacity, double baseOpacityWhenZeroDistanceFactor, double distanceFactor, bool isMouseScrolling, LyricsEffectSettings lyricsEffect)
{
double targetOpacity;
if (distanceFactor == 0)
{
targetOpacity = baseOpacityWhenZeroDistanceFactor;
}
else
{
if (isMouseScrolling)
{
targetOpacity = baseOpacity;
}
else
{
if (lyricsEffect.IsLyricsFadeOutEffectEnabled)
{
targetOpacity = (1 - distanceFactor) * baseOpacity;
}
else
{
targetOpacity = baseOpacity;
}
}
}
return targetOpacity;
}
} }
} }

View File

@@ -213,6 +213,17 @@ namespace BetterLyrics.WinUI3.Logic
if (value >= mousePosition.Y) { result = mid; right = mid - 1; } if (value >= mousePosition.Y) { result = mid; right = mid - 1; }
else { left = mid + 1; } else { left = mid + 1; }
} }
if (result != -1)
{
var line = lines[result];
double lineTopY = offset + line.TopLeftPosition.Y;
if (mousePosition.Y < lineTopY)
{
result = -1;
}
}
return result; return result;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LocalMediaFolder : ObservableRecipient
{
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsRealTimeWatchEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string Path { get; set; }
public LocalMediaFolder() { }
public LocalMediaFolder(string path)
{
Path = path;
}
}
}

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.ResourceService; using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -9,9 +9,9 @@ namespace BetterLyrics.WinUI3.Models
{ {
public class LyricsData public class LyricsData
{ {
private static readonly IResourceService _resourceService = Ioc.Default.GetRequiredService<IResourceService>(); private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public List<LyricsLine> LyricsLines { get; set; } public List<LyricsLine> LyricsLines { get; set; } = [];
public string? LanguageCode public string? LanguageCode
{ {
get => field ?? LanguageHelper.DetectLanguageCode(WrappedOriginalText); get => field ?? LanguageHelper.DetectLanguageCode(WrappedOriginalText);
@@ -22,7 +22,6 @@ namespace BetterLyrics.WinUI3.Models
public LyricsData() public LyricsData()
{ {
LyricsLines = [];
} }
public LyricsData(List<LyricsLine> lyricsLines) public LyricsData(List<LyricsLine> lyricsLines)
@@ -30,100 +29,15 @@ namespace BetterLyrics.WinUI3.Models
LyricsLines = lyricsLines; LyricsLines = lyricsLines;
} }
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 50)
{
foreach (var line in LyricsLines)
{
// 在翻译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = translationData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
// 此处 transLine.OriginalText 指翻译中的“原文”属性
line.TranslatedText = transLine.OriginalText;
}
else
{
// 没有匹配的翻译
line.TranslatedText = "";
}
}
}
public void SetPhoneticText(LyricsData phoneticData, int toleranceMs = 50)
{
foreach (var line in LyricsLines)
{
// 在音译歌词中查找与当前行开始时间最接近且在容忍范围内的行
var transLine = phoneticData.LyricsLines
.FirstOrDefault(t => Math.Abs(t.StartMs - line.StartMs) <= toleranceMs);
if (transLine != null)
{
// 此处 transLine.OriginalText 指音译中的“原文”属性
line.PhoneticText = transLine.OriginalText;
}
else
{
// 没有匹配的音译
line.PhoneticText = "";
}
}
}
public void SetTranslation(string translation)
{
List<string> translationArr = translation.Split(StringHelper.NewLine).ToList();
int i = 0;
foreach (var line in LyricsLines)
{
if (i >= translationArr.Count)
{
line.TranslatedText = ""; // No translation available, keep empty
}
else
{
line.TranslatedText = translationArr[i];
}
i++;
}
}
public static LyricsData GetNotfoundPlaceholder() public static LyricsData GetNotfoundPlaceholder()
{ {
return new LyricsData([new LyricsLine return new LyricsData([new LyricsLine
{ {
StartMs = 0, StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = _resourceService.GetLocalizedString("LyricsNotFound"), OriginalText = _localizationService.GetLocalizedString("LyricsNotFound"),
}]); }]);
} }
public static LyricsData GetLoadingPlaceholder()
{
return new LyricsData([
new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = "● ● ●",
},
]);
}
public LyricsLine? GetLyricsLine(double sec)
{
for (int i = 0; i < LyricsLines.Count; i++)
{
var line = LyricsLines[i];
if (line.StartMs > sec * 1000)
{
return LyricsLines.ElementAtOrDefault(i - 1);
}
}
return null;
}
} }
} }

View File

@@ -22,6 +22,7 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysOnTopPolling { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysOnTopPolling { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsShownInSwitchers { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsShownInSwitchers { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLocked { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLocked { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsAlwaysHideUnlockButton { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPinToTaskbar { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPinToTaskbar { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TaskbarPlacement TaskbarPlacement { get; set; } = TaskbarPlacement.Right; [ObservableProperty][NotifyPropertyChangedRecipients] public partial TaskbarPlacement TaskbarPlacement { get; set; } = TaskbarPlacement.Right;
@@ -200,6 +201,7 @@ namespace BetterLyrics.WinUI3.Models
IsAlwaysOnTopPolling = this.IsAlwaysOnTopPolling, IsAlwaysOnTopPolling = this.IsAlwaysOnTopPolling,
IsShownInSwitchers = this.IsShownInSwitchers, IsShownInSwitchers = this.IsShownInSwitchers,
IsLocked = this.IsLocked, IsLocked = this.IsLocked,
IsAlwaysHideUnlockButton = this.IsAlwaysHideUnlockButton,
IsPinToTaskbar = this.IsPinToTaskbar, IsPinToTaskbar = this.IsPinToTaskbar,
TaskbarPlacement = this.TaskbarPlacement, TaskbarPlacement = this.TaskbarPlacement,

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,10 @@
using ATL; namespace BetterLyrics.WinUI3.Models
namespace BetterLyrics.WinUI3.Models
{ {
public class PlayQueueItem public class PlayQueueItem
{ {
public Track Track { get; set; } public ExtendedTrack Track { get; set; }
public PlayQueueItem(Track track) public PlayQueueItem(ExtendedTrack track)
{ {
Track = track; Track = track;
} }

View File

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

View File

@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial MusicGallerySettings MusicGallerySettings { get; set; } = new MusicGallerySettings(); [ObservableProperty][NotifyPropertyChangedRecipients] public partial MusicGallerySettings MusicGallerySettings { get; set; } = new MusicGallerySettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AdvancedSettings AdvancedSettings { get; set; } = new AdvancedSettings(); [ObservableProperty][NotifyPropertyChangedRecipients] public partial AdvancedSettings AdvancedSettings { get; set; } = new AdvancedSettings();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LocalMediaFolder> LocalMediaFolders { get; set; } = []; [ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaFolder> LocalMediaFolders { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = []; [ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = []; [ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsWindowStatus> WindowBoundsRecords { get; set; } = []; [ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsWindowStatus> WindowBoundsRecords { get; set; } = [];

View File

@@ -12,6 +12,11 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsPureColorOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int PureColorOverlayOpacity { get; set; } = 100; // 100 % = 1.0 [ObservableProperty][NotifyPropertyChangedRecipients] public partial int PureColorOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsCoverOverlayEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayOpacity { get; set; } = 100; // 100 % = 1.0
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlaySpeed { get; set; } = 50;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int CoverOverlayBlurAmount { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsFluidOverlayEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int FluidOverlayOpacity { get; set; } = 100; [ObservableProperty][NotifyPropertyChangedRecipients] public partial int FluidOverlayOpacity { get; set; } = 100;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.MedianCut; [ObservableProperty][NotifyPropertyChangedRecipients] public partial PaletteGeneratorType PaletteGeneratorType { get; set; } = PaletteGeneratorType.MedianCut;
@@ -39,14 +44,25 @@ namespace BetterLyrics.WinUI3.Models.Settings
IsPureColorOverlayEnabled = this.IsPureColorOverlayEnabled, IsPureColorOverlayEnabled = this.IsPureColorOverlayEnabled,
PureColorOverlayOpacity = this.PureColorOverlayOpacity, PureColorOverlayOpacity = this.PureColorOverlayOpacity,
IsCoverOverlayEnabled = this.IsCoverOverlayEnabled,
CoverOverlayOpacity = this.CoverOverlayOpacity,
CoverOverlaySpeed = this.CoverOverlaySpeed,
CoverOverlayBlurAmount = this.CoverOverlayBlurAmount,
IsFluidOverlayEnabled = this.IsFluidOverlayEnabled, IsFluidOverlayEnabled = this.IsFluidOverlayEnabled,
FluidOverlayOpacity = this.FluidOverlayOpacity, FluidOverlayOpacity = this.FluidOverlayOpacity,
PaletteGeneratorType = this.PaletteGeneratorType, PaletteGeneratorType = this.PaletteGeneratorType,
IsSpectrumOverlayEnabled = this.IsSpectrumOverlayEnabled, IsSpectrumOverlayEnabled = this.IsSpectrumOverlayEnabled,
SpectrumPlacement = this.SpectrumPlacement,
SpectrumStyle = this.SpectrumStyle,
SpectrumCount = this.SpectrumCount,
IsSnowFlakeOverlayEnabled = this.IsSnowFlakeOverlayEnabled, IsSnowFlakeOverlayEnabled = this.IsSnowFlakeOverlayEnabled,
SnowFlakeOverlayAmount = this.SnowFlakeOverlayAmount, SnowFlakeOverlayAmount = this.SnowFlakeOverlayAmount,
SnowFlakeOverlaySpeed = this.SnowFlakeOverlaySpeed,
IsFogOverlayEnabled = this.IsFogOverlayEnabled,
}; };
} }
} }

View File

@@ -7,6 +7,8 @@ namespace BetterLyrics.WinUI3.Models.Settings
public partial class LyricsEffectSettings : ObservableRecipient, ICloneable public partial class LyricsEffectSettings : ObservableRecipient, ICloneable
{ {
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsBlurEffectEnabled { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsBlurEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsFadeOutEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsOutOfSightEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsGlowEffectEnabled { get; set; } = true; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsLyricsGlowEffectEnabled { get; set; } = true;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectScope LyricsGlowEffectScope { get; set; } = LyricsEffectScope.LongDurationSyllable; [ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsEffectScope LyricsGlowEffectScope { get; set; } = LyricsEffectScope.LongDurationSyllable;
@@ -52,11 +54,14 @@ namespace BetterLyrics.WinUI3.Models.Settings
return new LyricsEffectSettings(this.LyricsScrollTopDuration, this.LyricsScrollDuration, this.LyricsScrollBottomDuration, this.LyricsScrollEasingType) return new LyricsEffectSettings(this.LyricsScrollTopDuration, this.LyricsScrollDuration, this.LyricsScrollBottomDuration, this.LyricsScrollEasingType)
{ {
IsLyricsBlurEffectEnabled = this.IsLyricsBlurEffectEnabled, IsLyricsBlurEffectEnabled = this.IsLyricsBlurEffectEnabled,
IsLyricsFadeOutEffectEnabled = this.IsLyricsFadeOutEffectEnabled,
IsLyricsOutOfSightEffectEnabled = this.IsLyricsOutOfSightEffectEnabled,
IsLyricsGlowEffectEnabled = this.IsLyricsGlowEffectEnabled, IsLyricsGlowEffectEnabled = this.IsLyricsGlowEffectEnabled,
LyricsGlowEffectLongSyllableDuration = this.LyricsGlowEffectLongSyllableDuration, LyricsGlowEffectLongSyllableDuration = this.LyricsGlowEffectLongSyllableDuration,
IsLyricsGlowEffectAmountAutoAdjust = this.IsLyricsGlowEffectAmountAutoAdjust, IsLyricsGlowEffectAmountAutoAdjust = this.IsLyricsGlowEffectAmountAutoAdjust,
LyricsGlowEffectAmount = this.LyricsGlowEffectAmount, LyricsGlowEffectAmount = this.LyricsGlowEffectAmount,
LyricsGlowEffectScope = this.LyricsGlowEffectScope,
IsLyricsScaleEffectEnabled = this.IsLyricsScaleEffectEnabled, IsLyricsScaleEffectEnabled = this.IsLyricsScaleEffectEnabled,
LyricsScaleEffectLongSyllableDuration = this.LyricsScaleEffectLongSyllableDuration, LyricsScaleEffectLongSyllableDuration = this.LyricsScaleEffectLongSyllableDuration,

View File

@@ -1,9 +1,7 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI; using Microsoft.UI;
using System; using System;
using System.Linq;
using Windows.UI; using Windows.UI;
namespace BetterLyrics.WinUI3.Models.Settings namespace BetterLyrics.WinUI3.Models.Settings

View File

@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool AutoPlay { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool AutoPlay { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsWindowStatus LyricsWindowStatus { get; set; } = new(); [ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsWindowStatus LyricsWindowStatus { get; set; } = new();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnWindowClosed { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool ExitOnWindowClosed { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool StopOnWindowClosed { get; set; } = false;
public MusicGallerySettings() { } public MusicGallerySettings() { }
} }

View File

@@ -13,6 +13,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial ChineseRomanization ChineseRomanization { get; set; } [ObservableProperty][NotifyPropertyChangedRecipients] public partial ChineseRomanization ChineseRomanization { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsChineseRomanizationEnabled { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsChineseRomanizationEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsJapaneseRomanizationEnabled { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsJapaneseRomanizationEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string CutletDockerServer { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTraditionalChineseEnabled { get; set; } = false; [ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTraditionalChineseEnabled { get; set; } = false;
public TranslationSettings() { } public TranslationSettings() { }

View File

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

View File

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

View File

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

View File

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

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