Compare commits
517 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4fba4f44 | ||
|
|
6092bab75c | ||
|
|
365d9e1223 | ||
|
|
9c3b2717ac | ||
|
|
8b6320730c | ||
|
|
c34eeba859 | ||
|
|
d6914c42b3 | ||
|
|
39778247f6 | ||
|
|
1d91b183fd | ||
|
|
b2a4875ba7 | ||
|
|
077b31e4c9 | ||
|
|
dbcc19cac1 | ||
|
|
83de915e54 | ||
|
|
8ce33736a0 | ||
|
|
3edac65ae8 | ||
|
|
db3b74e33f | ||
|
|
89a077be5c | ||
|
|
76a5b6221d | ||
|
|
18f8831b7e | ||
|
|
b674d102e3 | ||
|
|
86e52eec4c | ||
|
|
fd55383778 | ||
|
|
f29385ccef | ||
|
|
3993ff8a8e | ||
|
|
a130b5db98 | ||
|
|
2d22501d08 | ||
|
|
b478427522 | ||
|
|
70164fa3f7 | ||
|
|
8e1b2be073 | ||
|
|
b6b67884f4 | ||
|
|
fe97a485c7 | ||
|
|
86c64fdd05 | ||
|
|
da56c66168 | ||
|
|
5bd6b38908 | ||
|
|
81cfe3efe1 | ||
|
|
0a9897f6a4 | ||
|
|
0b495f100f | ||
|
|
70b55e5fdd | ||
|
|
9c2f3d3f86 | ||
|
|
5452b3de4f | ||
|
|
b1666095a6 | ||
|
|
7fa6d81dc8 | ||
|
|
04a10e62d6 | ||
|
|
ecce23589a | ||
|
|
b6aa6aebb9 | ||
|
|
4bd4178cbf | ||
|
|
04a157c64a | ||
|
|
ac60ac417b | ||
|
|
1efd62803a | ||
|
|
218e829fd4 | ||
|
|
acb3784071 | ||
|
|
f87957b170 | ||
|
|
043310ca00 | ||
|
|
43d71bb368 | ||
|
|
12eb430d8c | ||
|
|
cfb42075dc | ||
|
|
9b5457ffc0 | ||
|
|
3099bd6ca1 | ||
|
|
ea32f705f5 | ||
|
|
66b7d27dc4 | ||
|
|
05b512e8cc | ||
|
|
a2da381f1a | ||
|
|
e4654d63c3 | ||
|
|
38b1af2696 | ||
|
|
81c6abb879 | ||
|
|
d4ad738888 | ||
|
|
a62670eecf | ||
|
|
25adc4face | ||
|
|
8fd62cf2f3 | ||
|
|
a360212dc7 | ||
|
|
d7dec1bc4d | ||
|
|
8be86a2d95 | ||
|
|
34949b8a7f | ||
|
|
40502e3bff | ||
|
|
0de2603e30 | ||
|
|
e330359192 | ||
|
|
ab80b2a5af | ||
|
|
f642bfcf48 | ||
|
|
805a63cf59 | ||
|
|
4d430ba42c | ||
|
|
5f734758b4 | ||
|
|
8157dbc530 | ||
|
|
391d862b17 | ||
|
|
271856ca89 | ||
|
|
d7eb734aaf | ||
|
|
1d4eabb770 | ||
|
|
906c21e252 | ||
|
|
7ae92970ef | ||
|
|
cf0bf1e587 | ||
|
|
616c129ffd | ||
|
|
1326cc4966 | ||
|
|
35bc4a6ece | ||
|
|
e54a0f127f | ||
|
|
070ecad54b | ||
|
|
205ae2bf55 | ||
|
|
d35c85f389 | ||
|
|
026e40855c | ||
|
|
553be52260 | ||
|
|
69f9fb398f | ||
|
|
98985a7fa4 | ||
|
|
3f71e79809 | ||
|
|
55138957b7 | ||
|
|
901e8d9cb8 | ||
|
|
f140fc53ad | ||
|
|
9b8b699ace | ||
|
|
39a355ab4c | ||
|
|
22f9285627 | ||
|
|
152eaf2627 | ||
|
|
d15b8091bc | ||
|
|
de9eb2292e | ||
|
|
9b86e24513 | ||
|
|
9a97a5d110 | ||
|
|
964668c982 | ||
|
|
0514c0d999 | ||
|
|
4a782332d3 | ||
|
|
72734d4b4e | ||
|
|
8d34e6f340 | ||
|
|
c899ea95e1 | ||
|
|
0b57cd3555 | ||
|
|
f9b4f587c2 | ||
|
|
279f586a90 | ||
|
|
2f3f712256 | ||
|
|
6748a20ddb | ||
|
|
90ccb86a6f | ||
|
|
574bf861f0 | ||
|
|
5bff1747e6 | ||
|
|
17ea416c98 | ||
|
|
ab57aee8c1 | ||
|
|
8c80fc3578 | ||
|
|
85ab250551 | ||
|
|
3f3a1a6d7f | ||
|
|
68fe3bbd4b | ||
|
|
a8054be82e | ||
|
|
3b6fd8019b | ||
|
|
91af974bd4 | ||
|
|
024a249e6b | ||
|
|
024e74115e | ||
|
|
7b4f08bb05 | ||
|
|
f75036cb8e | ||
|
|
0510fbb65a | ||
|
|
9e4bc24365 | ||
|
|
0f41d5b2f8 | ||
|
|
a282baf5a2 | ||
|
|
dea29054e6 | ||
|
|
efaff0ae79 | ||
|
|
2d75d89825 | ||
|
|
bcd0d63db7 | ||
|
|
26f921b7e4 | ||
|
|
4d1a9517e1 | ||
|
|
222070feba | ||
|
|
b28882cff5 | ||
|
|
fb22e5ab66 | ||
|
|
11a0f2faca | ||
|
|
dd6ff101d1 | ||
|
|
286193f08f | ||
|
|
6353ecc13e | ||
|
|
767e93615c | ||
|
|
76998e7761 | ||
|
|
df205f2b9d | ||
|
|
3e63875659 | ||
|
|
fcb7330970 | ||
|
|
b19c718a2a | ||
|
|
661e7bfa78 | ||
|
|
867efecc54 | ||
|
|
bd31ab5d07 | ||
|
|
bd1ffb0f24 | ||
|
|
a8fa4d72f3 | ||
|
|
2d1697064d | ||
|
|
a915650bb6 | ||
|
|
1da30d5d8f | ||
|
|
a2f72ee3f3 | ||
|
|
2e4c24393d | ||
|
|
e7b229a60f | ||
|
|
562f9035e8 | ||
|
|
7689fe8aa4 | ||
|
|
ceca78368d | ||
|
|
3fa6d9820f | ||
|
|
2f4c739f0b | ||
|
|
4e68c765c5 | ||
|
|
0dfc4e15bd | ||
|
|
e8147680e6 | ||
|
|
2b3d326c41 | ||
|
|
6414b377da | ||
|
|
ea80d9a39c | ||
|
|
ef671f6503 | ||
|
|
cfc66e4364 | ||
|
|
1477a9058a | ||
|
|
cdeb843a84 | ||
|
|
07d2b3b464 | ||
|
|
a49caa871d | ||
|
|
fb004a0bb9 | ||
|
|
6f69a45195 | ||
|
|
877732e1e7 | ||
|
|
caa58e9d7d | ||
|
|
2cfad80214 | ||
|
|
9b3c3efb09 | ||
|
|
c491b5283b | ||
|
|
7f70ee5045 | ||
|
|
57fa8b4f3e | ||
|
|
974a74a3c7 | ||
|
|
478b71d6b3 | ||
|
|
5940c4f032 | ||
|
|
9e50a195a4 | ||
|
|
b7b3460248 | ||
|
|
36bf6f4ceb | ||
|
|
56491591ab | ||
|
|
0b05edd6ff | ||
|
|
c090cae1a1 | ||
|
|
a46bde68f5 | ||
|
|
ddf7d82656 | ||
|
|
23813eb224 | ||
|
|
77e4a30bc5 | ||
|
|
15f4ae2567 | ||
|
|
b3f117d28e | ||
|
|
17a75da540 | ||
|
|
f8caa46eab | ||
|
|
8d4bbc1a1c | ||
|
|
b5f2510cce | ||
|
|
978f27c700 | ||
|
|
b4ca42e0c0 | ||
|
|
4abffeed32 | ||
|
|
9b5628cb65 | ||
|
|
85f06ed65d | ||
|
|
f6b5d358e0 | ||
|
|
a42881ba9f | ||
|
|
d5991b4354 | ||
|
|
101e49fe74 | ||
|
|
1cbeacbd0f | ||
|
|
4b6b3e8377 | ||
|
|
b3ab417c85 | ||
|
|
defc6911d6 | ||
|
|
6c757ec395 | ||
|
|
b876840d08 | ||
|
|
30bad3a066 | ||
|
|
ca993df0c6 | ||
|
|
451a84e696 | ||
|
|
e65ec1b0b9 | ||
|
|
aed45b08ac | ||
|
|
7f93b42a1b | ||
|
|
a831b41623 | ||
|
|
4d193a1f72 | ||
|
|
51750a4ad5 | ||
|
|
8fe6e3f4b7 | ||
|
|
6d7b0e8dd5 | ||
|
|
43409826f3 | ||
|
|
bb6bd95e9b | ||
|
|
d4d1602b45 | ||
|
|
bd3c76ef43 | ||
|
|
3722ff1f33 | ||
|
|
dc1cca0d4c | ||
|
|
3dad24e7b4 | ||
|
|
c591b57f22 | ||
|
|
91389f91d1 | ||
|
|
ec811f75e6 | ||
|
|
51e88939d6 | ||
|
|
f4470c383e | ||
|
|
ed99aee3fd | ||
|
|
40fb93f036 | ||
|
|
64f7ba2a1a | ||
|
|
6a45f993ae | ||
|
|
0bdf620c2f | ||
|
|
b8d2ff7e9b | ||
|
|
91142be3bd | ||
|
|
8159e1b1df | ||
|
|
27b05098cc | ||
|
|
1e851d34b6 | ||
|
|
f10aa38bfd | ||
|
|
9a1b15029e | ||
|
|
2063c366c2 | ||
|
|
afe812e2be | ||
|
|
738cd61825 | ||
|
|
c28729af5b | ||
|
|
4d7d9abc60 | ||
|
|
8c7001c801 | ||
|
|
039e1696dd | ||
|
|
636e083044 | ||
|
|
fcaba24cee | ||
|
|
33b8902375 | ||
|
|
65eecb8dcf | ||
|
|
e0fe16fd14 | ||
|
|
7bb0307e6a | ||
|
|
cba70c3507 | ||
|
|
f779ed63e8 | ||
|
|
07e34eb17b | ||
|
|
f220db96ed | ||
|
|
a0abd472e0 | ||
|
|
0d27d88719 | ||
|
|
e212144250 | ||
|
|
2f5a3d66fc | ||
|
|
ff0ff42222 | ||
|
|
0dc209d30a | ||
|
|
2aeecb05d3 | ||
|
|
65404ce356 | ||
|
|
246061c69e | ||
|
|
92f96c93f0 | ||
|
|
993c1f309a | ||
|
|
7856857cca | ||
|
|
1f2f00d49c | ||
|
|
3afdd9d3f3 | ||
|
|
42fa4a2fff | ||
|
|
3d4bcbc082 | ||
|
|
4c0443ec28 | ||
|
|
8b28a31d09 | ||
|
|
e6e9ce7d57 | ||
|
|
9ad57dccb0 | ||
|
|
95caf111ae | ||
|
|
abdde1f811 | ||
|
|
ae901c709d | ||
|
|
a2af297a84 | ||
|
|
f9e28d1de9 | ||
|
|
a2ef4e6f84 | ||
|
|
e5f3c3c922 | ||
|
|
6f4321ae14 | ||
|
|
a5c7ec0d60 | ||
|
|
6bc0a8b4aa | ||
|
|
538494b7ec | ||
|
|
ed60c274fc | ||
|
|
bbc498f882 | ||
|
|
0932b3d625 | ||
|
|
9d4d37f2e7 | ||
|
|
6fc7e47111 | ||
|
|
c05ad1e724 | ||
|
|
5ed86b9165 | ||
|
|
75cbd20f54 | ||
|
|
3c07b7347b | ||
|
|
d0ebedac0a | ||
|
|
d86caac189 | ||
|
|
c2b02b9b8d | ||
|
|
a4e8ea37aa | ||
|
|
f56ca9c082 | ||
|
|
e27476bc32 | ||
|
|
8ca4f7c8d3 | ||
|
|
1c4eb0766b | ||
|
|
87a812b7e0 | ||
|
|
f42a6200ed | ||
|
|
a252ee0655 | ||
|
|
498988c2e3 | ||
|
|
261922d73a | ||
|
|
ebe08c23e4 | ||
|
|
70edd4cc3a | ||
|
|
fa48a07970 | ||
|
|
0259ca963a | ||
|
|
8dc9f68584 | ||
|
|
4db7711a36 | ||
|
|
7b9e4b2f82 | ||
|
|
07c04a9e7e | ||
|
|
8427ebc36e | ||
|
|
a99fc8fa72 | ||
|
|
5959288491 | ||
|
|
0522dd5ad4 | ||
|
|
d886569dc3 | ||
|
|
12c711424b | ||
|
|
cb6ead96d1 | ||
|
|
c4e7263ed6 | ||
|
|
4972e64cad | ||
|
|
5ea8a7d313 | ||
|
|
296cd863d2 | ||
|
|
9ccf91659f | ||
|
|
f0e3b776bb | ||
|
|
3638d65008 | ||
|
|
2cc9324f08 | ||
|
|
bc8907b3ef | ||
|
|
14f8ec37c5 | ||
|
|
2b567e7cb3 | ||
|
|
b58a3ec044 | ||
|
|
2d0d578bb4 | ||
|
|
54ba05c4aa | ||
|
|
27b251b06e | ||
|
|
5643ebfe48 | ||
|
|
d9c2f6bf91 | ||
|
|
3eb404a9e2 | ||
|
|
bc9c20c509 | ||
|
|
7cc0c83df1 | ||
|
|
41daefa6c4 | ||
|
|
38fa8a10b7 | ||
|
|
07d37a1209 | ||
|
|
509f0d1266 | ||
|
|
7966bab62d | ||
|
|
a136c150ad | ||
|
|
a89fe6b026 | ||
|
|
56460c937d | ||
|
|
f2080bfb7b | ||
|
|
012d55452e | ||
|
|
6ac482ed5e | ||
|
|
68df173558 | ||
|
|
d9c6c31a4d | ||
|
|
d3d2715418 | ||
|
|
a93fbd4444 | ||
|
|
9fee9a4cf1 | ||
|
|
4bbc008788 | ||
|
|
671b6e1ef7 | ||
|
|
634bae915a | ||
|
|
a7bbfc983e | ||
|
|
17548e935e | ||
|
|
15f84712cd | ||
|
|
2f34ae7d45 | ||
|
|
16cbe7e43c | ||
|
|
8d633377ae | ||
|
|
0b867c254f | ||
|
|
08a47e6c1d | ||
|
|
6c9cd8b120 | ||
|
|
71e7219084 | ||
|
|
c13063b230 | ||
|
|
26ca69cb83 | ||
|
|
afc8c5f873 | ||
|
|
4d3f739a0c | ||
|
|
1781fdb7ca | ||
|
|
32aa37505c | ||
|
|
9f9ed7dd4b | ||
|
|
03e3b897cf | ||
|
|
3bc20ce1d4 | ||
|
|
9ce9940306 | ||
|
|
da35cf471e | ||
|
|
c517df2c09 | ||
|
|
02dee71670 | ||
|
|
1eadcd41f6 | ||
|
|
e8185535b0 | ||
|
|
b68bebfa2e | ||
|
|
3801bdf9d7 | ||
|
|
9a6ba82467 | ||
|
|
3a52c1199c | ||
|
|
ea5c0584cc | ||
|
|
01b30d942b | ||
|
|
5aa5308a50 | ||
|
|
de029b7043 | ||
|
|
a45da453ce | ||
|
|
e1b73f4766 | ||
|
|
99b19e7b03 | ||
|
|
37bd849a86 | ||
|
|
4eb6f78a38 | ||
|
|
68f03f2311 | ||
|
|
2a60a9b393 | ||
|
|
1d4b08672b | ||
|
|
b0d9a1dada | ||
|
|
796494e53f | ||
|
|
cef7bfd534 | ||
|
|
36ff4a0ed3 | ||
|
|
6a6894030b | ||
|
|
497d31ddf7 | ||
|
|
783218429c | ||
|
|
0ccd15047b | ||
|
|
fe2a6ec006 | ||
|
|
a3ecf59fae | ||
|
|
4f4f89a1d7 | ||
|
|
ece3bdd2e8 | ||
|
|
f403ed1a21 | ||
|
|
17e3a0206a | ||
|
|
5da86d85de | ||
|
|
d3cbc95235 | ||
|
|
a7eebcc209 | ||
|
|
fca22eb592 | ||
|
|
1202e5ec0f | ||
|
|
03830533eb | ||
|
|
850e5a199e | ||
|
|
2d11158ecd | ||
|
|
a34c18b262 | ||
|
|
560b1e40cc | ||
|
|
3cd512857c | ||
|
|
356adbef5c | ||
|
|
42d7445d83 | ||
|
|
3a0f32fce7 | ||
|
|
6bc128cfda | ||
|
|
6f2d697748 | ||
|
|
4de180c23a | ||
|
|
af289c533f | ||
|
|
82d615fbbf | ||
|
|
457f2ea6c7 | ||
|
|
41ad5c45ed | ||
|
|
e9da2e8d6b | ||
|
|
a8cfbb12fd | ||
|
|
6d89b7769e | ||
|
|
2d86daec83 | ||
|
|
a5e8594611 | ||
|
|
99810ef512 | ||
|
|
2317b831db | ||
|
|
e073086cf4 | ||
|
|
b14844f459 | ||
|
|
8719c8f639 | ||
|
|
d3cec0ec72 | ||
|
|
a8daf02610 | ||
|
|
f9b844fb1a | ||
|
|
6d1d6b575a | ||
|
|
0a5a094e54 | ||
|
|
754da4777a | ||
|
|
216e3e606e | ||
|
|
bb013a8fe6 | ||
|
|
6b6449f023 | ||
|
|
fcf3348371 | ||
|
|
f90f759667 | ||
|
|
b02e6c04b9 | ||
|
|
08dc04f874 | ||
|
|
4776b84c7c | ||
|
|
78d13b586a | ||
|
|
f522ecd42d | ||
|
|
44fa2a8c3e | ||
|
|
ff30c8c2bf | ||
|
|
4aaaffbcea | ||
|
|
21da122902 | ||
|
|
849904ad45 | ||
|
|
1c0bae600f | ||
|
|
f1433c6e9b | ||
|
|
2dc106adcb | ||
|
|
df6738f607 | ||
|
|
ee64f1e7f1 | ||
|
|
d921f6176b | ||
|
|
7009c3400a | ||
|
|
7bd481b090 | ||
|
|
7fafa88eb7 | ||
|
|
cb3e57feec | ||
|
|
9a7d73cb6b | ||
|
|
f5c2bd47d5 | ||
|
|
c154d25f7a | ||
|
|
8c259205f5 | ||
|
|
849329b66b | ||
|
|
f542565dc5 | ||
|
|
08aedbf0b0 | ||
|
|
09c8a41c52 |
9
.github/ISSUE_TEMPLATE/bug-反馈.yml
vendored
@@ -11,9 +11,18 @@ body:
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的 bug report。
|
||||
required: true
|
||||
- label: 无视上一条 => block
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本号
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/功能请求.yml
vendored
@@ -11,6 +11,8 @@ body:
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的功能请求。
|
||||
required: true
|
||||
- label: 无视上一条 => block
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
|
||||
|
||||
13
.github/workflows/android.yml
vendored
@@ -33,19 +33,6 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: 修复3.24的stable显示中文不正确问题 // from orz12
|
||||
run: |
|
||||
version=$(grep -m 1 'flutter:' pubspec.yaml | awk '{print $2}')
|
||||
if [ "$(echo "$version < 3.27.0" | awk '{print ($1 < $2)}')" -eq 1 ]; then
|
||||
cd $FLUTTER_ROOT
|
||||
git config --global user.name "orz12"
|
||||
git config --global user.email "orz12@test.com"
|
||||
git cherry-pick d4124bd --strategy-option theirs
|
||||
# flutter precache
|
||||
flutter --version
|
||||
cd -
|
||||
fi
|
||||
|
||||
- name: 下载项目依赖
|
||||
run: flutter pub get
|
||||
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -2,5 +2,9 @@
|
||||
"editor.formatOnSave": true,
|
||||
"[dart]": {
|
||||
"editor.formatOnType": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit",
|
||||
// "source.fixAll": "explicit",
|
||||
}
|
||||
}
|
||||
21
README.md
@@ -47,9 +47,24 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 修改消息设置
|
||||
- [x] 修改聊天设置
|
||||
- [x] 展示折叠消息
|
||||
- [x] 查看用户图文
|
||||
- [x] 动态话题
|
||||
- [x] 直播分区
|
||||
- [x] 分享`视频`/`番剧`/`动态`/`专栏`/`直播`至消息
|
||||
- [x] 创建/修改/删除关注分组
|
||||
- [x] 移除粉丝
|
||||
- [x] 直播弹幕发送表情
|
||||
- [x] 收藏夹排序
|
||||
- [x] 稍后再看`未看`/`未看完`/`已看完`分类
|
||||
- [x] WebDAV 备份/恢复设置
|
||||
- [x] 保存评论/动态
|
||||
- [x] 高级弹幕 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 取消/置顶评论
|
||||
- [x] 记笔记
|
||||
- [x] 多账号支持 by @My-Responsitories
|
||||
- [x] 多账号支持 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 屏蔽带货动态/评论
|
||||
- [x] 互动视频
|
||||
- [x] 发评/动态反诈
|
||||
@@ -74,7 +89,6 @@
|
||||
- [x] 评论楼中楼定位点击查看的评论
|
||||
- [x] 评论楼中楼按热度/时间排序
|
||||
- [x] 评论点踩
|
||||
- [x] 显示ops专栏
|
||||
- [x] 私信发图
|
||||
- [x] 投币动画
|
||||
- [x] 取消/追番,更新追番状态
|
||||
@@ -90,7 +104,7 @@
|
||||
- [x] 合集图片
|
||||
- [x] 删除/置顶/撤回私信
|
||||
- [x] 举报用户/评论/视频/动态
|
||||
- [x] 删除/发布文本/图片动态
|
||||
- [x] 删除/发布/置顶文本/图片动态
|
||||
- [x] 其他
|
||||
|
||||
## opt
|
||||
@@ -159,7 +173,6 @@
|
||||
- [x] 音质选择(视视频而定)
|
||||
- [x] 解码格式选择(视视频而定)
|
||||
- [x] 弹幕
|
||||
- [ ] 直播弹幕
|
||||
- [x] 字幕
|
||||
- [x] 记忆播放
|
||||
- [x] 视频比例:高度/宽度适应、填充、包含等
|
||||
|
||||
@@ -21,9 +21,37 @@ linter:
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
# https://dart.dev/tools/linter-rules
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# - always_specify_types
|
||||
# - avoid_positional_boolean_parameters
|
||||
# - use_null_aware_elements
|
||||
- always_declare_return_types
|
||||
- always_use_package_imports
|
||||
- avoid_empty_else
|
||||
- avoid_field_initializers_in_const_classes
|
||||
- avoid_print
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
- avoid_slow_async_io
|
||||
- avoid_type_to_string
|
||||
- avoid_types_as_parameter_names
|
||||
- avoid_unnecessary_containers
|
||||
- avoid_void_async
|
||||
- await_only_futures
|
||||
- camel_case_extensions
|
||||
- camel_case_types
|
||||
- cancel_subscriptions
|
||||
- cascade_invocations
|
||||
- prefer_const_constructors
|
||||
- prefer_const_declarations
|
||||
- sized_box_for_whitespace
|
||||
- unnecessary_late
|
||||
- use_colored_box
|
||||
- use_decorated_box
|
||||
- use_named_constants
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
2
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/.cxx
|
||||
/build
|
||||
@@ -41,12 +41,12 @@ android {
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -85,6 +85,10 @@ android {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig _storeFile != null ? signingConfigs.release : signingConfigs.debug
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
|
||||
1
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1 @@
|
||||
-keep class com.yalantis.ucrop.util.RectUtils { *; }
|
||||
@@ -44,6 +44,9 @@
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
tools:replace="android:allowBackup">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -172,7 +175,7 @@
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
android:theme="@style/Ucrop.CropTheme"/>
|
||||
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
|
||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
@@ -7,8 +7,10 @@ import com.ryanheise.audioservice.AudioServiceActivity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@@ -55,6 +57,22 @@ class MainActivity : AudioServiceActivity() {
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {}
|
||||
} else if (call.method == "linkVerifySettings") {
|
||||
try {
|
||||
val intent = Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:" + context.packageName))
|
||||
context.startActivity(intent)
|
||||
} catch (t: Throwable) {
|
||||
try {
|
||||
val intent = Intent("android.intent.action.MAIN", Uri.parse("package:" + context.packageName))
|
||||
intent.setClassName("com.android.settings", "com.android.settings.applications.InstalledAppOpenByDefaultActivity")
|
||||
context.startActivity(intent)
|
||||
} catch (t2: Throwable) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.parse("package:" + context.packageName))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
|
||||
6
android/app/src/main/res/values-v35/styles.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -21,4 +21,6 @@
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
||||
</style>
|
||||
|
||||
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar"/>
|
||||
</resources>
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url "https://maven.aliyun.com/repository/google" }
|
||||
maven { url "https://maven.aliyun.com/repository/central" }
|
||||
maven { url "https://maven.aliyun.com/repository/jcenter" }
|
||||
maven { url "https://maven.aliyun.com/repository/public" }
|
||||
maven { url "http://download.flutter.io"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.extensions.findByName("android") != null) {
|
||||
if (project.hasProperty('android')) {
|
||||
project.android {
|
||||
if (namespace == null) {
|
||||
namespace project.group
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Integer pluginCompileSdk = project.android.compileSdk
|
||||
if (pluginCompileSdk != null) {
|
||||
if (pluginCompileSdk < 31) {
|
||||
@@ -34,18 +45,6 @@ subprojects {
|
||||
compileSdk 31
|
||||
}
|
||||
}
|
||||
if (pluginCompileSdk > 34) {
|
||||
project.logger.error(
|
||||
"Warning: Overriding compileSdk version in Flutter plugin: "
|
||||
+ project.name
|
||||
+ " from "
|
||||
+ pluginCompileSdk
|
||||
+ " to 34"
|
||||
)
|
||||
project.android {
|
||||
compileSdk 34
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -10,13 +10,6 @@ pluginManagement {
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
maven { url "https://maven.aliyun.com/repository/google" }
|
||||
maven { url "https://maven.aliyun.com/repository/central" }
|
||||
maven { url "https://maven.aliyun.com/repository/jcenter" }
|
||||
maven { url "https://maven.aliyun.com/repository/public" }
|
||||
maven { url "http://download.flutter.io"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
@@ -25,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.2.0" apply false
|
||||
id "com.android.application" version '8.4.1' apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
|
||||
}
|
||||
|
||||
|
||||
BIN
assets/fonts/custom_icon.ttf
Normal file
BIN
assets/images/live/live.gif
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 915 B |
|
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 518 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 991 B |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 912 B |
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/lv/lv6_s.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 45 KiB |
BIN
assets/images/topic-header-bg.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/images/trending_banner.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
class StyleString {
|
||||
static const double cardSpace = 8;
|
||||
static const double safeSpace = 12;
|
||||
static BorderRadius mdRadius = BorderRadius.circular(10);
|
||||
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
||||
static const Radius imgRadius = Radius.circular(10);
|
||||
static const double aspectRatio = 16 / 10;
|
||||
}
|
||||
@@ -26,31 +26,37 @@ class Constants {
|
||||
'{"appId":5,"platform":3,"version":"1.46.2","abtest":""}';
|
||||
// 请求时会自动encodeComponent
|
||||
|
||||
// app
|
||||
static const String userAgentApp =
|
||||
'Mozilla/5.0 BiliDroid/8.43.0 (bbcallen@gmail.com) os/android model/android mobi_app/android build/8430300 channel/bili innerVer/8430300 osVer/15 network/2';
|
||||
static const String statisticsApp =
|
||||
'{"appId":5,"platform":3,"version":"8.43.0","abtest":""}';
|
||||
|
||||
static const urlPattern =
|
||||
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]';
|
||||
|
||||
static get goodsUrlPrefix => "https://gaoneng.bilibili.com/tetris";
|
||||
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
|
||||
|
||||
// 超分辨率滤镜
|
||||
static List<String> get mpvAnime4KShaders => [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
'Anime4K_Restore_CNN_VL.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_VL.glsl',
|
||||
'Anime4K_AutoDownscalePre_x2.glsl',
|
||||
'Anime4K_AutoDownscalePre_x4.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl'
|
||||
];
|
||||
static const List<String> mpvAnime4KShaders = [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
'Anime4K_Restore_CNN_VL.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_VL.glsl',
|
||||
'Anime4K_AutoDownscalePre_x2.glsl',
|
||||
'Anime4K_AutoDownscalePre_x4.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl'
|
||||
];
|
||||
|
||||
// 超分辨率滤镜 (轻量)
|
||||
static List<String> get mpvAnime4KShadersLite => [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
'Anime4K_Restore_CNN_M.glsl',
|
||||
'Anime4K_Restore_CNN_S.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl',
|
||||
'Anime4K_AutoDownscalePre_x2.glsl',
|
||||
'Anime4K_AutoDownscalePre_x4.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_S.glsl'
|
||||
];
|
||||
static const mpvAnime4KShadersLite = [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
'Anime4K_Restore_CNN_M.glsl',
|
||||
'Anime4K_Restore_CNN_S.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl',
|
||||
'Anime4K_AutoDownscalePre_x2.glsl',
|
||||
'Anime4K_AutoDownscalePre_x4.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_S.glsl'
|
||||
];
|
||||
|
||||
//内容来自 https://passport.bilibili.com/web/generic/country/list
|
||||
static List<Map<String, dynamic>> get internationalDialingPrefix => [
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class DynamicCardSkeleton extends StatelessWidget {
|
||||
const DynamicCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final color = theme.colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||
@@ -13,7 +15,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 8,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.05),
|
||||
color: theme.dividerColor.withValues(alpha: 0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -25,8 +27,8 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: color,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -34,13 +36,13 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 50,
|
||||
height: 11,
|
||||
),
|
||||
@@ -55,31 +57,31 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: double.infinity,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: double.infinity,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 300,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 250,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
@@ -87,6 +89,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
@@ -99,10 +102,8 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.2),
|
||||
foregroundColor:
|
||||
theme.colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
label: Text(
|
||||
i == 0
|
||||
|
||||
67
lib/common/skeleton/fav_pgc_item.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FavPgcItemSkeleton extends StatelessWidget {
|
||||
const FavPgcItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
width: boxConstraints.maxWidth,
|
||||
height: boxConstraints.maxHeight,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 175,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: 55,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Container(
|
||||
width: 35,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
|
||||
import 'skeleton.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaBangumiSkeleton extends StatefulWidget {
|
||||
const MediaBangumiSkeleton({super.key});
|
||||
@@ -24,8 +23,9 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
width: 111,
|
||||
height: 148,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
color: bgColor),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
color: bgColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
@@ -35,25 +35,25 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 200,
|
||||
height: 20,
|
||||
margin: const EdgeInsets.only(bottom: 15),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 150,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 150,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
@@ -64,7 +64,7 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
53
lib/common/skeleton/msg_feed_sys_msg_.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MsgFeedSysMsgSkeleton extends StatelessWidget {
|
||||
const MsgFeedSysMsgSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 125,
|
||||
height: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 10,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/common/skeleton/msg_feed_top.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MsgFeedTopSkeleton extends StatelessWidget {
|
||||
const MsgFeedTopSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
title: UnconstrainedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
subtitle: Container(
|
||||
color: color,
|
||||
width: 125,
|
||||
height: 11,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ class Skeleton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
|
||||
var shimmerGradient = LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Theme.of(context).colorScheme.surface.withAlpha(10),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(10),
|
||||
color,
|
||||
color,
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [
|
||||
@@ -99,7 +100,7 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child ?? const SizedBox();
|
||||
return widget.child ?? const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ class _ShimmerLoadingState extends State<ShimmerLoading> {
|
||||
|
||||
final shimmer = Shimmer.of(context)!;
|
||||
if (!shimmer.isSized) {
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final shimmerSize = shimmer.size;
|
||||
final gradient = shimmer.gradient;
|
||||
|
||||
48
lib/common/skeleton/space_opus.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SpaceOpusSkeleton extends StatelessWidget {
|
||||
const SpaceOpusSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final surface = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: (0.68 + 0.82 * Utils.random.nextDouble()) *
|
||||
constraints.maxWidth,
|
||||
color: surface,
|
||||
),
|
||||
Container(
|
||||
height: 10,
|
||||
color: surface,
|
||||
margin: const EdgeInsets.all(10),
|
||||
width: constraints.maxWidth * 0.7,
|
||||
),
|
||||
Container(
|
||||
height: 10,
|
||||
color: surface,
|
||||
margin:
|
||||
const EdgeInsets.only(left: 10, right: 10, bottom: 10),
|
||||
width: constraints.maxWidth,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,80 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class VideoCardHSkeleton extends StatelessWidget {
|
||||
const VideoCardHSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
),
|
||||
// VideoContent(videoItem: videoItem)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 200,
|
||||
height: 11,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: color,
|
||||
width: 200,
|
||||
height: 11,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: color,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
color: color,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
color: color,
|
||||
width: 40,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
Container(
|
||||
color: color,
|
||||
width: 40,
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class VideoCardVSkeleton extends StatelessWidget {
|
||||
const VideoCardVSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -14,9 +15,9 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
);
|
||||
@@ -37,24 +38,24 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
width: 200,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
Container(
|
||||
width: 150,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
Container(
|
||||
width: 110,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
Container(
|
||||
width: 75,
|
||||
height: 13,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class VideoReplySkeleton extends StatelessWidget {
|
||||
const VideoReplySkeleton({super.key});
|
||||
|
||||
41
lib/common/skeleton/whisper_item.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WhisperItemSkeleton extends StatelessWidget {
|
||||
const WhisperItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
title: UnconstrainedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
subtitle: Container(
|
||||
color: color,
|
||||
width: 125,
|
||||
height: 11,
|
||||
),
|
||||
trailing: Container(
|
||||
color: color,
|
||||
width: 50,
|
||||
height: 11,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/no_splash_factory.dart';
|
||||
import 'package:PiliPlus/common/widgets/overlay_pop.dart';
|
||||
import 'package:PiliPlus/models/model_video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedDialog extends StatefulWidget {
|
||||
const AnimatedDialog({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
required this.closeFn,
|
||||
});
|
||||
|
||||
final BaseVideoItemModel videoItem;
|
||||
final Function closeFn;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AnimatedDialogState();
|
||||
}
|
||||
|
||||
class AnimatedDialogState extends State<AnimatedDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController controller;
|
||||
late Animation<double> opacityAnimation;
|
||||
late Animation<double> scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 255));
|
||||
opacityAnimation = Tween<double>(begin: 0.0, end: 0.6)
|
||||
.animate(CurvedAnimation(parent: controller, curve: Curves.linear));
|
||||
scaleAnimation = CurvedAnimation(parent: controller, curve: Curves.linear);
|
||||
controller.addListener(listener);
|
||||
controller.forward();
|
||||
}
|
||||
|
||||
void listener() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.removeListener(listener);
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void closeFn() async {
|
||||
await controller.reverse();
|
||||
widget.closeFn();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.black.withOpacity(opacityAnimation.value),
|
||||
child: InkWell(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
splashFactory: NoSplashFactory(),
|
||||
onTap: closeFn,
|
||||
child: Center(
|
||||
child: FadeTransition(
|
||||
opacity: scaleAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: scaleAnimation,
|
||||
child: OverlayPop(
|
||||
videoItem: widget.videoItem,
|
||||
closeFn: closeFn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
|
||||
|
||||
class _SaltedKey<S, V> extends LocalKey {
|
||||
const _SaltedKey(this.salt, this.value);
|
||||
|
||||
final S salt;
|
||||
final V value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is _SaltedKey<S, V> &&
|
||||
other.salt == salt &&
|
||||
other.value == value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, salt, value);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String saltString = S == String ? "<'$salt'>" : '<$salt>';
|
||||
final String valueString = V == String ? "<'$value'>" : '<$value>';
|
||||
return '[$saltString $valueString]';
|
||||
}
|
||||
}
|
||||
|
||||
class AppExpansionPanelList extends StatefulWidget {
|
||||
/// Creates an expansion panel list widget. The [expansionCallback] is
|
||||
/// triggered when an expansion panel expand/collapse button is pushed.
|
||||
///
|
||||
/// The [children] and [animationDuration] arguments must not be null.
|
||||
const AppExpansionPanelList({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.expansionCallback,
|
||||
this.animationDuration = kThemeAnimationDuration,
|
||||
this.expandedHeaderPadding = EdgeInsets.zero,
|
||||
this.dividerColor,
|
||||
this.elevation = 2,
|
||||
}) : _allowOnlyOnePanelOpen = false,
|
||||
initialOpenPanelValue = null;
|
||||
|
||||
/// The children of the expansion panel list. They are laid out in a similar
|
||||
/// fashion to [ListBody].
|
||||
final List<AppExpansionPanel> children;
|
||||
|
||||
/// The callback that gets called whenever one of the expand/collapse buttons
|
||||
/// is pressed. The arguments passed to the callback are the index of the
|
||||
/// pressed panel and whether the panel is currently expanded or not.
|
||||
///
|
||||
/// If AppExpansionPanelList.radio is used, the callback may be called a
|
||||
/// second time if a different panel was previously open. The arguments
|
||||
/// passed to the second callback are the index of the panel that will close
|
||||
/// and false, marking that it will be closed.
|
||||
///
|
||||
/// For AppExpansionPanelList, the callback needs to setState when it's notified
|
||||
/// about the closing/opening panel. On the other hand, the callback for
|
||||
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of
|
||||
/// changes, as the radio panels' open/close states are managed internally.
|
||||
///
|
||||
/// This callback is useful in order to keep track of the expanded/collapsed
|
||||
/// panels in a parent widget that may need to react to these changes.
|
||||
final ExpansionPanelCallback? expansionCallback;
|
||||
|
||||
/// The duration of the expansion animation.
|
||||
final Duration animationDuration;
|
||||
|
||||
// Whether multiple panels can be open simultaneously
|
||||
final bool _allowOnlyOnePanelOpen;
|
||||
|
||||
/// The value of the panel that initially begins open. (This value is
|
||||
/// only used when initializing with the [AppExpansionPanelList.radio]
|
||||
/// constructor.)
|
||||
final Object? initialOpenPanelValue;
|
||||
|
||||
/// The padding that surrounds the panel header when expanded.
|
||||
///
|
||||
/// By default, 16px of space is added to the header vertically (above and below)
|
||||
/// during expansion.
|
||||
final EdgeInsets expandedHeaderPadding;
|
||||
|
||||
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.
|
||||
///
|
||||
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
|
||||
/// is null, then [ThemeData.dividerColor] is used.
|
||||
final Color? dividerColor;
|
||||
|
||||
/// Defines elevation for the [AppExpansionPanel] while it's expanded.
|
||||
///
|
||||
/// By default, the value of elevation is 2.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();
|
||||
}
|
||||
|
||||
class _AppExpansionPanelListState extends State<AppExpansionPanelList> {
|
||||
ExpansionPanelRadio? _currentOpenPanel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
assert(_allIdentifiersUnique(),
|
||||
'All ExpansionPanelRadio identifier values must be unique.');
|
||||
if (widget.initialOpenPanelValue != null) {
|
||||
_currentOpenPanel = searchPanelByValue(
|
||||
widget.children.cast<ExpansionPanelRadio>(),
|
||||
widget.initialOpenPanelValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AppExpansionPanelList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
assert(_allIdentifiersUnique(),
|
||||
'All ExpansionPanelRadio identifier values must be unique.');
|
||||
// If the previous widget was non-radio AppExpansionPanelList, initialize the
|
||||
// open panel to widget.initialOpenPanelValue
|
||||
if (!oldWidget._allowOnlyOnePanelOpen) {
|
||||
_currentOpenPanel = searchPanelByValue(
|
||||
widget.children.cast<ExpansionPanelRadio>(),
|
||||
widget.initialOpenPanelValue);
|
||||
}
|
||||
} else {
|
||||
_currentOpenPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _allIdentifiersUnique() {
|
||||
final Map<Object, bool> identifierMap = <Object, bool>{};
|
||||
for (final ExpansionPanelRadio child
|
||||
in widget.children.cast<ExpansionPanelRadio>()) {
|
||||
identifierMap[child.value] = true;
|
||||
}
|
||||
return identifierMap.length == widget.children.length;
|
||||
}
|
||||
|
||||
bool _isChildExpanded(int index) {
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
final ExpansionPanelRadio radioWidget =
|
||||
widget.children[index] as ExpansionPanelRadio;
|
||||
return _currentOpenPanel?.value == radioWidget.value;
|
||||
}
|
||||
return widget.children[index].isExpanded;
|
||||
}
|
||||
|
||||
void _handlePressed(bool isExpanded, int index) {
|
||||
widget.expansionCallback?.call(index, isExpanded);
|
||||
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
final ExpansionPanelRadio pressedChild =
|
||||
widget.children[index] as ExpansionPanelRadio;
|
||||
|
||||
// If another ExpansionPanelRadio was already open, apply its
|
||||
// expansionCallback (if any) to false, because it's closing.
|
||||
for (int childIndex = 0;
|
||||
childIndex < widget.children.length;
|
||||
childIndex += 1) {
|
||||
final ExpansionPanelRadio child =
|
||||
widget.children[childIndex] as ExpansionPanelRadio;
|
||||
if (widget.expansionCallback != null &&
|
||||
childIndex != index &&
|
||||
child.value == _currentOpenPanel?.value) {
|
||||
widget.expansionCallback?.call(childIndex, false);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentOpenPanel = isExpanded ? null : pressedChild;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ExpansionPanelRadio? searchPanelByValue(
|
||||
List<ExpansionPanelRadio> panels, Object? value) {
|
||||
for (final ExpansionPanelRadio panel in panels) {
|
||||
if (panel.value == value) return panel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
kElevationToShadow.containsKey(widget.elevation),
|
||||
'Invalid value for elevation. See the kElevationToShadow constant for'
|
||||
' possible elevation values.',
|
||||
);
|
||||
|
||||
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
|
||||
|
||||
for (int index = 0; index < widget.children.length; index += 1) {
|
||||
//todo: Uncomment to add gap between selected panels
|
||||
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
|
||||
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/
|
||||
|
||||
final AppExpansionPanel child = widget.children[index];
|
||||
final Widget headerWidget = child.headerBuilder(
|
||||
context,
|
||||
_isChildExpanded(index),
|
||||
);
|
||||
|
||||
Widget? expandIconContainer = ExpandIcon(
|
||||
isExpanded: _isChildExpanded(index),
|
||||
onPressed: !child.canTapOnHeader
|
||||
? (bool isExpanded) => _handlePressed(isExpanded, index)
|
||||
: null,
|
||||
);
|
||||
if (!child.canTapOnHeader) {
|
||||
final MaterialLocalizations localizations =
|
||||
MaterialLocalizations.of(context);
|
||||
expandIconContainer = Semantics(
|
||||
label: _isChildExpanded(index)
|
||||
? localizations.expandedIconTapHint
|
||||
: localizations.collapsedIconTapHint,
|
||||
container: true,
|
||||
child: expandIconContainer,
|
||||
);
|
||||
}
|
||||
|
||||
final iconContainer = child.iconBuilder;
|
||||
if (iconContainer != null) {
|
||||
expandIconContainer = iconContainer(
|
||||
expandIconContainer,
|
||||
_isChildExpanded(index),
|
||||
);
|
||||
}
|
||||
|
||||
Widget header = Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: widget.animationDuration,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
margin: _isChildExpanded(index)
|
||||
? widget.expandedHeaderPadding
|
||||
: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: _kPanelHeaderCollapsedHeight),
|
||||
child: headerWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (expandIconContainer != null) expandIconContainer,
|
||||
],
|
||||
);
|
||||
if (child.canTapOnHeader) {
|
||||
header = MergeSemantics(
|
||||
child: InkWell(
|
||||
onTap: () => _handlePressed(_isChildExpanded(index), index),
|
||||
child: header,
|
||||
),
|
||||
);
|
||||
}
|
||||
items.add(
|
||||
MaterialSlice(
|
||||
key: _SaltedKey<BuildContext, int>(context, index * 2),
|
||||
color: child.backgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
header,
|
||||
AnimatedCrossFade(
|
||||
firstChild: Container(height: 0.0),
|
||||
secondChild: child.body,
|
||||
firstCurve:
|
||||
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
|
||||
secondCurve:
|
||||
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
|
||||
sizeCurve: Curves.fastOutSlowIn,
|
||||
crossFadeState: _isChildExpanded(index)
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: widget.animationDuration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (_isChildExpanded(index) && index != widget.children.length - 1) {
|
||||
items.add(MaterialGap(
|
||||
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
return MergeableMaterial(
|
||||
hasDividers: true,
|
||||
dividerColor: widget.dividerColor,
|
||||
elevation: widget.elevation,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef ExpansionPanelIconBuilder = Widget? Function(
|
||||
Widget child,
|
||||
bool isExpanded,
|
||||
);
|
||||
|
||||
class AppExpansionPanel {
|
||||
/// Creates an expansion panel to be used as a child for [ExpansionPanelList].
|
||||
/// See [ExpansionPanelList] for an example on how to use this widget.
|
||||
///
|
||||
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
|
||||
AppExpansionPanel({
|
||||
required this.headerBuilder,
|
||||
required this.body,
|
||||
this.iconBuilder,
|
||||
this.isExpanded = false,
|
||||
this.canTapOnHeader = false,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
/// The widget builder that builds the expansion panels' header.
|
||||
final ExpansionPanelHeaderBuilder headerBuilder;
|
||||
|
||||
/// The widget builder that builds the expansion panels' icon.
|
||||
///
|
||||
/// If not pass any function, then default icon will be displayed.
|
||||
///
|
||||
/// If builder function return null, then icon will not displayed.
|
||||
final ExpansionPanelIconBuilder? iconBuilder;
|
||||
|
||||
/// The body of the expansion panel that's displayed below the header.
|
||||
///
|
||||
/// This widget is visible only when the panel is expanded.
|
||||
final Widget body;
|
||||
|
||||
/// Whether the panel is expanded.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool isExpanded;
|
||||
|
||||
/// Whether tapping on the panel's header will expand/collapse it.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool canTapOnHeader;
|
||||
|
||||
/// Defines the background color of the panel.
|
||||
///
|
||||
/// Defaults to [ThemeData.cardColor].
|
||||
final Color? backgroundColor;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
|
||||
const AppBarWidget({
|
||||
required this.child,
|
||||
required this.controller,
|
||||
required this.visible,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PreferredSizeWidget child;
|
||||
final AnimationController controller;
|
||||
final bool visible;
|
||||
|
||||
@override
|
||||
Size get preferredSize => child.preferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
visible ? controller.reverse() : controller.forward();
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0, -1),
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOutBack,
|
||||
)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||
show SourceModel;
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
|
||||
Widget articleContent({
|
||||
required BuildContext context,
|
||||
required List<ArticleContentModel> list,
|
||||
Function(List<String>, int)? callback,
|
||||
required double maxWidth,
|
||||
}) {
|
||||
debugPrint('articleContent');
|
||||
List<String>? imgList = list
|
||||
.where((item) => item.pic != null)
|
||||
.toList()
|
||||
.map((item) => item.pic?.pics?.first.url ?? '')
|
||||
.toList();
|
||||
return SliverList.separated(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
ArticleContentModel item = list[index];
|
||||
if (item.text != null) {
|
||||
List<InlineSpan> spanList = [];
|
||||
item.text?.nodes?.forEach((item) {
|
||||
spanList.add(TextSpan(
|
||||
text: item.word?.words,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.3,
|
||||
fontSize: 17,
|
||||
height: LineHeight.percent(125).size,
|
||||
fontStyle:
|
||||
item.word?.style?.italic == true ? FontStyle.italic : null,
|
||||
color: item.word?.color != null
|
||||
? Color(int.parse(
|
||||
item.word!.color!.replaceFirst('#', 'FF'),
|
||||
radix: 16,
|
||||
))
|
||||
: null,
|
||||
decoration: item.word?.style?.strikethrough == true
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
fontWeight:
|
||||
item.word?.style?.bold == true ? FontWeight.bold : null,
|
||||
),
|
||||
));
|
||||
});
|
||||
return SelectableText.rich(TextSpan(children: spanList));
|
||||
} else if (item.line != null) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: item.line?.pic?.url?.http2https ?? '',
|
||||
height: item.line?.pic?.height?.toDouble(),
|
||||
),
|
||||
);
|
||||
} else if (item.pic != null) {
|
||||
return Hero(
|
||||
tag: item.pic!.pics!.first.url!,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback(
|
||||
imgList,
|
||||
imgList.indexOf(item.pic!.pics!.first.url!),
|
||||
);
|
||||
} else {
|
||||
context.imageView(
|
||||
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
|
||||
imgList: imgList.map((url) => SourceModel(url: url)).toList(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: maxWidth,
|
||||
height: maxWidth *
|
||||
item.pic!.pics!.first.height! /
|
||||
item.pic!.pics!.first.width!,
|
||||
src: item.pic!.pics!.first.url,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
// return Text('unsupported content');
|
||||
}
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 10),
|
||||
);
|
||||
}
|
||||
@@ -1,71 +1,85 @@
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PBadge extends StatelessWidget {
|
||||
final String? text;
|
||||
|
||||
final bool isStack;
|
||||
final double? top;
|
||||
final double? right;
|
||||
final double? bottom;
|
||||
final double? left;
|
||||
final String? type;
|
||||
final String? size;
|
||||
final String? stack;
|
||||
final double? fs;
|
||||
final String? semanticsLabel;
|
||||
final bool bold;
|
||||
final double? textScaleFactor;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
final PBadgeType type;
|
||||
final PBadgeSize size;
|
||||
|
||||
final double fontSize;
|
||||
final bool isBold;
|
||||
final double? textScaleFactor;
|
||||
|
||||
const PBadge({
|
||||
super.key,
|
||||
this.text,
|
||||
required this.text,
|
||||
this.top,
|
||||
this.right,
|
||||
this.bottom,
|
||||
this.left,
|
||||
this.type = 'primary',
|
||||
this.size = 'medium',
|
||||
this.stack = 'position',
|
||||
this.fs = 11,
|
||||
this.semanticsLabel,
|
||||
this.bold = true,
|
||||
this.type = PBadgeType.primary,
|
||||
this.size = PBadgeSize.medium,
|
||||
this.isStack = true,
|
||||
this.fontSize = 11,
|
||||
this.isBold = true,
|
||||
this.textScaleFactor,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ColorScheme t = Theme.of(context).colorScheme;
|
||||
// 背景色
|
||||
Color bgColor = t.primary;
|
||||
// 前景色
|
||||
Color color = t.onPrimary;
|
||||
// 边框色
|
||||
if (text.isNullOrEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
ColorScheme theme = Theme.of(context).colorScheme;
|
||||
|
||||
Color bgColor;
|
||||
Color color;
|
||||
Color borderColor = Colors.transparent;
|
||||
if (type == 'gray') {
|
||||
bgColor = Colors.black54.withOpacity(0.4);
|
||||
color = Colors.white;
|
||||
} else if (type == 'color') {
|
||||
bgColor = t.secondaryContainer.withOpacity(0.5);
|
||||
color = t.onSecondaryContainer;
|
||||
} else if (type == 'line') {
|
||||
bgColor = Colors.transparent;
|
||||
color = t.primary;
|
||||
borderColor = t.primary;
|
||||
} else if (type == 'error') {
|
||||
bgColor = t.error;
|
||||
color = t.onError;
|
||||
|
||||
switch (type) {
|
||||
case PBadgeType.primary:
|
||||
bgColor = theme.primary;
|
||||
color = theme.onPrimary;
|
||||
case PBadgeType.secondary:
|
||||
bgColor = theme.secondaryContainer.withValues(alpha: 0.5);
|
||||
color = theme.onSecondaryContainer;
|
||||
case PBadgeType.gray:
|
||||
bgColor = Colors.black45;
|
||||
color = Colors.white;
|
||||
case PBadgeType.error:
|
||||
bgColor = theme.error;
|
||||
color = theme.onError;
|
||||
case PBadgeType.line_primary:
|
||||
color = theme.primary;
|
||||
bgColor = Colors.transparent;
|
||||
borderColor = theme.primary;
|
||||
case PBadgeType.line_secondary:
|
||||
color = theme.secondary;
|
||||
bgColor = Colors.transparent;
|
||||
borderColor = theme.secondary;
|
||||
case PBadgeType.free:
|
||||
bgColor =
|
||||
Get.isDarkMode ? const Color(0xFFD66011) : const Color(0xFFFF7F24);
|
||||
color = Colors.white;
|
||||
}
|
||||
|
||||
late EdgeInsets paddingStyle =
|
||||
const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
|
||||
double fontSize = 11;
|
||||
BorderRadius br = BorderRadius.circular(4);
|
||||
|
||||
if (size == 'small') {
|
||||
paddingStyle = const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
|
||||
fontSize = 11;
|
||||
br = BorderRadius.circular(3);
|
||||
}
|
||||
BorderRadius br = size == PBadgeSize.small
|
||||
? const BorderRadius.all(Radius.circular(3))
|
||||
: const BorderRadius.all(Radius.circular(4));
|
||||
|
||||
Widget content = Container(
|
||||
padding: padding ?? paddingStyle,
|
||||
@@ -75,26 +89,25 @@ class PBadge extends StatelessWidget {
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
child: Text(
|
||||
text ?? "",
|
||||
text!,
|
||||
textScaler: textScaleFactor != null
|
||||
? TextScaler.linear(textScaleFactor!)
|
||||
: null,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: fs ?? fontSize,
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
fontWeight: bold ? FontWeight.bold : null,
|
||||
fontWeight: isBold ? FontWeight.bold : null,
|
||||
),
|
||||
strutStyle: StrutStyle(
|
||||
leading: 0,
|
||||
height: 1,
|
||||
fontSize: fs ?? fontSize,
|
||||
fontWeight: bold ? FontWeight.bold : null,
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? FontWeight.bold : null,
|
||||
),
|
||||
semanticsLabel: semanticsLabel,
|
||||
),
|
||||
);
|
||||
if (stack == 'position') {
|
||||
if (isStack) {
|
||||
return Positioned(
|
||||
top: top,
|
||||
left: left,
|
||||
|
||||
@@ -10,6 +10,7 @@ Widget iconButton({
|
||||
Color? bgColor,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
late final theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -19,12 +20,11 @@ Widget iconButton({
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: iconSize ?? size / 2,
|
||||
color: iconColor ?? Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
color: iconColor ?? theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor:
|
||||
bgColor ?? Theme.of(context).colorScheme.secondaryContainer,
|
||||
backgroundColor: bgColor ?? theme.colorScheme.secondaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -16,6 +16,7 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
@@ -23,16 +24,14 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
highlightColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
highlightColor: theme.colorScheme.secondaryContainer,
|
||||
color: selected
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.outline,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return selected
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: null;
|
||||
return selected ? theme.colorScheme.secondaryContainer : null;
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ContentContainer extends StatelessWidget {
|
||||
final Widget? contentWidget;
|
||||
final Widget? bottomWidget;
|
||||
final bool isScrollable;
|
||||
final Clip? childClipBehavior;
|
||||
|
||||
const ContentContainer({
|
||||
super.key,
|
||||
this.contentWidget,
|
||||
this.bottomWidget,
|
||||
this.isScrollable = true,
|
||||
this.childClipBehavior,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return SingleChildScrollView(
|
||||
clipBehavior: childClipBehavior ?? Clip.hardEdge,
|
||||
physics: isScrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: constraints.copyWith(
|
||||
minHeight: constraints.maxHeight,
|
||||
maxHeight: double.infinity,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (contentWidget != null)
|
||||
Expanded(
|
||||
child: contentWidget!,
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
if (bottomWidget != null) bottomWidget!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
29
lib/common/widgets/custom_icon.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CustomIcon {
|
||||
static const IconData coin = _CustomIconData(0xe800);
|
||||
static const IconData dm_off = _CustomIconData(0xe801);
|
||||
static const IconData dm_on = _CustomIconData(0xe802);
|
||||
static const IconData dm_settings = _CustomIconData(0xe803);
|
||||
static const IconData dyn = _CustomIconData(0xe804);
|
||||
static const IconData fav = _CustomIconData(0xe805);
|
||||
static const IconData share = _CustomIconData(0xe806);
|
||||
static const IconData share_line = _CustomIconData(0xe807);
|
||||
static const IconData share_node = _CustomIconData(0xe808);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe809);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80a);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe810);
|
||||
static const IconData topic_tag = _CustomIconData(0xe811);
|
||||
static const IconData watch_later = _CustomIconData(0xe812);
|
||||
}
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon');
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomToast extends StatelessWidget {
|
||||
const CustomToast({super.key, required this.msg});
|
||||
@@ -8,6 +8,7 @@ class CustomToast extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final double toastOpacity = GStorage.setting
|
||||
.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0) as double;
|
||||
return Container(
|
||||
@@ -15,17 +16,15 @@ class CustomToast extends StatelessWidget {
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 30),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(toastOpacity),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color:
|
||||
theme.colorScheme.primaryContainer.withValues(alpha: toastOpacity),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Text(
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -40,26 +39,25 @@ class LoadingWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: theme.dialogBackgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
//loading animation
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
||||
),
|
||||
|
||||
//msg
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:get/get.dart';
|
||||
void showConfirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
String? content,
|
||||
dynamic content,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
showDialog(
|
||||
@@ -12,7 +12,11 @@ void showConfirmDialog({
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: content == null ? null : Text(content),
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
@@ -26,7 +30,7 @@ void showConfirmDialog({
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: Text('确认'),
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -85,10 +89,10 @@ void showPgcFollowDialog({
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
'取消$type',
|
||||
style: TextStyle(fontSize: 14),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
@@ -7,7 +7,8 @@ import 'package:get/get.dart';
|
||||
void autoWrapReportDialog(
|
||||
BuildContext context,
|
||||
Map<String, Map<int, String>> options,
|
||||
Future<Map> Function(int, String?, bool) onSuccess,
|
||||
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess,
|
||||
) {
|
||||
int? reasonType;
|
||||
String? reasonDesc;
|
||||
@@ -61,7 +62,11 @@ void autoWrapReportDialog(
|
||||
),
|
||||
),
|
||||
),
|
||||
BanUserCheckbox(onChanged: (value) => banUid = value),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
text: '拉黑该用户', onChanged: (value) => banUid = value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -136,52 +141,65 @@ class _ReasonFieldState extends State<ReasonField> {
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
onChanged: (value) {
|
||||
widget.onChanged(value);
|
||||
},
|
||||
onChanged: widget.onChanged,
|
||||
validator: widget._validator,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BanUserCheckbox extends StatefulWidget {
|
||||
class CheckBoxText extends StatefulWidget {
|
||||
final String text;
|
||||
final ValueChanged<bool> onChanged;
|
||||
final bool selected;
|
||||
|
||||
const BanUserCheckbox({super.key, required this.onChanged});
|
||||
const CheckBoxText({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onChanged,
|
||||
this.selected = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BanUserCheckbox> createState() => _BanUserCheckboxState();
|
||||
State<CheckBoxText> createState() => _CheckBoxTextState();
|
||||
}
|
||||
|
||||
class _BanUserCheckboxState extends State<BanUserCheckbox> {
|
||||
bool _banUid = false;
|
||||
class _CheckBoxTextState extends State<CheckBoxText> {
|
||||
late bool _selected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selected = widget.selected;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() => _banUid = !_banUid);
|
||||
widget.onChanged(_banUid);
|
||||
setState(() {
|
||||
_selected = !_selected;
|
||||
});
|
||||
widget.onChanged(_selected);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 18, top: 10),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 22,
|
||||
_banUid
|
||||
_selected
|
||||
? Icons.check_box_outlined
|
||||
: Icons.check_box_outline_blank,
|
||||
color: _banUid
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
color: _selected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
' 拉黑该用户',
|
||||
style: TextStyle(
|
||||
color: _banUid ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
' ${widget.text}',
|
||||
style: TextStyle(color: _selected ? colorScheme.primary : null),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -192,7 +210,7 @@ class _BanUserCheckboxState extends State<BanUserCheckbox> {
|
||||
|
||||
class ReportOptions {
|
||||
// from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js
|
||||
static Map<String, Map<int, String>> get commentReport => {
|
||||
static Map<String, Map<int, String>> get commentReport => const {
|
||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||
'谣言类不实信息': {19: '涉政谣言', 22: '虚假不实信息', 20: '涉社会事件谣言'},
|
||||
'侵犯个人权益': {7: '人身攻击', 15: '侵犯隐私'},
|
||||
@@ -208,7 +226,7 @@ class ReportOptions {
|
||||
'其他': {0: '其他'},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get dynamicReport => {
|
||||
static Map<String, Map<int, String>> get dynamicReport => const {
|
||||
'': {
|
||||
4: '垃圾广告',
|
||||
8: '引战',
|
||||
126
lib/common/widgets/dialog/report_member.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/http/member.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MemberReportPanel extends StatefulWidget {
|
||||
const MemberReportPanel({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.mid,
|
||||
});
|
||||
|
||||
final dynamic name;
|
||||
final dynamic mid;
|
||||
|
||||
@override
|
||||
State<MemberReportPanel> createState() => _MemberReportPanelState();
|
||||
}
|
||||
|
||||
class _MemberReportPanelState extends State<MemberReportPanel> {
|
||||
final List<bool> _reasonList = List.generate(3, (_) => false);
|
||||
final Set<int> _reason = {};
|
||||
int? _reasonV2;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'举报: ${widget.name}',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('uid: ${widget.mid}'),
|
||||
const SizedBox(height: 10),
|
||||
const Text('举报内容(必选,可多选)'),
|
||||
...List.generate(
|
||||
3,
|
||||
(index) => _checkBoxWidget(
|
||||
_reasonList[index],
|
||||
(value) {
|
||||
setState(() => _reasonList[index] = value);
|
||||
if (value) {
|
||||
_reason.add(index + 1);
|
||||
} else {
|
||||
_reason.remove(index + 1);
|
||||
}
|
||||
},
|
||||
['头像违规', '昵称违规', '签名违规'][index],
|
||||
),
|
||||
),
|
||||
const Text('举报理由(单选,非必选)'),
|
||||
...List.generate(
|
||||
5,
|
||||
(index) => RadioWidget<int>(
|
||||
value: index,
|
||||
groupValue: _reasonV2,
|
||||
onChanged: (value) {
|
||||
setState(() => _reasonV2 = value);
|
||||
},
|
||||
title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: theme.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (_reason.isEmpty) {
|
||||
SmartDialog.showToast('至少选择一项作为举报内容');
|
||||
} else {
|
||||
Get.back();
|
||||
dynamic result = await MemberHttp.reportMember(
|
||||
widget.mid,
|
||||
reason: _reason.join(','),
|
||||
reasonV2: _reasonV2 != null ? _reasonV2! + 1 : null,
|
||||
);
|
||||
if (result['msg'] is String && result['msg'].isNotEmpty) {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
} else {
|
||||
SmartDialog.showToast('举报失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _checkBoxWidget(
|
||||
bool defValue,
|
||||
ValueChanged onChanged,
|
||||
String title,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () => onChanged(!defValue),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: defValue,
|
||||
onChanged: onChanged,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
Text(title),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
101
lib/common/widgets/disabled_icon.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
|
||||
final Color? color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
const DisabledIcon({
|
||||
super.key,
|
||||
required T child,
|
||||
this.color,
|
||||
double? lineLengthScale,
|
||||
StrokeCap? strokeCap,
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderMaskedIcon(
|
||||
color ??
|
||||
(child is Icon
|
||||
? (child as Icon).color ?? IconTheme.of(context).color!
|
||||
: IconTheme.of(context).color!),
|
||||
lineLengthScale,
|
||||
strokeCap,
|
||||
);
|
||||
}
|
||||
|
||||
T enable() => child as T;
|
||||
}
|
||||
|
||||
class RenderMaskedIcon extends RenderProxyBox {
|
||||
final Color color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
RenderMaskedIcon(this.color, this.lineLengthScale, this.strokeCap);
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final strokeWidth = size.width / 12;
|
||||
|
||||
final canvas = context.canvas;
|
||||
var rect = offset & size;
|
||||
|
||||
final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4
|
||||
|
||||
// final path = Path.combine(
|
||||
// PathOperation.difference,
|
||||
// Path()..addRect(rect),
|
||||
// Path()..moveTo(rect.left, rect.top)
|
||||
// ..relativeLineTo(sqrt2Width, 0)
|
||||
// ..lineTo(rect.right, rect.bottom - sqrt2Width)
|
||||
// ..lineTo(rect.right, rect.bottom)
|
||||
// ..close(),
|
||||
// );
|
||||
|
||||
final path = Path.combine(
|
||||
PathOperation.union,
|
||||
Path() // bottom
|
||||
..moveTo(rect.left, rect.bottom)
|
||||
..lineTo(rect.left, rect.top + sqrt2Width)
|
||||
..lineTo(rect.right - sqrt2Width, rect.bottom)
|
||||
..close(),
|
||||
Path() // top
|
||||
..moveTo(rect.right, rect.top)
|
||||
..lineTo(rect.right, rect.bottom - sqrt2Width)
|
||||
..lineTo(rect.left + sqrt2Width, rect.top));
|
||||
|
||||
canvas
|
||||
..save()
|
||||
..clipPath(path, doAntiAlias: false);
|
||||
super.paint(context, offset);
|
||||
|
||||
context.canvas.restore();
|
||||
|
||||
final linePaint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = strokeCap;
|
||||
|
||||
final strokeOffset = strokeWidth * sqrt1_2 / 2;
|
||||
rect = rect
|
||||
.translate(-strokeOffset, strokeOffset)
|
||||
.deflate(size.width * lineLengthScale);
|
||||
canvas.drawLine(
|
||||
rect.topLeft,
|
||||
rect.bottomRight,
|
||||
linePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DisabledIconExt on Icon {
|
||||
DisabledIcon<Icon> disable([double? lineLengthScale]) =>
|
||||
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class DynamicSliverAppBar extends StatefulWidget {
|
||||
this.stretchTriggerOffset = 100.0,
|
||||
this.onStretchTrigger,
|
||||
this.shape,
|
||||
this.toolbarHeight = kToolbarHeight + 20,
|
||||
this.toolbarHeight = kToolbarHeight,
|
||||
this.leadingWidth,
|
||||
this.toolbarTextStyle,
|
||||
this.titleTextStyle,
|
||||
@@ -43,8 +43,10 @@ class DynamicSliverAppBar extends StatefulWidget {
|
||||
this.forceMaterialTransparency = false,
|
||||
this.clipBehavior,
|
||||
this.appBarClipper,
|
||||
this.callback,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? callback;
|
||||
final Widget? flexibleSpace;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
@@ -95,7 +97,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
|
||||
// to calculate dynamically the size for the sliver app bar
|
||||
double _height = 0;
|
||||
Orientation? _orientation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -103,13 +104,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
_updateHeight();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DynamicSliverAppBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
_updateHeight();
|
||||
}
|
||||
|
||||
void _updateHeight() {
|
||||
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
|
||||
// otherwise this will throw an error
|
||||
@@ -119,27 +113,32 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
|
||||
.size
|
||||
.height;
|
||||
widget.callback?.call(_height);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
|
||||
Orientation orientation = MediaQuery.orientationOf(context);
|
||||
if (_orientation != orientation) {
|
||||
_orientation = orientation;
|
||||
_height = 0;
|
||||
}
|
||||
if (_height == 0) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
child: widget.flexibleSpace ?? SizedBox(height: kToolbarHeight),
|
||||
child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MediaQuery.orientationOf(context);
|
||||
|
||||
return SliverAppBar(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
|
||||
179
lib/common/widgets/dynamic_sliver_appbar_medium.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
|
||||
class DynamicSliverAppBarMedium extends StatefulWidget {
|
||||
const DynamicSliverAppBarMedium({
|
||||
this.flexibleSpace,
|
||||
super.key,
|
||||
this.leading,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.bottom,
|
||||
this.elevation,
|
||||
this.scrolledUnderElevation,
|
||||
this.shadowColor,
|
||||
this.surfaceTintColor,
|
||||
this.forceElevated = false,
|
||||
this.backgroundColor,
|
||||
this.backgroundGradient,
|
||||
this.foregroundColor,
|
||||
this.iconTheme,
|
||||
this.actionsIconTheme,
|
||||
this.primary = true,
|
||||
this.centerTitle,
|
||||
this.excludeHeaderSemantics = false,
|
||||
this.titleSpacing,
|
||||
this.collapsedHeight,
|
||||
this.expandedHeight,
|
||||
this.floating = false,
|
||||
this.pinned = false,
|
||||
this.snap = false,
|
||||
this.stretch = false,
|
||||
this.stretchTriggerOffset = 100.0,
|
||||
this.onStretchTrigger,
|
||||
this.shape,
|
||||
this.toolbarHeight = kToolbarHeight,
|
||||
this.leadingWidth,
|
||||
this.toolbarTextStyle,
|
||||
this.titleTextStyle,
|
||||
this.systemOverlayStyle,
|
||||
this.forceMaterialTransparency = false,
|
||||
this.clipBehavior,
|
||||
this.appBarClipper,
|
||||
this.callback,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? callback;
|
||||
final Widget? flexibleSpace;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
final Widget? title;
|
||||
final List<Widget>? actions;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final double? elevation;
|
||||
final double? scrolledUnderElevation;
|
||||
final Color? shadowColor;
|
||||
final Color? surfaceTintColor;
|
||||
final bool forceElevated;
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// If backgroundGradient is non null, backgroundColor will be ignored
|
||||
final LinearGradient? backgroundGradient;
|
||||
final Color? foregroundColor;
|
||||
final IconThemeData? iconTheme;
|
||||
final IconThemeData? actionsIconTheme;
|
||||
final bool primary;
|
||||
final bool? centerTitle;
|
||||
final bool excludeHeaderSemantics;
|
||||
final double? titleSpacing;
|
||||
final double? expandedHeight;
|
||||
final double? collapsedHeight;
|
||||
final bool floating;
|
||||
final bool pinned;
|
||||
final ShapeBorder? shape;
|
||||
final double toolbarHeight;
|
||||
final double? leadingWidth;
|
||||
final TextStyle? toolbarTextStyle;
|
||||
final TextStyle? titleTextStyle;
|
||||
final SystemUiOverlayStyle? systemOverlayStyle;
|
||||
final bool forceMaterialTransparency;
|
||||
final Clip? clipBehavior;
|
||||
final bool snap;
|
||||
final bool stretch;
|
||||
final double stretchTriggerOffset;
|
||||
final AsyncCallback? onStretchTrigger;
|
||||
final CustomClipper<Path>? appBarClipper;
|
||||
|
||||
@override
|
||||
State<DynamicSliverAppBarMedium> createState() =>
|
||||
_DynamicSliverAppBarMediumState();
|
||||
}
|
||||
|
||||
class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
|
||||
final GlobalKey _childKey = GlobalKey();
|
||||
|
||||
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
|
||||
// to calculate dynamically the size for the sliver app bar
|
||||
double _height = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateHeight();
|
||||
}
|
||||
|
||||
void _updateHeight() {
|
||||
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
|
||||
// otherwise this will throw an error
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
if (_childKey.currentContext == null) return;
|
||||
setState(() {
|
||||
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
|
||||
.size
|
||||
.height;
|
||||
widget.callback?.call(_height);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
|
||||
if (_height == 0) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverAppBar.medium(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
title: widget.title,
|
||||
actions: widget.actions,
|
||||
bottom: widget.bottom,
|
||||
elevation: widget.elevation,
|
||||
scrolledUnderElevation: widget.scrolledUnderElevation,
|
||||
shadowColor: widget.shadowColor,
|
||||
surfaceTintColor: widget.surfaceTintColor,
|
||||
forceElevated: widget.forceElevated,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
foregroundColor: widget.foregroundColor,
|
||||
iconTheme: widget.iconTheme,
|
||||
actionsIconTheme: widget.actionsIconTheme,
|
||||
primary: widget.primary,
|
||||
centerTitle: widget.centerTitle,
|
||||
excludeHeaderSemantics: widget.excludeHeaderSemantics,
|
||||
titleSpacing: widget.titleSpacing,
|
||||
collapsedHeight: widget.collapsedHeight,
|
||||
floating: widget.floating,
|
||||
pinned: widget.pinned,
|
||||
snap: widget.snap,
|
||||
stretch: widget.stretch,
|
||||
stretchTriggerOffset: widget.stretchTriggerOffset,
|
||||
onStretchTrigger: widget.onStretchTrigger,
|
||||
shape: widget.shape,
|
||||
toolbarHeight: widget.toolbarHeight,
|
||||
expandedHeight: _height - MediaQuery.paddingOf(context).top,
|
||||
leadingWidth: widget.leadingWidth,
|
||||
toolbarTextStyle: widget.toolbarTextStyle,
|
||||
titleTextStyle: widget.titleTextStyle,
|
||||
systemOverlayStyle: widget.systemOverlayStyle,
|
||||
forceMaterialTransparency: widget.forceMaterialTransparency,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||
show SourceModel;
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
Widget htmlRender({
|
||||
required BuildContext context,
|
||||
String? htmlContent,
|
||||
int? imgCount,
|
||||
List<String>? imgList,
|
||||
required double constrainedWidth,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
debugPrint('htmlRender');
|
||||
return SelectionArea(
|
||||
child: Html(
|
||||
data: htmlContent,
|
||||
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
|
||||
extensions: [
|
||||
TagExtension(
|
||||
tagsToExtend: <String>{'img'},
|
||||
builder: (ExtensionContext extensionContext) {
|
||||
try {
|
||||
final Map<String, dynamic> attributes = extensionContext.attributes;
|
||||
final List<dynamic> key = attributes.keys.toList();
|
||||
String imgUrl = key.contains('src')
|
||||
? attributes['src'] as String
|
||||
: attributes['data-src'] as String;
|
||||
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
|
||||
final bool isEmote = imgUrl.contains('/emote/');
|
||||
final bool isMall = imgUrl.contains('/mall/');
|
||||
if (isMall) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// bool inTable =
|
||||
// extensionContext.element!.previousElementSibling == null ||
|
||||
// extensionContext.element!.nextElementSibling == null;
|
||||
// imgUrl = Utils().imageUrl(imgUrl!);
|
||||
// return CachedNetworkImage(
|
||||
// imageUrl: imgUrl,
|
||||
// width: isEmote ? 22 : null,
|
||||
// height: isEmote ? 22 : null,
|
||||
// );
|
||||
String? height = RegExp(r'max-height:(\d+)px')
|
||||
.firstMatch('${attributes['style']}')
|
||||
?.group(1);
|
||||
if (height != null) {
|
||||
return NetworkImgLayer(
|
||||
width: constrainedWidth,
|
||||
height: double.parse(height),
|
||||
src: imgUrl,
|
||||
type: 'emote',
|
||||
boxFit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
return Hero(
|
||||
tag: imgUrl,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback([imgUrl], 0);
|
||||
} else {
|
||||
context.imageView(
|
||||
imgList: [SourceModel(url: imgUrl)],
|
||||
);
|
||||
}
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: isEmote ? 22 : constrainedWidth,
|
||||
height: isEmote ? 22 : 200,
|
||||
src: imgUrl,
|
||||
ignoreHeight: !isEmote,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
style: {
|
||||
'html': Style(
|
||||
fontSize: FontSize(16),
|
||||
lineHeight: LineHeight.percent(160),
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
// 'br': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||
'a': Style(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textDecoration: TextDecoration.none,
|
||||
),
|
||||
'br': Style(
|
||||
lineHeight: LineHeight.percent(-1),
|
||||
),
|
||||
'p': Style(
|
||||
margin: Margins.only(bottom: 4),
|
||||
// margin: Margins.zero,
|
||||
),
|
||||
'span': Style(
|
||||
fontSize: FontSize.large,
|
||||
height: Height(1.8),
|
||||
),
|
||||
'div': Style(height: Height.auto()),
|
||||
'li > p': Style(
|
||||
display: Display.inline,
|
||||
),
|
||||
'li': Style(
|
||||
padding: HtmlPaddings.only(bottom: 4),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
'img': Style(margin: Margins.only(top: 4, bottom: 4)),
|
||||
'h1,h2': Style(
|
||||
fontSize: FontSize.xLarge,
|
||||
fontWeight: FontWeight.bold,
|
||||
margin: Margins.only(bottom: 8),
|
||||
),
|
||||
'h3,h4,h5': Style(
|
||||
fontSize: FontSize(16),
|
||||
fontWeight: FontWeight.bold,
|
||||
margin: Margins.only(bottom: 4),
|
||||
),
|
||||
'figcaption': Style(
|
||||
fontSize: FontSize.large,
|
||||
textAlign: TextAlign.center,
|
||||
// margin: Margins.only(top: 4),
|
||||
),
|
||||
'strong': Style(fontWeight: FontWeight.bold),
|
||||
'figure': Style(
|
||||
margin: Margins.zero,
|
||||
),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class HttpError extends StatelessWidget {
|
||||
const HttpError({
|
||||
this.isSliver = true,
|
||||
this.errMsg,
|
||||
this.callback,
|
||||
this.btnText,
|
||||
this.extraWidget,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isSliver;
|
||||
final String? errMsg;
|
||||
final Function()? callback;
|
||||
final String? btnText;
|
||||
final Widget? extraWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return isSliver
|
||||
? SliverToBoxAdapter(child: content(context))
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: content(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget content(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
SvgPicture.asset(
|
||||
"assets/images/error.svg",
|
||||
height: 200,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SelectableText(
|
||||
errMsg ?? '没有数据',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
if (extraWidget != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
extraWidget!,
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
if (callback != null) ...[
|
||||
if (extraWidget == null) const SizedBox(height: 20),
|
||||
FilledButton.tonal(
|
||||
onPressed: callback,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return Theme.of(context).colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
),
|
||||
child: Text(
|
||||
btnText ?? '点击重试',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
);
|
||||
}
|
||||
115
lib/common/widgets/image/image_save.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
void imageSaveDialog({
|
||||
required String? title,
|
||||
required String? cover,
|
||||
}) {
|
||||
final double imgWidth = Get.mediaQuery.size.shortestSide - 8 * 2;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: SmartDialog.dismiss,
|
||||
child: NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: cover,
|
||||
quality: 100,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title ?? '',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
if (cover?.isNotEmpty == true) ...[
|
||||
const SizedBox(width: 4),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '分享',
|
||||
onPressed: () {
|
||||
SmartDialog.dismiss();
|
||||
DownloadUtils.onShareImg(cover!);
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.share,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[cover!],
|
||||
);
|
||||
if (saveStatus) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.download,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,10 @@ import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||
show SourceModel, SourceType;
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/nine_grid_view.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/nine_grid_view.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -31,7 +31,7 @@ class ImageModel {
|
||||
bool get isLivePhoto => _isLivePhoto ??= liveUrl?.isNotEmpty == true;
|
||||
}
|
||||
|
||||
Widget imageview(
|
||||
Widget imageView(
|
||||
double maxWidth,
|
||||
List<ImageModel> picArr, {
|
||||
VoidCallback? onViewImage,
|
||||
@@ -61,32 +61,22 @@ Widget imageview(
|
||||
}
|
||||
final int row = picArr.length == 4 ? 2 : 3;
|
||||
return BorderRadius.only(
|
||||
topLeft: Radius.circular(
|
||||
(index - row >= 0 ||
|
||||
((index - 1) >= 0 && (index - 1) % row < index % row))
|
||||
? 0
|
||||
: 10,
|
||||
),
|
||||
topRight: Radius.circular(
|
||||
(index - row >= 0 ||
|
||||
((index + 1) < picArr.length &&
|
||||
(index + 1) % row > index % row))
|
||||
? 0
|
||||
: 10,
|
||||
),
|
||||
bottomLeft: Radius.circular(
|
||||
(index + row < picArr.length ||
|
||||
((index - 1) >= 0 && (index - 1) % row < index % row))
|
||||
? 0
|
||||
: 10,
|
||||
),
|
||||
bottomRight: Radius.circular(
|
||||
(index + row < picArr.length ||
|
||||
((index + 1) < picArr.length &&
|
||||
(index + 1) % row > index % row))
|
||||
? 0
|
||||
: 10,
|
||||
),
|
||||
topLeft: index - row >= 0 ||
|
||||
((index - 1) >= 0 && (index - 1) % row < index % row)
|
||||
? Radius.zero
|
||||
: StyleString.imgRadius,
|
||||
topRight: index - row >= 0 ||
|
||||
((index + 1) < picArr.length && (index + 1) % row > index % row)
|
||||
? Radius.zero
|
||||
: StyleString.imgRadius,
|
||||
bottomLeft: index + row < picArr.length ||
|
||||
((index - 1) >= 0 && (index - 1) % row < index % row)
|
||||
? Radius.zero
|
||||
: StyleString.imgRadius,
|
||||
bottomRight: index + row < picArr.length ||
|
||||
((index + 1) < picArr.length && (index + 1) % row > index % row)
|
||||
? Radius.zero
|
||||
: StyleString.imgRadius,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,6 +91,31 @@ Widget imageview(
|
||||
};
|
||||
}
|
||||
|
||||
void onTap(BuildContext context, int index) {
|
||||
if (callback != null) {
|
||||
callback(picArr.map((item) => item.url).toList(), index);
|
||||
} else {
|
||||
onViewImage?.call();
|
||||
context.imageView(
|
||||
initialPage: index,
|
||||
imgList: picArr.map(
|
||||
(item) {
|
||||
bool isLive = item.isLivePhoto && enableLivePhoto;
|
||||
return SourceModel(
|
||||
sourceType:
|
||||
isLive ? SourceType.livePhoto : SourceType.networkImage,
|
||||
url: item.url,
|
||||
liveUrl: isLive ? item.liveUrl : null,
|
||||
width: isLive ? parseSize(item.width) : null,
|
||||
height: isLive ? parseSize(item.height) : null,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
onDismissed: onDismissed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NineGridView(
|
||||
type: NineGridType.weiBo,
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
@@ -113,31 +128,9 @@ Widget imageview(
|
||||
itemBuilder: (context, index) => Hero(
|
||||
tag: picArr[index].url,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback(picArr.map((item) => item.url).toList(), index);
|
||||
} else {
|
||||
onViewImage?.call();
|
||||
context.imageView(
|
||||
initialPage: index,
|
||||
imgList: picArr.map(
|
||||
(item) {
|
||||
bool isLive = item.isLivePhoto && enableLivePhoto;
|
||||
return SourceModel(
|
||||
sourceType:
|
||||
isLive ? SourceType.livePhoto : SourceType.networkImage,
|
||||
url: item.url,
|
||||
liveUrl: isLive ? item.liveUrl : null,
|
||||
width: isLive ? parseSize(item.width) : null,
|
||||
height: isLive ? parseSize(item.height) : null,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
onDismissed: onDismissed,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: () => onTap(context, index),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
@@ -158,7 +151,7 @@ Widget imageview(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface
|
||||
.withOpacity(0.4),
|
||||
.withValues(alpha: 0.4),
|
||||
borderRadius: borderRadius(index),
|
||||
),
|
||||
child: Center(
|
||||
@@ -178,7 +171,7 @@ Widget imageview(
|
||||
text: 'Live',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
type: 'gray',
|
||||
type: PBadgeType.gray,
|
||||
)
|
||||
else if (picArr[index].isLongPic)
|
||||
const PBadge(
|
||||
@@ -1,22 +1,22 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
class NetworkImgLayer extends StatelessWidget {
|
||||
const NetworkImgLayer({
|
||||
super.key,
|
||||
this.src,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.type,
|
||||
this.height,
|
||||
this.type = ImageType.def,
|
||||
this.fadeOutDuration,
|
||||
this.fadeInDuration,
|
||||
// 图片质量 默认1%
|
||||
this.quality,
|
||||
this.semanticsLabel,
|
||||
this.ignoreHeight,
|
||||
this.radius,
|
||||
this.imageBuilder,
|
||||
this.isLongPic,
|
||||
@@ -27,13 +27,12 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
|
||||
final String? src;
|
||||
final double width;
|
||||
final double height;
|
||||
final String? type;
|
||||
final double? height;
|
||||
final ImageType type;
|
||||
final Duration? fadeOutDuration;
|
||||
final Duration? fadeInDuration;
|
||||
final int? quality;
|
||||
final String? semanticsLabel;
|
||||
final bool? ignoreHeight;
|
||||
final double? radius;
|
||||
final ImageWidgetBuilder? imageBuilder;
|
||||
final Function? isLongPic;
|
||||
@@ -44,14 +43,14 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return src.isNullOrEmpty.not
|
||||
? type == 'avatar'
|
||||
? type == ImageType.avatar
|
||||
? ClipOval(child: _buildImage(context))
|
||||
: radius == 0 || type == 'emote'
|
||||
: radius == 0 || type == ImageType.emote
|
||||
? _buildImage(context)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
radius ?? StyleString.imgRadius.x,
|
||||
),
|
||||
borderRadius: radius != null
|
||||
? BorderRadius.circular(radius!)
|
||||
: StyleString.mdRadius,
|
||||
child: _buildImage(context),
|
||||
)
|
||||
: getPlaceHolder?.call() ?? placeholder(context);
|
||||
@@ -59,7 +58,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
|
||||
Widget _buildImage(context) {
|
||||
int? memCacheWidth, memCacheHeight;
|
||||
if (callback?.call() == true || width <= height) {
|
||||
if (height == null || callback?.call() == true || width <= height!) {
|
||||
memCacheWidth = width.cacheSize(context);
|
||||
} else {
|
||||
memCacheHeight = height.cacheSize(context);
|
||||
@@ -67,7 +66,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: Utils.thumbnailImgUrl(src, quality),
|
||||
width: width,
|
||||
height: ignoreHeight == null || ignoreHeight == false ? height : null,
|
||||
height: height,
|
||||
memCacheWidth: memCacheWidth,
|
||||
memCacheHeight: memCacheHeight,
|
||||
fit: boxFit ?? BoxFit.cover,
|
||||
@@ -83,33 +82,33 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget placeholder(BuildContext context) {
|
||||
int cacheWidth = width.cacheSize(context);
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
shape: type == 'avatar' ? BoxShape.circle : BoxShape.rectangle,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
|
||||
borderRadius: type == 'avatar' || type == 'emote' || radius == 0
|
||||
? null
|
||||
: BorderRadius.circular(
|
||||
radius ?? StyleString.imgRadius.x,
|
||||
),
|
||||
shape: type == ImageType.avatar ? BoxShape.circle : BoxShape.rectangle,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface
|
||||
.withValues(alpha: 0.4),
|
||||
borderRadius:
|
||||
type == ImageType.avatar || type == ImageType.emote || radius == 0
|
||||
? null
|
||||
: radius != null
|
||||
? BorderRadius.circular(radius!)
|
||||
: StyleString.mdRadius,
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
type == ImageType.avatar
|
||||
? 'assets/images/noface.jpeg'
|
||||
: 'assets/images/loading.png',
|
||||
width: width,
|
||||
height: height,
|
||||
cacheWidth: width.cacheSize(context),
|
||||
),
|
||||
),
|
||||
child: type == 'bg'
|
||||
? const SizedBox()
|
||||
: Center(
|
||||
child: Image.asset(
|
||||
type == 'avatar'
|
||||
? 'assets/images/noface.jpeg'
|
||||
: 'assets/images/loading.png',
|
||||
width: width,
|
||||
height: height,
|
||||
cacheWidth: cacheWidth == 0 ? null : cacheWidth,
|
||||
// cacheHeight: height.cacheSize(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/**
|
||||
* @Author: Sky24n
|
||||
* @GitHub: https://github.com/Sky24n
|
||||
@@ -172,6 +172,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
)));
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: list,
|
||||
);
|
||||
}
|
||||
@@ -260,6 +261,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
)));
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: list,
|
||||
);
|
||||
}
|
||||
@@ -286,6 +288,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
}
|
||||
return ClipOval(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
@@ -372,7 +375,10 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
return Stack(children: children);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
/// double is zero.
|
||||
@@ -480,7 +486,7 @@ class _ImageUtil {
|
||||
}
|
||||
},
|
||||
);
|
||||
imageStream = image.image.resolve(const ImageConfiguration());
|
||||
imageStream = image.image.resolve(ImageConfiguration.empty);
|
||||
imageStream.addListener(listener);
|
||||
return completer.future;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
void imageSaveDialog({
|
||||
required BuildContext context,
|
||||
required String? title,
|
||||
required String? cover,
|
||||
}) {
|
||||
final double imgWidth = min(Get.width, Get.height) - 8 * 2;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (_) => Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: SmartDialog.dismiss,
|
||||
child: NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: cover,
|
||||
quality: 100,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title ?? '',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '分享',
|
||||
onPressed: () {
|
||||
DownloadUtils.onShareImg(cover ?? '');
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.share,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[cover ?? ''],
|
||||
);
|
||||
// 保存成功,自动关闭弹窗
|
||||
if (saveStatus) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.download,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -771,14 +771,16 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
widget.onInteractionStart?.call(details);
|
||||
|
||||
if (_controller.isAnimating) {
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
_controller
|
||||
..stop()
|
||||
..reset();
|
||||
_animation?.removeListener(_onAnimate);
|
||||
_animation = null;
|
||||
}
|
||||
if (_scaleController.isAnimating) {
|
||||
_scaleController.stop();
|
||||
_scaleController.reset();
|
||||
_scaleController
|
||||
..stop()
|
||||
..reset();
|
||||
_scaleAnimation?.removeListener(_onScaleAnimate);
|
||||
_scaleAnimation = null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'interactive_viewer.dart' as custom;
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
|
||||
as custom;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
|
||||
as custom;
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
@@ -11,8 +15,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'interactive_viewer_boundary.dart';
|
||||
import 'interactive_viewer.dart' as custom;
|
||||
|
||||
/// https://github.com/qq326646683/interactiveviewer_gallery
|
||||
|
||||
@@ -31,24 +33,6 @@ typedef IndexedFocusedWidgetBuilder = Widget Function(
|
||||
|
||||
typedef IndexedTagStringBuilder = String Function(int index);
|
||||
|
||||
enum SourceType { fileImage, networkImage, livePhoto }
|
||||
|
||||
class SourceModel {
|
||||
final SourceType sourceType;
|
||||
final String url;
|
||||
final String? liveUrl;
|
||||
final int? width;
|
||||
final int? height;
|
||||
|
||||
const SourceModel({
|
||||
this.sourceType = SourceType.networkImage,
|
||||
required this.url,
|
||||
this.liveUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
}
|
||||
|
||||
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
const InteractiveviewerGallery({
|
||||
super.key,
|
||||
@@ -61,8 +45,11 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
this.onDismissed,
|
||||
this.setStatusBar,
|
||||
this.onClose,
|
||||
required this.quality,
|
||||
});
|
||||
|
||||
final int quality;
|
||||
|
||||
final ValueChanged? onClose;
|
||||
|
||||
final bool? setStatusBar;
|
||||
@@ -126,8 +113,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
setStatusBar();
|
||||
}
|
||||
|
||||
if (widget.sources[currentIndex.value].sourceType == SourceType.livePhoto) {
|
||||
_onPlay(currentIndex.value);
|
||||
var item = widget.sources[currentIndex.value];
|
||||
if (item.sourceType == SourceType.livePhoto) {
|
||||
_onPlay(item.liveUrl!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +124,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
}
|
||||
|
||||
SystemUiMode? mode;
|
||||
setStatusBar() async {
|
||||
Future<void> setStatusBar() async {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.immersiveSticky,
|
||||
@@ -153,8 +141,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
widget.onClose?.call(true);
|
||||
_player?.dispose();
|
||||
_pageController?.dispose();
|
||||
_animationController.removeListener(listener);
|
||||
_animationController.dispose();
|
||||
_animationController
|
||||
..removeListener(listener)
|
||||
..dispose();
|
||||
if (widget.setStatusBar != false) {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
@@ -163,9 +152,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < widget.sources.length; index++) {
|
||||
if (widget.sources[index].sourceType == SourceType.networkImage) {
|
||||
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
||||
for (var item in widget.sources) {
|
||||
if (item.sourceType == SourceType.networkImage) {
|
||||
CachedNetworkImageProvider(_getActualUrl(item.url)).evict();
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
@@ -224,10 +213,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlay(int index) {
|
||||
void _onPlay(String liveUrl) {
|
||||
_player ??= Player();
|
||||
_videoController ??= VideoController(_player!);
|
||||
_player!.open(Media(widget.sources[index].liveUrl!));
|
||||
_player!.open(Media(liveUrl));
|
||||
}
|
||||
|
||||
/// When the page view changed its page, the source will animate back into the
|
||||
@@ -237,8 +226,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
void _onPageChanged(int page) {
|
||||
_player?.pause();
|
||||
currentIndex.value = page;
|
||||
if (widget.sources[page].sourceType == SourceType.livePhoto) {
|
||||
_onPlay(page);
|
||||
var item = widget.sources[page];
|
||||
if (item.sourceType == SourceType.livePhoto) {
|
||||
_onPlay(item.liveUrl!);
|
||||
}
|
||||
widget.onPageChanged?.call(page);
|
||||
if (_transformationController!.value != Matrix4.identity()) {
|
||||
@@ -255,10 +245,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
}
|
||||
}
|
||||
|
||||
String _getActualUrl(int index) {
|
||||
String _getActualUrl(String url) {
|
||||
return _quality != 100
|
||||
? Utils.thumbnailImgUrl(widget.sources[index].url, _quality)
|
||||
: widget.sources[index].url.http2https;
|
||||
? Utils.thumbnailImgUrl(url, _quality)
|
||||
: url.http2https;
|
||||
}
|
||||
|
||||
void onClose() {
|
||||
@@ -276,6 +266,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
InteractiveViewerBoundary(
|
||||
controller: _transformationController,
|
||||
@@ -301,6 +292,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
_enablePageView ? null : const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.sources.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = widget.sources[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onClose,
|
||||
@@ -308,10 +300,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
_doubleTapLocalPosition = details.localPosition;
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress:
|
||||
widget.sources[index].sourceType == SourceType.fileImage
|
||||
? null
|
||||
: onLongPress,
|
||||
onLongPress: item.sourceType == SourceType.fileImage
|
||||
? null
|
||||
: () => onLongPress(item),
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
context,
|
||||
@@ -319,7 +310,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
index == currentIndex.value,
|
||||
_enablePageView,
|
||||
)
|
||||
: _itemBuilder(index),
|
||||
: _itemBuilder(index, item),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -329,12 +320,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
8,
|
||||
20,
|
||||
MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
padding: MediaQuery.paddingOf(context) +
|
||||
const EdgeInsets.fromLTRB(12, 8, 20, 8),
|
||||
decoration: _enablePageView
|
||||
? BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -342,12 +329,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.3)
|
||||
Colors.black.withValues(alpha: 0.3)
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
@@ -373,53 +361,40 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
alignment: Alignment.centerRight,
|
||||
child: PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
final item = widget.sources[currentIndex.value];
|
||||
return [
|
||||
PopupMenuItem(
|
||||
onTap: () => DownloadUtils.onShareImg(
|
||||
widget.sources[currentIndex.value].url),
|
||||
onTap: () => DownloadUtils.onShareImg(item.url),
|
||||
child: const Text("分享图片"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
Utils.copyText(
|
||||
widget.sources[currentIndex.value].url);
|
||||
},
|
||||
onTap: () => Utils.copyText(item.url),
|
||||
child: const Text("复制链接"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex.value].url],
|
||||
);
|
||||
},
|
||||
onTap: () => DownloadUtils.downloadImg(
|
||||
this.context,
|
||||
[item.url],
|
||||
),
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources
|
||||
.map((item) => item.url)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
onTap: () => DownloadUtils.downloadImg(
|
||||
this.context,
|
||||
widget.sources.map((item) => item.url).toList(),
|
||||
),
|
||||
child: const Text("保存全部"),
|
||||
),
|
||||
if (widget.sources[currentIndex.value].sourceType ==
|
||||
SourceType.livePhoto)
|
||||
if (item.sourceType == SourceType.livePhoto)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
DownloadUtils.downloadLivePhoto(
|
||||
context: context,
|
||||
url: widget.sources[currentIndex.value].url,
|
||||
liveUrl: widget
|
||||
.sources[currentIndex.value].liveUrl!,
|
||||
width:
|
||||
widget.sources[currentIndex.value].width!,
|
||||
height: widget
|
||||
.sources[currentIndex.value].height!,
|
||||
context: this.context,
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width!,
|
||||
height: item.height!,
|
||||
);
|
||||
},
|
||||
child: const Text("保存 Live Photo"),
|
||||
@@ -437,44 +412,27 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(index) {
|
||||
Widget _itemBuilder(int index, SourceModel item) {
|
||||
return Center(
|
||||
child: Hero(
|
||||
tag: widget.sources[index].url,
|
||||
child: switch (widget.sources[index].sourceType) {
|
||||
tag: item.url,
|
||||
child: switch (item.sourceType) {
|
||||
SourceType.fileImage => Image(
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(widget.sources[index].url)),
|
||||
image: FileImage(File(item.url)),
|
||||
),
|
||||
SourceType.networkImage => CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: _getActualUrl(index),
|
||||
imageUrl: _getActualUrl(item.url),
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
placeholder: (context, url) {
|
||||
return CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: Utils.thumbnailImgUrl(widget.sources[index].url),
|
||||
imageUrl: Utils.thumbnailImgUrl(item.url, widget.quality),
|
||||
);
|
||||
},
|
||||
// fit: BoxFit.contain,
|
||||
// progressIndicatorBuilder: (context, url, progress) {
|
||||
// return Center(
|
||||
// child: SizedBox(
|
||||
// width: 150.0,
|
||||
// child:
|
||||
// LinearProgressIndicator(value: progress.progress ?? 0),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// errorListener: (value) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// setState(() {
|
||||
// _thumbList[index] = false;
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
),
|
||||
SourceType.livePhoto => Obx(() => currentIndex.value == index
|
||||
? IgnorePointer(
|
||||
@@ -489,7 +447,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
);
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
void onDoubleTap() {
|
||||
Matrix4 matrix = _transformationController!.value.clone();
|
||||
double currentScale = matrix.row0.x;
|
||||
|
||||
@@ -536,7 +494,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
.whenComplete(() => _onScaleChanged(targetScale));
|
||||
}
|
||||
|
||||
onLongPress() {
|
||||
void onLongPress(SourceModel item) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@@ -548,8 +506,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
DownloadUtils.onShareImg(
|
||||
widget.sources[currentIndex.value].url);
|
||||
DownloadUtils.onShareImg(item.url);
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
@@ -558,7 +515,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
Utils.copyText(widget.sources[currentIndex.value].url);
|
||||
Utils.copyText(item.url);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
||||
@@ -567,8 +524,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex.value].url],
|
||||
this.context,
|
||||
[item.url],
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
@@ -579,24 +536,23 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
this.context,
|
||||
widget.sources.map((item) => item.url).toList(),
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (widget.sources[currentIndex.value].sourceType ==
|
||||
SourceType.livePhoto)
|
||||
if (item.sourceType == SourceType.livePhoto)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadLivePhoto(
|
||||
context: context,
|
||||
url: widget.sources[currentIndex.value].url,
|
||||
liveUrl: widget.sources[currentIndex.value].liveUrl!,
|
||||
width: widget.sources[currentIndex.value].width!,
|
||||
height: widget.sources[currentIndex.value].height!,
|
||||
context: this.context,
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width!,
|
||||
height: item.height!,
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
class LiveCard extends StatelessWidget {
|
||||
final dynamic liveItem;
|
||||
|
||||
const LiveCard({
|
||||
super.key,
|
||||
required this.liveItem,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String heroTag = Utils.makeHeroTag(liveItem.roomid);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.08),
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder:
|
||||
(BuildContext context, BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: liveItem.cover as String,
|
||||
type: 'emote',
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedOpacity(
|
||||
opacity: 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: liveStat(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
liveContent(context)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget liveContent(context) {
|
||||
return Padding(
|
||||
// 多列
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 6, 7),
|
||||
// 单列
|
||||
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
liveItem.title as String,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
liveItem.uname as String,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget liveStat(context) {
|
||||
return Container(
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(top: 22, left: 8, right: 8),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black54,
|
||||
],
|
||||
tileMode: TileMode.mirror,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
// Row(
|
||||
// children: [
|
||||
// StatView(
|
||||
// theme: 'white',
|
||||
// view: view,
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
// StatDanMu(
|
||||
// theme: 'white',
|
||||
// danmu: danmaku,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
Text(
|
||||
liveItem.online.toString(),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/http_error.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget get loadingWidget => Center(child: CircularProgressIndicator());
|
||||
|
||||
Widget errorWidget({errMsg, callback}) => HttpError(
|
||||
isSliver: false,
|
||||
errMsg: errMsg,
|
||||
callback: callback,
|
||||
);
|
||||
|
||||
Widget scrollErrorWidget({errMsg, callback}) => CustomScrollView(
|
||||
controller: ScrollController(),
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: errMsg,
|
||||
callback: callback,
|
||||
)
|
||||
],
|
||||
);
|
||||
67
lib/common/widgets/loading_widget/http_error.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class HttpError extends StatelessWidget {
|
||||
const HttpError({
|
||||
this.isSliver = true,
|
||||
this.errMsg,
|
||||
this.onReload,
|
||||
this.btnText,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isSliver;
|
||||
final String? errMsg;
|
||||
final VoidCallback? onReload;
|
||||
final String? btnText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return isSliver
|
||||
? SliverToBoxAdapter(child: content(context))
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: content(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget content(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
SvgPicture.asset(
|
||||
"assets/images/error.svg",
|
||||
height: 200,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
child: SelectableText(
|
||||
errMsg ?? '没有数据',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleSmall,
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
),
|
||||
),
|
||||
if (onReload != null)
|
||||
FilledButton.tonal(
|
||||
onPressed: onReload,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return theme.colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
),
|
||||
child: Text(
|
||||
btnText ?? '点击重试',
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/common/widgets/loading_widget/loading_widget.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget get loadingWidget => const Center(child: CircularProgressIndicator());
|
||||
|
||||
Widget errorWidget({errMsg, onReload}) => HttpError(
|
||||
isSliver: false,
|
||||
errMsg: errMsg,
|
||||
onReload: onReload,
|
||||
);
|
||||
|
||||
Widget scrollErrorWidget({errMsg, onReload, controller}) => CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: errMsg,
|
||||
onReload: onReload,
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NoSplashFactory extends InteractiveInkFeatureFactory {
|
||||
@override
|
||||
InteractiveInkFeature create(
|
||||
{required MaterialInkController controller,
|
||||
required RenderBox referenceBox,
|
||||
required Offset position,
|
||||
required Color color,
|
||||
required TextDirection textDirection,
|
||||
bool containedInkWell = false,
|
||||
RectCallback? rectCallback,
|
||||
BorderRadius? borderRadius,
|
||||
ShapeBorder? customBorder,
|
||||
double? radius,
|
||||
VoidCallback? onRemoved}) {
|
||||
return _NoInteractiveInkFeature(
|
||||
controller: controller,
|
||||
referenceBox: referenceBox,
|
||||
color: color,
|
||||
onRemoved: onRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoInteractiveInkFeature extends InteractiveInkFeature {
|
||||
@override
|
||||
void paintFeature(Canvas canvas, Matrix4 transform) {}
|
||||
_NoInteractiveInkFeature({
|
||||
required super.controller,
|
||||
required super.referenceBox,
|
||||
required super.color,
|
||||
super.onRemoved,
|
||||
});
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../utils/download.dart';
|
||||
import '../constants.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
class OverlayPop extends StatelessWidget {
|
||||
const OverlayPop({super.key, this.videoItem, this.closeFn});
|
||||
|
||||
final dynamic videoItem;
|
||||
final Function? closeFn;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double imgWidth = min(Get.height, Get.width) - 8 * 2;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
width: imgWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: videoItem is card.Card
|
||||
? (videoItem as card.Card).smallCoverV5.base.cover
|
||||
: videoItem.pic,
|
||||
quality: 100,
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20))),
|
||||
child: IconButton(
|
||||
tooltip: '关闭',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => closeFn?.call(),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
videoItem is card.Card
|
||||
? (videoItem as card.Card).smallCoverV5.base.title
|
||||
: videoItem.title,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[
|
||||
videoItem is card.Card
|
||||
? (videoItem as card.Card).smallCoverV5.base.cover
|
||||
: videoItem.pic ?? videoItem.cover
|
||||
],
|
||||
);
|
||||
closeFn?.call();
|
||||
},
|
||||
icon: const Icon(Icons.download, size: 20),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
444
lib/common/widgets/page/page_view.dart
Normal file
@@ -0,0 +1,444 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
///
|
||||
/// @docImport 'single_child_scroll_view.dart';
|
||||
/// @docImport 'text.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class _ForceImplicitScrollPhysics extends ScrollPhysics {
|
||||
const _ForceImplicitScrollPhysics(
|
||||
{required this.allowImplicitScrolling, super.parent});
|
||||
|
||||
@override
|
||||
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return _ForceImplicitScrollPhysics(
|
||||
allowImplicitScrolling: allowImplicitScrolling,
|
||||
parent: buildParent(ancestor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final bool allowImplicitScrolling;
|
||||
}
|
||||
|
||||
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
|
||||
|
||||
/// A scrollable list that works page by page.
|
||||
///
|
||||
/// Each child of a page view is forced to be the same size as the viewport.
|
||||
///
|
||||
/// You can use a [PageController] to control which page is visible in the view.
|
||||
/// In addition to being able to control the pixel offset of the content inside
|
||||
/// the [CustomPageView], a [PageController] also lets you control the offset in terms
|
||||
/// of pages, which are increments of the viewport size.
|
||||
///
|
||||
/// The [PageController] can also be used to control the
|
||||
/// [PageController.initialPage], which determines which page is shown when the
|
||||
/// [CustomPageView] is first constructed, and the [PageController.viewportFraction],
|
||||
/// which determines the size of the pages as a fraction of the viewport size.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Here is an example of [CustomPageView]. It creates a centered [Text] in each of the three pages
|
||||
/// which scroll horizontally.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Persisting the scroll position during a session
|
||||
///
|
||||
/// Scroll views attempt to persist their scroll position using [PageStorage].
|
||||
/// For a [CustomPageView], this can be disabled by setting [PageController.keepPage]
|
||||
/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
|
||||
/// the [key] of this widget is recommended to help disambiguate different
|
||||
/// scroll views from each other.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [PageController], which controls which page is visible in the view.
|
||||
/// * [SingleChildScrollView], when you need to make a single child scrollable.
|
||||
/// * [ListView], for a scrollable list of boxes.
|
||||
/// * [GridView], for a scrollable grid of boxes.
|
||||
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
|
||||
/// the scroll position without using a [ScrollController].
|
||||
class CustomPageView extends StatefulWidget {
|
||||
/// Creates a scrollable list that works page by page from an explicit [List]
|
||||
/// of widgets.
|
||||
///
|
||||
/// This constructor is appropriate for page views with a small number of
|
||||
/// children because constructing the [List] requires doing work for every
|
||||
/// child that could possibly be displayed in the page view, instead of just
|
||||
/// those children that are actually visible.
|
||||
///
|
||||
/// Like other widgets in the framework, this widget expects that
|
||||
/// the [children] list will not be mutated after it has been passed in here.
|
||||
/// See the documentation at [SliverChildListDelegate.children] for more details.
|
||||
///
|
||||
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
|
||||
/// If [allowImplicitScrolling] is true, the [CustomPageView] will participate in
|
||||
/// accessibility scrolling more like a [ListView], where implicit scroll
|
||||
/// actions will move to the next page rather than into the contents of the
|
||||
/// [CustomPageView].
|
||||
/// {@endtemplate}
|
||||
CustomPageView({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
List<Widget> children = const <Widget>[],
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
}) : childrenDelegate = SliverChildListDelegate(children);
|
||||
|
||||
final Widget? header;
|
||||
final Color bgColor;
|
||||
|
||||
/// Creates a scrollable list that works page by page using widgets that are
|
||||
/// created on demand.
|
||||
///
|
||||
/// This constructor is appropriate for page views with a large (or infinite)
|
||||
/// number of children because the builder is called only for those children
|
||||
/// that are actually visible.
|
||||
///
|
||||
/// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum
|
||||
/// scroll extent.
|
||||
///
|
||||
/// [itemBuilder] will be called only with indices greater than or equal to
|
||||
/// zero and less than [itemCount].
|
||||
///
|
||||
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
|
||||
///
|
||||
/// {@template flutter.widgets.PageView.findChildIndexCallback}
|
||||
/// The [findChildIndexCallback] corresponds to the
|
||||
/// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
|
||||
/// a child widget may not map to its existing [RenderObject] when the order
|
||||
/// of children returned from the children builder changes.
|
||||
/// This may result in state-loss. This callback needs to be implemented if
|
||||
/// the order of the children may change at a later time.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
CustomPageView.builder({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
required NullableIndexedWidgetBuilder itemBuilder,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
int? itemCount,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
}) : childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: itemCount,
|
||||
);
|
||||
|
||||
/// Creates a scrollable list that works page by page with a custom child
|
||||
/// model.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a [CustomPageView] that uses a custom [SliverChildBuilderDelegate] to support child
|
||||
/// reordering.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
const CustomPageView.custom({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
required this.childrenDelegate,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
});
|
||||
|
||||
/// Controls whether the widget's pages will respond to
|
||||
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
|
||||
/// scrolling.
|
||||
///
|
||||
/// With this flag set to false, when accessibility focus reaches the end of
|
||||
/// the current page and the user attempts to move it to the next element, the
|
||||
/// focus will traverse to the next widget outside of the page view.
|
||||
///
|
||||
/// With this flag set to true, when accessibility focus reaches the end of
|
||||
/// the current page and user attempts to move it to the next element, focus
|
||||
/// will traverse to the next page in the page view.
|
||||
final bool allowImplicitScrolling;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||
final String? restorationId;
|
||||
|
||||
/// The [Axis] along which the scroll view's offset increases with each page.
|
||||
///
|
||||
/// For the direction in which active scrolling may be occurring, see
|
||||
/// [ScrollDirection].
|
||||
///
|
||||
/// Defaults to [Axis.horizontal].
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// Whether the page view scrolls in the reading direction.
|
||||
///
|
||||
/// For example, if the reading direction is left-to-right and
|
||||
/// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
|
||||
/// left to right when [reverse] is false and from right to left when
|
||||
/// [reverse] is true.
|
||||
///
|
||||
/// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
|
||||
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||
/// when [reverse] is true.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool reverse;
|
||||
|
||||
/// An object that can be used to control the position to which this page
|
||||
/// view is scrolled.
|
||||
final PageController? controller;
|
||||
|
||||
/// How the page view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the page view continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||
/// [physics].
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// Set to false to disable page snapping, useful for custom scroll behavior.
|
||||
///
|
||||
/// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
|
||||
/// the page will snap to the beginning of the viewport; otherwise, the page
|
||||
/// will snap to the center of the viewport.
|
||||
final bool pageSnapping;
|
||||
|
||||
/// Called whenever the page in the center of the viewport changes.
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
|
||||
/// A delegate that provides the children for the [CustomPageView].
|
||||
///
|
||||
/// The [PageView.custom] constructor lets you specify this delegate
|
||||
/// explicitly. The [CustomPageView] and [PageView.builder] constructors create a
|
||||
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
|
||||
/// respectively.
|
||||
final SliverChildDelegate childrenDelegate;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.hitTestBehavior}
|
||||
///
|
||||
/// Defaults to [HitTestBehavior.opaque].
|
||||
final HitTestBehavior hitTestBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.scrollBehavior}
|
||||
///
|
||||
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
|
||||
/// modified by default to not apply a [Scrollbar].
|
||||
final ScrollBehavior? scrollBehavior;
|
||||
|
||||
/// Whether to add padding to both ends of the list.
|
||||
///
|
||||
/// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
|
||||
/// such that the first and last child slivers will be in the center of
|
||||
/// the viewport when scrolled all the way to the start or end, respectively.
|
||||
///
|
||||
/// If [PageController.viewportFraction] >= 1.0, this property has no effect.
|
||||
///
|
||||
/// This property defaults to true.
|
||||
final bool padEnds;
|
||||
|
||||
@override
|
||||
State<CustomPageView> createState() => _CustomPageViewState();
|
||||
}
|
||||
|
||||
class _CustomPageViewState extends State<CustomPageView> {
|
||||
int _lastReportedPage = 0;
|
||||
|
||||
late PageController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initController();
|
||||
_lastReportedPage = _controller.initialPage;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.controller == null) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initController() {
|
||||
_controller = widget.controller ?? PageController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomPageView oldWidget) {
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
if (oldWidget.controller == null) {
|
||||
_controller.dispose();
|
||||
}
|
||||
_initController();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
AxisDirection _getDirection(BuildContext context) {
|
||||
switch (widget.scrollDirection) {
|
||||
case Axis.horizontal:
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
final AxisDirection axisDirection =
|
||||
textDirectionToAxisDirection(textDirection);
|
||||
return widget.reverse
|
||||
? flipAxisDirection(axisDirection)
|
||||
: axisDirection;
|
||||
case Axis.vertical:
|
||||
return widget.reverse ? AxisDirection.up : AxisDirection.down;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AxisDirection axisDirection = _getDirection(context);
|
||||
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
|
||||
allowImplicitScrolling: widget.allowImplicitScrolling,
|
||||
).applyTo(
|
||||
widget.pageSnapping
|
||||
? _kPagePhysics.applyTo(
|
||||
widget.physics ??
|
||||
widget.scrollBehavior?.getScrollPhysics(context),
|
||||
)
|
||||
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context),
|
||||
);
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
if (notification.depth == 0 &&
|
||||
widget.onPageChanged != null &&
|
||||
notification is ScrollUpdateNotification) {
|
||||
final PageMetrics metrics = notification.metrics as PageMetrics;
|
||||
final int currentPage = metrics.page!.round();
|
||||
if (currentPage != _lastReportedPage) {
|
||||
_lastReportedPage = currentPage;
|
||||
widget.onPageChanged!(currentPage);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: CustomScrollable(
|
||||
header: widget.header,
|
||||
bgColor: widget.bgColor,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
axisDirection: axisDirection,
|
||||
controller: _controller,
|
||||
physics: physics,
|
||||
restorationId: widget.restorationId,
|
||||
hitTestBehavior: widget.hitTestBehavior,
|
||||
scrollBehavior: widget.scrollBehavior ??
|
||||
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset position) {
|
||||
return Viewport(
|
||||
// TODO(dnfield): we should provide a way to set cacheExtent
|
||||
// independent of implicit scrolling:
|
||||
// https://github.com/flutter/flutter/issues/45632
|
||||
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
|
||||
cacheExtentStyle: CacheExtentStyle.viewport,
|
||||
axisDirection: axisDirection,
|
||||
offset: position,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
slivers: <Widget>[
|
||||
SliverFillViewport(
|
||||
viewportFraction: _controller.viewportFraction,
|
||||
delegate: widget.childrenDelegate,
|
||||
padEnds: widget.padEnds,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description
|
||||
..add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection))
|
||||
..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'))
|
||||
..add(
|
||||
DiagnosticsProperty<PageController>('controller', _controller,
|
||||
showName: false),
|
||||
)
|
||||
..add(DiagnosticsProperty<ScrollPhysics>('physics', widget.physics,
|
||||
showName: false))
|
||||
..add(
|
||||
FlagProperty('pageSnapping',
|
||||
value: widget.pageSnapping, ifFalse: 'snapping disabled'),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'allowImplicitScrolling',
|
||||
value: widget.allowImplicitScrolling,
|
||||
ifTrue: 'allow implicit scrolling',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2125
lib/common/widgets/page/scrollable.dart
Normal file
367
lib/common/widgets/page/tabs.dart
Normal file
@@ -0,0 +1,367 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/page_view.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide TabBarView, PageView;
|
||||
|
||||
/// A page view that displays the widget which corresponds to the currently
|
||||
/// selected tab.
|
||||
///
|
||||
/// This widget is typically used in conjunction with a [TabBar].
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
|
||||
///
|
||||
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
|
||||
/// ancestor.
|
||||
///
|
||||
/// The tab controller's [TabController.length] must equal the length of the
|
||||
/// [children] list and the length of the [TabBar.tabs] list.
|
||||
///
|
||||
/// To see a sample implementation, visit the [TabController] documentation.
|
||||
class CustomTabBarView extends StatefulWidget {
|
||||
/// Creates a page view with one child per tab.
|
||||
///
|
||||
/// The length of [children] must be the same as the [controller]'s length.
|
||||
const CustomTabBarView({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.viewportFraction = 1.0,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
});
|
||||
|
||||
final Widget? header;
|
||||
final Color bgColor;
|
||||
|
||||
/// This widget's selection and animation state.
|
||||
///
|
||||
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
|
||||
/// will be used.
|
||||
final TabController? controller;
|
||||
|
||||
/// One widget per tab.
|
||||
///
|
||||
/// Its length must match the length of the [TabBar.tabs]
|
||||
/// list, as well as the [controller]'s [TabController.length].
|
||||
final List<Widget> children;
|
||||
|
||||
/// How the page view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the page view continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.pageview.viewportFraction}
|
||||
final double viewportFraction;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
final Axis scrollDirection;
|
||||
|
||||
@override
|
||||
State<CustomTabBarView> createState() => _CustomTabBarViewState();
|
||||
}
|
||||
|
||||
class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
TabController? _controller;
|
||||
PageController? _pageController;
|
||||
late List<Widget> _childrenWithKey;
|
||||
int? _currentIndex;
|
||||
int _warpUnderwayCount = 0;
|
||||
int _scrollUnderwayCount = 0;
|
||||
bool _debugHasScheduledValidChildrenCountCheck = false;
|
||||
|
||||
// If the TabBarView is rebuilt with a new tab controller, the caller should
|
||||
// dispose the old one. In that case the old controller's animation will be
|
||||
// null and should not be accessed.
|
||||
bool get _controllerIsValid => _controller?.animation != null;
|
||||
|
||||
void _updateTabController() {
|
||||
final TabController? newController =
|
||||
widget.controller ?? DefaultTabController.maybeOf(context);
|
||||
assert(() {
|
||||
if (newController == null) {
|
||||
throw FlutterError(
|
||||
'No TabController for ${widget.runtimeType}.\n'
|
||||
'When creating a ${widget.runtimeType}, you must either provide an explicit '
|
||||
'TabController using the "controller" property, or you must ensure that there '
|
||||
'is a DefaultTabController above the ${widget.runtimeType}.\n'
|
||||
'In this case, there was neither an explicit controller nor a default controller.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (newController == _controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
_controller = newController;
|
||||
if (_controller != null) {
|
||||
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
}
|
||||
|
||||
void _jumpToPage(int page) {
|
||||
_warpUnderwayCount += 1;
|
||||
_pageController!.jumpToPage(page);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
Future<void> _animateToPage(int page,
|
||||
{required Duration duration, required Curve curve}) async {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController!
|
||||
.animateToPage(page, duration: duration, curve: curve);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
if (_pageController == null) {
|
||||
_pageController = PageController(
|
||||
initialPage: _currentIndex!,
|
||||
viewportFraction: widget.viewportFraction,
|
||||
);
|
||||
} else {
|
||||
_pageController!.jumpToPage(_currentIndex!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomTabBarView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
_jumpToPage(_currentIndex!);
|
||||
}
|
||||
if (widget.viewportFraction != oldWidget.viewportFraction) {
|
||||
_pageController?.dispose();
|
||||
_pageController = PageController(
|
||||
initialPage: _currentIndex!,
|
||||
viewportFraction: widget.viewportFraction,
|
||||
);
|
||||
}
|
||||
// While a warp is under way, we stop updating the tab page contents.
|
||||
// This is tracked in https://github.com/flutter/flutter/issues/31269.
|
||||
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
|
||||
_updateChildren();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
_controller = null;
|
||||
_pageController?.dispose();
|
||||
// We don't own the _controller Animation, so it's not disposed here.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
|
||||
widget.children.map<Widget>((Widget child) {
|
||||
return Semantics(role: SemanticsRole.tabPanel, child: child);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
|
||||
return;
|
||||
} // This widget is driving the controller's animation.
|
||||
|
||||
if (_controller!.index != _currentIndex) {
|
||||
_currentIndex = _controller!.index;
|
||||
_warpToCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void _warpToCurrentIndex() {
|
||||
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool adjacentDestination =
|
||||
(_currentIndex! - _controller!.previousIndex).abs() == 1;
|
||||
if (adjacentDestination) {
|
||||
_warpToAdjacentTab(_controller!.animationDuration);
|
||||
} else {
|
||||
_warpToNonAdjacentTab(_controller!.animationDuration);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warpToAdjacentTab(Duration duration) async {
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(_currentIndex!,
|
||||
duration: duration, curve: Curves.ease);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_updateChildren();
|
||||
});
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
Future<void> _warpToNonAdjacentTab(Duration duration) async {
|
||||
final int previousIndex = _controller!.previousIndex;
|
||||
assert((_currentIndex! - previousIndex).abs() > 1);
|
||||
|
||||
// initialPage defines which page is shown when starting the animation.
|
||||
// This page is adjacent to the destination page.
|
||||
final int initialPage = _currentIndex! > previousIndex
|
||||
? _currentIndex! - 1
|
||||
: _currentIndex! + 1;
|
||||
|
||||
setState(() {
|
||||
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
|
||||
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
|
||||
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
|
||||
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
|
||||
final Widget temp = _childrenWithKey[initialPage];
|
||||
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
||||
_childrenWithKey[previousIndex] = temp;
|
||||
});
|
||||
|
||||
// Make a first jump to the adjacent page.
|
||||
_jumpToPage(initialPage);
|
||||
|
||||
// Jump or animate to the destination page.
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(_currentIndex!,
|
||||
duration: duration, curve: Curves.ease);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_updateChildren();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _syncControllerOffset() {
|
||||
_controller!.offset =
|
||||
clampDouble(_pageController!.page! - _controller!.index, -1.0, 1.0);
|
||||
}
|
||||
|
||||
// Called when the PageView scrolls
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (notification.depth != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_controllerIsValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_scrollUnderwayCount += 1;
|
||||
final double page = _pageController!.page!;
|
||||
if (notification is ScrollUpdateNotification &&
|
||||
!_controller!.indexIsChanging) {
|
||||
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
|
||||
if (pageChanged) {
|
||||
_controller!.index = page.round();
|
||||
_currentIndex = _controller!.index;
|
||||
}
|
||||
_syncControllerOffset();
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_controller!.index = page.round();
|
||||
_currentIndex = _controller!.index;
|
||||
if (!_controller!.indexIsChanging) {
|
||||
_syncControllerOffset();
|
||||
}
|
||||
}
|
||||
_scrollUnderwayCount -= 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _debugScheduleCheckHasValidChildrenCount() {
|
||||
if (_debugHasScheduledValidChildrenCountCheck) {
|
||||
return true;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
_debugHasScheduledValidChildrenCountCheck = false;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
assert(() {
|
||||
if (_controller!.length != widget.children.length) {
|
||||
throw FlutterError(
|
||||
"Controller's length property (${_controller!.length}) does not match the "
|
||||
"number of children (${widget.children.length}) present in TabBarView's children property.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}, debugLabel: 'TabBarView.validChildrenCountCheck');
|
||||
_debugHasScheduledValidChildrenCountCheck = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(_debugScheduleCheckHasValidChildrenCount());
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: CustomPageView(
|
||||
scrollDirection: widget.scrollDirection,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
controller: _pageController,
|
||||
physics: widget.physics == null
|
||||
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
|
||||
: const PageScrollPhysics().applyTo(widget.physics),
|
||||
header: widget.header,
|
||||
bgColor: widget.bgColor,
|
||||
children: _childrenWithKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
159
lib/common/widgets/pendant_avatar.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/common/avatar_badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PendantAvatar extends StatelessWidget {
|
||||
final BadgeType _badgeType;
|
||||
final String? avatar;
|
||||
final double size;
|
||||
final double badgeSize;
|
||||
final String? garbPendantImage;
|
||||
final dynamic roomId;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const PendantAvatar({
|
||||
super.key,
|
||||
required this.avatar,
|
||||
this.size = 80,
|
||||
double? badgeSize,
|
||||
bool? isVip,
|
||||
int? officialType,
|
||||
this.garbPendantImage,
|
||||
this.roomId,
|
||||
this.onTap,
|
||||
}) : _badgeType = officialType == null || officialType < 0
|
||||
? isVip == true
|
||||
? BadgeType.vip
|
||||
: BadgeType.none
|
||||
: officialType == 0
|
||||
? BadgeType.person
|
||||
: officialType == 1
|
||||
? BadgeType.institution
|
||||
: BadgeType.none,
|
||||
badgeSize = badgeSize ?? size / 3;
|
||||
|
||||
static bool showDynDecorate = GStorage.showDynDecorate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
onTap == null
|
||||
? _buildAvatar(colorScheme)
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: _buildAvatar(colorScheme),
|
||||
),
|
||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty)
|
||||
Positioned(
|
||||
top: -0.375 *
|
||||
(size == 80 ? size - 4 : size), // -(size * 1.75 - size) / 2
|
||||
child: IgnorePointer(
|
||||
child: CachedNetworkImage(
|
||||
width: size * 1.75,
|
||||
height: size * 1.75,
|
||||
imageUrl: Utils.thumbnailImgUrl(garbPendantImage),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (roomId != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: InkWell(
|
||||
onTap: () => Get.toNamed('/liveRoom?roomid=$roomId'),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(36)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.equalizer_rounded,
|
||||
size: MediaQuery.textScalerOf(context).scale(16),
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
Text(
|
||||
'直播中',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_badgeType != BadgeType.none)
|
||||
_buildBadge(colorScheme),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(ColorScheme colorScheme) => size == 80
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: NetworkImgLayer(
|
||||
src: avatar,
|
||||
width: size,
|
||||
height: size,
|
||||
type: ImageType.avatar,
|
||||
),
|
||||
),
|
||||
)
|
||||
: NetworkImgLayer(
|
||||
src: avatar,
|
||||
width: size,
|
||||
height: size,
|
||||
type: ImageType.avatar,
|
||||
);
|
||||
|
||||
Widget _buildBadge(ColorScheme colorScheme) {
|
||||
final child = switch (_badgeType) {
|
||||
BadgeType.vip => Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
height: badgeSize,
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
_ => Icon(
|
||||
Icons.offline_bolt,
|
||||
color: _badgeType.color,
|
||||
size: badgeSize,
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
};
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: child),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -267,9 +267,10 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
onDragUpdate: onDragUpdate,
|
||||
onDragEnd: onDragEnd,
|
||||
barHeight: barHeight,
|
||||
baseBarColor: baseBarColor ?? primaryColor.withOpacity(0.24),
|
||||
baseBarColor: baseBarColor ?? primaryColor.withValues(alpha: 0.24),
|
||||
progressBarColor: progressBarColor ?? primaryColor,
|
||||
bufferedBarColor: bufferedBarColor ?? primaryColor.withOpacity(0.24),
|
||||
bufferedBarColor:
|
||||
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24),
|
||||
barCapShape: barCapShape,
|
||||
thumbRadius: thumbRadius,
|
||||
thumbColor: thumbColor ?? primaryColor,
|
||||
@@ -300,9 +301,10 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
..onDragUpdate = onDragUpdate
|
||||
..onDragEnd = onDragEnd
|
||||
..barHeight = barHeight
|
||||
..baseBarColor = baseBarColor ?? primaryColor.withOpacity(0.24)
|
||||
..baseBarColor = baseBarColor ?? primaryColor.withValues(alpha: 0.24)
|
||||
..progressBarColor = progressBarColor ?? primaryColor
|
||||
..bufferedBarColor = bufferedBarColor ?? primaryColor.withOpacity(0.24)
|
||||
..bufferedBarColor =
|
||||
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24)
|
||||
..barCapShape = barCapShape
|
||||
..thumbRadius = thumbRadius
|
||||
..thumbColor = thumbColor ?? primaryColor
|
||||
@@ -320,43 +322,42 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(StringProperty('progress', progress.toString()));
|
||||
properties.add(StringProperty('total', total.toString()));
|
||||
properties.add(StringProperty('buffered', buffered.toString()));
|
||||
properties.add(ObjectFlagProperty<ValueChanged<Duration>>('onSeek', onSeek,
|
||||
ifNull: 'unimplemented'));
|
||||
properties.add(ObjectFlagProperty<ThumbDragStartCallback>(
|
||||
'onDragStart', onDragStart,
|
||||
ifNull: 'unimplemented'));
|
||||
properties.add(ObjectFlagProperty<ThumbDragUpdateCallback>(
|
||||
'onDragUpdate', onDragUpdate,
|
||||
ifNull: 'unimplemented'));
|
||||
properties.add(ObjectFlagProperty<VoidCallback>('onDragEnd', onDragEnd,
|
||||
ifNull: 'unimplemented'));
|
||||
properties.add(DoubleProperty('barHeight', barHeight));
|
||||
properties.add(ColorProperty('baseBarColor', baseBarColor));
|
||||
properties.add(ColorProperty('progressBarColor', progressBarColor));
|
||||
properties.add(ColorProperty('bufferedBarColor', bufferedBarColor));
|
||||
properties.add(StringProperty('barCapShape', barCapShape.toString()));
|
||||
properties.add(DoubleProperty('thumbRadius', thumbRadius));
|
||||
properties.add(ColorProperty('thumbColor', thumbColor));
|
||||
properties.add(ColorProperty('thumbGlowColor', thumbGlowColor));
|
||||
properties.add(DoubleProperty('thumbGlowRadius', thumbGlowRadius));
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'thumbCanPaintOutsideBar',
|
||||
value: thumbCanPaintOutsideBar,
|
||||
ifTrue: 'true',
|
||||
ifFalse: 'false',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties
|
||||
.add(StringProperty('timeLabelLocation', timeLabelLocation.toString()));
|
||||
properties.add(StringProperty('timeLabelType', timeLabelType.toString()));
|
||||
properties
|
||||
.add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle));
|
||||
properties.add(DoubleProperty('timeLabelPadding', timeLabelPadding));
|
||||
..add(StringProperty('progress', progress.toString()))
|
||||
..add(StringProperty('total', total.toString()))
|
||||
..add(StringProperty('buffered', buffered.toString()))
|
||||
..add(ObjectFlagProperty<ValueChanged<Duration>>('onSeek', onSeek,
|
||||
ifNull: 'unimplemented'))
|
||||
..add(ObjectFlagProperty<ThumbDragStartCallback>(
|
||||
'onDragStart', onDragStart,
|
||||
ifNull: 'unimplemented'))
|
||||
..add(ObjectFlagProperty<ThumbDragUpdateCallback>(
|
||||
'onDragUpdate', onDragUpdate,
|
||||
ifNull: 'unimplemented'))
|
||||
..add(ObjectFlagProperty<VoidCallback>('onDragEnd', onDragEnd,
|
||||
ifNull: 'unimplemented'))
|
||||
..add(DoubleProperty('barHeight', barHeight))
|
||||
..add(ColorProperty('baseBarColor', baseBarColor))
|
||||
..add(ColorProperty('progressBarColor', progressBarColor))
|
||||
..add(ColorProperty('bufferedBarColor', bufferedBarColor))
|
||||
..add(StringProperty('barCapShape', barCapShape.toString()))
|
||||
..add(DoubleProperty('thumbRadius', thumbRadius))
|
||||
..add(ColorProperty('thumbColor', thumbColor))
|
||||
..add(ColorProperty('thumbGlowColor', thumbGlowColor))
|
||||
..add(DoubleProperty('thumbGlowRadius', thumbGlowRadius))
|
||||
..add(
|
||||
FlagProperty(
|
||||
'thumbCanPaintOutsideBar',
|
||||
value: thumbCanPaintOutsideBar,
|
||||
ifTrue: 'true',
|
||||
ifFalse: 'false',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(StringProperty('timeLabelLocation', timeLabelLocation.toString()))
|
||||
..add(StringProperty('timeLabelType', timeLabelType.toString()))
|
||||
..add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle))
|
||||
..add(DoubleProperty('timeLabelPadding', timeLabelPadding));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,11 +620,10 @@ class _RenderProgressBar extends RenderBox {
|
||||
|
||||
TextPainter _layoutText(String text) {
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: _timeLabelTextStyle),
|
||||
textDirection: TextDirection.ltr,
|
||||
textScaleFactor: textScaleFactor,
|
||||
);
|
||||
textPainter.layout(minWidth: 0, maxWidth: double.infinity);
|
||||
text: TextSpan(text: text, style: _timeLabelTextStyle),
|
||||
textDirection: TextDirection.ltr,
|
||||
textScaler: TextScaler.linear(textScaleFactor))
|
||||
..layout(minWidth: 0, maxWidth: double.infinity);
|
||||
return textPainter;
|
||||
}
|
||||
|
||||
@@ -919,9 +919,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final canvas = context.canvas;
|
||||
canvas.save();
|
||||
canvas.translate(offset.dx, offset.dy);
|
||||
final canvas = context.canvas
|
||||
..save()
|
||||
..translate(offset.dx, offset.dy);
|
||||
|
||||
switch (_timeLabelLocation) {
|
||||
case TimeLabelLocation.above:
|
||||
@@ -1013,8 +1013,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
}
|
||||
|
||||
void _drawProgressBar(Canvas canvas, Offset offset, Size localSize) {
|
||||
canvas.save();
|
||||
canvas.translate(offset.dx, offset.dy);
|
||||
canvas
|
||||
..save()
|
||||
..translate(offset.dx, offset.dy);
|
||||
_drawBaseBar(canvas, localSize);
|
||||
_drawBufferedBar(canvas, localSize);
|
||||
_drawCurrentProgressBar(canvas, localSize);
|
||||
@@ -1109,17 +1110,19 @@ class _RenderProgressBar extends RenderBox {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
|
||||
// description
|
||||
config.textDirection = TextDirection.ltr;
|
||||
config.label = '进度条'; //'Progress bar';
|
||||
config.value = '${(_thumbValue * 100).round()}%';
|
||||
config
|
||||
..textDirection = TextDirection.ltr
|
||||
..label = '进度条' //'Progress bar';
|
||||
..value = '${(_thumbValue * 100).round()}%'
|
||||
|
||||
// increase action
|
||||
config.onIncrease = increaseAction;
|
||||
// increase action
|
||||
..onIncrease = increaseAction;
|
||||
final increased = _thumbValue + _semanticActionUnit;
|
||||
config.increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%';
|
||||
config
|
||||
..increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%'
|
||||
|
||||
// decrease action
|
||||
config.onDecrease = decreaseAction;
|
||||
// decrease action
|
||||
..onDecrease = decreaseAction;
|
||||
final decreased = _thumbValue - _semanticActionUnit;
|
||||
config.decreasedValue = '${((decreased).clamp(0.0, 1.0) * 100).round()}%';
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class SegmentProgressBar extends CustomPainter {
|
||||
size.width,
|
||||
0,
|
||||
),
|
||||
Paint()..color = Colors.grey[600]!.withOpacity(0.45),
|
||||
Paint()..color = Colors.grey[600]!.withValues(alpha: 0.45),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget videoProgressIndicator(double progress) => ClipRect(
|
||||
clipper: ProgressClipper(),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: StyleString.imgRadius,
|
||||
bottomRight: StyleString.imgRadius,
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10,
|
||||
@@ -46,15 +46,25 @@ const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
|
||||
/// Used by [RefreshIndicator.onRefresh].
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
|
||||
// The state machine moves through these modes only when the scrollable
|
||||
// identified by scrollableKey has been scrolled to its min or max limit.
|
||||
enum _RefreshIndicatorMode {
|
||||
drag, // Pointer is down.
|
||||
armed, // Dragged far enough that an up event will run the onRefresh callback.
|
||||
snap, // Animating to the indicator's final "displacement".
|
||||
refresh, // Running the refresh callback.
|
||||
done, // Animating the indicator's fade-out after refreshing.
|
||||
canceled, // Animating the indicator's fade-out after not arming.
|
||||
/// Indicates current status of Material `RefreshIndicator`.
|
||||
enum RefreshIndicatorStatus {
|
||||
/// Pointer is down.
|
||||
drag,
|
||||
|
||||
/// Dragged far enough that an up event will run the onRefresh callback.
|
||||
armed,
|
||||
|
||||
/// Animating to the indicator's final "displacement".
|
||||
snap,
|
||||
|
||||
/// Running the refresh callback.
|
||||
refresh,
|
||||
|
||||
/// Animating the indicator's fade-out after refreshing.
|
||||
done,
|
||||
|
||||
/// Animating the indicator's fade-out after not arming.
|
||||
canceled,
|
||||
}
|
||||
|
||||
/// Used to configure how [RefreshIndicator] can be triggered.
|
||||
@@ -68,7 +78,7 @@ enum RefreshIndicatorTriggerMode {
|
||||
onEdge,
|
||||
}
|
||||
|
||||
enum _IndicatorType { material, adaptive }
|
||||
enum _IndicatorType { material, adaptive, noSpinner }
|
||||
|
||||
/// A widget that supports the Material "swipe to refresh" idiom.
|
||||
///
|
||||
@@ -96,6 +106,12 @@ enum _IndicatorType { material, adaptive }
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to use [RefreshIndicator] without the spinner.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Troubleshooting
|
||||
///
|
||||
/// ### Refresh indicator does not show up
|
||||
@@ -149,7 +165,10 @@ class RefreshIndicator extends StatefulWidget {
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
}) : _indicatorType = _IndicatorType.material;
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.material,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates an adaptive [RefreshIndicator] based on whether the target
|
||||
/// platform is iOS or macOS, following Material design's
|
||||
@@ -180,7 +199,35 @@ class RefreshIndicator extends StatefulWidget {
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
}) : _indicatorType = _IndicatorType.adaptive;
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.adaptive,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates a [RefreshIndicator] with no spinner and calls `onRefresh` when
|
||||
/// successfully armed by a drag event.
|
||||
///
|
||||
/// Events can be optionally listened by using the `onStatusChange` callback.
|
||||
const RefreshIndicator.noSpinner({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onRefresh,
|
||||
this.onStatusChange,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.noSpinner,
|
||||
// The following parameters aren't used because [_IndicatorType.noSpinner] is being used,
|
||||
// which involves showing no spinner, hence the following parameters are useless since
|
||||
// their only use is to change the spinner's appearance.
|
||||
displacement = 0.0,
|
||||
edgeOffset = 0.0,
|
||||
color = null,
|
||||
backgroundColor = null,
|
||||
strokeWidth = 0.0,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
@@ -220,6 +267,10 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// [Future] must complete when the refresh operation is finished.
|
||||
final RefreshCallback onRefresh;
|
||||
|
||||
/// Called to get the current status of the [RefreshIndicator] to update the UI as needed.
|
||||
/// This is an optional parameter, used to fine tune app cases.
|
||||
final ValueChanged<RefreshIndicatorStatus?>? onStatusChange;
|
||||
|
||||
/// The progress indicator's foreground color. The current theme's
|
||||
/// [ColorScheme.primary] by default.
|
||||
final Color? color;
|
||||
@@ -266,6 +317,11 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
|
||||
final RefreshIndicatorTriggerMode triggerMode;
|
||||
|
||||
/// Defines the elevation of the underlying [RefreshIndicator].
|
||||
///
|
||||
/// Defaults to 2.0.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
RefreshIndicatorState createState() => RefreshIndicatorState();
|
||||
}
|
||||
@@ -281,7 +337,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
late Animation<double> _value;
|
||||
late Animation<Color?> _valueColor;
|
||||
|
||||
_RefreshIndicatorMode? _mode;
|
||||
RefreshIndicatorStatus? _status;
|
||||
late Future<void> _pendingRefreshFuture;
|
||||
bool? _isIndicatorAtTop;
|
||||
double? _dragOffset;
|
||||
@@ -290,29 +346,37 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
|
||||
static final Animatable<double> _threeQuarterTween =
|
||||
Tween<double>(begin: 0.0, end: 0.75);
|
||||
static final Animatable<double> _kDragSizeFactorLimitTween =
|
||||
Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit);
|
||||
|
||||
static final Animatable<double> _kDragSizeFactorLimitTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: _kDragSizeFactorLimit,
|
||||
);
|
||||
|
||||
static final Animatable<double> _oneToZeroTween =
|
||||
Tween<double>(begin: 1.0, end: 0.0);
|
||||
|
||||
@protected
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_positionController = AnimationController(vsync: this);
|
||||
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
|
||||
_value = _positionController.drive(
|
||||
_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
|
||||
|
||||
// The "value" of the circular progress indicator during a drag.
|
||||
_value = _positionController.drive(_threeQuarterTween);
|
||||
|
||||
_scaleController = AnimationController(vsync: this);
|
||||
_scaleFactor = _scaleController.drive(_oneToZeroTween);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_setupColorTween();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void didUpdateWidget(covariant RefreshIndicator oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -321,6 +385,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
_positionController.dispose();
|
||||
@@ -342,11 +407,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
ColorTween(
|
||||
begin: color.withAlpha(0),
|
||||
end: color.withAlpha(color.alpha),
|
||||
).chain(
|
||||
CurveTween(
|
||||
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit),
|
||||
),
|
||||
),
|
||||
).chain(CurveTween(
|
||||
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -364,7 +426,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
notification.metrics.extentAfter == 0.0) ||
|
||||
(notification.metrics.axisDirection == AxisDirection.down &&
|
||||
notification.metrics.extentBefore == 0.0)) &&
|
||||
_mode == null &&
|
||||
_status == null &&
|
||||
_start(notification.metrics.axisDirection);
|
||||
}
|
||||
|
||||
@@ -374,7 +436,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
if (_shouldStart(notification)) {
|
||||
setState(() {
|
||||
_mode = _RefreshIndicatorMode.drag;
|
||||
_status = RefreshIndicatorStatus.drag;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -384,13 +447,13 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
AxisDirection.left || AxisDirection.right => null,
|
||||
};
|
||||
if (indicatorAtTopNow != _isIndicatorAtTop) {
|
||||
if (_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed) {
|
||||
_dismiss(_RefreshIndicatorMode.canceled);
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
}
|
||||
} else if (notification is ScrollUpdateNotification) {
|
||||
if (_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
@@ -398,7 +461,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
if (_mode == _RefreshIndicatorMode.armed &&
|
||||
if (_status == RefreshIndicatorStatus.armed &&
|
||||
notification.dragDetails == null) {
|
||||
// On iOS start the refresh when the Scrollable bounces back from the
|
||||
// overscroll (ScrollNotification indicating this don't have dragDetails
|
||||
@@ -406,8 +469,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
_show();
|
||||
}
|
||||
} else if (notification is OverscrollNotification) {
|
||||
if (_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
@@ -416,19 +479,19 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
switch (_mode) {
|
||||
case _RefreshIndicatorMode.armed:
|
||||
switch (_status) {
|
||||
case RefreshIndicatorStatus.armed:
|
||||
if (_positionController.value < 1.0) {
|
||||
_dismiss(_RefreshIndicatorMode.canceled);
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
} else {
|
||||
_show();
|
||||
}
|
||||
case _RefreshIndicatorMode.drag:
|
||||
_dismiss(_RefreshIndicatorMode.canceled);
|
||||
case _RefreshIndicatorMode.canceled:
|
||||
case _RefreshIndicatorMode.done:
|
||||
case _RefreshIndicatorMode.refresh:
|
||||
case _RefreshIndicatorMode.snap:
|
||||
case RefreshIndicatorStatus.drag:
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
case RefreshIndicatorStatus.done:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
case null:
|
||||
// do nothing
|
||||
break;
|
||||
@@ -442,7 +505,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
if (notification.depth != 0 || !notification.leading) {
|
||||
return false;
|
||||
}
|
||||
if (_mode == _RefreshIndicatorMode.drag) {
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
notification.disallowIndicator();
|
||||
return true;
|
||||
}
|
||||
@@ -450,7 +513,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
|
||||
bool _start(AxisDirection direction) {
|
||||
assert(_mode == null);
|
||||
assert(_status == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
assert(_dragOffset == null);
|
||||
switch (direction) {
|
||||
@@ -470,75 +533,77 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
|
||||
void _checkDragOffset(double containerExtent) {
|
||||
assert(_mode == _RefreshIndicatorMode.drag ||
|
||||
_mode == _RefreshIndicatorMode.armed);
|
||||
assert(_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed);
|
||||
double newValue =
|
||||
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
|
||||
if (_mode == _RefreshIndicatorMode.armed) {
|
||||
if (_status == RefreshIndicatorStatus.armed) {
|
||||
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
|
||||
}
|
||||
_positionController.value =
|
||||
clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds
|
||||
if (_mode == _RefreshIndicatorMode.drag &&
|
||||
clampDouble(newValue, 0.0, 1.0); // This triggers various rebuilds.
|
||||
if (_status == RefreshIndicatorStatus.drag &&
|
||||
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
|
||||
_mode = _RefreshIndicatorMode.armed;
|
||||
_status = RefreshIndicatorStatus.armed;
|
||||
widget.onStatusChange?.call(_status);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop showing the refresh indicator.
|
||||
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
|
||||
Future<void> _dismiss(RefreshIndicatorStatus newMode) async {
|
||||
await Future<void>.value();
|
||||
// This can only be called from _show() when refreshing and
|
||||
// _handleScrollNotification in response to a ScrollEndNotification or
|
||||
// direction change.
|
||||
assert(newMode == _RefreshIndicatorMode.canceled ||
|
||||
newMode == _RefreshIndicatorMode.done);
|
||||
assert(newMode == RefreshIndicatorStatus.canceled ||
|
||||
newMode == RefreshIndicatorStatus.done);
|
||||
setState(() {
|
||||
_mode = newMode;
|
||||
_status = newMode;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
switch (_mode!) {
|
||||
case _RefreshIndicatorMode.done:
|
||||
switch (_status!) {
|
||||
case RefreshIndicatorStatus.done:
|
||||
await _scaleController.animateTo(1.0,
|
||||
duration: _kIndicatorScaleDuration);
|
||||
case _RefreshIndicatorMode.canceled:
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
await _positionController.animateTo(0.0,
|
||||
duration: _kIndicatorScaleDuration);
|
||||
case _RefreshIndicatorMode.armed:
|
||||
case _RefreshIndicatorMode.drag:
|
||||
case _RefreshIndicatorMode.refresh:
|
||||
case _RefreshIndicatorMode.snap:
|
||||
case RefreshIndicatorStatus.armed:
|
||||
case RefreshIndicatorStatus.drag:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
assert(false);
|
||||
}
|
||||
if (mounted && _mode == newMode) {
|
||||
if (mounted && _status == newMode) {
|
||||
_dragOffset = null;
|
||||
_isIndicatorAtTop = null;
|
||||
setState(() {
|
||||
_mode = null;
|
||||
_status = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _show() {
|
||||
assert(_mode != _RefreshIndicatorMode.refresh);
|
||||
assert(_mode != _RefreshIndicatorMode.snap);
|
||||
assert(_status != RefreshIndicatorStatus.refresh);
|
||||
assert(_status != RefreshIndicatorStatus.snap);
|
||||
final Completer<void> completer = Completer<void>();
|
||||
_pendingRefreshFuture = completer.future;
|
||||
_mode = _RefreshIndicatorMode.snap;
|
||||
_status = RefreshIndicatorStatus.snap;
|
||||
widget.onStatusChange?.call(_status);
|
||||
_positionController
|
||||
.animateTo(1.0 / _kDragSizeFactorLimit,
|
||||
duration: _kIndicatorSnapDuration)
|
||||
.then<void>((void value) {
|
||||
if (mounted && _mode == _RefreshIndicatorMode.snap) {
|
||||
.whenComplete(() {
|
||||
if (mounted && _status == RefreshIndicatorStatus.snap) {
|
||||
setState(() {
|
||||
// Show the indeterminate progress indicator.
|
||||
_mode = _RefreshIndicatorMode.refresh;
|
||||
_status = RefreshIndicatorStatus.refresh;
|
||||
});
|
||||
|
||||
final Future<void> refreshResult = widget.onRefresh();
|
||||
refreshResult.whenComplete(() {
|
||||
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
|
||||
widget.onRefresh().whenComplete(() {
|
||||
if (mounted && _status == RefreshIndicatorStatus.refresh) {
|
||||
completer.complete();
|
||||
_dismiss(_RefreshIndicatorMode.done);
|
||||
_dismiss(RefreshIndicatorStatus.done);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -562,9 +627,9 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
/// actual scroll view. It defaults to showing the indicator at the top. To
|
||||
/// show it at the bottom, set `atTop` to false.
|
||||
Future<void> show({bool atTop = true}) {
|
||||
if (_mode != _RefreshIndicatorMode.refresh &&
|
||||
_mode != _RefreshIndicatorMode.snap) {
|
||||
if (_mode == null) {
|
||||
if (_status != RefreshIndicatorStatus.refresh &&
|
||||
_status != RefreshIndicatorStatus.snap) {
|
||||
if (_status == null) {
|
||||
_start(atTop ? AxisDirection.down : AxisDirection.up);
|
||||
}
|
||||
_show();
|
||||
@@ -572,6 +637,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
return _pendingRefreshFuture;
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
@@ -583,7 +649,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
),
|
||||
);
|
||||
assert(() {
|
||||
if (_mode == null) {
|
||||
if (_status == null) {
|
||||
assert(_dragOffset == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
} else {
|
||||
@@ -594,13 +660,14 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}());
|
||||
|
||||
final bool showIndeterminateIndicator =
|
||||
_mode == _RefreshIndicatorMode.refresh ||
|
||||
_mode == _RefreshIndicatorMode.done;
|
||||
_status == RefreshIndicatorStatus.refresh ||
|
||||
_status == RefreshIndicatorStatus.done;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
child,
|
||||
if (_mode != null)
|
||||
if (_status != null)
|
||||
Positioned(
|
||||
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
@@ -608,41 +675,44 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
right: 0.0,
|
||||
child: SizeTransition(
|
||||
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
|
||||
sizeFactor: _positionFactor, // this is what brings it down
|
||||
child: Container(
|
||||
sizeFactor: _positionFactor, // This is what brings it down.
|
||||
child: Padding(
|
||||
padding: _isIndicatorAtTop!
|
||||
? EdgeInsets.only(top: widget.displacement)
|
||||
: EdgeInsets.only(bottom: widget.displacement),
|
||||
alignment: _isIndicatorAtTop!
|
||||
? Alignment.topCenter
|
||||
: Alignment.bottomCenter,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleFactor,
|
||||
child: AnimatedBuilder(
|
||||
animation: _positionController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final Widget materialIndicator = RefreshProgressIndicator(
|
||||
semanticsLabel: widget.semanticsLabel ??
|
||||
MaterialLocalizations.of(context)
|
||||
.refreshIndicatorSemanticLabel,
|
||||
semanticsValue: widget.semanticsValue,
|
||||
value: showIndeterminateIndicator ? null : _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
);
|
||||
child: Align(
|
||||
alignment: _isIndicatorAtTop!
|
||||
? Alignment.topCenter
|
||||
: Alignment.bottomCenter,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleFactor,
|
||||
child: AnimatedBuilder(
|
||||
animation: _positionController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final Widget materialIndicator =
|
||||
RefreshProgressIndicator(
|
||||
semanticsLabel: widget.semanticsLabel ??
|
||||
MaterialLocalizations.of(context)
|
||||
.refreshIndicatorSemanticLabel,
|
||||
semanticsValue: widget.semanticsValue,
|
||||
value:
|
||||
showIndeterminateIndicator ? null : _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
elevation: widget.elevation,
|
||||
);
|
||||
|
||||
final Widget cupertinoIndicator =
|
||||
CupertinoActivityIndicator(
|
||||
color: widget.color,
|
||||
);
|
||||
final Widget cupertinoIndicator =
|
||||
CupertinoActivityIndicator(
|
||||
color: widget.color,
|
||||
);
|
||||
|
||||
switch (widget._indicatorType) {
|
||||
case _IndicatorType.material:
|
||||
return materialIndicator;
|
||||
switch (widget._indicatorType) {
|
||||
case _IndicatorType.material:
|
||||
return materialIndicator;
|
||||
|
||||
case _IndicatorType.adaptive:
|
||||
{
|
||||
case _IndicatorType.adaptive:
|
||||
final ThemeData theme = Theme.of(context);
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
@@ -654,9 +724,12 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
case TargetPlatform.macOS:
|
||||
return cupertinoIndicator;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
case _IndicatorType.noSpinner:
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
98
lib/common/widgets/scroll_physics.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget videoTabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) =>
|
||||
TabBarView(
|
||||
physics: const CustomTabBarViewClampingScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
|
||||
Widget tabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) =>
|
||||
TabBarView(
|
||||
physics: const CustomTabBarViewScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
|
||||
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
|
||||
const CustomTabBarViewScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
CustomTabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => CustomSpringDescription();
|
||||
}
|
||||
|
||||
class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
const CustomTabBarViewClampingScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
CustomTabBarViewClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return CustomTabBarViewClampingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => CustomSpringDescription();
|
||||
}
|
||||
|
||||
class MemberVideoScrollPhysics extends AlwaysScrollableScrollPhysics {
|
||||
const MemberVideoScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
MemberVideoScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return MemberVideoScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
double adjustPositionForNewDimensions({
|
||||
required ScrollMetrics oldPosition,
|
||||
required ScrollMetrics newPosition,
|
||||
required bool isScrolling,
|
||||
required double velocity,
|
||||
}) {
|
||||
if (newPosition.maxScrollExtent < oldPosition.maxScrollExtent) {
|
||||
return 0;
|
||||
}
|
||||
return super.adjustPositionForNewDimensions(
|
||||
oldPosition: oldPosition,
|
||||
newPosition: newPosition,
|
||||
isScrolling: isScrolling,
|
||||
velocity: velocity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSpringDescription implements SpringDescription {
|
||||
@override
|
||||
final mass = GStorage.springDescription[0];
|
||||
|
||||
@override
|
||||
final stiffness = GStorage.springDescription[1];
|
||||
|
||||
@override
|
||||
final damping = GStorage.springDescription[2];
|
||||
|
||||
CustomSpringDescription._();
|
||||
|
||||
static final _instance = CustomSpringDescription._();
|
||||
|
||||
factory CustomSpringDescription() => _instance;
|
||||
|
||||
/// Defaults to 0.
|
||||
@override
|
||||
double bounce = 0.0;
|
||||
|
||||
/// Defaults to 0.5 seconds.
|
||||
@override
|
||||
Duration duration = const Duration(milliseconds: 500);
|
||||
}
|
||||
@@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
|
||||
/// https://stackoverflow.com/a/76605401
|
||||
|
||||
class SelfSizedHorizontalList extends StatefulWidget {
|
||||
final Widget Function(int) childBuilder;
|
||||
final Widget Function(int index) childBuilder;
|
||||
final int itemCount;
|
||||
final double gapSize;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final ScrollController? controller;
|
||||
|
||||
const SelfSizedHorizontalList({
|
||||
super.key,
|
||||
@@ -14,6 +15,7 @@ class SelfSizedHorizontalList extends StatefulWidget {
|
||||
required this.itemCount,
|
||||
this.gapSize = 5,
|
||||
this.padding,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -33,23 +35,35 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
|
||||
|
||||
bool get isInit => height == null;
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(SelfSizedHorizontalList oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// if (BuildConfig.isDebug) {
|
||||
// prevHeight = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (height == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((v) => setState(() {}));
|
||||
}
|
||||
if (widget.itemCount == 0) return const SizedBox();
|
||||
if (widget.itemCount == 0) return const SizedBox.shrink();
|
||||
if (isInit) {
|
||||
return Container(
|
||||
key: infoKey,
|
||||
padding: widget.padding,
|
||||
child: widget.childBuilder(0),
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
key: infoKey,
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
child: widget.childBuilder(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: ListView.separated(
|
||||
controller: widget.controller,
|
||||
padding: widget.padding,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.itemCount,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
SliverHeaderDelegate({required this.height, required this.child});
|
||||
|
||||
final double height;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => height;
|
||||
|
||||
@override
|
||||
double get minExtent => height;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||
true;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget videoTabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) =>
|
||||
TabBarView(
|
||||
physics: const CustomTabBarViewClampingScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
|
||||
Widget tabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) =>
|
||||
TabBarView(
|
||||
physics: const CustomTabBarViewScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
|
||||
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
|
||||
const CustomTabBarViewScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
CustomTabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => SpringDescription(
|
||||
mass: GStorage.springDescription[0],
|
||||
stiffness: GStorage.springDescription[1],
|
||||
damping: GStorage.springDescription[2],
|
||||
);
|
||||
}
|
||||
|
||||
class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
const CustomTabBarViewClampingScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
CustomTabBarViewClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return CustomTabBarViewClampingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => SpringDescription(
|
||||
mass: GStorage.springDescription[0],
|
||||
stiffness: GStorage.springDescription[1],
|
||||
damping: GStorage.springDescription[2],
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ abstract class _StatItemBase extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
final Object value;
|
||||
final String? theme;
|
||||
final String? size;
|
||||
final Color? textColor;
|
||||
final double iconSize;
|
||||
|
||||
@@ -13,7 +12,6 @@ abstract class _StatItemBase extends StatelessWidget {
|
||||
required this.context,
|
||||
required this.value,
|
||||
this.theme,
|
||||
this.size,
|
||||
this.textColor,
|
||||
this.iconSize = 13,
|
||||
});
|
||||
@@ -24,8 +22,10 @@ abstract class _StatItemBase extends StatelessWidget {
|
||||
Color get color {
|
||||
return textColor ??
|
||||
switch (theme) {
|
||||
'gray' => Theme.of(context).colorScheme.outline.withOpacity(0.8),
|
||||
'black' => Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
'gray' =>
|
||||
Theme.of(context).colorScheme.outline.withValues(alpha: 0.8),
|
||||
'black' =>
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
_ => Colors.white,
|
||||
};
|
||||
}
|
||||
@@ -42,7 +42,7 @@ abstract class _StatItemBase extends StatelessWidget {
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
Utils.numFormat(value),
|
||||
style: TextStyle(fontSize: size == 'medium' ? 12 : 11, color: color),
|
||||
style: TextStyle(fontSize: 12, color: color),
|
||||
overflow: TextOverflow.clip,
|
||||
semanticsLabel: semanticsLabel,
|
||||
)
|
||||
@@ -59,7 +59,6 @@ class StatView extends _StatItemBase {
|
||||
required super.value,
|
||||
this.goto,
|
||||
super.theme,
|
||||
super.size,
|
||||
super.textColor,
|
||||
}) : super(iconSize: 13);
|
||||
|
||||
@@ -68,6 +67,7 @@ class StatView extends _StatItemBase {
|
||||
'picture' => Icons.remove_red_eye_outlined,
|
||||
'like' => Icons.thumb_up_outlined,
|
||||
'reply' => Icons.comment_outlined,
|
||||
'follow' => Icons.favorite_border,
|
||||
_ => Icons.play_circle_outlined,
|
||||
};
|
||||
|
||||
@@ -81,7 +81,6 @@ class StatDanMu extends _StatItemBase {
|
||||
required super.context,
|
||||
required super.value,
|
||||
super.theme,
|
||||
super.size,
|
||||
super.textColor,
|
||||
}) : super(iconSize: 14);
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide TabBarView;
|
||||
|
||||
@@ -124,11 +126,8 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
Future<void> _animateToPage(
|
||||
int page, {
|
||||
required Duration duration,
|
||||
required Curve curve,
|
||||
}) async {
|
||||
Future<void> _animateToPage(int page,
|
||||
{required Duration duration, required Curve curve}) async {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController!
|
||||
.animateToPage(page, duration: duration, curve: curve);
|
||||
@@ -190,7 +189,11 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
|
||||
widget.children.map<Widget>((Widget child) {
|
||||
return Semantics(role: SemanticsRole.tabPanel, child: child);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_progress_indicator.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_popup_menu.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/model_hot_video_item.dart';
|
||||
import 'package:PiliPlus/models/model_video.dart';
|
||||
import 'package:PiliPlus/models/search/result.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../../http/search.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'badge.dart';
|
||||
import 'network_img_layer.dart';
|
||||
import 'stat/stat.dart';
|
||||
import 'video_popup_menu.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// 视频卡片 - 水平布局
|
||||
class VideoCardH extends StatelessWidget {
|
||||
@@ -42,9 +45,6 @@ class VideoCardH extends StatelessWidget {
|
||||
final int aid = videoItem.aid!;
|
||||
final String bvid = videoItem.bvid!;
|
||||
String type = 'video';
|
||||
// try {
|
||||
// type = videoItem.type;
|
||||
// } catch (_) {}
|
||||
if (videoItem is SearchVideoItemModel) {
|
||||
var typeOrNull = (videoItem as SearchVideoItemModel).type;
|
||||
if (typeOrNull?.isNotEmpty == true) {
|
||||
@@ -59,18 +59,12 @@ class VideoCardH extends StatelessWidget {
|
||||
Semantics(
|
||||
label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
// customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
// for (var item in actions)
|
||||
// CustomSemanticsAction(
|
||||
// label: item.title.isEmpty ? 'label' : item.title): item.onTap!,
|
||||
// },
|
||||
child: InkWell(
|
||||
onLongPress: () {
|
||||
if (onLongPress != null) {
|
||||
onLongPress!();
|
||||
} else {
|
||||
imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.pic,
|
||||
);
|
||||
@@ -84,11 +78,22 @@ class VideoCardH extends StatelessWidget {
|
||||
if (type == 'ketang') {
|
||||
SmartDialog.showToast('课堂视频暂不支持播放');
|
||||
return;
|
||||
} else if (type == 'live_room') {
|
||||
if (videoItem is SearchVideoItemModel) {
|
||||
int? roomId = (videoItem as SearchVideoItemModel).id;
|
||||
if (roomId != null) {
|
||||
Get.toNamed('/liveRoom?roomid=$roomId');
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast(
|
||||
'err: live_room : ${videoItem.runtimeType}');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((videoItem is HotVideoItemModel) &&
|
||||
(videoItem as HotVideoItemModel).redirectUrl?.isNotEmpty ==
|
||||
true) {
|
||||
if (Utils.viewPgcFromUri(
|
||||
if (PageUtils.viewPgcFromUri(
|
||||
(videoItem as HotVideoItemModel).redirectUrl!)) {
|
||||
return;
|
||||
}
|
||||
@@ -99,7 +104,7 @@ class VideoCardH extends StatelessWidget {
|
||||
if (source == 'later') {
|
||||
onViewLater!(cid);
|
||||
} else {
|
||||
Utils.toViewPage(
|
||||
PageUtils.toVideoPage(
|
||||
'bvid=$bvid&cid=$cid',
|
||||
arguments: {
|
||||
'videoItem': videoItem,
|
||||
@@ -123,8 +128,7 @@ class VideoCardH extends StatelessWidget {
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
builder: (context, boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
num? progress;
|
||||
@@ -141,11 +145,7 @@ class VideoCardH extends StatelessWidget {
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (videoItem is HotVideoItemModel &&
|
||||
(videoItem as HotVideoItemModel)
|
||||
.pgcLabel
|
||||
?.isNotEmpty ==
|
||||
true)
|
||||
if (videoItem is HotVideoItemModel)
|
||||
PBadge(
|
||||
text:
|
||||
(videoItem as HotVideoItemModel).pgcLabel,
|
||||
@@ -159,7 +159,7 @@ class VideoCardH extends StatelessWidget {
|
||||
: '${Utils.timeFormat(progress)}/${Utils.timeFormat(videoItem.duration)}',
|
||||
right: 6,
|
||||
bottom: 8,
|
||||
type: 'gray',
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
@@ -176,19 +176,15 @@ class VideoCardH extends StatelessWidget {
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
if (type != 'video')
|
||||
PBadge(
|
||||
text: type,
|
||||
left: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'primary',
|
||||
type: PBadgeType.primary,
|
||||
),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
// pBadge(videoItem.rcmdReason.content, context,
|
||||
// 6.0, 6.0, null, null),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -217,6 +213,7 @@ class VideoCardH extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget videoContent(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
String pubdate = showPubdate
|
||||
? Utils.dateFormat(videoItem.pubdate!, formatType: 'day')
|
||||
: '';
|
||||
@@ -238,13 +235,12 @@ class VideoCardH extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: i['text'],
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
color: i['type'] == 'em'
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -257,7 +253,7 @@ class VideoCardH extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
@@ -265,36 +261,15 @@ class VideoCardH extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// const Spacer(),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
// Container(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(4),
|
||||
// border: Border.all(
|
||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||
// ),
|
||||
// child: Text(
|
||||
// videoItem.rcmdReason.content,
|
||||
// style: TextStyle(
|
||||
// fontSize: 9,
|
||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
if (showOwner || showPubdate)
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Text(
|
||||
"$pubdate ${showOwner ? videoItem.owner.name : ''}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
Text(
|
||||
"$pubdate ${showOwner ? videoItem.owner.name : ''}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_popup_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_progress_indicator.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/space_archive/item.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'badge.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
// 视频卡片 - 水平布局
|
||||
class VideoCardHMemberVideo extends StatelessWidget {
|
||||
@@ -19,18 +21,19 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
this.bvid,
|
||||
this.fromViewAid,
|
||||
});
|
||||
final Item videoItem;
|
||||
final SpaceArchiveItem videoItem;
|
||||
final VoidCallback? onTap;
|
||||
final dynamic bvid;
|
||||
final String? fromViewAid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
InkWell(
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
),
|
||||
@@ -40,7 +43,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
if (videoItem.isPgc == true && videoItem.uri?.isNotEmpty == true) {
|
||||
if (Utils.viewPgcFromUri(videoItem.uri!)) {
|
||||
if (PageUtils.viewPgcFromUri(videoItem.uri!)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +51,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Utils.toViewPage(
|
||||
PageUtils.toVideoPage(
|
||||
'bvid=${videoItem.bvid}&cid=${videoItem.cid}',
|
||||
arguments: {
|
||||
'heroTag': Utils.makeHeroTag(videoItem.bvid),
|
||||
@@ -77,27 +80,27 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
// videoItem.season?['cover'] ?? videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (fromViewAid == videoItem.param)
|
||||
Positioned.fill(
|
||||
const Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: StyleString.mdRadius,
|
||||
color: Colors.black54,
|
||||
),
|
||||
child: Center(
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'上次观看',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
letterSpacing: 1.5,
|
||||
letterSpacing: 5,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -111,8 +114,8 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
right: 6.0,
|
||||
top: 6.0,
|
||||
type: videoItem.badges!.first.text == '充电专属'
|
||||
? 'error'
|
||||
: 'primary',
|
||||
? PBadgeType.error
|
||||
: PBadgeType.primary,
|
||||
),
|
||||
if (videoItem.history != null) ...[
|
||||
Builder(builder: (context) {
|
||||
@@ -139,7 +142,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
: '${Utils.timeFormat(videoItem.history!['progress'])}/${Utils.timeFormat(videoItem.history!['duration'])}',
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
type: PBadgeType.gray,
|
||||
);
|
||||
} catch (_) {
|
||||
return PBadge(
|
||||
@@ -147,7 +150,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
Utils.timeFormat(videoItem.duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
type: PBadgeType.gray,
|
||||
);
|
||||
}
|
||||
}),
|
||||
@@ -156,7 +159,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
type: PBadgeType.gray,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -164,7 +167,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
videoContent(context),
|
||||
videoContent(context, theme),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -184,26 +187,23 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget videoContent(context) {
|
||||
Widget videoContent(BuildContext context, ThemeData theme) {
|
||||
final isCurr = fromViewAid == videoItem.param ||
|
||||
(videoItem.bvid != null && videoItem.bvid == bvid);
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
// videoItem.season?['title'] ?? videoItem.title ?? '',
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontWeight: videoItem.bvid != null && videoItem.bvid == bvid
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
fontWeight: isCurr ? FontWeight.bold : null,
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
color: videoItem.bvid != null && videoItem.bvid == bvid
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
color: isCurr ? theme.colorScheme.primary : null,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -215,9 +215,9 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
: videoItem.publishTimeText ?? '',
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
fontSize: 12,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
color: theme.colorScheme.outline,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
@@ -227,15 +227,12 @@ class VideoCardHMemberVideo extends StatelessWidget {
|
||||
StatView(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
// view: videoItem.season?['view_content'] ??
|
||||
// videoItem.viewContent,
|
||||
value: videoItem.stat.viewStr,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
// danmu: videoItem.season?['danmaku'] ?? videoItem.danmaku,
|
||||
value: videoItem.stat.danmuStr,
|
||||
),
|
||||
],
|
||||
281
lib/common/widgets/video_card/video_card_v.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/common/widgets/video_popup_menu.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/home/rcmd/result.dart';
|
||||
import 'package:PiliPlus/models/model_rec_video_item.dart';
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardV extends StatelessWidget {
|
||||
final BaseRecVideoItemModel videoItem;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
const VideoCardV({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.onRemove,
|
||||
});
|
||||
|
||||
bool isStringNumeric(String str) {
|
||||
RegExp numericRegex = RegExp(r'^\d+$');
|
||||
return numericRegex.hasMatch(str);
|
||||
}
|
||||
|
||||
Future<void> onPushDetail(heroTag) async {
|
||||
String? goto = videoItem.goto;
|
||||
switch (goto) {
|
||||
case 'bangumi':
|
||||
PageUtils.viewBangumi(epId: videoItem.param!);
|
||||
break;
|
||||
case 'av':
|
||||
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid!);
|
||||
int? cid = videoItem.cid;
|
||||
if (cid == null || cid == 0 || cid == -1) {
|
||||
cid = await SearchHttp.ab2c(aid: videoItem.aid, bvid: bvid);
|
||||
}
|
||||
PageUtils.toVideoPage(
|
||||
'bvid=$bvid&cid=$cid',
|
||||
arguments: {
|
||||
'pic': videoItem.pic,
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
break;
|
||||
// 动态
|
||||
case 'picture':
|
||||
try {
|
||||
String type = 'picture';
|
||||
String uri = videoItem.uri!;
|
||||
String id = '';
|
||||
if (uri.startsWith('bilibili://article/')) {
|
||||
type = 'read';
|
||||
RegExp regex = RegExp(r'\d+');
|
||||
Match match = regex.firstMatch(uri)!;
|
||||
String matchedNumber = match.group(0)!;
|
||||
videoItem.param = int.parse(matchedNumber);
|
||||
id = '${videoItem.param}';
|
||||
}
|
||||
if (uri.startsWith('http')) {
|
||||
String id = Uri.parse(uri).path.split('/')[1];
|
||||
if (isStringNumeric(id)) {
|
||||
PageUtils.pushDynFromId(id: id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Get.toNamed(
|
||||
'/articlePage',
|
||||
parameters: {
|
||||
'id': id,
|
||||
'type': type,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (videoItem.uri?.isNotEmpty == true) {
|
||||
PiliScheme.routePushFromUrl(videoItem.uri!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Semantics(
|
||||
label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.aid)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: videoItem.title,
|
||||
cover: videoItem.pic,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (videoItem.duration > 0)
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.gray,
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
videoContent(context)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av')
|
||||
Positioned(
|
||||
right: -5,
|
||||
bottom: -2,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
videoItem: videoItem,
|
||||
onRemove: onRemove,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget videoContent(context) {
|
||||
final theme = Theme.of(context);
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 5, 6, 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${videoItem.title}\n",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1.38,
|
||||
),
|
||||
),
|
||||
),
|
||||
videoStat(context, theme),
|
||||
Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
if (videoItem.goto == 'bangumi')
|
||||
PBadge(
|
||||
text: videoItem.bangumiBadge,
|
||||
isStack: false,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.line_primary,
|
||||
fontSize: 9,
|
||||
),
|
||||
if (videoItem.rcmdReason != null)
|
||||
PBadge(
|
||||
text: videoItem.rcmdReason,
|
||||
isStack: false,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.secondary,
|
||||
),
|
||||
if (videoItem.goto == 'picture')
|
||||
const PBadge(
|
||||
text: '动态',
|
||||
isStack: false,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.line_primary,
|
||||
fontSize: 9,
|
||||
),
|
||||
if (videoItem.isFollowed)
|
||||
const PBadge(
|
||||
text: '已关注',
|
||||
isStack: false,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.secondary,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
videoItem.owner.name.toString(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
fontSize: theme.textTheme.labelMedium!.fontSize,
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av') const SizedBox(width: 10)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget videoStat(BuildContext context, ThemeData theme) {
|
||||
return Row(
|
||||
children: [
|
||||
StatView(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
value: videoItem.stat.viewStr,
|
||||
goto: videoItem.goto,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (videoItem.goto != 'picture')
|
||||
StatDanMu(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
value: videoItem.stat.danmuStr,
|
||||
),
|
||||
if (videoItem is RecVideoItemModel) ...[
|
||||
const Spacer(),
|
||||
Text.rich(
|
||||
maxLines: 1,
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: theme.textTheme.labelSmall!.fontSize,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.8),
|
||||
),
|
||||
text: Utils.formatTimestampToRelativeTime(videoItem.pubdate)),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
] else if (videoItem is RecVideoItemAppModel &&
|
||||
videoItem.desc != null &&
|
||||
videoItem.desc!.contains(' · ')) ...[
|
||||
const Spacer(),
|
||||
Text.rich(
|
||||
maxLines: 1,
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: theme.textTheme.labelSmall!.fontSize,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.8),
|
||||
),
|
||||
text: Utils.shortenChineseDateString(
|
||||
videoItem.desc!.split(' · ').last)),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
120
lib/common/widgets/video_card/video_card_v_member_home.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/space/item.dart';
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardVMemberHome extends StatelessWidget {
|
||||
final SpaceItem videoItem;
|
||||
|
||||
const VideoCardVMemberHome({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
});
|
||||
|
||||
Future<void> onPushDetail(heroTag) async {
|
||||
String? goto = videoItem.goto;
|
||||
switch (goto) {
|
||||
case 'bangumi':
|
||||
PageUtils.viewBangumi(epId: videoItem.param);
|
||||
break;
|
||||
case 'av':
|
||||
if (videoItem.isPgc == true && videoItem.uri?.isNotEmpty == true) {
|
||||
if (PageUtils.viewPgcFromUri(videoItem.uri!)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
String? aid = videoItem.param;
|
||||
String? bvid = videoItem.bvid;
|
||||
if (aid == null && bvid == null) {
|
||||
return;
|
||||
}
|
||||
int? cid = videoItem.firstCid;
|
||||
cid ??= await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||
PageUtils.toVideoPage(
|
||||
'bvid=${bvid ?? IdUtils.av2bv(int.parse(aid!))}&cid=$cid',
|
||||
arguments: {
|
||||
'pic': videoItem.cover,
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
if (videoItem.uri?.isNotEmpty == true) {
|
||||
PiliScheme.routePushFromUrl(videoItem.uri!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.bvid)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if ((videoItem.duration ?? -1) > 0)
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: PBadgeSize.small,
|
||||
type: PBadgeType.gray,
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
videoContent(context, videoItem)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget videoContent(BuildContext context, SpaceItem videoItem) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 5, 6, 5),
|
||||
child: Text(
|
||||
'${videoItem.title}\n',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1.38,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'badge.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
// 视频卡片 - 水平布局
|
||||
class VideoCardHGrpc extends StatelessWidget {
|
||||
const VideoCardHGrpc({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.source = 'normal',
|
||||
this.showOwner = true,
|
||||
this.showView = true,
|
||||
this.showDanmaku = true,
|
||||
this.showPubdate = false,
|
||||
});
|
||||
final card.Card videoItem;
|
||||
final String source;
|
||||
final bool showOwner;
|
||||
final bool showView;
|
||||
final bool showDanmaku;
|
||||
final bool showPubdate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final int aid = videoItem.smallCoverV5.base.args.aid.toInt();
|
||||
// final String bvid = IdUtils.av2bv(aid);
|
||||
String type = 'video';
|
||||
// try {
|
||||
// type = videoItem.type;
|
||||
// } catch (_) {}
|
||||
// List<VideoCustomAction> actions =
|
||||
// VideoCustomActions(videoItem, context).actions;
|
||||
final String heroTag = Utils.makeHeroTag(aid);
|
||||
return Stack(children: [
|
||||
Semantics(
|
||||
// label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
// customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
// for (var item in actions)
|
||||
// CustomSemanticsAction(label: item.title): item.onTap!,
|
||||
// },
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.smallCoverV5.base.title,
|
||||
cover: videoItem.smallCoverV5.base.cover,
|
||||
),
|
||||
onTap: () async {
|
||||
if (type == 'ketang') {
|
||||
SmartDialog.showToast('课堂视频暂不支持播放');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
PiliScheme.routePushFromUrl(videoItem.smallCoverV5.base.uri);
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints boxConstraints) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context,
|
||||
BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.smallCoverV5.base.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
if (videoItem
|
||||
.smallCoverV5.coverRightText1.isNotEmpty)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(
|
||||
videoItem.smallCoverV5.coverRightText1),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
if (type != 'video')
|
||||
PBadge(
|
||||
text: type,
|
||||
left: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'primary',
|
||||
),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
// pBadge(videoItem.rcmdReason.content, context,
|
||||
// 6.0, 6.0, null, null),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
videoContent(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// if (source == 'normal')
|
||||
// Positioned(
|
||||
// bottom: 0,
|
||||
// right: 0,
|
||||
// child: VideoPopupMenu(
|
||||
// size: 29,
|
||||
// iconSize: 17,
|
||||
// actions: actions,
|
||||
// ),
|
||||
// ),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget videoContent(context) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
videoItem.smallCoverV5.base.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// const Spacer(),
|
||||
// if (videoItem.rcmdReason != null &&
|
||||
// videoItem.rcmdReason.content != '')
|
||||
// Container(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5),
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(4),
|
||||
// border: Border.all(
|
||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||
// ),
|
||||
// child: Text(
|
||||
// videoItem.rcmdReason.content,
|
||||
// style: TextStyle(
|
||||
// fontSize: 9,
|
||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
if (showOwner || showPubdate)
|
||||
Text(
|
||||
videoItem.smallCoverV5.rightDesc1,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
videoItem.smallCoverV5.rightDesc2,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
// Row(
|
||||
// children: [
|
||||
// if (showView) ...[
|
||||
// StatView(
|
||||
// theme: 'gray',
|
||||
// view: videoItem.stat.view as int,
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
// ],
|
||||
// if (showDanmaku)
|
||||
// StatDanMu(
|
||||
// theme: 'gray',
|
||||
// danmu: videoItem.stat.danmu as int,
|
||||
// ),
|
||||
// const Spacer(),
|
||||
// if (source == 'normal') const SizedBox(width: 24),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../models/home/rcmd/result.dart';
|
||||
import '../../models/model_rec_video_item.dart';
|
||||
import 'stat/stat.dart';
|
||||
import '../../http/dynamics.dart';
|
||||
import '../../utils/id_utils.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'badge.dart';
|
||||
import 'network_img_layer.dart';
|
||||
import 'video_popup_menu.dart';
|
||||
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardV extends StatelessWidget {
|
||||
final BaseRecVideoItemModel videoItem;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
const VideoCardV({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.onRemove,
|
||||
});
|
||||
|
||||
bool isStringNumeric(String str) {
|
||||
RegExp numericRegex = RegExp(r'^\d+$');
|
||||
return numericRegex.hasMatch(str);
|
||||
}
|
||||
|
||||
void onPushDetail(heroTag) async {
|
||||
String goto = videoItem.goto!;
|
||||
switch (goto) {
|
||||
case 'bangumi':
|
||||
Utils.viewBangumi(epId: videoItem.param!);
|
||||
break;
|
||||
case 'av':
|
||||
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid!);
|
||||
int cid = videoItem.cid!;
|
||||
if (cid == -1) {
|
||||
cid = await SearchHttp.ab2c(aid: videoItem.aid, bvid: bvid);
|
||||
}
|
||||
Utils.toViewPage(
|
||||
'bvid=$bvid&cid=$cid',
|
||||
arguments: {
|
||||
// 'videoItem': videoItem,
|
||||
'pic': videoItem.pic,
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
break;
|
||||
// 动态
|
||||
case 'picture':
|
||||
try {
|
||||
String dynamicType = 'picture';
|
||||
String uri = videoItem.uri!;
|
||||
String id = '';
|
||||
if (uri.startsWith('bilibili://article/')) {
|
||||
// https://www.bilibili.com/read/cv27063554
|
||||
dynamicType = 'read';
|
||||
RegExp regex = RegExp(r'\d+');
|
||||
Match match = regex.firstMatch(uri)!;
|
||||
String matchedNumber = match.group(0)!;
|
||||
videoItem.param = int.parse(matchedNumber);
|
||||
id = 'cv${videoItem.param}';
|
||||
}
|
||||
if (uri.startsWith('http')) {
|
||||
String path = Uri.parse(uri).path;
|
||||
if (isStringNumeric(path.split('/')[1])) {
|
||||
// 请求接口
|
||||
var res =
|
||||
await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
|
||||
if (res['status']) {
|
||||
Get.toNamed('/dynamicDetail', arguments: {
|
||||
'item': res['data'],
|
||||
'floor': 1,
|
||||
'action': 'detail'
|
||||
});
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
Get.toNamed('/htmlRender', parameters: {
|
||||
'url': uri,
|
||||
'title': videoItem.title,
|
||||
'id': id,
|
||||
'dynamicType': dynamicType
|
||||
});
|
||||
} catch (err) {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
SmartDialog.showToast(goto);
|
||||
Utils.handleWebview(videoItem.uri!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(children: [
|
||||
Semantics(
|
||||
label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
// customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
// for (var item in actions)
|
||||
// CustomSemanticsAction(label: item.title): item.onTap!,
|
||||
// },
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.aid)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.pic,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if (videoItem.duration > 0)
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: 'small',
|
||||
type: 'gray',
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
// semanticsLabel:
|
||||
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
videoContent(context)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av')
|
||||
Positioned(
|
||||
right: -5,
|
||||
bottom: -2,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
videoItem: videoItem,
|
||||
onRemove: onRemove,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget videoContent(context) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 5, 6, 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text("${videoItem.title}\n",
|
||||
// semanticsLabel: "${videoItem.title}",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1.38,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
// const SizedBox(height: 2),
|
||||
videoStat(context),
|
||||
Row(
|
||||
children: [
|
||||
if (videoItem.goto == 'bangumi') ...[
|
||||
PBadge(
|
||||
text: videoItem.bangumiBadge,
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'line',
|
||||
fs: 9,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem.rcmdReason != null) ...[
|
||||
PBadge(
|
||||
text: videoItem.rcmdReason,
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'color',
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem.goto == 'picture') ...[
|
||||
const PBadge(
|
||||
text: '动态',
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'line',
|
||||
fs: 9,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem.isFollowed) ...[
|
||||
const PBadge(
|
||||
text: '已关注',
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'color',
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
videoItem.owner.name.toString(),
|
||||
// semanticsLabel: "Up主:${videoItem.owner.name}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av') const SizedBox(width: 10)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget videoStat(context) {
|
||||
return Row(
|
||||
children: [
|
||||
StatView(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
value: videoItem.stat.viewStr,
|
||||
goto: videoItem.goto,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (videoItem.goto != 'picture')
|
||||
StatDanMu(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
value: videoItem.stat.danmuStr,
|
||||
),
|
||||
if (videoItem is RecVideoItemModel) ...<Widget>[
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Text.rich(
|
||||
maxLines: 1,
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
text:
|
||||
Utils.formatTimestampToRelativeTime(videoItem.pubdate)),
|
||||
)),
|
||||
const SizedBox(width: 2),
|
||||
],
|
||||
if (videoItem is RecVideoItemAppModel &&
|
||||
videoItem.desc != null &&
|
||||
videoItem.desc!.contains(' · ')) ...<Widget>[
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Text.rich(
|
||||
maxLines: 1,
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.8),
|
||||
),
|
||||
text: Utils.shortenChineseDateString(
|
||||
videoItem.desc!.split(' · ').last)),
|
||||
)),
|
||||
const SizedBox(width: 2),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/http/search.dart';
|
||||
import 'package:PiliPlus/models/space/item.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'badge.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardVMemberHome extends StatelessWidget {
|
||||
final Item videoItem;
|
||||
|
||||
const VideoCardVMemberHome({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
});
|
||||
|
||||
void onPushDetail(heroTag) async {
|
||||
String goto = videoItem.goto ?? '';
|
||||
switch (goto) {
|
||||
case 'bangumi':
|
||||
Utils.viewBangumi(epId: videoItem.param);
|
||||
break;
|
||||
case 'av':
|
||||
if (videoItem.isPgc == true && videoItem.uri?.isNotEmpty == true) {
|
||||
if (Utils.viewPgcFromUri(videoItem.uri!)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
String? aid = videoItem.param;
|
||||
String? bvid = videoItem.bvid;
|
||||
if (aid == null && bvid == null) {
|
||||
return;
|
||||
}
|
||||
int? cid = videoItem.firstCid;
|
||||
cid ??= await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||
Utils.toViewPage(
|
||||
'bvid=${bvid ?? IdUtils.av2bv(int.parse(aid!))}&cid=$cid',
|
||||
arguments: {
|
||||
// 'videoItem': videoItem,
|
||||
'pic': videoItem.cover,
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
break;
|
||||
// 动态
|
||||
// case 'picture':
|
||||
// try {
|
||||
// String dynamicType = 'picture';
|
||||
// String uri = videoItem.uri;
|
||||
// String id = '';
|
||||
// if (videoItem.uri.startsWith('bilibili://article/')) {
|
||||
// // https://www.bilibili.com/read/cv27063554
|
||||
// dynamicType = 'read';
|
||||
// RegExp regex = RegExp(r'\d+');
|
||||
// Match match = regex.firstMatch(videoItem.uri)!;
|
||||
// String matchedNumber = match.group(0)!;
|
||||
// videoItem.param = int.parse(matchedNumber);
|
||||
// id = 'cv${videoItem.param}';
|
||||
// }
|
||||
// if (uri.startsWith('http')) {
|
||||
// String path = Uri.parse(uri).path;
|
||||
// if (isStringNumeric(path.split('/')[1])) {
|
||||
// // 请求接口
|
||||
// var res =
|
||||
// await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
|
||||
// if (res['status']) {
|
||||
// Get.toNamed('/dynamicDetail', arguments: {
|
||||
// 'item': res['data'],
|
||||
// 'floor': 1,
|
||||
// 'action': 'detail'
|
||||
// });
|
||||
// } else {
|
||||
// SmartDialog.showToast(res['msg']);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// Get.toNamed('/htmlRender', parameters: {
|
||||
// 'url': uri,
|
||||
// 'title': videoItem.title,
|
||||
// 'id': id,
|
||||
// 'dynamicType': dynamicType
|
||||
// });
|
||||
// } catch (err) {
|
||||
// SmartDialog.showToast(err.toString());
|
||||
// }
|
||||
// break;
|
||||
default:
|
||||
SmartDialog.showToast(goto);
|
||||
Utils.handleWebview(videoItem.uri ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// List<VideoCustomAction> actions =
|
||||
// VideoCustomActions(videoItem, context).actions;
|
||||
return Stack(children: [
|
||||
Semantics(
|
||||
// label: Utils.videoItemSemantics(videoItem),
|
||||
excludeSemantics: true,
|
||||
// customSemanticsActions: <CustomSemanticsAction, void Function()>{
|
||||
// for (var item in actions)
|
||||
// CustomSemanticsAction(label: item.title): item.onTap!,
|
||||
// },
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.bvid)),
|
||||
onLongPress: () => imageSaveDialog(
|
||||
context: context,
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
if ((videoItem.duration ?? -1) > 0)
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: 'small',
|
||||
type: 'gray',
|
||||
text: Utils.timeFormat(videoItem.duration),
|
||||
// semanticsLabel:
|
||||
// '时长${Utils.durationReadFormat(Utils.timeFormat(videoItem.duration))}',
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
videoContent(context, videoItem)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// if (videoItem.goto == 'av')
|
||||
// Positioned(
|
||||
// right: -5,
|
||||
// bottom: -2,
|
||||
// child: VideoPopupMenu(
|
||||
// size: 29,
|
||||
// iconSize: 17,
|
||||
// actions: actions,
|
||||
// )),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Widget videoContent(BuildContext context, Item videoItem) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(6, 5, 6, 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('${videoItem.title}\n',
|
||||
// semanticsLabel: "${videoItem.title}",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1.38,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
// const Spacer(),
|
||||
// const SizedBox(height: 2),
|
||||
// VideoStat(
|
||||
// videoItem: videoItem,
|
||||
// ),
|
||||
// Row(
|
||||
// children: [
|
||||
// if (videoItem.goto == 'bangumi') ...[
|
||||
// PBadge(
|
||||
// text: videoItem.bangumiBadge,
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'line',
|
||||
// fs: 9,
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.rcmdReason != null) ...[
|
||||
// PBadge(
|
||||
// text: videoItem.rcmdReason,
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'color',
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.goto == 'picture') ...[
|
||||
// const PBadge(
|
||||
// text: '动态',
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'line',
|
||||
// fs: 9,
|
||||
// )
|
||||
// ],
|
||||
// if (videoItem.isFollowed == 1) ...[
|
||||
// const PBadge(
|
||||
// text: '已关注',
|
||||
// stack: 'normal',
|
||||
// size: 'small',
|
||||
// type: 'color',
|
||||
// )
|
||||
// ],
|
||||
// Expanded(
|
||||
// flex: 1,
|
||||
// child: Text(
|
||||
// videoItem.author ?? '',
|
||||
// // semanticsLabel: "Up主:${videoItem.owner.name}",
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.clip,
|
||||
// style: TextStyle(
|
||||
// height: 1.5,
|
||||
// fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
// color: Theme.of(context).colorScheme.outline,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// if (videoItem.goto == 'av') const SizedBox(width: 10)
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget videoStat(BuildContext context, Item videoItem) {
|
||||
// return Row(
|
||||
// children: [
|
||||
// StatView(
|
||||
// theme: 'gray',
|
||||
// view: videoItem.stat.view,
|
||||
// goto: videoItem.goto,
|
||||
// ),
|
||||
// const SizedBox(width: 6),
|
||||
// if (videoItem.goto != 'picture')
|
||||
// StatDanMu(
|
||||
// theme: 'gray',
|
||||
// danmu: videoItem.stat.danmu,
|
||||
// ),
|
||||
// if (videoItem is RecVideoItemModel) ...<Widget>[
|
||||
// const Spacer(),
|
||||
// Expanded(
|
||||
// flex: 0,
|
||||
// child: RichText(
|
||||
// maxLines: 1,
|
||||
// text: TextSpan(
|
||||
// style: TextStyle(
|
||||
// fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
// color:
|
||||
// Theme.of(context).colorScheme.outline.withOpacity(0.8),
|
||||
// ),
|
||||
// text: Utils.formatTimestampToRelativeTime(videoItem.pubdate)),
|
||||
// )),
|
||||
// const SizedBox(width: 2),
|
||||
// ],
|
||||
// if (videoItem is RecVideoItemAppModel &&
|
||||
// videoItem.desc != null &&
|
||||
// videoItem.desc.contains(' · ')) ...<Widget>[
|
||||
// const Spacer(),
|
||||
// Expanded(
|
||||
// flex: 0,
|
||||
// child: RichText(
|
||||
// maxLines: 1,
|
||||
// text: TextSpan(
|
||||
// style: TextStyle(
|
||||
// fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
// color:
|
||||
// Theme.of(context).colorScheme.outline.withOpacity(0.8),
|
||||
// ),
|
||||
// text: Utils.shortenChineseDateString(
|
||||
// videoItem.desc.split(' · ').last)),
|
||||
// )),
|
||||
// const SizedBox(width: 2),
|
||||
// ]
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
@@ -1,18 +1,18 @@
|
||||
import 'package:PiliPlus/http/user.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/common/account_type.dart';
|
||||
import 'package:PiliPlus/models/home/rcmd/result.dart';
|
||||
import 'package:PiliPlus/models/model_video.dart';
|
||||
import 'package:PiliPlus/models/space_archive/item.dart';
|
||||
import 'package:PiliPlus/pages/mine/controller.dart';
|
||||
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
|
||||
import '../../http/user.dart';
|
||||
import '../../http/video.dart';
|
||||
import '../../models/home/rcmd/result.dart';
|
||||
import '../../pages/mine/controller.dart';
|
||||
import '../../utils/storage.dart';
|
||||
import 'package:PiliPlus/models/space_archive/item.dart';
|
||||
|
||||
class VideoCustomAction {
|
||||
String title;
|
||||
String value;
|
||||
@@ -33,213 +33,204 @@ class VideoCustomActions {
|
||||
VideoCustomAction(
|
||||
videoItem.bvid!,
|
||||
'copy',
|
||||
Stack(
|
||||
const Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Icon(MdiIcons.identifier, size: 16),
|
||||
Icon(MdiIcons.circleOutline, size: 16),
|
||||
],
|
||||
),
|
||||
() {
|
||||
Utils.copyText(videoItem.bvid!);
|
||||
},
|
||||
() => Utils.copyText(videoItem.bvid!),
|
||||
),
|
||||
VideoCustomAction(
|
||||
'稍后再看',
|
||||
'pause',
|
||||
Icon(MdiIcons.clockTimeEightOutline, size: 16),
|
||||
const Icon(MdiIcons.clockTimeEightOutline, size: 16),
|
||||
() async {
|
||||
var res = await UserHttp.toViewLater(bvid: videoItem.bvid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (videoItem is! Item)
|
||||
if (videoItem is! SpaceArchiveItem)
|
||||
VideoCustomAction(
|
||||
'访问:${videoItem.owner.name}',
|
||||
'visit',
|
||||
Icon(MdiIcons.accountCircleOutline, size: 16),
|
||||
() async {
|
||||
Get.toNamed('/member?mid=${videoItem.owner.mid}', arguments: {
|
||||
// 'face': videoItem.owner.face,
|
||||
'heroTag': '${videoItem.owner.mid}',
|
||||
});
|
||||
},
|
||||
const Icon(MdiIcons.accountCircleOutline, size: 16),
|
||||
() => Get.toNamed('/member?mid=${videoItem.owner.mid}', arguments: {
|
||||
'heroTag': '${videoItem.owner.mid}',
|
||||
}),
|
||||
),
|
||||
if (videoItem is! Item)
|
||||
if (videoItem is! SpaceArchiveItem)
|
||||
VideoCustomAction(
|
||||
'不感兴趣', 'dislike', Icon(MdiIcons.thumbDownOutline, size: 16),
|
||||
() async {
|
||||
String? accessKey = Accounts.get(AccountType.recommend).accessKey;
|
||||
if (accessKey == null || accessKey == "") {
|
||||
SmartDialog.showToast("请退出账号后重新登录");
|
||||
return;
|
||||
}
|
||||
if (videoItem is RecVideoItemAppModel) {
|
||||
RecVideoItemAppModel v = videoItem as RecVideoItemAppModel;
|
||||
ThreePoint? tp = v.threePoint;
|
||||
if (tp == null) {
|
||||
SmartDialog.showToast("未能获取threePoint");
|
||||
'不感兴趣',
|
||||
'dislike',
|
||||
const Icon(MdiIcons.thumbDownOutline, size: 16),
|
||||
() {
|
||||
String? accessKey = Accounts.get(AccountType.recommend).accessKey;
|
||||
if (accessKey == null || accessKey == "") {
|
||||
SmartDialog.showToast("请退出账号后重新登录");
|
||||
return;
|
||||
}
|
||||
if (tp.dislikeReasons == null && tp.feedbacks == null) {
|
||||
SmartDialog.showToast("未能获取dislikeReasons或feedbacks");
|
||||
return;
|
||||
}
|
||||
Widget actionButton(Reason? r, Reason? f) {
|
||||
return SearchText(
|
||||
text: r?.name ?? f?.name ?? '未知',
|
||||
onTap: (_) async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.feedDislike(
|
||||
reasonId: r?.id,
|
||||
feedbackId: f?.id,
|
||||
id: v.param!,
|
||||
goto: v.goto!,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? (r?.toast ?? f?.toast) : res['msg'],
|
||||
);
|
||||
if (res['status']) {
|
||||
onRemove?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (tp.dislikeReasons != null) ...[
|
||||
Text('我不想看'),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: tp.dislikeReasons!.map((item) {
|
||||
return actionButton(item, null);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
if (tp.feedbacks != null) ...[
|
||||
const SizedBox(height: 5),
|
||||
Text('反馈'),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: tp.feedbacks!.map((item) {
|
||||
return actionButton(null, item);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
const Divider(),
|
||||
Center(
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.feedDislikeCancel(
|
||||
// reasonId: r?.id,
|
||||
// feedbackId: f?.id,
|
||||
id: v.param!,
|
||||
goto: v.goto!,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "成功" : res['msg']);
|
||||
Get.back();
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text("撤销"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (videoItem is RecVideoItemAppModel) {
|
||||
RecVideoItemAppModel v = videoItem as RecVideoItemAppModel;
|
||||
ThreePoint? tp = v.threePoint;
|
||||
if (tp == null) {
|
||||
SmartDialog.showToast("未能获取threePoint");
|
||||
return;
|
||||
}
|
||||
if (tp.dislikeReasons == null && tp.feedbacks == null) {
|
||||
SmartDialog.showToast("未能获取dislikeReasons或feedbacks");
|
||||
return;
|
||||
}
|
||||
Widget actionButton(Reason? r, Reason? f) {
|
||||
return SearchText(
|
||||
text: r?.name ?? f?.name ?? '未知',
|
||||
onTap: (_) async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.feedDislike(
|
||||
reasonId: r?.id,
|
||||
feedbackId: f?.id,
|
||||
id: v.param!,
|
||||
goto: v.goto!,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? (r?.toast ?? f?.toast) : res['msg'],
|
||||
);
|
||||
if (res['status']) {
|
||||
onRemove?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
const Text("web端暂不支持精细选择"),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
runSpacing: 2.0,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.dislikeVideo(
|
||||
bvid: videoItem.bvid as String, type: true);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "点踩成功" : res['msg'],
|
||||
);
|
||||
if (res['status']) {
|
||||
onRemove?.call();
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
child: const Text("点踩"),
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (tp.dislikeReasons != null) ...[
|
||||
const Text('我不想看'),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: tp.dislikeReasons!.map((item) {
|
||||
return actionButton(item, null);
|
||||
}).toList(),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
],
|
||||
if (tp.feedbacks != null) ...[
|
||||
const SizedBox(height: 5),
|
||||
const Text('反馈'),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: tp.feedbacks!.map((item) {
|
||||
return actionButton(null, item);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
const Divider(),
|
||||
Center(
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.dislikeVideo(
|
||||
bvid: videoItem.bvid as String,
|
||||
type: false);
|
||||
var res = await VideoHttp.feedDislikeCancel(
|
||||
id: v.param!,
|
||||
goto: v.goto!,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "取消踩" : res['msg']);
|
||||
res['status'] ? "成功" : res['msg']);
|
||||
Get.back();
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -2,
|
||||
vertical: -2,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: const Text("撤销"),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
if (videoItem is! Item)
|
||||
VideoCustomAction('拉黑:${videoItem.owner.name}', 'block',
|
||||
Icon(MdiIcons.cancel, size: 16), () async {
|
||||
await showDialog(
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
const Text("web端暂不支持精细选择"),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 5.0,
|
||||
runSpacing: 2.0,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.dislikeVideo(
|
||||
bvid: videoItem.bvid as String,
|
||||
type: true);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "点踩成功" : res['msg'],
|
||||
);
|
||||
if (res['status']) {
|
||||
onRemove?.call();
|
||||
}
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: const Text("点踩"),
|
||||
),
|
||||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
SmartDialog.showLoading(msg: '正在提交');
|
||||
var res = await VideoHttp.dislikeVideo(
|
||||
bvid: videoItem.bvid as String,
|
||||
type: false);
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast(
|
||||
res['status'] ? "取消踩" : res['msg']);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: const Text("撤销"),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (videoItem is! SpaceArchiveItem)
|
||||
VideoCustomAction(
|
||||
'拉黑:${videoItem.owner.name}',
|
||||
'block',
|
||||
const Icon(MdiIcons.cancel, size: 16),
|
||||
() => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
@@ -249,7 +240,7 @@ class VideoCustomActions {
|
||||
'\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'点错了',
|
||||
style: TextStyle(
|
||||
@@ -272,18 +263,16 @@ class VideoCustomActions {
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
VideoCustomAction(
|
||||
"${MineController.anonymity.value ? '退出' : '进入'}无痕模式",
|
||||
'anonymity',
|
||||
Icon(
|
||||
MineController.anonymity.value
|
||||
? MdiIcons.incognitoOff
|
||||
: MdiIcons.incognito,
|
||||
size: 16,
|
||||
),
|
||||
() => MineController.onChangeAnonymity(context))
|
||||
),
|
||||
VideoCustomAction(
|
||||
"${MineController.anonymity.value ? '退出' : '进入'}无痕模式",
|
||||
'anonymity',
|
||||
MineController.anonymity.value
|
||||
? const Icon(MdiIcons.incognitoOff, size: 16)
|
||||
: const Icon(MdiIcons.incognito, size: 16),
|
||||
() => MineController.onChangeAnonymity(context),
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -291,7 +280,7 @@ class VideoCustomActions {
|
||||
class VideoPopupMenu extends StatelessWidget {
|
||||
final double? size;
|
||||
final double? iconSize;
|
||||
final double menuItemHeight = 45;
|
||||
final double menuItemHeight;
|
||||
final dynamic videoItem;
|
||||
final VoidCallback? onRemove;
|
||||
|
||||
@@ -301,6 +290,7 @@ class VideoPopupMenu extends StatelessWidget {
|
||||
required this.iconSize,
|
||||
required this.videoItem,
|
||||
this.onRemove,
|
||||
this.menuItemHeight = 45,
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: bilibili/app/archive/middleware/v1/preload.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: bilibili/app/card/v1/ad.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:fixnum/fixnum.dart' as $fixnum;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class AdInfo extends $pb.GeneratedMessage {
|
||||
factory AdInfo({
|
||||
$fixnum.Int64? creativeId,
|
||||
$core.int? creativeType,
|
||||
$core.int? cardType,
|
||||
CreativeContent? creativeContent,
|
||||
$core.String? adCb,
|
||||
$fixnum.Int64? resource,
|
||||
$core.int? source,
|
||||
$core.String? requestId,
|
||||
$core.bool? isAd,
|
||||
$fixnum.Int64? cmMark,
|
||||
$core.int? index,
|
||||
$core.bool? isAdLoc,
|
||||
$core.int? cardIndex,
|
||||
$core.String? clientIp,
|
||||
$core.List<$core.int>? extra,
|
||||
$core.int? creativeStyle,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (creativeId != null) {
|
||||
$result.creativeId = creativeId;
|
||||
}
|
||||
if (creativeType != null) {
|
||||
$result.creativeType = creativeType;
|
||||
}
|
||||
if (cardType != null) {
|
||||
$result.cardType = cardType;
|
||||
}
|
||||
if (creativeContent != null) {
|
||||
$result.creativeContent = creativeContent;
|
||||
}
|
||||
if (adCb != null) {
|
||||
$result.adCb = adCb;
|
||||
}
|
||||
if (resource != null) {
|
||||
$result.resource = resource;
|
||||
}
|
||||
if (source != null) {
|
||||
$result.source = source;
|
||||
}
|
||||
if (requestId != null) {
|
||||
$result.requestId = requestId;
|
||||
}
|
||||
if (isAd != null) {
|
||||
$result.isAd = isAd;
|
||||
}
|
||||
if (cmMark != null) {
|
||||
$result.cmMark = cmMark;
|
||||
}
|
||||
if (index != null) {
|
||||
$result.index = index;
|
||||
}
|
||||
if (isAdLoc != null) {
|
||||
$result.isAdLoc = isAdLoc;
|
||||
}
|
||||
if (cardIndex != null) {
|
||||
$result.cardIndex = cardIndex;
|
||||
}
|
||||
if (clientIp != null) {
|
||||
$result.clientIp = clientIp;
|
||||
}
|
||||
if (extra != null) {
|
||||
$result.extra = extra;
|
||||
}
|
||||
if (creativeStyle != null) {
|
||||
$result.creativeStyle = creativeStyle;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
AdInfo._() : super();
|
||||
factory AdInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory AdInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AdInfo', package: const $pb.PackageName(_omitMessageNames ? '' : 'bilibili.app.card.v1'), createEmptyInstance: create)
|
||||
..aInt64(1, _omitFieldNames ? '' : 'creativeId')
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'creativeType', $pb.PbFieldType.O3)
|
||||
..a<$core.int>(3, _omitFieldNames ? '' : 'cardType', $pb.PbFieldType.O3)
|
||||
..aOM<CreativeContent>(4, _omitFieldNames ? '' : 'creativeContent', subBuilder: CreativeContent.create)
|
||||
..aOS(5, _omitFieldNames ? '' : 'adCb')
|
||||
..aInt64(6, _omitFieldNames ? '' : 'resource')
|
||||
..a<$core.int>(7, _omitFieldNames ? '' : 'source', $pb.PbFieldType.O3)
|
||||
..aOS(8, _omitFieldNames ? '' : 'requestId')
|
||||
..aOB(9, _omitFieldNames ? '' : 'isAd')
|
||||
..aInt64(10, _omitFieldNames ? '' : 'cmMark')
|
||||
..a<$core.int>(11, _omitFieldNames ? '' : 'index', $pb.PbFieldType.O3)
|
||||
..aOB(12, _omitFieldNames ? '' : 'isAdLoc')
|
||||
..a<$core.int>(13, _omitFieldNames ? '' : 'cardIndex', $pb.PbFieldType.O3)
|
||||
..aOS(14, _omitFieldNames ? '' : 'clientIp')
|
||||
..a<$core.List<$core.int>>(15, _omitFieldNames ? '' : 'extra', $pb.PbFieldType.OY)
|
||||
..a<$core.int>(16, _omitFieldNames ? '' : 'creativeStyle', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
AdInfo clone() => AdInfo()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
AdInfo copyWith(void Function(AdInfo) updates) => super.copyWith((message) => updates(message as AdInfo)) as AdInfo;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static AdInfo create() => AdInfo._();
|
||||
AdInfo createEmptyInstance() => create();
|
||||
static $pb.PbList<AdInfo> createRepeated() => $pb.PbList<AdInfo>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static AdInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AdInfo>(create);
|
||||
static AdInfo? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$fixnum.Int64 get creativeId => $_getI64(0);
|
||||
@$pb.TagNumber(1)
|
||||
set creativeId($fixnum.Int64 v) { $_setInt64(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasCreativeId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearCreativeId() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get creativeType => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set creativeType($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasCreativeType() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearCreativeType() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get cardType => $_getIZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set cardType($core.int v) { $_setSignedInt32(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasCardType() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearCardType() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
CreativeContent get creativeContent => $_getN(3);
|
||||
@$pb.TagNumber(4)
|
||||
set creativeContent(CreativeContent v) { setField(4, v); }
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasCreativeContent() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearCreativeContent() => clearField(4);
|
||||
@$pb.TagNumber(4)
|
||||
CreativeContent ensureCreativeContent() => $_ensure(3);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.String get adCb => $_getSZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set adCb($core.String v) { $_setString(4, v); }
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasAdCb() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearAdCb() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$fixnum.Int64 get resource => $_getI64(5);
|
||||
@$pb.TagNumber(6)
|
||||
set resource($fixnum.Int64 v) { $_setInt64(5, v); }
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasResource() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearResource() => clearField(6);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
$core.int get source => $_getIZ(6);
|
||||
@$pb.TagNumber(7)
|
||||
set source($core.int v) { $_setSignedInt32(6, v); }
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasSource() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearSource() => clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.String get requestId => $_getSZ(7);
|
||||
@$pb.TagNumber(8)
|
||||
set requestId($core.String v) { $_setString(7, v); }
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasRequestId() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearRequestId() => clearField(8);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool get isAd => $_getBF(8);
|
||||
@$pb.TagNumber(9)
|
||||
set isAd($core.bool v) { $_setBool(8, v); }
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasIsAd() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearIsAd() => clearField(9);
|
||||
|
||||
@$pb.TagNumber(10)
|
||||
$fixnum.Int64 get cmMark => $_getI64(9);
|
||||
@$pb.TagNumber(10)
|
||||
set cmMark($fixnum.Int64 v) { $_setInt64(9, v); }
|
||||
@$pb.TagNumber(10)
|
||||
$core.bool hasCmMark() => $_has(9);
|
||||
@$pb.TagNumber(10)
|
||||
void clearCmMark() => clearField(10);
|
||||
|
||||
@$pb.TagNumber(11)
|
||||
$core.int get index => $_getIZ(10);
|
||||
@$pb.TagNumber(11)
|
||||
set index($core.int v) { $_setSignedInt32(10, v); }
|
||||
@$pb.TagNumber(11)
|
||||
$core.bool hasIndex() => $_has(10);
|
||||
@$pb.TagNumber(11)
|
||||
void clearIndex() => clearField(11);
|
||||
|
||||
@$pb.TagNumber(12)
|
||||
$core.bool get isAdLoc => $_getBF(11);
|
||||
@$pb.TagNumber(12)
|
||||
set isAdLoc($core.bool v) { $_setBool(11, v); }
|
||||
@$pb.TagNumber(12)
|
||||
$core.bool hasIsAdLoc() => $_has(11);
|
||||
@$pb.TagNumber(12)
|
||||
void clearIsAdLoc() => clearField(12);
|
||||
|
||||
@$pb.TagNumber(13)
|
||||
$core.int get cardIndex => $_getIZ(12);
|
||||
@$pb.TagNumber(13)
|
||||
set cardIndex($core.int v) { $_setSignedInt32(12, v); }
|
||||
@$pb.TagNumber(13)
|
||||
$core.bool hasCardIndex() => $_has(12);
|
||||
@$pb.TagNumber(13)
|
||||
void clearCardIndex() => clearField(13);
|
||||
|
||||
@$pb.TagNumber(14)
|
||||
$core.String get clientIp => $_getSZ(13);
|
||||
@$pb.TagNumber(14)
|
||||
set clientIp($core.String v) { $_setString(13, v); }
|
||||
@$pb.TagNumber(14)
|
||||
$core.bool hasClientIp() => $_has(13);
|
||||
@$pb.TagNumber(14)
|
||||
void clearClientIp() => clearField(14);
|
||||
|
||||
@$pb.TagNumber(15)
|
||||
$core.List<$core.int> get extra => $_getN(14);
|
||||
@$pb.TagNumber(15)
|
||||
set extra($core.List<$core.int> v) { $_setBytes(14, v); }
|
||||
@$pb.TagNumber(15)
|
||||
$core.bool hasExtra() => $_has(14);
|
||||
@$pb.TagNumber(15)
|
||||
void clearExtra() => clearField(15);
|
||||
|
||||
@$pb.TagNumber(16)
|
||||
$core.int get creativeStyle => $_getIZ(15);
|
||||
@$pb.TagNumber(16)
|
||||
set creativeStyle($core.int v) { $_setSignedInt32(15, v); }
|
||||
@$pb.TagNumber(16)
|
||||
$core.bool hasCreativeStyle() => $_has(15);
|
||||
@$pb.TagNumber(16)
|
||||
void clearCreativeStyle() => clearField(16);
|
||||
}
|
||||
|
||||
class CreativeContent extends $pb.GeneratedMessage {
|
||||
factory CreativeContent({
|
||||
$core.String? title,
|
||||
$core.String? description,
|
||||
$fixnum.Int64? videoId,
|
||||
$core.String? username,
|
||||
$core.String? imageUrl,
|
||||
$core.String? imageMd5,
|
||||
$core.String? logUrl,
|
||||
$core.String? logMd5,
|
||||
$core.String? url,
|
||||
$core.String? clickUrl,
|
||||
$core.String? showUrl,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (title != null) {
|
||||
$result.title = title;
|
||||
}
|
||||
if (description != null) {
|
||||
$result.description = description;
|
||||
}
|
||||
if (videoId != null) {
|
||||
$result.videoId = videoId;
|
||||
}
|
||||
if (username != null) {
|
||||
$result.username = username;
|
||||
}
|
||||
if (imageUrl != null) {
|
||||
$result.imageUrl = imageUrl;
|
||||
}
|
||||
if (imageMd5 != null) {
|
||||
$result.imageMd5 = imageMd5;
|
||||
}
|
||||
if (logUrl != null) {
|
||||
$result.logUrl = logUrl;
|
||||
}
|
||||
if (logMd5 != null) {
|
||||
$result.logMd5 = logMd5;
|
||||
}
|
||||
if (url != null) {
|
||||
$result.url = url;
|
||||
}
|
||||
if (clickUrl != null) {
|
||||
$result.clickUrl = clickUrl;
|
||||
}
|
||||
if (showUrl != null) {
|
||||
$result.showUrl = showUrl;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
CreativeContent._() : super();
|
||||
factory CreativeContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory CreativeContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CreativeContent', package: const $pb.PackageName(_omitMessageNames ? '' : 'bilibili.app.card.v1'), createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'title')
|
||||
..aOS(2, _omitFieldNames ? '' : 'description')
|
||||
..aInt64(3, _omitFieldNames ? '' : 'videoId')
|
||||
..aOS(4, _omitFieldNames ? '' : 'username')
|
||||
..aOS(5, _omitFieldNames ? '' : 'imageUrl')
|
||||
..aOS(6, _omitFieldNames ? '' : 'imageMd5')
|
||||
..aOS(7, _omitFieldNames ? '' : 'logUrl')
|
||||
..aOS(8, _omitFieldNames ? '' : 'logMd5')
|
||||
..aOS(9, _omitFieldNames ? '' : 'url')
|
||||
..aOS(10, _omitFieldNames ? '' : 'clickUrl')
|
||||
..aOS(11, _omitFieldNames ? '' : 'showUrl')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
CreativeContent clone() => CreativeContent()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
CreativeContent copyWith(void Function(CreativeContent) updates) => super.copyWith((message) => updates(message as CreativeContent)) as CreativeContent;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static CreativeContent create() => CreativeContent._();
|
||||
CreativeContent createEmptyInstance() => create();
|
||||
static $pb.PbList<CreativeContent> createRepeated() => $pb.PbList<CreativeContent>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static CreativeContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<CreativeContent>(create);
|
||||
static CreativeContent? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get title => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set title($core.String v) { $_setString(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasTitle() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearTitle() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get description => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set description($core.String v) { $_setString(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasDescription() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearDescription() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$fixnum.Int64 get videoId => $_getI64(2);
|
||||
@$pb.TagNumber(3)
|
||||
set videoId($fixnum.Int64 v) { $_setInt64(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasVideoId() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearVideoId() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.String get username => $_getSZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set username($core.String v) { $_setString(3, v); }
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasUsername() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearUsername() => clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.String get imageUrl => $_getSZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set imageUrl($core.String v) { $_setString(4, v); }
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasImageUrl() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearImageUrl() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.String get imageMd5 => $_getSZ(5);
|
||||
@$pb.TagNumber(6)
|
||||
set imageMd5($core.String v) { $_setString(5, v); }
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasImageMd5() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearImageMd5() => clearField(6);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
$core.String get logUrl => $_getSZ(6);
|
||||
@$pb.TagNumber(7)
|
||||
set logUrl($core.String v) { $_setString(6, v); }
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasLogUrl() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearLogUrl() => clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.String get logMd5 => $_getSZ(7);
|
||||
@$pb.TagNumber(8)
|
||||
set logMd5($core.String v) { $_setString(7, v); }
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasLogMd5() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearLogMd5() => clearField(8);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$core.String get url => $_getSZ(8);
|
||||
@$pb.TagNumber(9)
|
||||
set url($core.String v) { $_setString(8, v); }
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasUrl() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearUrl() => clearField(9);
|
||||
|
||||
@$pb.TagNumber(10)
|
||||
$core.String get clickUrl => $_getSZ(9);
|
||||
@$pb.TagNumber(10)
|
||||
set clickUrl($core.String v) { $_setString(9, v); }
|
||||
@$pb.TagNumber(10)
|
||||
$core.bool hasClickUrl() => $_has(9);
|
||||
@$pb.TagNumber(10)
|
||||
void clearClickUrl() => clearField(10);
|
||||
|
||||
@$pb.TagNumber(11)
|
||||
$core.String get showUrl => $_getSZ(10);
|
||||
@$pb.TagNumber(11)
|
||||
set showUrl($core.String v) { $_setString(10, v); }
|
||||
@$pb.TagNumber(11)
|
||||
$core.bool hasShowUrl() => $_has(10);
|
||||
@$pb.TagNumber(11)
|
||||
void clearShowUrl() => clearField(11);
|
||||
}
|
||||
|
||||
|
||||
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||