Compare commits
639 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc6f51787b | ||
|
|
e72203afdb | ||
|
|
6741333367 | ||
|
|
6e1bc8d0e7 | ||
|
|
477b59ce89 | ||
|
|
70881ead22 | ||
|
|
b09a41af24 | ||
|
|
08a33d9ce5 | ||
|
|
84f7f14a29 | ||
|
|
331c9877a3 | ||
|
|
ac26022da1 | ||
|
|
7a5662c6ca | ||
|
|
659cff875f | ||
|
|
06a5c2c63b | ||
|
|
077293854c | ||
|
|
cf24f851e8 | ||
|
|
01a8631e00 | ||
|
|
5f8313901b | ||
|
|
56ffc2781f | ||
|
|
51d7e454de | ||
|
|
63419d5b1c | ||
|
|
91627df804 | ||
|
|
fb8a06787b | ||
|
|
dc7fe2cb3b | ||
|
|
1f22dcd73f | ||
|
|
09b0f19775 | ||
|
|
8498ea0618 | ||
|
|
a366b8a9e4 | ||
|
|
461e91239e | ||
|
|
4bba675063 | ||
|
|
08d64be5d4 | ||
|
|
7d30c9c66a | ||
|
|
fe191ef934 | ||
|
|
db8b5f5e66 | ||
|
|
6c52db1c6c | ||
|
|
f942b2a7ee | ||
|
|
288d554de9 | ||
|
|
a274f5ae8b | ||
|
|
ad0d9ecee0 | ||
|
|
ee819bb260 | ||
|
|
b77403f03f | ||
|
|
3c34e43827 | ||
|
|
6009668427 | ||
|
|
16a3e21db4 | ||
|
|
d69649f1b6 | ||
|
|
faaffd0f30 | ||
|
|
a9f1e3cf09 | ||
|
|
9e72fea67c | ||
|
|
8fc8bd99e5 | ||
|
|
4d3a74f2e0 | ||
|
|
272cfcb829 | ||
|
|
c7437225eb | ||
|
|
d4a1568b28 | ||
|
|
824ee53025 | ||
|
|
ee142e5e1d | ||
|
|
571bdb5eae | ||
|
|
2e5cb324a1 | ||
|
|
ed191e20b4 | ||
|
|
ba14e56ceb | ||
|
|
b6ce93cbd2 | ||
|
|
76f1d0129b | ||
|
|
e096ebcbba | ||
|
|
6c8baa5be5 | ||
|
|
4f2bfb8126 | ||
|
|
33738c90bc | ||
|
|
7ff95c00d2 | ||
|
|
fc4f92e0c0 | ||
|
|
ed57697fdc | ||
|
|
08c3789321 | ||
|
|
43fa00848d | ||
|
|
8c38699334 | ||
|
|
dcc5f51e6a | ||
|
|
dc6b76812c | ||
|
|
470545337d | ||
|
|
ab610e9da5 | ||
|
|
5420712bda | ||
|
|
55733d30c5 | ||
|
|
2090fd2312 | ||
|
|
f3bad60fb6 | ||
|
|
d805306d20 | ||
|
|
831a3052fa | ||
|
|
52151765f8 | ||
|
|
422b413778 | ||
|
|
1943b65788 | ||
|
|
629be129ff | ||
|
|
6ff256637a | ||
|
|
34e9afd7ad | ||
|
|
0cd57c9bb0 | ||
|
|
22d9fbddf9 | ||
|
|
65746ae2bd | ||
|
|
685852c0a4 | ||
|
|
b2100f3872 | ||
|
|
86125d5ecd | ||
|
|
086c93d24f | ||
|
|
aea1992f5d | ||
|
|
6b38322c3b | ||
|
|
865ddad147 | ||
|
|
6709fa4d21 | ||
|
|
705417f65b | ||
|
|
690c4f5786 | ||
|
|
e00c176bdf | ||
|
|
8d14f42fd8 | ||
|
|
6688fcf3e9 | ||
|
|
308bd26172 | ||
|
|
a94493705d | ||
|
|
e251eaf811 | ||
|
|
1826b6a059 | ||
|
|
be5a1af040 | ||
|
|
17b7eb7e0f | ||
|
|
60c25e4b65 | ||
|
|
2c92845af0 | ||
|
|
4a4aa569ec | ||
|
|
95f1d1485d | ||
|
|
e7f27e4913 | ||
|
|
dc61d9007f | ||
|
|
88c2ba8059 | ||
|
|
309c871919 | ||
|
|
745a510ffa | ||
|
|
8fbc8fda3d | ||
|
|
dbde90459b | ||
|
|
b788794f4b | ||
|
|
06b433aa60 | ||
|
|
6093848811 | ||
|
|
34c5d6812f | ||
|
|
aaad7fc6dc | ||
|
|
fac37e59aa | ||
|
|
11c6745fd7 | ||
|
|
30aa29598b | ||
|
|
85c72731f6 | ||
|
|
27c9c266c1 | ||
|
|
720f3e10e8 | ||
|
|
162a79145f | ||
|
|
9e31326bf5 | ||
|
|
e77fe2587c | ||
|
|
c75a68dacc | ||
|
|
16fa47e8e9 | ||
|
|
2df6c91a3d | ||
|
|
bd490b87ca | ||
|
|
597fca9fbf | ||
|
|
810505ea1d | ||
|
|
d108373c33 | ||
|
|
c0287d05be | ||
|
|
be998b8ee1 | ||
|
|
ef1ccabc8a | ||
|
|
edb5ea7a7a | ||
|
|
b4c1568869 | ||
|
|
83e25ec0bf | ||
|
|
6d55321699 | ||
|
|
26a5b7b7a7 | ||
|
|
f663301eae | ||
|
|
eb9f3cd21c | ||
|
|
05119edacb | ||
|
|
554e96c820 | ||
|
|
40a19f2766 | ||
|
|
b723529d7f | ||
|
|
9f33488248 | ||
|
|
80a4c8c24d | ||
|
|
170b2aa6d9 | ||
|
|
e2639b6951 | ||
|
|
b954c6f893 | ||
|
|
104d295389 | ||
|
|
3caa684b2e | ||
|
|
af7a1a6ee9 | ||
|
|
add519120c | ||
|
|
01552801f2 | ||
|
|
afb09e8a0a | ||
|
|
deb48d1ada | ||
|
|
cf84a92808 | ||
|
|
26ccb92b44 | ||
|
|
3fa697a037 | ||
|
|
f72c13df62 | ||
|
|
7b51f15753 | ||
|
|
d246462535 | ||
|
|
3208661a52 | ||
|
|
2e614fa03c | ||
|
|
b7f70ee0b3 | ||
|
|
cb52840bad | ||
|
|
bd3d6cf34c | ||
|
|
cf835e330b | ||
|
|
14fd660ce2 | ||
|
|
0a8282d3e3 | ||
|
|
574e432e09 | ||
|
|
4b9f251dae | ||
|
|
f0e2a63d11 | ||
|
|
3c964787df | ||
|
|
199ddc0e7e | ||
|
|
1071a29b26 | ||
|
|
90ce74cf91 | ||
|
|
05bb27ee2b | ||
|
|
53ef4219eb | ||
|
|
dd5c2229b3 | ||
|
|
5c28376210 | ||
|
|
aa8eef46da | ||
|
|
f7d4db6aad | ||
|
|
edc9a1ca7b | ||
|
|
05c9269531 | ||
|
|
e945daba3a | ||
|
|
1029621b63 | ||
|
|
c8613fbe07 | ||
|
|
c4e87925cf | ||
|
|
83e5095cc3 | ||
|
|
a57323e5a8 | ||
|
|
3eb9c5b8ba | ||
|
|
cf403aaf78 | ||
|
|
2325814f6d | ||
|
|
e5c86e1d2e | ||
|
|
26c420023f | ||
|
|
cbb838fff8 | ||
|
|
3c466d5748 | ||
|
|
db79a03ec4 | ||
|
|
65b432ed2c | ||
|
|
6ca7efe8d1 | ||
|
|
916931dd11 | ||
|
|
819a28c48c | ||
|
|
f281e6e36a | ||
|
|
c46058ef4d | ||
|
|
39cc42d542 | ||
|
|
3a78ead3a6 | ||
|
|
a05ecd020b | ||
|
|
e00f009a64 | ||
|
|
b977f5228e | ||
|
|
4003ca6c4d | ||
|
|
9072d6e051 | ||
|
|
bb36876d1e | ||
|
|
d17dbe139e | ||
|
|
d567c296f8 | ||
|
|
0c6bc9d58a | ||
|
|
6d48c70020 | ||
|
|
569484014e | ||
|
|
c89a39cf5c | ||
|
|
418a1e8d39 | ||
|
|
148e0872b4 | ||
|
|
b1432b5ff5 | ||
|
|
75e86952fd | ||
|
|
03b095905a | ||
|
|
77a444b896 | ||
|
|
e770e39c8f | ||
|
|
55bed2e830 | ||
|
|
a875ff3988 | ||
|
|
a4a866d3f5 | ||
|
|
4e5c4169fa | ||
|
|
fbf47d7485 | ||
|
|
ba16f3d597 | ||
|
|
8a62f5bbee | ||
|
|
042a7df7f3 | ||
|
|
610ed02dd4 | ||
|
|
f7184aff4e | ||
|
|
473515efc5 | ||
|
|
aee65b0a9c | ||
|
|
e46488d11e | ||
|
|
f43bc74868 | ||
|
|
f223befad6 | ||
|
|
e0243461bb | ||
|
|
2877372f67 | ||
|
|
d6c12195f8 | ||
|
|
e280f6ee4a | ||
|
|
4275719844 | ||
|
|
f41af00b31 | ||
|
|
10ed5f2ea4 | ||
|
|
44ba554e0e | ||
|
|
c346d586a5 | ||
|
|
52fb332378 | ||
|
|
5f5387b941 | ||
|
|
db682066ba | ||
|
|
3ee8c68eac | ||
|
|
a9ceb04d07 | ||
|
|
f60a714c06 | ||
|
|
e240a6caae | ||
|
|
829b966382 | ||
|
|
58f3949a22 | ||
|
|
dfb823c30c | ||
|
|
b32922af8f | ||
|
|
753e10ef20 | ||
|
|
05153fda72 | ||
|
|
8bf55ec95a | ||
|
|
d2023b1750 | ||
|
|
b51c6b65a1 | ||
|
|
e3337f1e7c | ||
|
|
5ff6ef8801 | ||
|
|
74f7c5d0ea | ||
|
|
b43c07bd51 | ||
|
|
7cdcd6df97 | ||
|
|
7439160f03 | ||
|
|
b496ea4da4 | ||
|
|
0f1665bf08 | ||
|
|
83459df3b7 | ||
|
|
9ce84fb997 | ||
|
|
708bf27710 | ||
|
|
dae64e74d5 | ||
|
|
8414c0f71f | ||
|
|
18f5ddd937 | ||
|
|
a231492f49 | ||
|
|
6f2570c5be | ||
|
|
721bf2d59f | ||
|
|
e5301c3cf8 | ||
|
|
20893ef65f | ||
|
|
12c13cd25a | ||
|
|
81f72e2c4a | ||
|
|
d2e5e71729 | ||
|
|
158e8f7cb8 | ||
|
|
7886a901a3 | ||
|
|
0264a4c01f | ||
|
|
2eb86658b7 | ||
|
|
0b95476d8f | ||
|
|
27023a305d | ||
|
|
ef7cfdd92e | ||
|
|
4b067c5ed2 | ||
|
|
7be3774675 | ||
|
|
fcf758e290 | ||
|
|
79e30047f5 | ||
|
|
c6a377b9d4 | ||
|
|
bc3ce66322 | ||
|
|
17568c8c27 | ||
|
|
a1555826c3 | ||
|
|
07b7c42f13 | ||
|
|
2d66b1d8ca | ||
|
|
604d78ad6a | ||
|
|
5f3f158932 | ||
|
|
345402d2fe | ||
|
|
0bc0c36f14 | ||
|
|
dcb893ed07 | ||
|
|
3bfb0db307 | ||
|
|
9b8d4a62fa | ||
|
|
6f48a97b4b | ||
|
|
5644e9a0e1 | ||
|
|
f440edf43b | ||
|
|
30a8b4d25c | ||
|
|
41245d5256 | ||
|
|
89b1a63946 | ||
|
|
448d7c38db | ||
|
|
cc4100d74f | ||
|
|
768f3e20b1 | ||
|
|
91a1b77d83 | ||
|
|
9d9784f3c2 | ||
|
|
6c6c4cffd2 | ||
|
|
cb167dae29 | ||
|
|
0bf9d13967 | ||
|
|
0963713fad | ||
|
|
d69a996be4 | ||
|
|
fcdb04b728 | ||
|
|
a2c24fb33c | ||
|
|
25f4ed6636 | ||
|
|
a0bed68c79 | ||
|
|
75c2cf70a0 | ||
|
|
f3b9749a85 | ||
|
|
c05fbde3fa | ||
|
|
f824477ddb | ||
|
|
54fe38047f | ||
|
|
de9d16cc61 | ||
|
|
7c6f82891d | ||
|
|
4e710fca79 | ||
|
|
4f3f01d80a | ||
|
|
4a4cd3017f | ||
|
|
89a418c7c5 | ||
|
|
f4d3ec39a0 | ||
|
|
3655c31a48 | ||
|
|
bc2de4828b | ||
|
|
206602e49a | ||
|
|
f5d52237c5 | ||
|
|
88288f4a7a | ||
|
|
bdf3cfc750 | ||
|
|
4c758bb1a3 | ||
|
|
5f77a8aa19 | ||
|
|
9fbe824d6d | ||
|
|
d61706d4f3 | ||
|
|
208db62d93 | ||
|
|
10efd96788 | ||
|
|
f1e4130201 | ||
|
|
63a286056c | ||
|
|
dc9b345e99 | ||
|
|
c67866a148 | ||
|
|
d893102939 | ||
|
|
82b4f76b95 | ||
|
|
a6ac2c4522 | ||
|
|
fdb817cadd | ||
|
|
c7dabba3b2 | ||
|
|
19e4ae6c04 | ||
|
|
6ec0d8f589 | ||
|
|
f151e63923 | ||
|
|
f77f853fd1 | ||
|
|
930afa4c60 | ||
|
|
bffcfd1f90 | ||
|
|
e3c920dc87 | ||
|
|
13f1392821 | ||
|
|
7376fc788a | ||
|
|
5c1312bbcd | ||
|
|
db4283af4a | ||
|
|
77e418e4b7 | ||
|
|
ccde326e38 | ||
|
|
4a00b45c5c | ||
|
|
b149ee4998 | ||
|
|
707d2f4b07 | ||
|
|
b960359a39 | ||
|
|
f50b1d2beb | ||
|
|
50efe1e24c | ||
|
|
daf5d302e3 | ||
|
|
84e24b5827 | ||
|
|
19cf085e3e | ||
|
|
459d7cb9f1 | ||
|
|
e56e216c59 | ||
|
|
08c9ebc42e | ||
|
|
924fb4bf81 | ||
|
|
f60c0b9a10 | ||
|
|
7c0d161b9a | ||
|
|
0a8b632200 | ||
|
|
401f5268a6 | ||
|
|
d508e0822e | ||
|
|
6147df2030 | ||
|
|
b990f9cf87 | ||
|
|
0fb01f1b7c | ||
|
|
91fe0492c1 | ||
|
|
19bbdaac65 | ||
|
|
1462e6ecf1 | ||
|
|
3364b52e33 | ||
|
|
4ac05caa28 | ||
|
|
132a7e15de | ||
|
|
c2c8a5166b | ||
|
|
a260b1640a | ||
|
|
3031d5e3b0 | ||
|
|
5f2e863cc2 | ||
|
|
9a63e23478 | ||
|
|
c9450992d9 | ||
|
|
8aeb035e55 | ||
|
|
924d51d41b | ||
|
|
b643cb1bd0 | ||
|
|
1f77ee178e | ||
|
|
6d599891dc | ||
|
|
4e9fdfbfbd | ||
|
|
d4ac9ab79a | ||
|
|
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 |
26
.github/ISSUE_TEMPLATE/bug-反馈.yml
vendored
@@ -9,15 +9,7 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的 bug report。1
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的 bug report。2
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的 bug report。3
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的 bug report。4
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的 bug report。5
|
||||
- label: 之前没有人提交过类似或相同的 bug report。
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
@@ -29,14 +21,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 请提供一个清晰而简明的问题描述。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
@@ -53,6 +37,14 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际行为
|
||||
description: 请描述实际的行为或结果。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/功能请求.yml
vendored
@@ -9,15 +9,7 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的功能请求。1
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。2
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。3
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。4
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。5
|
||||
- label: 之前没有人提交过类似或相同的功能请求。
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
@@ -30,14 +22,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: propose
|
||||
attributes:
|
||||
label: 目标
|
||||
description: 请描述你希望通过这个功能实现的目标。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
|
||||
24
.github/workflows/android.yml
vendored
@@ -1,6 +1,14 @@
|
||||
name: Android Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -33,19 +41,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
|
||||
|
||||
@@ -55,6 +50,7 @@ jobs:
|
||||
sed -i "s/version: .*/version: $version_name-$(git rev-parse --short HEAD)+$(git rev-list --count HEAD)/g" pubspec.yaml
|
||||
|
||||
- name: Write key
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
|
||||
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
|
||||
@@ -89,4 +85,4 @@ jobs:
|
||||
with:
|
||||
name: app-x86_64
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
|
||||
10
.github/workflows/ios.yml
vendored
@@ -1,6 +1,14 @@
|
||||
name: Build for iOS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
@@ -10,7 +18,7 @@ on:
|
||||
jobs:
|
||||
build-macos-app:
|
||||
name: Release IOS
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
4
.gitignore
vendored
@@ -135,4 +135,6 @@ app.*.symbols
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
!.vscode/settings.json
|
||||
|
||||
/lib/build_config.dart
|
||||
/lib/build_config.dart
|
||||
|
||||
devtools_options.yaml
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -2,5 +2,9 @@
|
||||
"editor.formatOnSave": true,
|
||||
"[dart]": {
|
||||
"editor.formatOnType": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit",
|
||||
// "source.fixAll": "explicit",
|
||||
}
|
||||
}
|
||||
13
README.md
@@ -47,12 +47,21 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 分享视频至消息
|
||||
- [x] 播放课堂视频
|
||||
- [x] 发起投票
|
||||
- [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户`
|
||||
- [x] 修改消息设置
|
||||
- [x] 修改聊天设置
|
||||
- [x] 展示折叠消息
|
||||
- [x] 查看用户图文
|
||||
- [x] 动态话题
|
||||
- [x] 直播分区
|
||||
- [x] 分享`视频`/`番剧`/`动态`/`专栏`/`直播`至消息
|
||||
- [x] 创建/修改/删除关注分组
|
||||
- [x] 移除粉丝
|
||||
- [x] 直播弹幕发送表情
|
||||
- [x] 收藏夹排序
|
||||
- [x] 稍后再看`未看`/`未看完`/`已看完`分类
|
||||
- [x] 稍后再看 ~~`未看`~~ / `未看完` / ~~`已看完`~~ 分类
|
||||
- [x] WebDAV 备份/恢复设置
|
||||
- [x] 保存评论/动态
|
||||
- [x] 高级弹幕 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/grpc/bilibili/**
|
||||
- lib/grpc/google/**
|
||||
|
||||
formatter:
|
||||
trailing_commas: preserve
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
@@ -21,9 +29,39 @@ 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
|
||||
- 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
|
||||
- use_null_aware_elements
|
||||
- unnecessary_lambdas
|
||||
- use_is_even_rather_than_modulo
|
||||
# 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
|
||||
@@ -1,106 +0,0 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def _filePath = System.getenv("KEYSTORE") ?: keystoreProperties["storeFile"]
|
||||
def _storeFile = _filePath != null ? file(_filePath) : null
|
||||
def _storePassword = System.getenv("KEYSTORE_PASSWORD") ?: keystoreProperties["storePassword"]
|
||||
def _keyAlias = System.getenv("KEY_ALIAS") ?: keystoreProperties["keyAlias"]
|
||||
def _keyPassword = System.getenv("KEY_PASSWORD") ?: keystoreProperties["keyPassword"]
|
||||
|
||||
android {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
|
||||
namespace 'com.example.piliplus'
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.piliplus"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
// 添加签名配置
|
||||
if(_storeFile != null) {
|
||||
release {
|
||||
// 配置密钥库文件的位置、别名、密码等信息
|
||||
storeFile _storeFile
|
||||
storePassword _storePassword
|
||||
keyAlias _keyAlias
|
||||
keyPassword _keyPassword
|
||||
v1SigningEnabled true
|
||||
v2SigningEnabled true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// 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
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
project.android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
output.versionCodeOverride = variant.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
78
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,78 @@
|
||||
import com.android.build.gradle.internal.api.ApkVariantOutputImpl
|
||||
import org.jetbrains.kotlin.konan.properties.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.piliplus"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.piliplus"
|
||||
// minSdk = flutter.minSdkVersion
|
||||
minSdkVersion(23)
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
packagingOptions.jniLibs.useLegacyPackaging = true
|
||||
|
||||
val keyProperties = Properties().also {
|
||||
val properties = rootProject.file("key.properties")
|
||||
if (properties.exists())
|
||||
it.load(properties.inputStream())
|
||||
}
|
||||
|
||||
val config = keyProperties.getProperty("storeFile")?.let {
|
||||
signingConfigs.create("release") {
|
||||
storeFile = file(it)
|
||||
storePassword = keyProperties.getProperty("storePassword")
|
||||
keyAlias = keyProperties.getProperty("keyAlias")
|
||||
keyPassword = keyProperties.getProperty("keyPassword")
|
||||
enableV1Signing = true
|
||||
enableV2Signing = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
all {
|
||||
signingConfig = config ?: signingConfigs["debug"]
|
||||
}
|
||||
// release {
|
||||
// proguardFiles(
|
||||
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
// "proguard-rules.pro"
|
||||
// )
|
||||
// }
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
val variant = this
|
||||
variant.outputs.forEach { output ->
|
||||
(output as ApkVariantOutputImpl).versionCodeOverride = flutter.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
1
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1 @@
|
||||
-keep class com.yalantis.ucrop.util.RectUtils { *; }
|
||||
3
android/app/src/debug/res/values/string.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">PiliPlus debug</string>
|
||||
</resources>
|
||||
@@ -36,14 +36,17 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="PiliPlus"
|
||||
android:label="@string/app_name"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
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"
|
||||
@@ -55,6 +58,9 @@
|
||||
android:supportsPictureInPicture="true"
|
||||
android:resizeableActivity="true"
|
||||
>
|
||||
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
|
||||
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@@ -103,11 +109,13 @@
|
||||
<data android:host="uper" />
|
||||
<data android:host="article"
|
||||
android:pathPattern="/readlist" />
|
||||
<data android:host="opus" />
|
||||
<data android:host="advertise" android:path="/home" />
|
||||
<data android:host="clip" />
|
||||
<data android:host="search" />
|
||||
<data android:host="search" android:pathPattern=".*" />
|
||||
<data android:host="stardust-search" />
|
||||
<data android:host="music" />
|
||||
<data android:host="cheese" />
|
||||
<data android:host="bangumi"
|
||||
android:pathPattern="/season.*" />
|
||||
<data android:host="bangumi" android:pathPattern="/.*" />
|
||||
@@ -139,7 +147,6 @@
|
||||
<data android:host="video" />
|
||||
<data android:host="story" />
|
||||
<data android:host="podcast" />
|
||||
<data android:host="search" />
|
||||
<data android:host="main" android:path="/favorite" />
|
||||
<data android:host="pgc" android:path="/theater/match" />
|
||||
<data android:host="pgc" android:path="/theater/square" />
|
||||
@@ -154,7 +161,6 @@
|
||||
<data android:host="history" />
|
||||
<data android:host="charge" android:path="/rank" />
|
||||
<data android:host="assistant" />
|
||||
<data android:host="assistant" />
|
||||
<data android:host="feedback" />
|
||||
<data android:host="auth" android:path="/launch" />
|
||||
</intent-filter>
|
||||
@@ -172,7 +178,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 |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
8
android/app/src/main/res/values-v35/styles.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:fitsSystemWindows">true</item>
|
||||
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
3
android/app/src/main/res/values/string.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">PiliPlus</string>
|
||||
</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,60 +0,0 @@
|
||||
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) {
|
||||
Integer pluginCompileSdk = project.android.compileSdk
|
||||
if (pluginCompileSdk != null) {
|
||||
if (pluginCompileSdk < 31) {
|
||||
project.logger.error(
|
||||
"Warning: Overriding compileSdk version in Flutter plugin: "
|
||||
+ project.name
|
||||
+ " from "
|
||||
+ pluginCompileSdk
|
||||
+ " to 31 (to work around https://issuetracker.google.com/issues/199180389)."
|
||||
+ "\nIf there is not a new version of " + project.name + ", consider filing an issue against "
|
||||
+ project.name
|
||||
+ " to increase their compileSdk to the latest (otherwise try updating to the latest version)."
|
||||
)
|
||||
project.android {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
67
android/build.gradle.kts
Normal file
@@ -0,0 +1,67 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate {
|
||||
if (project.extensions.findByName("android") != null) {
|
||||
val androidExtension =
|
||||
project.extensions.getByName("android") as com.android.build.gradle.BaseExtension
|
||||
|
||||
if (androidExtension.namespace == null) {
|
||||
androidExtension.namespace = project.group.toString()
|
||||
}
|
||||
|
||||
androidExtension.compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
project.tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
val pluginCompileSdkStr = androidExtension.compileSdkVersion
|
||||
val pluginCompileSdk = pluginCompileSdkStr
|
||||
?.removePrefix("android-")
|
||||
?.toIntOrNull()
|
||||
if (pluginCompileSdk != null && pluginCompileSdk < 31) {
|
||||
project.logger.error(
|
||||
"Warning: Overriding compileSdk version in Flutter plugin: ${project.name} " +
|
||||
"from $pluginCompileSdk to 31 (to work around https://issuetracker.google.com/issues/199180389).\n" +
|
||||
"If there is not a new version of ${project.name}, consider filing an issue against ${project.name} " +
|
||||
"to increase their compileSdk to the latest (otherwise try updating to the latest version)."
|
||||
)
|
||||
androidExtension.setCompileSdkVersion(31)
|
||||
}
|
||||
}
|
||||
|
||||
project.buildDir = File(rootProject.buildDir, project.name)
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.2.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
26
android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
BIN
assets/fonts/custom_icon.ttf
Normal file
|
Before Width: | Height: | Size: 31 KiB |
BIN
assets/images/loading.webp
Normal file
|
After Width: | Height: | Size: 25 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 |
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StyleString {
|
||||
@@ -6,6 +7,10 @@ class StyleString {
|
||||
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
||||
static const Radius imgRadius = Radius.circular(10);
|
||||
static const double aspectRatio = 16 / 10;
|
||||
static const bottomSheetRadius = BorderRadius.only(
|
||||
topLeft: Radius.circular(18),
|
||||
topRight: Radius.circular(18),
|
||||
);
|
||||
}
|
||||
|
||||
class Constants {
|
||||
@@ -21,253 +26,270 @@ class Constants {
|
||||
static const String traceId =
|
||||
'11111111111111111111111111111111:1111111111111111:0:0';
|
||||
static const String userAgent =
|
||||
'Mozilla/5.0 BiliDroid/1.46.2 (bbcallen@gmail.com) os/android model/vivo mobi_app/android_hd build/2001100 channel/yingyongbao innerVer/2001100 osVer/14 network/2';
|
||||
'Mozilla/5.0 BiliDroid/2.0.1 (bbcallen@gmail.com) os/android model/android_hd mobi_app/android_hd build/2001100 channel/master innerVer/2001100 osVer/15 network/2';
|
||||
static const String statistics =
|
||||
'{"appId":5,"platform":3,"version":"1.46.2","abtest":""}';
|
||||
'{"appId":5,"platform":3,"version":"2.0.1","abtest":""}';
|
||||
// 请求时会自动encodeComponent
|
||||
|
||||
static const urlPattern =
|
||||
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]';
|
||||
// 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/master innerVer/8430300 osVer/15 network/2';
|
||||
|
||||
static get goodsUrlPrefix => "https://gaoneng.bilibili.com/tetris";
|
||||
static const String statisticsApp =
|
||||
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
||||
|
||||
static const baseHeaders = {
|
||||
'connection': 'keep-alive',
|
||||
'accept-encoding': 'br,gzip',
|
||||
'referer': HttpString.baseUrl,
|
||||
'env': 'prod',
|
||||
'app-key': 'android64',
|
||||
'x-bili-aurora-zone': 'sh001',
|
||||
};
|
||||
|
||||
static final urlRegex = RegExp(
|
||||
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]',
|
||||
);
|
||||
|
||||
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 => [
|
||||
{"id": 1, "cname": "中国大陆", "country_id": "86"},
|
||||
{"id": 5, "cname": "中国香港特别行政区", "country_id": "852"},
|
||||
{"id": 2, "cname": "中国澳门特别行政区", "country_id": "853"},
|
||||
{"id": 3, "cname": "中国台湾", "country_id": "886"},
|
||||
{"id": 4, "cname": "美国", "country_id": "1"},
|
||||
{"id": 6, "cname": "比利时", "country_id": "32"},
|
||||
{"id": 7, "cname": "澳大利亚", "country_id": "61"},
|
||||
{"id": 8, "cname": "法国", "country_id": "33"},
|
||||
{"id": 9, "cname": "加拿大", "country_id": "1"},
|
||||
{"id": 10, "cname": "日本", "country_id": "81"},
|
||||
{"id": 11, "cname": "新加坡", "country_id": "65"},
|
||||
{"id": 12, "cname": "韩国", "country_id": "82"},
|
||||
{"id": 13, "cname": "马来西亚", "country_id": "60"},
|
||||
{"id": 14, "cname": "英国", "country_id": "44"},
|
||||
{"id": 15, "cname": "意大利", "country_id": "39"},
|
||||
{"id": 16, "cname": "德国", "country_id": "49"},
|
||||
{"id": 18, "cname": "俄罗斯", "country_id": "7"},
|
||||
{"id": 19, "cname": "新西兰", "country_id": "64"}, //common:1-19
|
||||
{"id": 153, "cname": "瓦利斯群岛和富图纳群岛", "country_id": "1681"},
|
||||
{"id": 152, "cname": "葡萄牙", "country_id": "351"},
|
||||
{"id": 151, "cname": "帕劳", "country_id": "680"},
|
||||
{"id": 150, "cname": "诺福克岛", "country_id": "672"},
|
||||
{"id": 149, "cname": "挪威", "country_id": "47"},
|
||||
{"id": 148, "cname": "纽埃岛", "country_id": "683"},
|
||||
{"id": 147, "cname": "尼日利亚", "country_id": "234"},
|
||||
{"id": 146, "cname": "尼日尔", "country_id": "227"},
|
||||
{"id": 145, "cname": "尼加拉瓜", "country_id": "505"},
|
||||
{"id": 144, "cname": "尼泊尔", "country_id": "977"},
|
||||
{"id": 143, "cname": "瑙鲁", "country_id": "674"},
|
||||
{"id": 154, "cname": "格鲁吉亚", "country_id": "995"},
|
||||
{"id": 155, "cname": "瑞典", "country_id": "46"},
|
||||
{"id": 165, "cname": "沙特阿拉伯", "country_id": "966"},
|
||||
{"id": 164, "cname": "桑给巴尔岛", "country_id": "259"},
|
||||
{"id": 163, "cname": "塞舌尔共和国", "country_id": "248"},
|
||||
{"id": 162, "cname": "塞浦路斯", "country_id": "357"},
|
||||
{"id": 161, "cname": "塞内加尔", "country_id": "221"},
|
||||
{"id": 160, "cname": "塞拉利昂", "country_id": "232"},
|
||||
{"id": 159, "cname": "萨摩亚,东部", "country_id": "684"},
|
||||
{"id": 158, "cname": "萨摩亚,西部", "country_id": "685"},
|
||||
{"id": 157, "cname": "萨尔瓦多", "country_id": "503"},
|
||||
{"id": 156, "cname": "瑞士", "country_id": "41"},
|
||||
{"id": 166, "cname": "圣多美和普林西比", "country_id": "239"},
|
||||
{"id": 142, "cname": "塞尔维亚", "country_id": "381"},
|
||||
{"id": 141, "cname": "南非", "country_id": "27"},
|
||||
{"id": 128, "cname": "毛里塔尼亚", "country_id": "222"},
|
||||
{"id": 127, "cname": "毛里求斯", "country_id": "230"},
|
||||
{"id": 126, "cname": "马歇尔岛", "country_id": "692"},
|
||||
{"id": 125, "cname": "马提尼克岛", "country_id": "596"},
|
||||
{"id": 124, "cname": "马其顿", "country_id": "389"},
|
||||
{"id": 123, "cname": "马里亚纳岛", "country_id": "1670"},
|
||||
{"id": 122, "cname": "马里", "country_id": "223"},
|
||||
{"id": 121, "cname": "马拉维", "country_id": "265"},
|
||||
{"id": 120, "cname": "马耳他", "country_id": "356"},
|
||||
{"id": 119, "cname": "马尔代夫", "country_id": "960"},
|
||||
{"id": 129, "cname": "蒙古", "country_id": "976"},
|
||||
{"id": 130, "cname": "蒙特塞拉特岛", "country_id": "1664"},
|
||||
{"id": 140, "cname": "纳米比亚", "country_id": "264"},
|
||||
{"id": 139, "cname": "墨西哥", "country_id": "52"},
|
||||
{"id": 138, "cname": "莫桑比克", "country_id": "258"},
|
||||
{"id": 137, "cname": "摩纳哥", "country_id": "377"},
|
||||
{"id": 136, "cname": "摩洛哥", "country_id": "212"},
|
||||
{"id": 135, "cname": "摩尔多瓦", "country_id": "373"},
|
||||
{"id": 134, "cname": "缅甸", "country_id": "95"},
|
||||
{"id": 133, "cname": "密克罗尼西亚", "country_id": "691"},
|
||||
{"id": 132, "cname": "秘鲁", "country_id": "51"},
|
||||
{"id": 131, "cname": "孟加拉国", "country_id": "880"},
|
||||
{"id": 118, "cname": "马达加斯加", "country_id": "261"},
|
||||
{"id": 167, "cname": "圣卢西亚", "country_id": "1784"},
|
||||
{"id": 216, "cname": "智利", "country_id": "56"},
|
||||
{"id": 203, "cname": "牙买加", "country_id": "1876"},
|
||||
{"id": 202, "cname": "叙利亚", "country_id": "963"},
|
||||
{"id": 201, "cname": "匈牙利", "country_id": "36"},
|
||||
{"id": 200, "cname": "科特迪瓦", "country_id": "225"},
|
||||
{"id": 199, "cname": "希腊", "country_id": "30"},
|
||||
{"id": 198, "cname": "西班牙", "country_id": "34"},
|
||||
{"id": 197, "cname": "乌兹别克斯坦", "country_id": "998"},
|
||||
{"id": 196, "cname": "乌拉圭", "country_id": "598"},
|
||||
{"id": 195, "cname": "乌克兰", "country_id": "380"},
|
||||
{"id": 194, "cname": "乌干达", "country_id": "256"},
|
||||
{"id": 204, "cname": "亚美尼亚", "country_id": "374"},
|
||||
{"id": 205, "cname": "也门", "country_id": "967"},
|
||||
{"id": 215, "cname": "直布罗陀", "country_id": "350"},
|
||||
{"id": 214, "cname": "乍得", "country_id": "235"},
|
||||
{"id": 213, "cname": "赞比亚", "country_id": "260"},
|
||||
{"id": 212, "cname": "越南", "country_id": "84"},
|
||||
{"id": 211, "cname": "约旦", "country_id": "962"},
|
||||
{"id": 210, "cname": "印尼", "country_id": "62"},
|
||||
{"id": 209, "cname": "印度", "country_id": "91"},
|
||||
{"id": 208, "cname": "以色列", "country_id": "972"},
|
||||
{"id": 207, "cname": "伊朗", "country_id": "98"},
|
||||
{"id": 206, "cname": "伊拉克", "country_id": "964"},
|
||||
{"id": 193, "cname": "文莱", "country_id": "673"},
|
||||
{"id": 192, "cname": "委内瑞拉", "country_id": "58"},
|
||||
{"id": 191, "cname": "维珍群岛(英属)", "country_id": "1284"},
|
||||
{"id": 178, "cname": "泰国", "country_id": "66"},
|
||||
{"id": 177, "cname": "索马里", "country_id": "252"},
|
||||
{"id": 176, "cname": "所罗门群岛", "country_id": "677"},
|
||||
{"id": 175, "cname": "苏里南", "country_id": "597"},
|
||||
{"id": 174, "cname": "苏丹", "country_id": "249"},
|
||||
{"id": 173, "cname": "斯威士兰", "country_id": "268"},
|
||||
{"id": 172, "cname": "斯洛文尼亚", "country_id": "386"},
|
||||
{"id": 171, "cname": "斯洛伐克", "country_id": "421"},
|
||||
{"id": 170, "cname": "斯里兰卡", "country_id": "94"},
|
||||
{"id": 169, "cname": "圣皮埃尔和密克隆群岛", "country_id": "508"},
|
||||
{"id": 179, "cname": "坦桑尼亚", "country_id": "255"},
|
||||
{"id": 180, "cname": "汤加", "country_id": "676"},
|
||||
{"id": 190, "cname": "维珍群岛(美属)", "country_id": "1340"},
|
||||
{"id": 189, "cname": "瓦努阿图", "country_id": "678"},
|
||||
{"id": 188, "cname": "托克劳岛", "country_id": "690"},
|
||||
{"id": 187, "cname": "土库曼斯坦", "country_id": "993"},
|
||||
{"id": 186, "cname": "土耳其", "country_id": "90"},
|
||||
{"id": 185, "cname": "图瓦卢", "country_id": "688"},
|
||||
{"id": 184, "cname": "突尼斯", "country_id": "216"},
|
||||
{"id": 183, "cname": "阿森松岛", "country_id": "247"},
|
||||
{"id": 182, "cname": "特立尼达和多巴哥", "country_id": "1868"},
|
||||
{"id": 181, "cname": "特克斯和凯科斯", "country_id": "1649"},
|
||||
{"id": 168, "cname": "圣马力诺", "country_id": "378"},
|
||||
{"id": 67, "cname": "法属圭亚那", "country_id": "594"},
|
||||
{"id": 54, "cname": "不丹", "country_id": "975"},
|
||||
{"id": 53, "cname": "博茨瓦纳", "country_id": "267"},
|
||||
{"id": 52, "cname": "伯利兹", "country_id": "501"},
|
||||
{"id": 51, "cname": "玻利维亚", "country_id": "591"},
|
||||
{"id": 50, "cname": "波兰", "country_id": "48"},
|
||||
{"id": 49, "cname": "波黑", "country_id": "387"},
|
||||
{"id": 48, "cname": "波多黎各", "country_id": "1787"},
|
||||
{"id": 47, "cname": "冰岛", "country_id": "354"},
|
||||
{"id": 46, "cname": "贝宁", "country_id": "229"},
|
||||
{"id": 45, "cname": "保加利亚", "country_id": "359"},
|
||||
{"id": 55, "cname": "布基纳法索", "country_id": "226"},
|
||||
{"id": 56, "cname": "布隆迪", "country_id": "257"},
|
||||
{"id": 66, "cname": "法属波利尼西亚", "country_id": "689"},
|
||||
{"id": 65, "cname": "法罗岛", "country_id": "298"},
|
||||
{"id": 64, "cname": "厄立特里亚", "country_id": "291"},
|
||||
{"id": 63, "cname": "厄瓜多尔", "country_id": "593"},
|
||||
{"id": 62, "cname": "多米尼加代表", "country_id": "1809"},
|
||||
{"id": 61, "cname": "多米尼加", "country_id": "1767"},
|
||||
{"id": 60, "cname": "多哥", "country_id": "228"},
|
||||
{"id": 59, "cname": "迪戈加西亚岛", "country_id": "246"},
|
||||
{"id": 58, "cname": "丹麦", "country_id": "45"},
|
||||
{"id": 57, "cname": "赤道几内亚", "country_id": "240"},
|
||||
{"id": 44, "cname": "百慕大群岛", "country_id": "1441"},
|
||||
{"id": 43, "cname": "白俄罗斯", "country_id": "375"},
|
||||
{"id": 42, "cname": "巴西", "country_id": "55"},
|
||||
{"id": 29, "cname": "爱尔兰", "country_id": "353"},
|
||||
{"id": 28, "cname": "埃塞俄比亚", "country_id": "251"},
|
||||
{"id": 27, "cname": "埃及", "country_id": "20"},
|
||||
{"id": 26, "cname": "阿塞拜疆", "country_id": "994"},
|
||||
{"id": 25, "cname": "阿曼", "country_id": "968"},
|
||||
{"id": 24, "cname": "阿联酋", "country_id": "971"},
|
||||
{"id": 23, "cname": "阿根廷", "country_id": "54"},
|
||||
{"id": 22, "cname": "阿富汗", "country_id": "93"},
|
||||
{"id": 21, "cname": "阿尔及利亚", "country_id": "213"},
|
||||
{"id": 20, "cname": "阿尔巴尼亚", "country_id": "355"},
|
||||
{"id": 30, "cname": "爱沙尼亚", "country_id": "372"},
|
||||
{"id": 31, "cname": "安道尔", "country_id": "376"},
|
||||
{"id": 41, "cname": "巴拿马", "country_id": "507"},
|
||||
{"id": 40, "cname": "巴林", "country_id": "973"},
|
||||
{"id": 39, "cname": "巴拉圭", "country_id": "595"},
|
||||
{"id": 38, "cname": "巴基斯坦", "country_id": "92"},
|
||||
{"id": 37, "cname": "巴哈马群岛", "country_id": "1242"},
|
||||
{"id": 36, "cname": "巴布亚新几内亚", "country_id": "675"},
|
||||
{"id": 35, "cname": "巴巴多斯", "country_id": "1246"},
|
||||
{"id": 34, "cname": "奥地利", "country_id": "43"},
|
||||
{"id": 33, "cname": "安提瓜岛和巴布达", "country_id": "1268"},
|
||||
{"id": 32, "cname": "安哥拉", "country_id": "244"},
|
||||
{"id": 68, "cname": "非洲中部", "country_id": "236"},
|
||||
{"id": 117, "cname": "罗马尼亚", "country_id": "40"},
|
||||
{"id": 104, "cname": "科威特", "country_id": "965"},
|
||||
{"id": 103, "cname": "科摩罗", "country_id": "269"},
|
||||
{"id": 102, "cname": "开曼群岛", "country_id": "1345"},
|
||||
{"id": 101, "cname": "卡塔尔", "country_id": "974"},
|
||||
{"id": 100, "cname": "喀麦隆", "country_id": "237"},
|
||||
{"id": 99, "cname": "聚会岛", "country_id": "262"},
|
||||
{"id": 98, "cname": "津巴布韦", "country_id": "263"},
|
||||
{"id": 97, "cname": "捷克", "country_id": "420"},
|
||||
{"id": 96, "cname": "柬埔寨", "country_id": "855"},
|
||||
{"id": 95, "cname": "加蓬", "country_id": "241"},
|
||||
{"id": 105, "cname": "克罗地亚", "country_id": "385"},
|
||||
{"id": 106, "cname": "肯尼亚", "country_id": "254"},
|
||||
{"id": 116, "cname": "卢旺达", "country_id": "250"},
|
||||
{"id": 115, "cname": "卢森堡", "country_id": "352"},
|
||||
{"id": 114, "cname": "利比亚", "country_id": "218"},
|
||||
{"id": 113, "cname": "利比里亚", "country_id": "231"},
|
||||
{"id": 112, "cname": "立陶宛", "country_id": "370"},
|
||||
{"id": 111, "cname": "黎巴嫩", "country_id": "961"},
|
||||
{"id": 110, "cname": "老挝", "country_id": "856"},
|
||||
{"id": 109, "cname": "莱索托", "country_id": "266"},
|
||||
{"id": 108, "cname": "拉脱维亚", "country_id": "371"},
|
||||
{"id": 107, "cname": "库克岛", "country_id": "682"},
|
||||
{"id": 94, "cname": "加纳", "country_id": "233"},
|
||||
{"id": 93, "cname": "几内亚比绍", "country_id": "245"},
|
||||
{"id": 92, "cname": "几内亚", "country_id": "224"},
|
||||
{"id": 79, "cname": "格林纳达", "country_id": "1473"},
|
||||
{"id": 78, "cname": "哥斯达黎加", "country_id": "506"},
|
||||
{"id": 77, "cname": "哥伦比亚", "country_id": "57"},
|
||||
{"id": 76, "cname": "刚果(金)", "country_id": "243"},
|
||||
{"id": 75, "cname": "刚果", "country_id": "242"},
|
||||
{"id": 74, "cname": "冈比亚", "country_id": "220"},
|
||||
{"id": 73, "cname": "福克兰岛", "country_id": "500"},
|
||||
{"id": 72, "cname": "佛得角", "country_id": "238"},
|
||||
{"id": 71, "cname": "芬兰", "country_id": "358"},
|
||||
{"id": 70, "cname": "斐济", "country_id": "679"},
|
||||
{"id": 80, "cname": "格陵兰岛", "country_id": "299"},
|
||||
{"id": 81, "cname": "古巴", "country_id": "53"},
|
||||
{"id": 91, "cname": "吉尔吉斯斯坦", "country_id": "996"},
|
||||
{"id": 90, "cname": "吉布提", "country_id": "253"},
|
||||
{"id": 89, "cname": "基里巴斯", "country_id": "686"},
|
||||
{"id": 88, "cname": "维克岛", "country_id": "1808"},
|
||||
{"id": 87, "cname": "洪都拉斯", "country_id": "504"},
|
||||
{"id": 86, "cname": "荷兰", "country_id": "31"},
|
||||
{"id": 85, "cname": "朝鲜", "country_id": "850"},
|
||||
{"id": 84, "cname": "海地", "country_id": "509"},
|
||||
{"id": 83, "cname": "关岛", "country_id": "1671"},
|
||||
{"id": 82, "cname": "瓜德罗普岛", "country_id": "590"},
|
||||
{"id": 69, "cname": "菲律宾", "country_id": "63"}
|
||||
];
|
||||
{"id": 1, "cname": "中国大陆", "country_id": "86"},
|
||||
{"id": 5, "cname": "中国香港特别行政区", "country_id": "852"},
|
||||
{"id": 2, "cname": "中国澳门特别行政区", "country_id": "853"},
|
||||
{"id": 3, "cname": "中国台湾", "country_id": "886"},
|
||||
{"id": 4, "cname": "美国", "country_id": "1"},
|
||||
{"id": 6, "cname": "比利时", "country_id": "32"},
|
||||
{"id": 7, "cname": "澳大利亚", "country_id": "61"},
|
||||
{"id": 8, "cname": "法国", "country_id": "33"},
|
||||
{"id": 9, "cname": "加拿大", "country_id": "1"},
|
||||
{"id": 10, "cname": "日本", "country_id": "81"},
|
||||
{"id": 11, "cname": "新加坡", "country_id": "65"},
|
||||
{"id": 12, "cname": "韩国", "country_id": "82"},
|
||||
{"id": 13, "cname": "马来西亚", "country_id": "60"},
|
||||
{"id": 14, "cname": "英国", "country_id": "44"},
|
||||
{"id": 15, "cname": "意大利", "country_id": "39"},
|
||||
{"id": 16, "cname": "德国", "country_id": "49"},
|
||||
{"id": 18, "cname": "俄罗斯", "country_id": "7"},
|
||||
{"id": 19, "cname": "新西兰", "country_id": "64"}, //common:1-19
|
||||
{"id": 153, "cname": "瓦利斯群岛和富图纳群岛", "country_id": "1681"},
|
||||
{"id": 152, "cname": "葡萄牙", "country_id": "351"},
|
||||
{"id": 151, "cname": "帕劳", "country_id": "680"},
|
||||
{"id": 150, "cname": "诺福克岛", "country_id": "672"},
|
||||
{"id": 149, "cname": "挪威", "country_id": "47"},
|
||||
{"id": 148, "cname": "纽埃岛", "country_id": "683"},
|
||||
{"id": 147, "cname": "尼日利亚", "country_id": "234"},
|
||||
{"id": 146, "cname": "尼日尔", "country_id": "227"},
|
||||
{"id": 145, "cname": "尼加拉瓜", "country_id": "505"},
|
||||
{"id": 144, "cname": "尼泊尔", "country_id": "977"},
|
||||
{"id": 143, "cname": "瑙鲁", "country_id": "674"},
|
||||
{"id": 154, "cname": "格鲁吉亚", "country_id": "995"},
|
||||
{"id": 155, "cname": "瑞典", "country_id": "46"},
|
||||
{"id": 165, "cname": "沙特阿拉伯", "country_id": "966"},
|
||||
{"id": 164, "cname": "桑给巴尔岛", "country_id": "259"},
|
||||
{"id": 163, "cname": "塞舌尔共和国", "country_id": "248"},
|
||||
{"id": 162, "cname": "塞浦路斯", "country_id": "357"},
|
||||
{"id": 161, "cname": "塞内加尔", "country_id": "221"},
|
||||
{"id": 160, "cname": "塞拉利昂", "country_id": "232"},
|
||||
{"id": 159, "cname": "萨摩亚,东部", "country_id": "684"},
|
||||
{"id": 158, "cname": "萨摩亚,西部", "country_id": "685"},
|
||||
{"id": 157, "cname": "萨尔瓦多", "country_id": "503"},
|
||||
{"id": 156, "cname": "瑞士", "country_id": "41"},
|
||||
{"id": 166, "cname": "圣多美和普林西比", "country_id": "239"},
|
||||
{"id": 142, "cname": "塞尔维亚", "country_id": "381"},
|
||||
{"id": 141, "cname": "南非", "country_id": "27"},
|
||||
{"id": 128, "cname": "毛里塔尼亚", "country_id": "222"},
|
||||
{"id": 127, "cname": "毛里求斯", "country_id": "230"},
|
||||
{"id": 126, "cname": "马歇尔岛", "country_id": "692"},
|
||||
{"id": 125, "cname": "马提尼克岛", "country_id": "596"},
|
||||
{"id": 124, "cname": "马其顿", "country_id": "389"},
|
||||
{"id": 123, "cname": "马里亚纳岛", "country_id": "1670"},
|
||||
{"id": 122, "cname": "马里", "country_id": "223"},
|
||||
{"id": 121, "cname": "马拉维", "country_id": "265"},
|
||||
{"id": 120, "cname": "马耳他", "country_id": "356"},
|
||||
{"id": 119, "cname": "马尔代夫", "country_id": "960"},
|
||||
{"id": 129, "cname": "蒙古", "country_id": "976"},
|
||||
{"id": 130, "cname": "蒙特塞拉特岛", "country_id": "1664"},
|
||||
{"id": 140, "cname": "纳米比亚", "country_id": "264"},
|
||||
{"id": 139, "cname": "墨西哥", "country_id": "52"},
|
||||
{"id": 138, "cname": "莫桑比克", "country_id": "258"},
|
||||
{"id": 137, "cname": "摩纳哥", "country_id": "377"},
|
||||
{"id": 136, "cname": "摩洛哥", "country_id": "212"},
|
||||
{"id": 135, "cname": "摩尔多瓦", "country_id": "373"},
|
||||
{"id": 134, "cname": "缅甸", "country_id": "95"},
|
||||
{"id": 133, "cname": "密克罗尼西亚", "country_id": "691"},
|
||||
{"id": 132, "cname": "秘鲁", "country_id": "51"},
|
||||
{"id": 131, "cname": "孟加拉国", "country_id": "880"},
|
||||
{"id": 118, "cname": "马达加斯加", "country_id": "261"},
|
||||
{"id": 167, "cname": "圣卢西亚", "country_id": "1784"},
|
||||
{"id": 216, "cname": "智利", "country_id": "56"},
|
||||
{"id": 203, "cname": "牙买加", "country_id": "1876"},
|
||||
{"id": 202, "cname": "叙利亚", "country_id": "963"},
|
||||
{"id": 201, "cname": "匈牙利", "country_id": "36"},
|
||||
{"id": 200, "cname": "科特迪瓦", "country_id": "225"},
|
||||
{"id": 199, "cname": "希腊", "country_id": "30"},
|
||||
{"id": 198, "cname": "西班牙", "country_id": "34"},
|
||||
{"id": 197, "cname": "乌兹别克斯坦", "country_id": "998"},
|
||||
{"id": 196, "cname": "乌拉圭", "country_id": "598"},
|
||||
{"id": 195, "cname": "乌克兰", "country_id": "380"},
|
||||
{"id": 194, "cname": "乌干达", "country_id": "256"},
|
||||
{"id": 204, "cname": "亚美尼亚", "country_id": "374"},
|
||||
{"id": 205, "cname": "也门", "country_id": "967"},
|
||||
{"id": 215, "cname": "直布罗陀", "country_id": "350"},
|
||||
{"id": 214, "cname": "乍得", "country_id": "235"},
|
||||
{"id": 213, "cname": "赞比亚", "country_id": "260"},
|
||||
{"id": 212, "cname": "越南", "country_id": "84"},
|
||||
{"id": 211, "cname": "约旦", "country_id": "962"},
|
||||
{"id": 210, "cname": "印尼", "country_id": "62"},
|
||||
{"id": 209, "cname": "印度", "country_id": "91"},
|
||||
{"id": 208, "cname": "以色列", "country_id": "972"},
|
||||
{"id": 207, "cname": "伊朗", "country_id": "98"},
|
||||
{"id": 206, "cname": "伊拉克", "country_id": "964"},
|
||||
{"id": 193, "cname": "文莱", "country_id": "673"},
|
||||
{"id": 192, "cname": "委内瑞拉", "country_id": "58"},
|
||||
{"id": 191, "cname": "维珍群岛(英属)", "country_id": "1284"},
|
||||
{"id": 178, "cname": "泰国", "country_id": "66"},
|
||||
{"id": 177, "cname": "索马里", "country_id": "252"},
|
||||
{"id": 176, "cname": "所罗门群岛", "country_id": "677"},
|
||||
{"id": 175, "cname": "苏里南", "country_id": "597"},
|
||||
{"id": 174, "cname": "苏丹", "country_id": "249"},
|
||||
{"id": 173, "cname": "斯威士兰", "country_id": "268"},
|
||||
{"id": 172, "cname": "斯洛文尼亚", "country_id": "386"},
|
||||
{"id": 171, "cname": "斯洛伐克", "country_id": "421"},
|
||||
{"id": 170, "cname": "斯里兰卡", "country_id": "94"},
|
||||
{"id": 169, "cname": "圣皮埃尔和密克隆群岛", "country_id": "508"},
|
||||
{"id": 179, "cname": "坦桑尼亚", "country_id": "255"},
|
||||
{"id": 180, "cname": "汤加", "country_id": "676"},
|
||||
{"id": 190, "cname": "维珍群岛(美属)", "country_id": "1340"},
|
||||
{"id": 189, "cname": "瓦努阿图", "country_id": "678"},
|
||||
{"id": 188, "cname": "托克劳岛", "country_id": "690"},
|
||||
{"id": 187, "cname": "土库曼斯坦", "country_id": "993"},
|
||||
{"id": 186, "cname": "土耳其", "country_id": "90"},
|
||||
{"id": 185, "cname": "图瓦卢", "country_id": "688"},
|
||||
{"id": 184, "cname": "突尼斯", "country_id": "216"},
|
||||
{"id": 183, "cname": "阿森松岛", "country_id": "247"},
|
||||
{"id": 182, "cname": "特立尼达和多巴哥", "country_id": "1868"},
|
||||
{"id": 181, "cname": "特克斯和凯科斯", "country_id": "1649"},
|
||||
{"id": 168, "cname": "圣马力诺", "country_id": "378"},
|
||||
{"id": 67, "cname": "法属圭亚那", "country_id": "594"},
|
||||
{"id": 54, "cname": "不丹", "country_id": "975"},
|
||||
{"id": 53, "cname": "博茨瓦纳", "country_id": "267"},
|
||||
{"id": 52, "cname": "伯利兹", "country_id": "501"},
|
||||
{"id": 51, "cname": "玻利维亚", "country_id": "591"},
|
||||
{"id": 50, "cname": "波兰", "country_id": "48"},
|
||||
{"id": 49, "cname": "波黑", "country_id": "387"},
|
||||
{"id": 48, "cname": "波多黎各", "country_id": "1787"},
|
||||
{"id": 47, "cname": "冰岛", "country_id": "354"},
|
||||
{"id": 46, "cname": "贝宁", "country_id": "229"},
|
||||
{"id": 45, "cname": "保加利亚", "country_id": "359"},
|
||||
{"id": 55, "cname": "布基纳法索", "country_id": "226"},
|
||||
{"id": 56, "cname": "布隆迪", "country_id": "257"},
|
||||
{"id": 66, "cname": "法属波利尼西亚", "country_id": "689"},
|
||||
{"id": 65, "cname": "法罗岛", "country_id": "298"},
|
||||
{"id": 64, "cname": "厄立特里亚", "country_id": "291"},
|
||||
{"id": 63, "cname": "厄瓜多尔", "country_id": "593"},
|
||||
{"id": 62, "cname": "多米尼加代表", "country_id": "1809"},
|
||||
{"id": 61, "cname": "多米尼加", "country_id": "1767"},
|
||||
{"id": 60, "cname": "多哥", "country_id": "228"},
|
||||
{"id": 59, "cname": "迪戈加西亚岛", "country_id": "246"},
|
||||
{"id": 58, "cname": "丹麦", "country_id": "45"},
|
||||
{"id": 57, "cname": "赤道几内亚", "country_id": "240"},
|
||||
{"id": 44, "cname": "百慕大群岛", "country_id": "1441"},
|
||||
{"id": 43, "cname": "白俄罗斯", "country_id": "375"},
|
||||
{"id": 42, "cname": "巴西", "country_id": "55"},
|
||||
{"id": 29, "cname": "爱尔兰", "country_id": "353"},
|
||||
{"id": 28, "cname": "埃塞俄比亚", "country_id": "251"},
|
||||
{"id": 27, "cname": "埃及", "country_id": "20"},
|
||||
{"id": 26, "cname": "阿塞拜疆", "country_id": "994"},
|
||||
{"id": 25, "cname": "阿曼", "country_id": "968"},
|
||||
{"id": 24, "cname": "阿联酋", "country_id": "971"},
|
||||
{"id": 23, "cname": "阿根廷", "country_id": "54"},
|
||||
{"id": 22, "cname": "阿富汗", "country_id": "93"},
|
||||
{"id": 21, "cname": "阿尔及利亚", "country_id": "213"},
|
||||
{"id": 20, "cname": "阿尔巴尼亚", "country_id": "355"},
|
||||
{"id": 30, "cname": "爱沙尼亚", "country_id": "372"},
|
||||
{"id": 31, "cname": "安道尔", "country_id": "376"},
|
||||
{"id": 41, "cname": "巴拿马", "country_id": "507"},
|
||||
{"id": 40, "cname": "巴林", "country_id": "973"},
|
||||
{"id": 39, "cname": "巴拉圭", "country_id": "595"},
|
||||
{"id": 38, "cname": "巴基斯坦", "country_id": "92"},
|
||||
{"id": 37, "cname": "巴哈马群岛", "country_id": "1242"},
|
||||
{"id": 36, "cname": "巴布亚新几内亚", "country_id": "675"},
|
||||
{"id": 35, "cname": "巴巴多斯", "country_id": "1246"},
|
||||
{"id": 34, "cname": "奥地利", "country_id": "43"},
|
||||
{"id": 33, "cname": "安提瓜岛和巴布达", "country_id": "1268"},
|
||||
{"id": 32, "cname": "安哥拉", "country_id": "244"},
|
||||
{"id": 68, "cname": "非洲中部", "country_id": "236"},
|
||||
{"id": 117, "cname": "罗马尼亚", "country_id": "40"},
|
||||
{"id": 104, "cname": "科威特", "country_id": "965"},
|
||||
{"id": 103, "cname": "科摩罗", "country_id": "269"},
|
||||
{"id": 102, "cname": "开曼群岛", "country_id": "1345"},
|
||||
{"id": 101, "cname": "卡塔尔", "country_id": "974"},
|
||||
{"id": 100, "cname": "喀麦隆", "country_id": "237"},
|
||||
{"id": 99, "cname": "聚会岛", "country_id": "262"},
|
||||
{"id": 98, "cname": "津巴布韦", "country_id": "263"},
|
||||
{"id": 97, "cname": "捷克", "country_id": "420"},
|
||||
{"id": 96, "cname": "柬埔寨", "country_id": "855"},
|
||||
{"id": 95, "cname": "加蓬", "country_id": "241"},
|
||||
{"id": 105, "cname": "克罗地亚", "country_id": "385"},
|
||||
{"id": 106, "cname": "肯尼亚", "country_id": "254"},
|
||||
{"id": 116, "cname": "卢旺达", "country_id": "250"},
|
||||
{"id": 115, "cname": "卢森堡", "country_id": "352"},
|
||||
{"id": 114, "cname": "利比亚", "country_id": "218"},
|
||||
{"id": 113, "cname": "利比里亚", "country_id": "231"},
|
||||
{"id": 112, "cname": "立陶宛", "country_id": "370"},
|
||||
{"id": 111, "cname": "黎巴嫩", "country_id": "961"},
|
||||
{"id": 110, "cname": "老挝", "country_id": "856"},
|
||||
{"id": 109, "cname": "莱索托", "country_id": "266"},
|
||||
{"id": 108, "cname": "拉脱维亚", "country_id": "371"},
|
||||
{"id": 107, "cname": "库克岛", "country_id": "682"},
|
||||
{"id": 94, "cname": "加纳", "country_id": "233"},
|
||||
{"id": 93, "cname": "几内亚比绍", "country_id": "245"},
|
||||
{"id": 92, "cname": "几内亚", "country_id": "224"},
|
||||
{"id": 79, "cname": "格林纳达", "country_id": "1473"},
|
||||
{"id": 78, "cname": "哥斯达黎加", "country_id": "506"},
|
||||
{"id": 77, "cname": "哥伦比亚", "country_id": "57"},
|
||||
{"id": 76, "cname": "刚果(金)", "country_id": "243"},
|
||||
{"id": 75, "cname": "刚果", "country_id": "242"},
|
||||
{"id": 74, "cname": "冈比亚", "country_id": "220"},
|
||||
{"id": 73, "cname": "福克兰岛", "country_id": "500"},
|
||||
{"id": 72, "cname": "佛得角", "country_id": "238"},
|
||||
{"id": 71, "cname": "芬兰", "country_id": "358"},
|
||||
{"id": 70, "cname": "斐济", "country_id": "679"},
|
||||
{"id": 80, "cname": "格陵兰岛", "country_id": "299"},
|
||||
{"id": 81, "cname": "古巴", "country_id": "53"},
|
||||
{"id": 91, "cname": "吉尔吉斯斯坦", "country_id": "996"},
|
||||
{"id": 90, "cname": "吉布提", "country_id": "253"},
|
||||
{"id": 89, "cname": "基里巴斯", "country_id": "686"},
|
||||
{"id": 88, "cname": "维克岛", "country_id": "1808"},
|
||||
{"id": 87, "cname": "洪都拉斯", "country_id": "504"},
|
||||
{"id": 86, "cname": "荷兰", "country_id": "31"},
|
||||
{"id": 85, "cname": "朝鲜", "country_id": "850"},
|
||||
{"id": 84, "cname": "海地", "country_id": "509"},
|
||||
{"id": 83, "cname": "关岛", "country_id": "1671"},
|
||||
{"id": 82, "cname": "瓜德罗普岛", "country_id": "590"},
|
||||
{"id": 69, "cname": "菲律宾", "country_id": "63"},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class DynamicCardSkeleton extends StatelessWidget {
|
||||
const DynamicCardSkeleton({super.key});
|
||||
@@ -15,7 +16,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 8,
|
||||
color: theme.dividerColor.withOpacity(0.05),
|
||||
color: theme.dividerColor.withValues(alpha: 0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -28,7 +29,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -47,7 +48,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
height: 11,
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
@@ -89,7 +90,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
@@ -102,19 +103,20 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor:
|
||||
theme.colorScheme.outline.withOpacity(0.2),
|
||||
foregroundColor: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.2,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
i == 0
|
||||
? '转发'
|
||||
: i == 1
|
||||
? '评论'
|
||||
: '点赞',
|
||||
? '评论'
|
||||
: '点赞',
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class FavPgcItemSkeleton extends StatelessWidget {
|
||||
const FavPgcItemSkeleton({super.key});
|
||||
@@ -15,7 +15,6 @@ class FavPgcItemSkeleton extends StatelessWidget {
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
@@ -25,7 +24,7 @@ class FavPgcItemSkeleton extends StatelessWidget {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
width: boxConstraints.maxWidth,
|
||||
height: boxConstraints.maxHeight,
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'skeleton.dart';
|
||||
|
||||
class MediaBangumiSkeleton extends StatefulWidget {
|
||||
const MediaBangumiSkeleton({super.key});
|
||||
class MediaPgcSkeleton extends StatefulWidget {
|
||||
const MediaPgcSkeleton({super.key});
|
||||
|
||||
@override
|
||||
State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();
|
||||
State<MediaPgcSkeleton> createState() => _MediaPgcSkeletonState();
|
||||
}
|
||||
|
||||
class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
class _MediaPgcSkeletonState extends State<MediaPgcSkeleton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
|
||||
StyleString.safeSpace,
|
||||
7,
|
||||
StyleString.safeSpace,
|
||||
7,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
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(
|
||||
@@ -62,8 +66,9 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
width: 90,
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
color: bgColor,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class MsgFeedSysMsgSkeleton extends StatelessWidget {
|
||||
const MsgFeedSysMsgSkeleton({super.key});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class MsgFeedTopSkeleton extends StatelessWidget {
|
||||
const MsgFeedTopSkeleton({super.key});
|
||||
|
||||
@@ -74,14 +74,14 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
||||
}
|
||||
|
||||
LinearGradient get gradient => LinearGradient(
|
||||
colors: widget.linearGradient.colors,
|
||||
stops: widget.linearGradient.stops,
|
||||
begin: widget.linearGradient.begin,
|
||||
end: widget.linearGradient.end,
|
||||
transform: _SlidingGradientTransform(
|
||||
slidePercent: _shimmerController.value,
|
||||
),
|
||||
);
|
||||
colors: widget.linearGradient.colors,
|
||||
stops: widget.linearGradient.stops,
|
||||
begin: widget.linearGradient.begin,
|
||||
end: widget.linearGradient.end,
|
||||
transform: _SlidingGradientTransform(
|
||||
slidePercent: _shimmerController.value,
|
||||
),
|
||||
);
|
||||
|
||||
bool get isSized =>
|
||||
(context.findRenderObject() as RenderBox?)?.hasSize ?? false;
|
||||
|
||||
51
lib/common/skeleton/space_opus.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
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,
|
||||
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,6 +1,6 @@
|
||||
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});
|
||||
@@ -15,20 +15,15 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -69,7 +64,7 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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});
|
||||
@@ -13,15 +13,11 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
||||
@@ -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});
|
||||
@@ -26,17 +26,20 @@ class VideoReplySkeleton extends StatelessWidget {
|
||||
width: 80,
|
||||
height: 13,
|
||||
color: bgColor,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin:
|
||||
const EdgeInsets.only(top: 4, left: 57, right: 6, bottom: 6),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 4,
|
||||
left: 57,
|
||||
right: 6,
|
||||
bottom: 6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 300,
|
||||
@@ -72,9 +75,9 @@ class VideoReplySkeleton extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
color: bgColor,
|
||||
),
|
||||
const SizedBox(width: 8)
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class WhisperItemSkeleton extends StatelessWidget {
|
||||
const WhisperItemSkeleton({super.key});
|
||||
|
||||
59
lib/common/widgets/appbar/appbar.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:PiliPlus/pages/common/multi_select/base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MultiSelectAppBarWidget extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
final MultiSelectBase ctr;
|
||||
final bool? visible;
|
||||
final AppBar child;
|
||||
final List<Widget>? children;
|
||||
|
||||
const MultiSelectAppBarWidget({
|
||||
super.key,
|
||||
required this.ctr,
|
||||
this.visible,
|
||||
this.children,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (visible ?? ctr.enableMultiSelect.value) {
|
||||
return AppBar(
|
||||
bottom: child.bottom,
|
||||
leading: IconButton(
|
||||
tooltip: '取消',
|
||||
onPressed: ctr.handleSelect,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
),
|
||||
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: () => ctr.handleSelect(checked: true),
|
||||
child: const Text('全选'),
|
||||
),
|
||||
...?children,
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: ctr.onRemove,
|
||||
child: Text(
|
||||
'移除',
|
||||
style: TextStyle(color: Get.theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => child.preferredSize;
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
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,
|
||||
required this.text,
|
||||
@@ -23,12 +27,11 @@ class PBadge extends StatelessWidget {
|
||||
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,
|
||||
});
|
||||
@@ -40,37 +43,49 @@ class PBadge extends StatelessWidget {
|
||||
}
|
||||
|
||||
ColorScheme theme = Theme.of(context).colorScheme;
|
||||
// 背景色
|
||||
Color bgColor = theme.primary;
|
||||
// 前景色
|
||||
Color color = theme.onPrimary;
|
||||
// 边框色
|
||||
|
||||
Color bgColor;
|
||||
Color color;
|
||||
Color borderColor = Colors.transparent;
|
||||
if (type == 'gray') {
|
||||
bgColor = Colors.black45;
|
||||
color = Colors.white;
|
||||
} else if (type == 'color') {
|
||||
bgColor = theme.secondaryContainer.withOpacity(0.5);
|
||||
color = theme.onSecondaryContainer;
|
||||
} else if (type == 'line') {
|
||||
bgColor = Colors.transparent;
|
||||
color = theme.primary;
|
||||
borderColor = theme.primary;
|
||||
} else if (type == 'error') {
|
||||
bgColor = theme.error;
|
||||
color = theme.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:
|
||||
if (Get.isDarkMode) {
|
||||
bgColor = theme.errorContainer;
|
||||
color = theme.onErrorContainer;
|
||||
} else {
|
||||
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 = theme.freeColor;
|
||||
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);
|
||||
}
|
||||
late EdgeInsets paddingStyle = const EdgeInsets.symmetric(
|
||||
vertical: 2,
|
||||
horizontal: 3,
|
||||
);
|
||||
BorderRadius br = size == PBadgeSize.small
|
||||
? const BorderRadius.all(Radius.circular(3))
|
||||
: const BorderRadius.all(Radius.circular(4));
|
||||
|
||||
Widget content = Container(
|
||||
padding: padding ?? paddingStyle,
|
||||
@@ -86,20 +101,19 @@ class PBadge extends StatelessWidget {
|
||||
: 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,
|
||||
|
||||
@@ -41,8 +41,8 @@ Widget mediumButton({
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
icon: Icon(icon),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
@@ -29,10 +29,10 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.outline,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return selected ? theme.colorScheme.secondaryContainer : null;
|
||||
}),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
selected ? theme.colorScheme.secondaryContainer : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
77
lib/common/widgets/color_palette.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
class ColorPalette extends StatelessWidget {
|
||||
final Color color;
|
||||
final bool selected;
|
||||
|
||||
const ColorPalette({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.selected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final Hct hct = Hct.fromInt(color.value);
|
||||
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());
|
||||
final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
|
||||
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());
|
||||
final checkbox = Color(Hct.from(hct.hue, 30.0, 40.0).toInt());
|
||||
Widget coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
Widget child = ClipOval(
|
||||
child: Column(
|
||||
children: [
|
||||
coloredBox(primary),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
coloredBox(tertiary),
|
||||
coloredBox(primaryContainer),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (selected) {
|
||||
child = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
child,
|
||||
Container(
|
||||
width: 23,
|
||||
height: 23,
|
||||
decoration: BoxDecoration(
|
||||
color: checkbox,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_rounded,
|
||||
color: primary,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/common/widgets/custom_icon.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 live_reserve = _CustomIconData(0xe806);
|
||||
static const IconData share = _CustomIconData(0xe807);
|
||||
static const IconData share_line = _CustomIconData(0xe808);
|
||||
static const IconData share_node = _CustomIconData(0xe809);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80a);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe811);
|
||||
static const IconData topic_tag = _CustomIconData(0xe812);
|
||||
static const IconData watch_later = _CustomIconData(0xe813);
|
||||
}
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon');
|
||||
}
|
||||
@@ -6,31 +6,38 @@ class CustomSliverPersistentHeaderDelegate
|
||||
required this.child,
|
||||
required this.bgColor,
|
||||
double extent = 45,
|
||||
}) : _minExtent = extent,
|
||||
_maxExtent = extent;
|
||||
this.needRebuild,
|
||||
}) : _minExtent = extent,
|
||||
_maxExtent = extent;
|
||||
final double _minExtent;
|
||||
final double _maxExtent;
|
||||
final Widget child;
|
||||
final Color bgColor;
|
||||
final Color? bgColor;
|
||||
final bool? needRebuild;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
//创建child子组件
|
||||
//shrinkOffset:child偏移值minExtent~maxExtent
|
||||
//overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: bgColor,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
return bgColor != null
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: bgColor!,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
}
|
||||
|
||||
//SliverPersistentHeader最大高度
|
||||
@@ -42,8 +49,8 @@ class CustomSliverPersistentHeaderDelegate
|
||||
double get minExtent => _minExtent;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(
|
||||
covariant CustomSliverPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.bgColor != bgColor;
|
||||
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.bgColor != bgColor ||
|
||||
(needRebuild == true && oldDelegate.child != child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
|
||||
class CustomToast extends StatelessWidget {
|
||||
const CustomToast({super.key, required this.msg});
|
||||
|
||||
final String msg;
|
||||
|
||||
static double toastOpacity = Pref.defaultToastOp;
|
||||
|
||||
@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(
|
||||
margin:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 30),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.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,
|
||||
@@ -44,21 +47,24 @@ class LoadingWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dialogBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
//loading animation
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//loading animation
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
||||
),
|
||||
|
||||
//msg
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
]),
|
||||
//msg
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
390
lib/common/widgets/custom_tooltip.dart
Normal file
@@ -0,0 +1,390 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show clampDouble;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum TooltipType { top, right }
|
||||
|
||||
class CustomTooltip extends StatefulWidget {
|
||||
const CustomTooltip({
|
||||
super.key,
|
||||
this.type = TooltipType.top,
|
||||
required this.overlayWidget,
|
||||
required this.child,
|
||||
this.indicator,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
final Widget child;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
|
||||
static final List<CustomTooltipState> _openedTooltips =
|
||||
<CustomTooltipState>[];
|
||||
|
||||
static bool dismissAllToolTips() {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
|
||||
for (final CustomTooltipState state in openedTooltips) {
|
||||
assert(state.mounted);
|
||||
state._scheduleDismissTooltip();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
State<CustomTooltip> createState() => CustomTooltipState();
|
||||
}
|
||||
|
||||
class CustomTooltipState extends State<CustomTooltip>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const Duration _fadeInDuration = Duration(milliseconds: 150);
|
||||
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
|
||||
|
||||
final OverlayPortalController _overlayController = OverlayPortalController();
|
||||
|
||||
AnimationController? _backingController;
|
||||
AnimationController get _controller {
|
||||
return _backingController ??= AnimationController(
|
||||
duration: _fadeInDuration,
|
||||
reverseDuration: _fadeOutDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener(_handleStatusChanged);
|
||||
}
|
||||
|
||||
CurvedAnimation? _backingOverlayAnimation;
|
||||
CurvedAnimation get _overlayAnimation {
|
||||
return _backingOverlayAnimation ??= CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
}
|
||||
|
||||
LongPressGestureRecognizer? _longPressRecognizer;
|
||||
|
||||
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
assert(mounted);
|
||||
switch ((_animationStatus.isDismissed, status.isDismissed)) {
|
||||
case (false, true):
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_overlayController.hide();
|
||||
case (true, false):
|
||||
_overlayController.show();
|
||||
CustomTooltip._openedTooltips.add(this);
|
||||
case (true, true) || (false, false):
|
||||
break;
|
||||
}
|
||||
_animationStatus = status;
|
||||
}
|
||||
|
||||
void _scheduleShowTooltip() {
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _scheduleDismissTooltip() {
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
assert(mounted);
|
||||
const Set<PointerDeviceKind> triggerModeDeviceKinds = <PointerDeviceKind>{
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.unknown,
|
||||
PointerDeviceKind.trackpad,
|
||||
};
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer(
|
||||
debugOwner: this,
|
||||
supportedDevices: triggerModeDeviceKinds,
|
||||
);
|
||||
_longPressRecognizer!
|
||||
..onLongPress = _scheduleShowTooltip
|
||||
..addPointer(event);
|
||||
}
|
||||
|
||||
Widget _buildCustomTooltipOverlay(BuildContext context) {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
);
|
||||
final RenderBox box = this.context.findRenderObject()! as RenderBox;
|
||||
final Offset target = box.localToGlobal(
|
||||
box.size.center(Offset.zero),
|
||||
ancestor: overlayState.context.findRenderObject(),
|
||||
);
|
||||
|
||||
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
|
||||
verticalOffset: box.size.height / 2,
|
||||
horizontslOffset: box.size.width / 2,
|
||||
type: widget.type,
|
||||
animation: _overlayAnimation,
|
||||
target: target,
|
||||
onDismiss: _scheduleDismissTooltip,
|
||||
overlayWidget: widget.overlayWidget,
|
||||
indicator: widget.indicator,
|
||||
);
|
||||
|
||||
return SelectionContainer.maybeOf(context) == null
|
||||
? overlayChild
|
||||
: SelectionContainer.disabled(child: overlayChild);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_longPressRecognizer?.onLongPressCancel = null;
|
||||
_longPressRecognizer?.dispose();
|
||||
_backingController?.dispose();
|
||||
_backingOverlayAnimation?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget result = Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: widget.child,
|
||||
);
|
||||
return OverlayPortal(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildCustomTooltipOverlay,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomTooltipOverlay extends StatelessWidget {
|
||||
const _CustomTooltipOverlay({
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.type,
|
||||
required this.animation,
|
||||
required this.target,
|
||||
required this.onDismiss,
|
||||
required this.overlayWidget,
|
||||
this.indicator,
|
||||
});
|
||||
|
||||
final double verticalOffset;
|
||||
final double horizontslOffset;
|
||||
final TooltipType type;
|
||||
final Animation<double> animation;
|
||||
final Offset target;
|
||||
final VoidCallback onDismiss;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _CustomMultiTooltipPositionDelegate(
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: false,
|
||||
),
|
||||
children: [
|
||||
LayoutId(
|
||||
id: 'overlay',
|
||||
child: overlayWidget(),
|
||||
),
|
||||
if (indicator != null)
|
||||
LayoutId(
|
||||
id: 'indicator',
|
||||
child: indicator!(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomMultiTooltipPositionDelegate({
|
||||
required this.type,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.preferBelow,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
|
||||
final Offset target;
|
||||
|
||||
final double verticalOffset;
|
||||
|
||||
final double horizontslOffset;
|
||||
|
||||
final bool preferBelow;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
case TooltipType.right:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
offset.dx - indicatorSize.width + 1,
|
||||
target.dy - indicatorSize.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) {
|
||||
return target != oldDelegate.target ||
|
||||
verticalOffset != oldDelegate.verticalOffset ||
|
||||
preferBelow != oldDelegate.preferBelow;
|
||||
}
|
||||
}
|
||||
|
||||
class TrianglePainter extends CustomPainter {
|
||||
TrianglePainter(this.color, {this.type = TooltipType.top});
|
||||
final TooltipType type;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
Path path;
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
path = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width / 2, size.height)
|
||||
..close();
|
||||
case TooltipType.right:
|
||||
path = Path()
|
||||
..moveTo(0, size.height / 2)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width, size.height)
|
||||
..close();
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color;
|
||||
}
|
||||
|
||||
Offset positionDependentBox({
|
||||
required TooltipType type,
|
||||
required Size size,
|
||||
required Size childSize,
|
||||
required Offset target,
|
||||
required bool preferBelow,
|
||||
double verticalOffset = 0.0,
|
||||
double horizontslOffset = 0.0,
|
||||
double margin = 10.0,
|
||||
}) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
// VERTICAL DIRECTION
|
||||
final bool fitsBelow =
|
||||
target.dy + verticalOffset + childSize.height <= size.height - margin;
|
||||
final bool fitsAbove =
|
||||
target.dy - verticalOffset - childSize.height >= margin;
|
||||
final bool tooltipBelow = fitsAbove == fitsBelow
|
||||
? preferBelow
|
||||
: fitsBelow;
|
||||
final double y;
|
||||
if (tooltipBelow) {
|
||||
y = math.min(target.dy + verticalOffset, size.height - margin);
|
||||
} else {
|
||||
y = math.max(target.dy - verticalOffset - childSize.height, margin);
|
||||
} // HORIZONTAL DIRECTION
|
||||
final double flexibleSpace = size.width - childSize.width;
|
||||
final double x = flexibleSpace <= 2 * margin
|
||||
// If there's not enough horizontal space for margin + child, center the
|
||||
// child.
|
||||
? flexibleSpace / 2.0
|
||||
: clampDouble(
|
||||
target.dx - childSize.width / 2,
|
||||
margin,
|
||||
flexibleSpace - margin,
|
||||
);
|
||||
return Offset(x, y);
|
||||
case TooltipType.right:
|
||||
final double dy = math.max(margin, target.dy - childSize.height / 2);
|
||||
final double dx = math.min(
|
||||
target.dx + horizontslOffset,
|
||||
size.width - childSize.width - margin,
|
||||
);
|
||||
return Offset(dx, dy);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ void showConfirmDialog({
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
@@ -30,7 +30,7 @@ void showConfirmDialog({
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: Text('确认'),
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -65,42 +65,43 @@ void showPgcFollowDialog({
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...[
|
||||
{'followStatus': 3, 'title': '看过'},
|
||||
{'followStatus': 2, 'title': '在看'},
|
||||
{'followStatus': 1, 'title': '想看'},
|
||||
].map(
|
||||
(Map item) => statusItem(
|
||||
enabled: followStatus != item['followStatus'],
|
||||
text: item['title'],
|
||||
onTap: () {
|
||||
Get.back();
|
||||
onUpdateStatus(item['followStatus']);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
'取消$type',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
onUpdateStatus(-1);
|
||||
},
|
||||
)
|
||||
],
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...const [
|
||||
(followStatus: 3, title: '看过'),
|
||||
(followStatus: 2, title: '在看'),
|
||||
(followStatus: 1, title: '想看'),
|
||||
].map(
|
||||
(item) => statusItem(
|
||||
enabled: followStatus != item.followStatus,
|
||||
text: item.title,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
onUpdateStatus(item.followStatus);
|
||||
},
|
||||
),
|
||||
));
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
'取消$type',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
onUpdateStatus(-1);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -21,8 +22,11 @@ void autoWrapReportDialog(
|
||||
title: const Text('举报'),
|
||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
||||
actionsPadding:
|
||||
const EdgeInsets.only(left: 16, right: 16, bottom: 10),
|
||||
actionsPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
),
|
||||
content: Form(
|
||||
key: key,
|
||||
child: Column(
|
||||
@@ -55,13 +59,20 @@ void autoWrapReportDialog(
|
||||
),
|
||||
if (reasonType == 0)
|
||||
ReasonField(
|
||||
onChanged: (value) => reasonDesc = value),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
BanUserCheckbox(onChanged: (value) => banUid = value),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
text: '拉黑该用户',
|
||||
onChanged: (value) => banUid = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -136,53 +147,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) {
|
||||
final theme = Theme.of(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.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
color: _selected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
' 拉黑该用户',
|
||||
style: TextStyle(
|
||||
color: _banUid ? theme.colorScheme.primary : null,
|
||||
),
|
||||
' ${widget.text}',
|
||||
style: TextStyle(color: _selected ? colorScheme.primary : null),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -193,34 +216,34 @@ 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 => {
|
||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||
'谣言类不实信息': {19: '涉政谣言', 22: '虚假不实信息', 20: '涉社会事件谣言'},
|
||||
'侵犯个人权益': {7: '人身攻击', 15: '侵犯隐私'},
|
||||
'有害社区环境': {
|
||||
1: '垃圾广告',
|
||||
4: '引战',
|
||||
5: '剧透',
|
||||
3: '刷屏',
|
||||
8: '视频不相关',
|
||||
18: '违规抽奖',
|
||||
17: '青少年不良信息',
|
||||
},
|
||||
'其他': {0: '其他'},
|
||||
};
|
||||
static Map<String, Map<int, String>> get commentReport => const {
|
||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||
'谣言类不实信息': {19: '涉政谣言', 22: '虚假不实信息', 20: '涉社会事件谣言'},
|
||||
'侵犯个人权益': {7: '人身攻击', 15: '侵犯隐私'},
|
||||
'有害社区环境': {
|
||||
1: '垃圾广告',
|
||||
4: '引战',
|
||||
5: '剧透',
|
||||
3: '刷屏',
|
||||
8: '视频不相关',
|
||||
18: '违规抽奖',
|
||||
17: '青少年不良信息',
|
||||
},
|
||||
'其他': {0: '其他'},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get dynamicReport => {
|
||||
'': {
|
||||
4: '垃圾广告',
|
||||
8: '引战',
|
||||
1: '色情',
|
||||
5: '人身攻击',
|
||||
3: '违法信息',
|
||||
9: '涉政谣言',
|
||||
10: '涉社会事件谣言',
|
||||
12: '虚假不实信息',
|
||||
13: '违法信息外链',
|
||||
0: '其他',
|
||||
},
|
||||
};
|
||||
static Map<String, Map<int, String>> get dynamicReport => const {
|
||||
'': {
|
||||
4: '垃圾广告',
|
||||
8: '引战',
|
||||
1: '色情',
|
||||
5: '人身攻击',
|
||||
3: '违法信息',
|
||||
9: '涉政谣言',
|
||||
10: '涉社会事件谣言',
|
||||
12: '虚假不实信息',
|
||||
13: '违法信息外链',
|
||||
0: '其他',
|
||||
},
|
||||
};
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
const ['头像违规', '昵称违规', '签名违规'][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();
|
||||
var 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -14,9 +14,9 @@ class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
|
||||
this.color,
|
||||
double? lineLengthScale,
|
||||
StrokeCap? strokeCap,
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
@@ -60,20 +60,21 @@ class RenderMaskedIcon extends RenderProxyBox {
|
||||
// );
|
||||
|
||||
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));
|
||||
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();
|
||||
|
||||
canvas.clipPath(path, doAntiAlias: false);
|
||||
canvas
|
||||
..save()
|
||||
..clipPath(path, doAntiAlias: false);
|
||||
super.paint(context, offset);
|
||||
|
||||
context.canvas.restore();
|
||||
|
||||
789
lib/common/widgets/dyn/button.dart
Normal file
@@ -0,0 +1,789 @@
|
||||
// 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 'elevated_button_theme.dart';
|
||||
/// @docImport 'menu_anchor.dart';
|
||||
/// @docImport 'text_button_theme.dart';
|
||||
/// @docImport 'text_theme.dart';
|
||||
/// @docImport 'theme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dyn/ink_well.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
|
||||
///
|
||||
/// See also:
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * [TextButton], a button with no outline or fill color.
|
||||
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
|
||||
/// the Material Design button types and how they should be used in designs.
|
||||
abstract class ButtonStyleButton extends StatefulWidget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const ButtonStyleButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.onLongPress,
|
||||
required this.onHover,
|
||||
required this.onFocusChange,
|
||||
required this.style,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.clipBehavior,
|
||||
this.statesController,
|
||||
this.isSemanticButton = true,
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
this.iconAlignment,
|
||||
this.tooltip,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// Called when the button is tapped or otherwise activated.
|
||||
///
|
||||
/// If this callback and [onLongPress] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Called when the button is long-pressed.
|
||||
///
|
||||
/// If this callback and [onPressed] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Called when a pointer enters or exits the button response area.
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered this
|
||||
/// part of the material and false if a pointer has exited this part of the
|
||||
/// material.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding
|
||||
/// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s
|
||||
/// that resolve to non-null values will similarly override the corresponding
|
||||
/// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf].
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
|
||||
/// [ButtonStyle.foregroundBuilder] is specified. In those
|
||||
/// cases the default is [Clip.antiAlias].
|
||||
final Clip? clipBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.material.inkwell.statesController}
|
||||
final WidgetStatesController? statesController;
|
||||
|
||||
/// Determine whether this subtree represents a button.
|
||||
///
|
||||
/// If this is null, the screen reader will not announce "button" when this
|
||||
/// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we
|
||||
/// traverse the menu system.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool? isSemanticButton;
|
||||
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
/// Text that describes the action that will occur when the button is pressed or
|
||||
/// hovered over.
|
||||
///
|
||||
/// This text is displayed when the user long-presses or hovers over the button
|
||||
/// in a tooltip. This string is also used for accessibility.
|
||||
///
|
||||
/// If null, the button will not display a tooltip.
|
||||
final String? tooltip;
|
||||
|
||||
/// Typically the button's label.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
/// Returns a [ButtonStyle] that's based primarily on the [Theme]'s
|
||||
/// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values
|
||||
/// filled out (non-null).
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter and by the
|
||||
/// style returned by [themeStyleOf] that some button-specific themes like
|
||||
/// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the
|
||||
/// default style of the [TextButton] subclass can be overridden with its
|
||||
/// [TextButton.style] constructor parameter, or with a [TextButtonTheme].
|
||||
///
|
||||
/// Concrete button subclasses should return a [ButtonStyle] with as many
|
||||
/// non-null properties as possible, where all of the non-null
|
||||
/// [WidgetStateProperty] properties resolve to non-null values.
|
||||
///
|
||||
/// ## Properties that can be null
|
||||
///
|
||||
/// Some properties, like [ButtonStyle.fixedSize] would override other values
|
||||
/// in the same [ButtonStyle] if set, so they are allowed to be null. Here is
|
||||
/// a summary of properties that are allowed to be null when returned in the
|
||||
/// [ButtonStyle] returned by this function, an why:
|
||||
///
|
||||
/// - [ButtonStyle.fixedSize] because it would override other values in the
|
||||
/// same [ButtonStyle], like [ButtonStyle.maximumSize].
|
||||
/// - [ButtonStyle.side] because null is a valid value for a button that has
|
||||
/// no side. [OutlinedButton] returns a non-null default for this, however.
|
||||
/// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]
|
||||
/// because they would override the [ButtonStyle.foregroundColor] and
|
||||
/// [ButtonStyle.backgroundColor] of the same [ButtonStyle].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [themeStyleOf], returns the ButtonStyle of this button's component
|
||||
/// theme.
|
||||
@protected
|
||||
ButtonStyle defaultStyleOf(BuildContext context);
|
||||
|
||||
/// Returns the ButtonStyle that belongs to the button's component theme.
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter.
|
||||
///
|
||||
/// Concrete button subclasses should return the ButtonStyle for the
|
||||
/// nearest subclass-specific inherited theme, and if no such theme
|
||||
/// exists, then the same value from the overall [Theme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
|
||||
@protected
|
||||
ButtonStyle? themeStyleOf(BuildContext context);
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// Buttons are disabled by default. To enable a button, set its [onPressed]
|
||||
/// or [onLongPress] properties to a non-null value.
|
||||
bool get enabled => onPressed != null || onLongPress != null;
|
||||
|
||||
@override
|
||||
State<ButtonStyleButton> createState() => _ButtonStyleState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
FlagProperty('enabled', value: enabled, ifFalse: 'disabled'),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<FocusNode>(
|
||||
'focusNode',
|
||||
focusNode,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns null if [value] is null, otherwise `WidgetStatePropertyAll<T>(value)`.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<T>? allOrNull<T>(T? value) =>
|
||||
value == null ? null : WidgetStatePropertyAll<T>(value);
|
||||
|
||||
/// Returns null if [enabled] and [disabled] are null.
|
||||
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
|
||||
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<Color?>? defaultColor(
|
||||
Color? enabled,
|
||||
Color? disabled,
|
||||
) {
|
||||
if ((enabled ?? disabled) == null) {
|
||||
return null;
|
||||
}
|
||||
return WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
|
||||
WidgetState.disabled: disabled,
|
||||
WidgetState.any: enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/// A convenience method used by subclasses in the framework, that returns an
|
||||
/// interpolated value based on the [fontSizeMultiplier] parameter:
|
||||
///
|
||||
/// * 0 - 1 [geometry1x]
|
||||
/// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1)
|
||||
/// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2)
|
||||
/// * otherwise [geometry3x]
|
||||
///
|
||||
/// This method is used by the framework for estimating the default paddings to
|
||||
/// use on a button with a text label, when the system text scaling setting
|
||||
/// changes. It's usually supplied with empirical [geometry1x], [geometry2x],
|
||||
/// [geometry3x] values adjusted for different system text scaling values, when
|
||||
/// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge]
|
||||
/// value).
|
||||
///
|
||||
/// The `fontSizeMultiplier` argument, for historical reasons, is the default
|
||||
/// font size specified in the [ButtonStyle], scaled by the ambient font
|
||||
/// scaler, then divided by 14.0 (the default font size used in buttons).
|
||||
static EdgeInsetsGeometry scaledPadding(
|
||||
EdgeInsetsGeometry geometry1x,
|
||||
EdgeInsetsGeometry geometry2x,
|
||||
EdgeInsetsGeometry geometry3x,
|
||||
double fontSizeMultiplier,
|
||||
) {
|
||||
return switch (fontSizeMultiplier) {
|
||||
<= 1 => geometry1x,
|
||||
< 2 => EdgeInsetsGeometry.lerp(
|
||||
geometry1x,
|
||||
geometry2x,
|
||||
fontSizeMultiplier - 1,
|
||||
)!,
|
||||
< 3 => EdgeInsetsGeometry.lerp(
|
||||
geometry2x,
|
||||
geometry3x,
|
||||
fontSizeMultiplier - 2,
|
||||
)!,
|
||||
_ => geometry3x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
|
||||
/// * [OutlinedButton], similar to [TextButton], but with an outline.
|
||||
/// * [TextButton], a simple button without a shadow.
|
||||
class _ButtonStyleState extends State<ButtonStyleButton>
|
||||
with TickerProviderStateMixin {
|
||||
AnimationController? controller;
|
||||
double? elevation;
|
||||
Color? backgroundColor;
|
||||
WidgetStatesController? internalStatesController;
|
||||
|
||||
void handleStatesControllerChange() {
|
||||
// Force a rebuild to resolve WidgetStateProperty properties
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
WidgetStatesController get statesController =>
|
||||
widget.statesController ?? internalStatesController!;
|
||||
|
||||
void initStatesController() {
|
||||
if (widget.statesController == null) {
|
||||
internalStatesController = WidgetStatesController();
|
||||
}
|
||||
statesController
|
||||
..update(WidgetState.disabled, !widget.enabled)
|
||||
..addListener(handleStatesControllerChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initStatesController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ButtonStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.statesController != oldWidget.statesController) {
|
||||
oldWidget.statesController?.removeListener(handleStatesControllerChange);
|
||||
if (widget.statesController != null) {
|
||||
internalStatesController?.dispose();
|
||||
internalStatesController = null;
|
||||
}
|
||||
initStatesController();
|
||||
}
|
||||
if (widget.enabled != oldWidget.enabled) {
|
||||
statesController.update(WidgetState.disabled, !widget.enabled);
|
||||
if (!widget.enabled) {
|
||||
// The button may have been disabled while a press gesture is currently underway.
|
||||
statesController.update(WidgetState.pressed, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
statesController.removeListener(handleStatesControllerChange);
|
||||
internalStatesController?.dispose();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
final ButtonStyle? widgetStyle = widget.style;
|
||||
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
|
||||
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
|
||||
|
||||
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
|
||||
final T? widgetValue = getProperty(widgetStyle);
|
||||
final T? themeValue = getProperty(themeStyle);
|
||||
final T? defaultValue = getProperty(defaultStyle);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
}
|
||||
|
||||
T? resolve<T>(
|
||||
WidgetStateProperty<T>? Function(ButtonStyle? style) getProperty,
|
||||
) {
|
||||
return effectiveValue((ButtonStyle? style) {
|
||||
return getProperty(style)?.resolve(statesController.value);
|
||||
});
|
||||
}
|
||||
|
||||
Color? effectiveIconColor() {
|
||||
return widgetStyle?.iconColor?.resolve(statesController.value) ??
|
||||
themeStyle?.iconColor?.resolve(statesController.value) ??
|
||||
widgetStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
themeStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
defaultStyle.iconColor?.resolve(statesController.value) ??
|
||||
// Fallback to foregroundColor if iconColor is null.
|
||||
defaultStyle.foregroundColor?.resolve(statesController.value);
|
||||
}
|
||||
|
||||
final double? resolvedElevation = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.elevation,
|
||||
);
|
||||
final TextStyle? resolvedTextStyle = resolve<TextStyle?>(
|
||||
(ButtonStyle? style) => style?.textStyle,
|
||||
);
|
||||
Color? resolvedBackgroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.backgroundColor,
|
||||
);
|
||||
final Color? resolvedForegroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.foregroundColor,
|
||||
);
|
||||
final Color? resolvedShadowColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.shadowColor,
|
||||
);
|
||||
final Color? resolvedSurfaceTintColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.surfaceTintColor,
|
||||
);
|
||||
final EdgeInsetsGeometry? resolvedPadding = resolve<EdgeInsetsGeometry?>(
|
||||
(ButtonStyle? style) => style?.padding,
|
||||
);
|
||||
final Size? resolvedMinimumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.minimumSize,
|
||||
);
|
||||
final Size? resolvedFixedSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.fixedSize,
|
||||
);
|
||||
final Size? resolvedMaximumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.maximumSize,
|
||||
);
|
||||
final Color? resolvedIconColor = effectiveIconColor();
|
||||
final double? resolvedIconSize = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.iconSize,
|
||||
);
|
||||
final BorderSide? resolvedSide = resolve<BorderSide?>(
|
||||
(ButtonStyle? style) => style?.side,
|
||||
);
|
||||
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>(
|
||||
(ButtonStyle? style) => style?.shape,
|
||||
);
|
||||
|
||||
final WidgetStateMouseCursor mouseCursor = _MouseCursor(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.mouseCursor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final WidgetStateProperty<Color?> overlayColor =
|
||||
WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.overlayColor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final VisualDensity? resolvedVisualDensity = effectiveValue(
|
||||
(ButtonStyle? style) => style?.visualDensity,
|
||||
);
|
||||
final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue(
|
||||
(ButtonStyle? style) => style?.tapTargetSize,
|
||||
);
|
||||
final Duration? resolvedAnimationDuration = effectiveValue(
|
||||
(ButtonStyle? style) => style?.animationDuration,
|
||||
);
|
||||
final bool resolvedEnableFeedback =
|
||||
effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true;
|
||||
final AlignmentGeometry? resolvedAlignment = effectiveValue(
|
||||
(ButtonStyle? style) => style?.alignment,
|
||||
);
|
||||
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
|
||||
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue(
|
||||
(ButtonStyle? style) => style?.splashFactory,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.backgroundBuilder,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.foregroundBuilder,
|
||||
);
|
||||
|
||||
final Clip effectiveClipBehavior =
|
||||
widget.clipBehavior ??
|
||||
((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null
|
||||
? Clip.antiAlias
|
||||
: Clip.none);
|
||||
|
||||
BoxConstraints effectiveConstraints = resolvedVisualDensity
|
||||
.effectiveConstraints(
|
||||
BoxConstraints(
|
||||
minWidth: resolvedMinimumSize!.width,
|
||||
minHeight: resolvedMinimumSize.height,
|
||||
maxWidth: resolvedMaximumSize!.width,
|
||||
maxHeight: resolvedMaximumSize.height,
|
||||
),
|
||||
);
|
||||
if (resolvedFixedSize != null) {
|
||||
final Size size = effectiveConstraints.constrain(resolvedFixedSize);
|
||||
if (size.width.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minWidth: size.width,
|
||||
maxWidth: size.width,
|
||||
);
|
||||
}
|
||||
if (size.height.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minHeight: size.height,
|
||||
maxHeight: size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Per the Material Design team: don't allow the VisualDensity
|
||||
// adjustment to reduce the width of the left/right padding. If we
|
||||
// did, VisualDensity.compact, the default for desktop/web, would
|
||||
// reduce the horizontal padding to zero.
|
||||
final double dy = densityAdjustment.dy;
|
||||
final double dx = math.max(0, densityAdjustment.dx);
|
||||
final EdgeInsetsGeometry padding = resolvedPadding!
|
||||
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
|
||||
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
|
||||
|
||||
// If an opaque button's background is becoming translucent while its
|
||||
// elevation is changing, change the elevation first. Material implicitly
|
||||
// animates its elevation but not its color. SKIA renders non-zero
|
||||
// elevations as a shadow colored fill behind the Material's background.
|
||||
if (resolvedAnimationDuration! > Duration.zero &&
|
||||
elevation != null &&
|
||||
backgroundColor != null &&
|
||||
elevation != resolvedElevation &&
|
||||
backgroundColor!.value != resolvedBackgroundColor!.value &&
|
||||
backgroundColor!.opacity == 1 &&
|
||||
resolvedBackgroundColor.opacity < 1 &&
|
||||
resolvedElevation == 0) {
|
||||
if (controller?.duration != resolvedAnimationDuration) {
|
||||
controller?.dispose();
|
||||
controller =
|
||||
AnimationController(
|
||||
duration: resolvedAnimationDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
setState(() {}); // Rebuild with the final background color.
|
||||
}
|
||||
});
|
||||
}
|
||||
resolvedBackgroundColor =
|
||||
backgroundColor; // Defer changing the background color.
|
||||
controller!.value = 0;
|
||||
controller!.forward();
|
||||
}
|
||||
elevation = resolvedElevation;
|
||||
backgroundColor = resolvedBackgroundColor;
|
||||
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Align(
|
||||
alignment: resolvedAlignment!,
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: resolvedForegroundBuilder != null
|
||||
? resolvedForegroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
widget.child,
|
||||
)
|
||||
: widget.child,
|
||||
),
|
||||
);
|
||||
if (resolvedBackgroundBuilder != null) {
|
||||
result = resolvedBackgroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
result = AnimatedTheme(
|
||||
duration: resolvedAnimationDuration,
|
||||
data: theme.copyWith(
|
||||
iconTheme: iconTheme.merge(
|
||||
IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
onHover: widget.onHover,
|
||||
mouseCursor: mouseCursor,
|
||||
enableFeedback: resolvedEnableFeedback,
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: widget.onFocusChange,
|
||||
autofocus: widget.autofocus,
|
||||
splashFactory: resolvedSplashFactory,
|
||||
overlayColor: overlayColor,
|
||||
highlightColor: Colors.transparent,
|
||||
customBorder: resolvedShape!.copyWith(side: resolvedSide),
|
||||
statesController: statesController,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
result = Tooltip(message: widget.tooltip, child: result);
|
||||
}
|
||||
|
||||
final Size minSize;
|
||||
switch (resolvedTapTargetSize!) {
|
||||
case MaterialTapTargetSize.padded:
|
||||
minSize = Size(
|
||||
kMinInteractiveDimension + densityAdjustment.dx,
|
||||
kMinInteractiveDimension + densityAdjustment.dy,
|
||||
);
|
||||
assert(minSize.width >= 0.0);
|
||||
assert(minSize.height >= 0.0);
|
||||
case MaterialTapTargetSize.shrinkWrap:
|
||||
minSize = Size.zero;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
button: widget.isSemanticButton,
|
||||
enabled: widget.enabled,
|
||||
child: _InputPadding(
|
||||
minSize: minSize,
|
||||
child: ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Material(
|
||||
elevation: resolvedElevation!,
|
||||
textStyle: resolvedTextStyle?.copyWith(
|
||||
color: resolvedForegroundColor,
|
||||
),
|
||||
shape: resolvedShape.copyWith(side: resolvedSide),
|
||||
color: resolvedBackgroundColor,
|
||||
shadowColor: resolvedShadowColor,
|
||||
surfaceTintColor: resolvedSurfaceTintColor,
|
||||
type: resolvedBackgroundColor == null
|
||||
? MaterialType.transparency
|
||||
: MaterialType.button,
|
||||
animationDuration: resolvedAnimationDuration,
|
||||
clipBehavior: effectiveClipBehavior,
|
||||
borderOnForeground: false,
|
||||
child: result,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MouseCursor extends WidgetStateMouseCursor {
|
||||
const _MouseCursor(this.resolveCallback);
|
||||
|
||||
final WidgetPropertyResolver<MouseCursor?> resolveCallback;
|
||||
|
||||
@override
|
||||
MouseCursor resolve(Set<WidgetState> states) => resolveCallback(states)!;
|
||||
|
||||
@override
|
||||
String get debugDescription => 'ButtonStyleButton_MouseCursor';
|
||||
}
|
||||
|
||||
/// A widget to pad the area around a [ButtonStyleButton]'s inner [Material].
|
||||
///
|
||||
/// Redirect taps that occur in the padded area around the child to the center
|
||||
/// of the child. This increases the size of the button and the button's
|
||||
/// "tap target", but not its material or its ink splashes.
|
||||
class _InputPadding extends SingleChildRenderObjectWidget {
|
||||
const _InputPadding({super.child, required this.minSize});
|
||||
|
||||
final Size minSize;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderInputPadding(minSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant _RenderInputPadding renderObject,
|
||||
) {
|
||||
renderObject.minSize = minSize;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderInputPadding extends RenderShiftedBox {
|
||||
_RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
|
||||
|
||||
Size get minSize => _minSize;
|
||||
Size _minSize;
|
||||
set minSize(Size value) {
|
||||
if (_minSize == value) {
|
||||
return;
|
||||
}
|
||||
_minSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Size _computeSize({
|
||||
required BoxConstraints constraints,
|
||||
required ChildLayouter layoutChild,
|
||||
}) {
|
||||
if (child != null) {
|
||||
final Size childSize = layoutChild(child!, constraints);
|
||||
final double height = math.max(childSize.width, minSize.width);
|
||||
final double width = math.max(childSize.height, minSize.height);
|
||||
return constraints.constrain(Size(height, width));
|
||||
}
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
return _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.dryLayoutChild,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(
|
||||
covariant BoxConstraints constraints,
|
||||
TextBaseline baseline,
|
||||
) {
|
||||
final RenderBox? child = this.child;
|
||||
if (child == null) {
|
||||
return null;
|
||||
}
|
||||
final double? result = child.getDryBaseline(constraints, baseline);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
final Size childSize = child.getDryLayout(constraints);
|
||||
return result +
|
||||
Alignment.center
|
||||
.alongOffset(getDryLayout(constraints) - childSize as Offset)
|
||||
.dy;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.layoutChild,
|
||||
);
|
||||
if (child != null) {
|
||||
final BoxParentData childParentData = child!.parentData! as BoxParentData;
|
||||
childParentData.offset = Alignment.center.alongOffset(
|
||||
size - child!.size as Offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
if (super.hitTest(result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
final Offset center = child!.size.center(Offset.zero);
|
||||
return result.addWithRawTransform(
|
||||
transform: MatrixUtils.forceToPoint(center),
|
||||
position: center,
|
||||
hitTest: (BoxHitTestResult result, Offset position) {
|
||||
assert(position == center);
|
||||
return child!.hitTest(result, position: center);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
1418
lib/common/widgets/dyn/ink_well.dart
Normal file
676
lib/common/widgets/dyn/text_button.dart
Normal file
@@ -0,0 +1,676 @@
|
||||
// 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 'elevated_button.dart';
|
||||
/// @docImport 'filled_button.dart';
|
||||
/// @docImport 'material.dart';
|
||||
/// @docImport 'outlined_button.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dyn/button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
|
||||
|
||||
/// A Material Design "Text Button".
|
||||
///
|
||||
/// Use text buttons on toolbars, in dialogs, or inline with other
|
||||
/// content but offset from that content with padding so that the
|
||||
/// button's presence is obvious. Text buttons do not have visible
|
||||
/// borders and must therefore rely on their position relative to
|
||||
/// other content for context. In dialogs and cards, they should be
|
||||
/// grouped together in one of the bottom corners. Avoid using text
|
||||
/// buttons where they would blend in with other content, for example
|
||||
/// in the middle of lists.
|
||||
///
|
||||
/// A text button is a label [child] displayed on a (zero elevation)
|
||||
/// [Material] widget. The label's [Text] and [Icon] widgets are
|
||||
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
|
||||
/// button reacts to touches by filling with the [style]'s
|
||||
/// [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// The text button's default style is defined by [defaultStyleOf].
|
||||
/// The style of this text button can be overridden with its [style]
|
||||
/// parameter. The style of all text buttons in a subtree can be
|
||||
/// overridden with the [TextButtonTheme] and the style of all of the
|
||||
/// text buttons in an app can be overridden with the [Theme]'s
|
||||
/// [ThemeData.textButtonTheme] property.
|
||||
///
|
||||
/// The static [styleFrom] method is a convenient way to create a
|
||||
/// text button [ButtonStyle] from simple values.
|
||||
///
|
||||
/// If the [onPressed] and [onLongPress] callbacks are null, then this
|
||||
/// button will be disabled, it will not react to touch.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows various ways to configure TextButtons, from the
|
||||
/// simplest default appearance to versions that don't resemble
|
||||
/// Material Design at all.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates using the [statesController] parameter to create a button
|
||||
/// that adds support for [WidgetState.selected].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * <https://material.io/design/components/buttons.html>
|
||||
/// * <https://m3.material.io/components/buttons>
|
||||
class TextButton extends ButtonStyleButton {
|
||||
/// Create a [TextButton].
|
||||
const TextButton({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
super.autofocus = false,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
super.isSemanticButton,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
/// Create a text button from a pair of widgets that serve as the button's
|
||||
/// [icon] and [label].
|
||||
///
|
||||
/// The icon and label are arranged in a row and padded by 8 logical pixels
|
||||
/// at the ends, with an 8 pixel gap in between.
|
||||
///
|
||||
/// If [icon] is null, will create a [TextButton] instead.
|
||||
///
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
///
|
||||
factory TextButton.icon({
|
||||
Key? key,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
ValueChanged<bool>? onHover,
|
||||
ValueChanged<bool>? onFocusChange,
|
||||
ButtonStyle? style,
|
||||
FocusNode? focusNode,
|
||||
bool? autofocus,
|
||||
Clip? clipBehavior,
|
||||
WidgetStatesController? statesController,
|
||||
Widget? icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) {
|
||||
if (icon == null) {
|
||||
return TextButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return _TextButtonWithIcon(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
icon: icon,
|
||||
label: label,
|
||||
iconAlignment: iconAlignment,
|
||||
);
|
||||
}
|
||||
|
||||
/// A static convenience method that constructs a text button
|
||||
/// [ButtonStyle] given simple values.
|
||||
///
|
||||
/// The [foregroundColor] and [disabledForegroundColor] colors are used
|
||||
/// to create a [WidgetStateProperty] [ButtonStyle.foregroundColor], and
|
||||
/// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
|
||||
///
|
||||
/// The [backgroundColor] and [disabledBackgroundColor] colors are
|
||||
/// used to create a [WidgetStateProperty] [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
|
||||
/// parameters are used to construct [ButtonStyle.mouseCursor].
|
||||
///
|
||||
/// The [iconColor], [disabledIconColor] are used to construct
|
||||
/// [ButtonStyle.iconColor] and [iconSize] is used to construct
|
||||
/// [ButtonStyle.iconSize].
|
||||
///
|
||||
/// If [iconColor] is null, the button icon will use [foregroundColor]. If [foregroundColor] is also
|
||||
/// null, the button icon will use the default icon color.
|
||||
///
|
||||
/// If [overlayColor] is specified and its value is [Colors.transparent]
|
||||
/// then the pressed/focused/hovered highlights are effectively defeated.
|
||||
/// Otherwise a [WidgetStateProperty] with the same opacities as the
|
||||
/// default is created.
|
||||
///
|
||||
/// All of the other parameters are either used directly or used to
|
||||
/// create a [WidgetStateProperty] with a single value for all
|
||||
/// states.
|
||||
///
|
||||
/// All parameters default to null. By default this method returns
|
||||
/// a [ButtonStyle] that doesn't override anything.
|
||||
///
|
||||
/// For example, to override the default text and icon colors for a
|
||||
/// [TextButton], as well as its overlay color, with all of the
|
||||
/// standard opacity adjustments for the pressed, focused, and
|
||||
/// hovered states, one could write:
|
||||
///
|
||||
/// ```dart
|
||||
/// TextButton(
|
||||
/// style: TextButton.styleFrom(foregroundColor: Colors.green),
|
||||
/// child: const Text('Give Kate a mix tape'),
|
||||
/// onPressed: () {
|
||||
/// // ...
|
||||
/// },
|
||||
/// ),
|
||||
/// ```
|
||||
static ButtonStyle styleFrom({
|
||||
Color? foregroundColor,
|
||||
Color? backgroundColor,
|
||||
Color? disabledForegroundColor,
|
||||
Color? disabledBackgroundColor,
|
||||
Color? shadowColor,
|
||||
Color? surfaceTintColor,
|
||||
Color? iconColor,
|
||||
double? iconSize,
|
||||
IconAlignment? iconAlignment,
|
||||
Color? disabledIconColor,
|
||||
Color? overlayColor,
|
||||
double? elevation,
|
||||
TextStyle? textStyle,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Size? minimumSize,
|
||||
Size? fixedSize,
|
||||
Size? maximumSize,
|
||||
BorderSide? side,
|
||||
OutlinedBorder? shape,
|
||||
MouseCursor? enabledMouseCursor,
|
||||
MouseCursor? disabledMouseCursor,
|
||||
VisualDensity? visualDensity,
|
||||
MaterialTapTargetSize? tapTargetSize,
|
||||
Duration? animationDuration,
|
||||
bool? enableFeedback,
|
||||
AlignmentGeometry? alignment,
|
||||
InteractiveInkFeatureFactory? splashFactory,
|
||||
ButtonLayerBuilder? backgroundBuilder,
|
||||
ButtonLayerBuilder? foregroundBuilder,
|
||||
}) {
|
||||
final WidgetStateProperty<Color?>? backgroundColorProp = switch ((
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? iconColorProp = switch ((
|
||||
iconColor,
|
||||
disabledIconColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(iconColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? overlayColorProp = switch ((
|
||||
foregroundColor,
|
||||
overlayColor,
|
||||
)) {
|
||||
(null, null) => null,
|
||||
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
|
||||
(_, final Color color) || (final Color color, _) =>
|
||||
WidgetStateProperty<Color?>.fromMap(<WidgetState, Color?>{
|
||||
WidgetState.pressed: color.withValues(alpha: 0.1),
|
||||
WidgetState.hovered: color.withValues(alpha: 0.08),
|
||||
WidgetState.focused: color.withValues(alpha: 0.1),
|
||||
}),
|
||||
};
|
||||
|
||||
return ButtonStyle(
|
||||
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
|
||||
foregroundColor: ButtonStyleButton.defaultColor(
|
||||
foregroundColor,
|
||||
disabledForegroundColor,
|
||||
),
|
||||
backgroundColor: backgroundColorProp,
|
||||
overlayColor: overlayColorProp,
|
||||
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
|
||||
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
|
||||
iconColor: iconColorProp,
|
||||
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
|
||||
iconAlignment: iconAlignment,
|
||||
elevation: ButtonStyleButton.allOrNull<double>(elevation),
|
||||
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
|
||||
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
|
||||
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
|
||||
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
|
||||
side: ButtonStyleButton.allOrNull<BorderSide>(side),
|
||||
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
|
||||
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
|
||||
<WidgetStatesConstraint, MouseCursor?>{
|
||||
WidgetState.disabled: disabledMouseCursor,
|
||||
WidgetState.any: enabledMouseCursor,
|
||||
},
|
||||
),
|
||||
visualDensity: visualDensity,
|
||||
tapTargetSize: tapTargetSize,
|
||||
animationDuration: animationDuration,
|
||||
enableFeedback: enableFeedback,
|
||||
alignment: alignment,
|
||||
splashFactory: splashFactory,
|
||||
backgroundBuilder: backgroundBuilder,
|
||||
foregroundBuilder: foregroundBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
/// Defines the button's default appearance.
|
||||
///
|
||||
/// {@template flutter.material.text_button.default_style_of}
|
||||
/// The button [child]'s [Text] and [Icon] widgets are rendered with
|
||||
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
|
||||
/// the style's overlay color when the button is focused, hovered
|
||||
/// or pressed. The button's background color becomes its [Material]
|
||||
/// color and is transparent by default.
|
||||
///
|
||||
/// All of the [ButtonStyle]'s defaults appear below.
|
||||
///
|
||||
/// In this list "Theme.foo" is shorthand for
|
||||
/// `Theme.of(context).foo`. Color scheme values like
|
||||
/// "onSurface(0.38)" are shorthand for
|
||||
/// `onSurface.withValues(alpha: 0.38)`. [WidgetStateProperty] valued
|
||||
/// properties that are not followed by a sublist have the same
|
||||
/// value for all states, otherwise the values are as specified for
|
||||
/// each state and "others" means all other states.
|
||||
///
|
||||
/// The "default font size" below refers to the font size specified in the
|
||||
/// [defaultStyleOf] method (or 14.0 if unspecified), scaled by the
|
||||
/// `MediaQuery.textScalerOf(context).scale` method. And the names of the
|
||||
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been abbreviated
|
||||
/// for readability.
|
||||
///
|
||||
/// The color of the [ButtonStyle.textStyle] is not used, the
|
||||
/// [ButtonStyle.foregroundColor] color is used instead.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// ## Material 2 defaults
|
||||
///
|
||||
/// * `textStyle` - Theme.textTheme.button
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.12)
|
||||
/// * `shadowColor` - Theme.shadowColor
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - (horizontal(12), vertical(8))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 36)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - InkRipple.splashFactory
|
||||
///
|
||||
/// The default padding values for the [TextButton.icon] factory are slightly different:
|
||||
///
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - all(8)
|
||||
/// * `14 < default font size <= 28 `- lerp(all(8), horizontal(4))
|
||||
/// * `28 < default font size` - horizontal(4)
|
||||
///
|
||||
/// The default value for `side`, which defines the appearance of the button's
|
||||
/// outline, is null. That means that the outline is defined by the button
|
||||
/// shape's [OutlinedBorder.side]. Typically the default value of an
|
||||
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
|
||||
///
|
||||
/// ## Material 3 defaults
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is set to true the following defaults will
|
||||
/// be used:
|
||||
///
|
||||
/// {@template flutter.material.text_button.material3_defaults}
|
||||
/// * `textStyle` - Theme.textTheme.labelLarge
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.1)
|
||||
/// * others - null
|
||||
/// * `shadowColor` - Colors.transparent,
|
||||
/// * `surfaceTintColor` - null
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - lerp(horizontal(12), horizontal(4))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 40)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - StadiumBorder()
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - Theme.splashFactory
|
||||
///
|
||||
/// For the [TextButton.icon] factory, the end (generally the right) value of
|
||||
/// `padding` is increased from 12 to 16.
|
||||
/// {@endtemplate}
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
|
||||
return Theme.of(context).useMaterial3
|
||||
? _TextButtonDefaultsM3(context)
|
||||
: styleFrom(
|
||||
foregroundColor: colorScheme.primary,
|
||||
disabledForegroundColor: colorScheme.onSurface.withValues(
|
||||
alpha: 0.38,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
shadowColor: theme.shadowColor,
|
||||
elevation: 0,
|
||||
textStyle: theme.textTheme.labelLarge,
|
||||
padding: _scaledPadding(context),
|
||||
minimumSize: const Size(64, 36),
|
||||
maximumSize: Size.infinite,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
enabledMouseCursor: SystemMouseCursors.click,
|
||||
disabledMouseCursor: SystemMouseCursors.basic,
|
||||
visualDensity: theme.visualDensity,
|
||||
tapTargetSize: theme.materialTapTargetSize,
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the [TextButtonThemeData.style] of the closest
|
||||
/// [TextButtonTheme] ancestor.
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
return TextButtonTheme.of(context).style;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final double defaultFontSize = theme.textTheme.labelLarge?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
return ButtonStyleButton.scaledPadding(
|
||||
theme.useMaterial3
|
||||
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
}
|
||||
|
||||
class _TextButtonWithIcon extends TextButton {
|
||||
_TextButtonWithIcon({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
bool? autofocus,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
required Widget icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) : super(
|
||||
autofocus: autofocus ?? false,
|
||||
child: _TextButtonWithIconChild(
|
||||
icon: icon,
|
||||
label: label,
|
||||
buttonStyle: style,
|
||||
iconAlignment: iconAlignment,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
||||
final ButtonStyle buttonStyle = super.defaultStyleOf(context);
|
||||
final double defaultFontSize =
|
||||
buttonStyle.textStyle?.resolve(const <WidgetState>{})?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
|
||||
useMaterial3
|
||||
? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
return buttonStyle.copyWith(
|
||||
padding: WidgetStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextButtonWithIconChild extends StatelessWidget {
|
||||
const _TextButtonWithIconChild({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.buttonStyle,
|
||||
required this.iconAlignment,
|
||||
});
|
||||
|
||||
final Widget label;
|
||||
final Widget icon;
|
||||
final ButtonStyle? buttonStyle;
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double defaultFontSize =
|
||||
buttonStyle?.textStyle?.resolve(const <WidgetState>{})?.fontSize ??
|
||||
14.0;
|
||||
final double scale =
|
||||
clampDouble(
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0,
|
||||
1.0,
|
||||
2.0,
|
||||
) -
|
||||
1.0;
|
||||
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
|
||||
final IconAlignment effectiveIconAlignment =
|
||||
iconAlignment ??
|
||||
textButtonTheme.style?.iconAlignment ??
|
||||
buttonStyle?.iconAlignment ??
|
||||
IconAlignment.start;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: lerpDouble(8, 4, scale)!,
|
||||
children: effectiveIconAlignment == IconAlignment.start
|
||||
? <Widget>[icon, Flexible(child: label)]
|
||||
: <Widget>[Flexible(child: label), icon],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - TextButton
|
||||
|
||||
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
||||
// "END GENERATED" comments are generated from data in the Material
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
class _TextButtonDefaultsM3 extends ButtonStyle {
|
||||
_TextButtonDefaultsM3(this.context)
|
||||
: super(
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
|
||||
@override
|
||||
WidgetStateProperty<TextStyle?> get textStyle =>
|
||||
WidgetStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get backgroundColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get foregroundColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get overlayColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary.withValues(alpha: 0.08);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get shadowColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get surfaceTintColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get elevation =>
|
||||
const WidgetStatePropertyAll<double>(0.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<EdgeInsetsGeometry>? get padding =>
|
||||
WidgetStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context));
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get minimumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size(64.0, 40.0));
|
||||
|
||||
// No default fixedSize
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get iconSize =>
|
||||
const WidgetStatePropertyAll<double>(18.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get iconColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get maximumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size.infinite);
|
||||
|
||||
// No default side
|
||||
|
||||
@override
|
||||
WidgetStateProperty<OutlinedBorder>? get shape =>
|
||||
const WidgetStatePropertyAll<OutlinedBorder>(StadiumBorder());
|
||||
|
||||
@override
|
||||
WidgetStateProperty<MouseCursor?>? get mouseCursor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return SystemMouseCursors.basic;
|
||||
}
|
||||
return SystemMouseCursors.click;
|
||||
});
|
||||
|
||||
@override
|
||||
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
|
||||
|
||||
@override
|
||||
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
|
||||
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - TextButton
|
||||
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
|
||||
class DynamicSliverAppBar extends StatefulWidget {
|
||||
const DynamicSliverAppBar({
|
||||
class DynamicSliverAppBarMedium extends StatefulWidget {
|
||||
const DynamicSliverAppBarMedium({
|
||||
this.flexibleSpace,
|
||||
super.key,
|
||||
this.leading,
|
||||
@@ -88,22 +88,17 @@ class DynamicSliverAppBar extends StatefulWidget {
|
||||
final CustomClipper<Path>? appBarClipper;
|
||||
|
||||
@override
|
||||
State<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
|
||||
State<DynamicSliverAppBarMedium> createState() =>
|
||||
_DynamicSliverAppBarMediumState();
|
||||
}
|
||||
|
||||
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
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
|
||||
@@ -118,11 +113,21 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
});
|
||||
}
|
||||
|
||||
Orientation? _orientation;
|
||||
late Size size;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
super.didChangeDependencies();
|
||||
size = MediaQuery.sizeOf(context);
|
||||
final orientation = size.width > size.height
|
||||
? Orientation.landscape
|
||||
: Orientation.portrait;
|
||||
if (orientation != _orientation) {
|
||||
_orientation = orientation;
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -130,16 +135,19 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
//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 ?? SizedBox(height: kToolbarHeight),
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
width: size.width,
|
||||
child: widget.flexibleSpace,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MediaQuery.orientationOf(context);
|
||||
|
||||
return SliverAppBar(
|
||||
final padding = MediaQuery.viewPaddingOf(context).top;
|
||||
return SliverAppBar.medium(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
title: widget.title,
|
||||
@@ -158,7 +166,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
centerTitle: widget.centerTitle,
|
||||
excludeHeaderSemantics: widget.excludeHeaderSemantics,
|
||||
titleSpacing: widget.titleSpacing,
|
||||
collapsedHeight: widget.collapsedHeight,
|
||||
floating: widget.floating,
|
||||
pinned: widget.pinned,
|
||||
snap: widget.snap,
|
||||
@@ -166,8 +173,9 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
stretchTriggerOffset: widget.stretchTriggerOffset,
|
||||
onStretchTrigger: widget.onStretchTrigger,
|
||||
shape: widget.shape,
|
||||
toolbarHeight: widget.toolbarHeight,
|
||||
expandedHeight: _height,
|
||||
toolbarHeight: kToolbarHeight,
|
||||
collapsedHeight: kToolbarHeight + padding + 1,
|
||||
expandedHeight: _height - padding,
|
||||
leadingWidth: widget.leadingWidth,
|
||||
toolbarTextStyle: widget.toolbarTextStyle,
|
||||
titleTextStyle: widget.titleTextStyle,
|
||||
@@ -1,656 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image_save.dart';
|
||||
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/bangumi/info.dart' as bangumi;
|
||||
import 'package:PiliPlus/models/video_detail_res.dart' as video;
|
||||
import 'package:PiliPlus/pages/common/common_slide_page.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.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 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
enum EpisodeType { part, season, bangumi }
|
||||
|
||||
extension EpisodeTypeExt on EpisodeType {
|
||||
String get title => ['分P', '合集', '番剧'][index];
|
||||
}
|
||||
|
||||
class EpisodePanel extends CommonSlidePage {
|
||||
const EpisodePanel({
|
||||
super.key,
|
||||
super.enableSlide,
|
||||
required this.videoIntroController,
|
||||
required this.heroTag,
|
||||
required this.type,
|
||||
// required this.count,
|
||||
// required this.name,
|
||||
required this.aid,
|
||||
required this.bvid,
|
||||
required this.cid,
|
||||
required this.cover,
|
||||
this.showTitle,
|
||||
required this.list,
|
||||
this.seasonId,
|
||||
this.initialTabIndex = 0,
|
||||
this.isSupportReverse,
|
||||
this.isReversed,
|
||||
this.onReverse,
|
||||
required this.changeFucCall,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final VideoIntroController videoIntroController;
|
||||
final String heroTag;
|
||||
final EpisodeType type;
|
||||
// final int count;
|
||||
// final String name;
|
||||
final int? aid;
|
||||
final String bvid;
|
||||
final int cid;
|
||||
final String? cover;
|
||||
final bool? showTitle;
|
||||
final List list;
|
||||
final int? seasonId;
|
||||
final int initialTabIndex;
|
||||
final bool? isSupportReverse;
|
||||
final bool? isReversed;
|
||||
final Function changeFucCall;
|
||||
final VoidCallback? onReverse;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
State<EpisodePanel> createState() => _EpisodePanelState();
|
||||
}
|
||||
|
||||
class _EpisodePanelState extends CommonSlidePageState<EpisodePanel>
|
||||
with SingleTickerProviderStateMixin {
|
||||
// tab
|
||||
late final TabController _tabController = TabController(
|
||||
initialIndex: widget.initialTabIndex,
|
||||
length: widget.list.length,
|
||||
vsync: this,
|
||||
)..addListener(listener);
|
||||
late final RxInt _currentTabIndex = _tabController.index.obs;
|
||||
|
||||
List get _getCurrEpisodes => widget.type == EpisodeType.season
|
||||
? widget.list[_currentTabIndex.value].episodes
|
||||
: widget.list[_currentTabIndex.value];
|
||||
|
||||
// item
|
||||
late RxInt _currentItemIndex;
|
||||
int get _findCurrentItemIndex => max(
|
||||
0,
|
||||
_getCurrEpisodes.indexWhere((item) => item.cid == widget.cid),
|
||||
);
|
||||
|
||||
late final List<bool> _isReversed;
|
||||
late final List<ItemScrollController> _itemScrollController;
|
||||
|
||||
// fav
|
||||
Rx<LoadingState>? _favState;
|
||||
|
||||
late bool _isInit = true;
|
||||
|
||||
void listener() {
|
||||
_currentTabIndex.value = _tabController.index;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EpisodePanel oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.showTitle != false) {
|
||||
return;
|
||||
}
|
||||
|
||||
void jumpToCurrent() {
|
||||
int newItemIndex = _findCurrentItemIndex;
|
||||
if (_currentItemIndex.value != _findCurrentItemIndex) {
|
||||
_currentItemIndex.value = newItemIndex;
|
||||
try {
|
||||
_itemScrollController[_currentTabIndex.value].jumpTo(
|
||||
index: newItemIndex,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// jump to current
|
||||
if (_currentTabIndex.value != widget.initialTabIndex) {
|
||||
_tabController.animateTo(
|
||||
widget.initialTabIndex,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 300)).then((_) {
|
||||
jumpToCurrent();
|
||||
});
|
||||
} else {
|
||||
jumpToCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_itemScrollController =
|
||||
List.generate(widget.list.length, (_) => ItemScrollController());
|
||||
_isReversed = List.generate(widget.list.length, (_) => false);
|
||||
|
||||
if (widget.type == EpisodeType.season && Accounts.main.isLogin) {
|
||||
_favState = LoadingState.loading().obs;
|
||||
VideoHttp.videoRelation(bvid: widget.bvid).then((result) {
|
||||
if (result['status']) {
|
||||
if (result['data']?['season_fav'] is bool) {
|
||||
_favState!.value =
|
||||
LoadingState.success(result['data']['season_fav']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_currentItemIndex = _findCurrentItemIndex.obs;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInit = false;
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
try {
|
||||
_itemScrollController[widget.initialTabIndex]
|
||||
.jumpTo(index: _currentItemIndex.value);
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.removeListener(listener);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isInit) {
|
||||
return CustomScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
|
||||
return super.build(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(ThemeData theme) {
|
||||
return Material(
|
||||
color: widget.showTitle == false
|
||||
? Colors.transparent
|
||||
: theme.colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildToolbar(theme),
|
||||
if (widget.type == EpisodeType.season && widget.list.length > 1) ...[
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
padding: const EdgeInsets.only(right: 60),
|
||||
isScrollable: true,
|
||||
tabs: widget.list.map((item) => Tab(text: item.title)).toList(),
|
||||
dividerHeight: 1,
|
||||
dividerColor: theme.dividerColor.withOpacity(0.1),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: tabBarView(
|
||||
controller: _tabController,
|
||||
children: List.generate(
|
||||
widget.list.length,
|
||||
(index) => _buildBody(
|
||||
theme,
|
||||
index,
|
||||
widget.list[index].episodes,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else
|
||||
Expanded(
|
||||
child: enableSlide ? slideList(theme) : buildList(theme),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildList(ThemeData theme) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildBody(theme, 0, _getCurrEpisodes),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(ThemeData theme, int index, episodes) {
|
||||
return KeepAliveWrapper(
|
||||
builder: (context) => ScrollablePositionedList.separated(
|
||||
padding: EdgeInsets.only(
|
||||
top: 7,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
reverse: _isReversed[index],
|
||||
itemCount: episodes.length,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final episode = episodes[index];
|
||||
return widget.type == EpisodeType.season &&
|
||||
widget.showTitle != false &&
|
||||
episode.pages.length > 1
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Obx(
|
||||
() => _buildEpisodeItem(
|
||||
theme: theme,
|
||||
episode: episode,
|
||||
index: index,
|
||||
length: episodes.length,
|
||||
isCurrentIndex:
|
||||
_currentTabIndex.value == widget.initialTabIndex
|
||||
? _currentItemIndex.value == index
|
||||
: false,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 5),
|
||||
child: PagesPanel(
|
||||
list:
|
||||
widget.initialTabIndex == _currentTabIndex.value &&
|
||||
index == _currentItemIndex.value
|
||||
? null
|
||||
: episode.pages,
|
||||
cover: episode.arc?.pic,
|
||||
heroTag: widget.heroTag,
|
||||
videoIntroController: widget.videoIntroController,
|
||||
bvid: IdUtils.av2bv(episode.aid),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Obx(
|
||||
() => _buildEpisodeItem(
|
||||
theme: theme,
|
||||
episode: episode,
|
||||
index: index,
|
||||
length: episodes.length,
|
||||
isCurrentIndex:
|
||||
_currentTabIndex.value == widget.initialTabIndex
|
||||
? _currentItemIndex.value == index
|
||||
: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemScrollController: _itemScrollController[index],
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEpisodeItem({
|
||||
required ThemeData theme,
|
||||
required dynamic episode,
|
||||
required int index,
|
||||
required int length,
|
||||
required bool isCurrentIndex,
|
||||
}) {
|
||||
late String title;
|
||||
String? cover;
|
||||
num? duration;
|
||||
int? pubdate;
|
||||
int? view;
|
||||
int? danmaku;
|
||||
|
||||
switch (episode) {
|
||||
case video.Part():
|
||||
cover = episode.firstFrame ?? widget.cover;
|
||||
title = episode.pagePart!;
|
||||
duration = episode.duration;
|
||||
pubdate = episode.ctime;
|
||||
break;
|
||||
case video.EpisodeItem():
|
||||
title = episode.title!;
|
||||
cover = episode.arc?.pic;
|
||||
duration = episode.arc?.duration;
|
||||
pubdate = episode.arc?.pubdate;
|
||||
view = episode.arc?.stat?.view;
|
||||
danmaku = episode.arc?.stat?.danmaku;
|
||||
break;
|
||||
case bangumi.EpisodeItem():
|
||||
if (episode.longTitle != null && episode.longTitle != "") {
|
||||
dynamic leading = episode.title ?? index + 1;
|
||||
title =
|
||||
"${Utils.isStringNumeric(leading) ? '第$leading话' : leading} ${episode.longTitle!}";
|
||||
} else {
|
||||
title = episode.title!;
|
||||
}
|
||||
|
||||
cover = episode.cover;
|
||||
duration = episode.duration == null ? null : episode.duration! ~/ 1000;
|
||||
pubdate = episode.pubTime;
|
||||
break;
|
||||
}
|
||||
late final Color primary = theme.colorScheme.primary;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 98,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (episode.badge != null && episode.badge == "会员") {
|
||||
dynamic userInfo = GStorage.userInfo.get('userInfoCache');
|
||||
int vipStatus = 0;
|
||||
if (userInfo != null) {
|
||||
vipStatus = userInfo.vipStatus;
|
||||
}
|
||||
if (vipStatus != 1) {
|
||||
SmartDialog.showToast('需要大会员');
|
||||
// return;
|
||||
}
|
||||
}
|
||||
SmartDialog.showToast('切换到:$title');
|
||||
widget.onClose?.call();
|
||||
if (widget.showTitle == false) {
|
||||
_currentItemIndex.value = index;
|
||||
}
|
||||
widget.changeFucCall(
|
||||
episode is bangumi.EpisodeItem ? episode.epId : null,
|
||||
episode.runtimeType.toString() == "EpisodeItem"
|
||||
? episode.bvid
|
||||
: widget.bvid,
|
||||
episode.cid,
|
||||
episode.runtimeType.toString() == "EpisodeItem"
|
||||
? episode.aid
|
||||
: widget.aid,
|
||||
cover,
|
||||
);
|
||||
if (widget.type == EpisodeType.season) {
|
||||
try {
|
||||
Get.find<VideoDetailController>(
|
||||
tag: widget.videoIntroController.heroTag)
|
||||
.seasonCid = episode.cid;
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (cover?.isNotEmpty == true) {
|
||||
imageSaveDialog(title: title, cover: cover);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (cover?.isNotEmpty == true)
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
src: cover,
|
||||
width: boxConstraints.maxWidth,
|
||||
height: boxConstraints.maxHeight,
|
||||
),
|
||||
if (duration != null && duration > 0)
|
||||
PBadge(
|
||||
text: Utils.timeFormat(duration),
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
else if (isCurrentIndex)
|
||||
Image.asset(
|
||||
'assets/images/live.png',
|
||||
color: primary,
|
||||
height: 12,
|
||||
semanticLabel: "正在播放:",
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: theme.textTheme.bodyMedium!.fontSize,
|
||||
height: 1.42,
|
||||
letterSpacing: 0.3,
|
||||
fontWeight: isCurrentIndex ? FontWeight.bold : null,
|
||||
color: isCurrentIndex ? primary : null,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (pubdate != null)
|
||||
Text(
|
||||
Utils.dateFormat(pubdate),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1,
|
||||
color: theme.colorScheme.outline,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
),
|
||||
if (view != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
value: view,
|
||||
),
|
||||
if (danmaku != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
value: danmaku,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (episode.badge != null) ...[
|
||||
if (episode.badge == '会员')
|
||||
Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
height: 20,
|
||||
semanticLabel: "大会员",
|
||||
)
|
||||
else
|
||||
Text(episode.badge),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFavBtn(LoadingState loadingState) {
|
||||
return switch (loadingState) {
|
||||
Success() => mediumButton(
|
||||
tooltip: loadingState.response ? '取消订阅' : '订阅',
|
||||
icon: loadingState.response
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_active_outlined,
|
||||
onPressed: () async {
|
||||
dynamic result = await VideoHttp.seasonFav(
|
||||
isFav: loadingState.response,
|
||||
seasonId: widget.seasonId,
|
||||
);
|
||||
if (result['status']) {
|
||||
SmartDialog.showToast('${loadingState.response ? '取消' : ''}订阅成功');
|
||||
_favState!.value = LoadingState.success(!loadingState.response);
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
},
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
|
||||
Widget get _buildReverseBtn => mediumButton(
|
||||
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',
|
||||
icon: widget.isReversed == true
|
||||
? MdiIcons.sortDescending
|
||||
: MdiIcons.sortAscending,
|
||||
onPressed: () {
|
||||
widget.onReverse?.call();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildToolbar(ThemeData theme) => Container(
|
||||
height: 45,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.showTitle != false ? 14 : 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.showTitle != false)
|
||||
Text(
|
||||
widget.type.title,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
if (_favState != null) Obx(() => _buildFavBtn(_favState!.value)),
|
||||
mediumButton(
|
||||
tooltip: '跳至顶部',
|
||||
icon: Icons.vertical_align_top,
|
||||
onPressed: () {
|
||||
try {
|
||||
_itemScrollController[_currentTabIndex.value].scrollTo(
|
||||
index: !_isReversed[_currentTabIndex.value]
|
||||
? 0
|
||||
: _getCurrEpisodes.length - 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('to top: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
mediumButton(
|
||||
tooltip: '跳至底部',
|
||||
icon: Icons.vertical_align_bottom,
|
||||
onPressed: () {
|
||||
try {
|
||||
_itemScrollController[_currentTabIndex.value].scrollTo(
|
||||
index: !_isReversed[_currentTabIndex.value]
|
||||
? _getCurrEpisodes.length - 1
|
||||
: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('to bottom: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
mediumButton(
|
||||
tooltip: '跳至当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () async {
|
||||
try {
|
||||
if (_currentTabIndex.value != widget.initialTabIndex) {
|
||||
_tabController.animateTo(widget.initialTabIndex);
|
||||
await Future.delayed(const Duration(milliseconds: 225));
|
||||
}
|
||||
_itemScrollController[_currentTabIndex.value].scrollTo(
|
||||
index: _currentItemIndex.value,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
if (widget.isSupportReverse == true)
|
||||
Obx(
|
||||
() {
|
||||
return _currentTabIndex.value == widget.initialTabIndex
|
||||
? _buildReverseBtn
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
Obx(
|
||||
() => mediumButton(
|
||||
tooltip: _isReversed[_currentTabIndex.value] ? '顺序' : '倒序',
|
||||
icon: !_isReversed[_currentTabIndex.value]
|
||||
? MdiIcons.sortNumericAscending
|
||||
: MdiIcons.sortNumericDescending,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isReversed[_currentTabIndex.value] =
|
||||
!_isReversed[_currentTabIndex.value];
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.onClose != null)
|
||||
mediumButton(
|
||||
tooltip: '关闭',
|
||||
icon: Icons.close,
|
||||
onPressed: widget.onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
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:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/http/user.dart';
|
||||
import 'package:PiliPlus/utils/image_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -11,18 +10,37 @@ import 'package:get/get.dart';
|
||||
void imageSaveDialog({
|
||||
required String? title,
|
||||
required String? cover,
|
||||
dynamic aid,
|
||||
String? bvid,
|
||||
}) {
|
||||
final double imgWidth = min(Get.width, Get.height) - 8 * 2;
|
||||
final double imgWidth = Get.mediaQuery.size.shortestSide - 8 * 2;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
late final iconColor = theme.colorScheme.onSurfaceVariant;
|
||||
|
||||
Widget iconBtn({
|
||||
String? tooltip,
|
||||
required IconData icon,
|
||||
required VoidCallback? onPressed,
|
||||
}) {
|
||||
return iconButton(
|
||||
context: context,
|
||||
onPressed: onPressed,
|
||||
iconSize: 20,
|
||||
icon: icon,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: iconColor,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -46,15 +64,15 @@ void imageSaveDialog({
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
child: const IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
@@ -68,31 +86,39 @@ void imageSaveDialog({
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title ?? '',
|
||||
style: theme.textTheme.titleSmall,
|
||||
if (title != null)
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
if (aid != null || bvid != null)
|
||||
iconBtn(
|
||||
tooltip: '稍后再看',
|
||||
onPressed: () => {
|
||||
SmartDialog.dismiss(),
|
||||
UserHttp.toViewLater(aid: aid, bvid: bvid).then(
|
||||
(res) => SmartDialog.showToast(res['msg']),
|
||||
),
|
||||
},
|
||||
icon: Icons.watch_later_outlined,
|
||||
),
|
||||
),
|
||||
if (cover?.isNotEmpty == true) ...[
|
||||
const SizedBox(width: 4),
|
||||
iconButton(
|
||||
context: context,
|
||||
iconBtn(
|
||||
tooltip: '分享',
|
||||
onPressed: () {
|
||||
SmartDialog.dismiss();
|
||||
DownloadUtils.onShareImg(cover!);
|
||||
ImageUtil.onShareImg(cover!);
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.share,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
iconBtn(
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await DownloadUtils.downloadImg(
|
||||
bool saveStatus = await ImageUtil.downloadImg(
|
||||
context,
|
||||
[cover!],
|
||||
);
|
||||
@@ -100,10 +126,7 @@ void imageSaveDialog({
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.download,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
200
lib/common/widgets/image/image_view.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.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/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageModel {
|
||||
ImageModel({
|
||||
required num? width,
|
||||
required num? height,
|
||||
required this.url,
|
||||
this.liveUrl,
|
||||
}) {
|
||||
this.width = width == null || width == 0 ? 1 : width;
|
||||
this.height = height == null || height == 0 ? 1 : height;
|
||||
}
|
||||
|
||||
late num width;
|
||||
late num height;
|
||||
String url;
|
||||
String? liveUrl;
|
||||
bool? _isLongPic;
|
||||
bool? _isLivePhoto;
|
||||
|
||||
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
|
||||
bool get isLivePhoto =>
|
||||
_isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
|
||||
|
||||
static bool enableLivePhoto = Pref.enableLivePhoto;
|
||||
}
|
||||
|
||||
const double _maxRatio = 22 / 9;
|
||||
|
||||
Widget imageView(
|
||||
double maxWidth,
|
||||
List<ImageModel> picArr, {
|
||||
VoidCallback? onViewImage,
|
||||
ValueChanged<int>? onDismissed,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
double imageWidth = (maxWidth - 10) / 3;
|
||||
double imageHeight = imageWidth;
|
||||
if (picArr.length == 1) {
|
||||
num width = picArr[0].width;
|
||||
num height = picArr[0].height;
|
||||
double ratioWH = width / height;
|
||||
double ratioHW = height / width;
|
||||
imageWidth = ratioWH > 1.5
|
||||
? maxWidth
|
||||
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
|
||||
? 2 * imageWidth
|
||||
: 1.5 * imageWidth;
|
||||
if (width != 1) {
|
||||
imageWidth = min(imageWidth, width.toDouble());
|
||||
}
|
||||
imageHeight = imageWidth * min(ratioHW, _maxRatio);
|
||||
} else if (picArr.length == 2) {
|
||||
imageWidth = imageHeight = 2 * imageWidth;
|
||||
}
|
||||
late final int row = picArr.length == 4 ? 2 : 3;
|
||||
BorderRadius borderRadius(index) {
|
||||
if (picArr.length == 1) {
|
||||
return StyleString.mdRadius;
|
||||
}
|
||||
return BorderRadius.only(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
int parseSize(size) {
|
||||
return switch (size) {
|
||||
int() => size,
|
||||
double() => size.round(),
|
||||
String() => int.tryParse(size) ?? 1,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
|
||||
void onTap(int index) {
|
||||
if (callback != null) {
|
||||
callback(picArr.map((item) => item.url).toList(), index);
|
||||
} else {
|
||||
onViewImage?.call();
|
||||
PageUtils.imageView(
|
||||
initialPage: index,
|
||||
imgList: picArr.map(
|
||||
(item) {
|
||||
bool isLive = item.isLivePhoto;
|
||||
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),
|
||||
bigImageWidth: imageWidth,
|
||||
bigImageHeight: imageHeight,
|
||||
space: 5,
|
||||
height: picArr.length == 1 ? imageHeight : null,
|
||||
width: picArr.length == 1 ? imageWidth : maxWidth,
|
||||
itemCount: picArr.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = picArr[index];
|
||||
return Hero(
|
||||
tag: item.url,
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(index),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius(index),
|
||||
child: NetworkImgLayer(
|
||||
radius: 0,
|
||||
src: item.url,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
isLongPic: item.isLongPic,
|
||||
forceUseCacheWidth: item.width <= item.height,
|
||||
getPlaceHolder: () {
|
||||
return Container(
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
||||
borderRadius: borderRadius(index),
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/loading.png',
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
cacheWidth: imageWidth.cacheSize(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (item.isLivePhoto)
|
||||
const PBadge(
|
||||
text: 'Live',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
type: PBadgeType.gray,
|
||||
)
|
||||
else if (item.isLongPic)
|
||||
const PBadge(
|
||||
text: '长图',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
134
lib/common/widgets/image/network_img_layer.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/image_util.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NetworkImgLayer extends StatelessWidget {
|
||||
const NetworkImgLayer({
|
||||
super.key,
|
||||
required this.src,
|
||||
required this.width,
|
||||
this.height,
|
||||
this.type = ImageType.def,
|
||||
this.fadeOutDuration,
|
||||
this.fadeInDuration,
|
||||
// 图片质量 默认1%
|
||||
this.quality,
|
||||
this.semanticsLabel,
|
||||
this.radius,
|
||||
this.imageBuilder,
|
||||
this.isLongPic = false,
|
||||
this.forceUseCacheWidth = false,
|
||||
this.getPlaceHolder,
|
||||
this.boxFit,
|
||||
});
|
||||
|
||||
final String? src;
|
||||
final double width;
|
||||
final double? height;
|
||||
final ImageType type;
|
||||
final Duration? fadeOutDuration;
|
||||
final Duration? fadeInDuration;
|
||||
final int? quality;
|
||||
final String? semanticsLabel;
|
||||
final double? radius;
|
||||
final ImageWidgetBuilder? imageBuilder;
|
||||
final bool isLongPic;
|
||||
final bool forceUseCacheWidth;
|
||||
final Widget Function()? getPlaceHolder;
|
||||
final BoxFit? boxFit;
|
||||
|
||||
static Color? reduceLuxColor = Pref.reduceLuxColor;
|
||||
static bool reduce = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final noRadius = type == ImageType.emote || radius == 0;
|
||||
final Widget child;
|
||||
|
||||
if (src?.isNotEmpty == true) {
|
||||
child = noRadius
|
||||
? _buildImage(context, noRadius)
|
||||
: type == ImageType.avatar
|
||||
? ClipOval(child: _buildImage(context, noRadius))
|
||||
: ClipRRect(
|
||||
borderRadius: radius != null
|
||||
? BorderRadius.circular(radius!)
|
||||
: StyleString.mdRadius,
|
||||
child: _buildImage(context, noRadius),
|
||||
);
|
||||
} else {
|
||||
child = getPlaceHolder?.call() ?? _placeholder(context, noRadius);
|
||||
}
|
||||
|
||||
return semanticsLabel?.isNotEmpty == true
|
||||
? Semantics(
|
||||
container: true,
|
||||
image: true,
|
||||
excludeSemantics: true,
|
||||
label: semanticsLabel,
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
}
|
||||
|
||||
Widget _buildImage(BuildContext context, bool noRadius) {
|
||||
int? memCacheWidth, memCacheHeight;
|
||||
if (height == null || forceUseCacheWidth || width <= height!) {
|
||||
memCacheWidth = width.cacheSize(context);
|
||||
} else {
|
||||
memCacheHeight = height.cacheSize(context);
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
imageUrl: ImageUtil.thumbnailUrl(src, quality),
|
||||
width: width,
|
||||
height: height,
|
||||
memCacheWidth: memCacheWidth,
|
||||
memCacheHeight: memCacheHeight,
|
||||
fit: boxFit ?? BoxFit.cover,
|
||||
alignment: isLongPic ? Alignment.topCenter : Alignment.center,
|
||||
fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120),
|
||||
fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120),
|
||||
filterQuality: FilterQuality.low,
|
||||
placeholder: (BuildContext context, String url) =>
|
||||
getPlaceHolder?.call() ?? _placeholder(context, noRadius),
|
||||
imageBuilder: imageBuilder,
|
||||
errorWidget: (context, url, error) => _placeholder(context, noRadius),
|
||||
colorBlendMode: reduce ? BlendMode.modulate : null,
|
||||
color: reduce ? reduceLuxColor : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _placeholder(BuildContext context, bool noRadius) {
|
||||
final isAvatar = type == ImageType.avatar;
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
clipBehavior: noRadius ? Clip.none : Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
shape: isAvatar ? BoxShape.circle : BoxShape.rectangle,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
||||
borderRadius: noRadius || isAvatar
|
||||
? null
|
||||
: radius != null
|
||||
? BorderRadius.circular(radius!)
|
||||
: StyleString.mdRadius,
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
isAvatar ? 'assets/images/noface.jpeg' : 'assets/images/loading.png',
|
||||
width: width,
|
||||
height: height,
|
||||
cacheWidth: width.cacheSize(context),
|
||||
colorBlendMode: reduce ? BlendMode.modulate : null,
|
||||
color: reduce ? reduceLuxColor : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -133,8 +133,9 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
if (widget.itemCount == 0) {
|
||||
return Rect.fromLTRB(0, 0, padding.horizontal, padding.vertical);
|
||||
}
|
||||
double width = widget.width ??
|
||||
(MediaQuery.of(context).size.width - widget.margin.horizontal);
|
||||
double width =
|
||||
widget.width ??
|
||||
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
|
||||
width = width - padding.horizontal;
|
||||
double space = widget.space;
|
||||
double itemW;
|
||||
@@ -158,18 +159,22 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
/// build nine grid view.
|
||||
Widget _buildChild(BuildContext context, double itemW) {
|
||||
double space = widget.space;
|
||||
int column =
|
||||
(widget.itemCount == 4 && widget.type != NineGridType.normal) ? 2 : 3;
|
||||
int column = (widget.itemCount == 4 && widget.type != NineGridType.normal)
|
||||
? 2
|
||||
: 3;
|
||||
List<Widget> list = [];
|
||||
for (int i = 0; i < widget.itemCount; i++) {
|
||||
list.add(Positioned(
|
||||
list.add(
|
||||
Positioned(
|
||||
top: (space + itemW) * (i ~/ column),
|
||||
left: (space + itemW) * (i % column),
|
||||
child: SizedBox(
|
||||
width: itemW,
|
||||
height: itemW,
|
||||
child: widget.itemBuilder(context, i),
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
@@ -191,11 +196,14 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
if (!_isZero(bigImgWidth) && !_isZero(bigImgHeight)) {
|
||||
return _getOneChild(context, bigImgWidth!, bigImgHeight!);
|
||||
} else {
|
||||
_ImageUtil().getImageSize(widget.bigImage)?.then((rect) {
|
||||
ngvBigImageSizeMap[bigImageUrl] = rect;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}).catchError((e) {});
|
||||
_ImageUtil()
|
||||
.getImageSize(widget.bigImage)
|
||||
?.then((rect) {
|
||||
ngvBigImageSizeMap[bigImageUrl] = rect;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
})
|
||||
.catchError((e) {});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -238,7 +246,8 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
for (int i = 0; i < widget.itemCount; i++) {
|
||||
double left;
|
||||
if (first > 0 && i < first) {
|
||||
left = (width - itemW * first - space * (first - 1)) / 2 +
|
||||
left =
|
||||
(width - itemW * first - space * (first - 1)) / 2 +
|
||||
(itemW + space) * i;
|
||||
} else {
|
||||
left = (space + itemW) * ((i - first) % column);
|
||||
@@ -248,17 +257,21 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
? 0
|
||||
: (first > 0 ? (i + column - first) : i) ~/ column;
|
||||
|
||||
double top = (width - itemW * row - space * (row - 1)) / 2 +
|
||||
double top =
|
||||
(width - itemW * row - space * (row - 1)) / 2 +
|
||||
(space + itemW) * itemIndex;
|
||||
|
||||
list.add(Positioned(
|
||||
list.add(
|
||||
Positioned(
|
||||
top: top,
|
||||
left: left,
|
||||
child: SizedBox(
|
||||
width: itemW,
|
||||
height: itemW,
|
||||
child: widget.itemBuilder(context, i),
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
@@ -273,18 +286,22 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
double itemW = (width - widget.space) / 2;
|
||||
List<Widget> children = [];
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
children.add(Positioned(
|
||||
children.add(
|
||||
Positioned(
|
||||
top: (widget.space + itemW) * (i ~/ 2),
|
||||
left: (widget.space + itemW) *
|
||||
left:
|
||||
(widget.space + itemW) *
|
||||
(((itemCount == 3 && i == 2) ? i + 1 : i) % 2),
|
||||
child: SizedBox(
|
||||
width: itemCount == 1 ? width : itemW,
|
||||
height:
|
||||
(itemCount == 1 || itemCount == 2 || (itemCount == 3 && i == 0))
|
||||
? width
|
||||
: itemW,
|
||||
? width
|
||||
: itemW,
|
||||
child: widget.itemBuilder(context, i),
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ClipOval(
|
||||
child: Stack(
|
||||
@@ -300,11 +317,12 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
int itemCount = math.min(5, widget.itemCount);
|
||||
if (itemCount == 1) {
|
||||
return ClipOval(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: width,
|
||||
child: widget.itemBuilder(context, 0),
|
||||
));
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: width,
|
||||
child: widget.itemBuilder(context, 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> children = [];
|
||||
@@ -323,7 +341,8 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
startDegree = 210;
|
||||
r = width / (2 + 4 * math.sin(math.pi * (3 - 2) / (2 * 3)));
|
||||
r1 = r / math.cos(math.pi * (3 - 2) / (2 * 3));
|
||||
double R = r *
|
||||
double R =
|
||||
r *
|
||||
(1 + math.sin(math.pi / itemCount)) /
|
||||
math.sin(math.pi / itemCount);
|
||||
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 3)));
|
||||
@@ -338,7 +357,8 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
startDegree = 126;
|
||||
r = width / (2 + 4 * math.sin(math.pi * (5 - 2) / (2 * 5)));
|
||||
r1 = r / math.cos(math.pi * (5 - 2) / (2 * 5));
|
||||
double R = r *
|
||||
double R =
|
||||
r *
|
||||
(1 + math.sin(math.pi / itemCount)) /
|
||||
math.sin(math.pi / itemCount);
|
||||
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 5)));
|
||||
@@ -388,12 +408,13 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
|
||||
/// get big image size.
|
||||
Rect _getBigImgSize(double originalWidth, double originalHeight) {
|
||||
double width = widget.width ??
|
||||
(MediaQuery.of(context).size.width - widget.margin.horizontal);
|
||||
double width =
|
||||
widget.width ??
|
||||
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
|
||||
width = width - widget.padding.horizontal;
|
||||
double itemW = (width - widget.space * 2) / 3;
|
||||
|
||||
//double devicePixelRatio = MediaQuery.of(context)?.devicePixelRatio ?? 3;
|
||||
// double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
double devicePixelRatio = 1.0;
|
||||
double tempWidth = originalWidth / devicePixelRatio;
|
||||
double tempHeight = originalHeight / devicePixelRatio;
|
||||
@@ -475,8 +496,14 @@ class _ImageUtil {
|
||||
(ImageInfo info, bool synchronousCall) {
|
||||
imageStream.removeListener(listener);
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(Rect.fromLTWH(
|
||||
0, 0, info.image.width.toDouble(), info.image.height.toDouble()));
|
||||
completer.complete(
|
||||
Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
info.image.width.toDouble(),
|
||||
info.image.height.toDouble(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (dynamic exception, StackTrace? stackTrace) {
|
||||
@@ -486,7 +513,7 @@ class _ImageUtil {
|
||||
}
|
||||
},
|
||||
);
|
||||
imageStream = image.image.resolve(const ImageConfiguration());
|
||||
imageStream = image.image.resolve(ImageConfiguration.empty);
|
||||
imageStream.addListener(listener);
|
||||
return completer.future;
|
||||
}
|
||||
@@ -535,8 +562,10 @@ class QQClipper extends CustomClipper<Path> {
|
||||
points.add(Offset(x1, y1));
|
||||
}
|
||||
|
||||
double spaceB = math.atan(
|
||||
r * math.sin(d2r(spaceA)) / (2 * r - r * math.cos(d2r(spaceA)))) /
|
||||
double spaceB =
|
||||
math.atan(
|
||||
r * math.sin(d2r(spaceA)) / (2 * r - r * math.cos(d2r(spaceA))),
|
||||
) /
|
||||
math.pi *
|
||||
180;
|
||||
double r1 = (2 * r - r * math.cos(d2r(spaceA))) / math.cos(d2r(spaceB));
|
||||
@@ -1,195 +0,0 @@
|
||||
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/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageModel {
|
||||
ImageModel({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.url,
|
||||
this.liveUrl,
|
||||
});
|
||||
|
||||
dynamic width;
|
||||
dynamic height;
|
||||
String url;
|
||||
String? liveUrl;
|
||||
bool? _isLongPic;
|
||||
bool? _isLivePhoto;
|
||||
|
||||
dynamic get safeWidth => width ?? 1;
|
||||
dynamic get safeHeight => height ?? 1;
|
||||
bool get isLongPic => _isLongPic ??= (safeHeight / safeWidth) > (22 / 9);
|
||||
bool get isLivePhoto => _isLivePhoto ??= liveUrl?.isNotEmpty == true;
|
||||
}
|
||||
|
||||
Widget imageView(
|
||||
double maxWidth,
|
||||
List<ImageModel> picArr, {
|
||||
VoidCallback? onViewImage,
|
||||
ValueChanged<int>? onDismissed,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
double imageWidth = (maxWidth - 2 * 5) / 3;
|
||||
double imageHeight = imageWidth;
|
||||
if (picArr.length == 1) {
|
||||
dynamic width = picArr[0].safeWidth;
|
||||
dynamic height = picArr[0].safeHeight;
|
||||
double ratioWH = width / height;
|
||||
double ratioHW = height / width;
|
||||
double maxRatio = 22 / 9;
|
||||
imageWidth = ratioWH > 1.5
|
||||
? maxWidth
|
||||
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
|
||||
? 2 * imageWidth
|
||||
: 1.5 * imageWidth;
|
||||
imageHeight = imageWidth * min(ratioHW, maxRatio);
|
||||
} else if (picArr.length == 2) {
|
||||
imageWidth = imageHeight = 2 * imageWidth;
|
||||
}
|
||||
BorderRadius borderRadius(index) {
|
||||
if (picArr.length == 1) {
|
||||
return StyleString.mdRadius;
|
||||
}
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
late final enableLivePhoto = GStorage.enableLivePhoto;
|
||||
|
||||
int parseSize(size) {
|
||||
return switch (size) {
|
||||
int() => size,
|
||||
double() => size.round(),
|
||||
String() => int.tryParse(size) ?? 1,
|
||||
_ => 1,
|
||||
};
|
||||
}
|
||||
|
||||
return NineGridView(
|
||||
type: NineGridType.weiBo,
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
bigImageWidth: imageWidth,
|
||||
bigImageHeight: imageHeight,
|
||||
space: 5,
|
||||
height: picArr.length == 1 ? imageHeight : null,
|
||||
width: picArr.length == 1 ? imageWidth : maxWidth,
|
||||
itemCount: picArr.length,
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius(index),
|
||||
child: NetworkImgLayer(
|
||||
radius: 0,
|
||||
src: picArr[index].url,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
isLongPic: () => picArr[index].isLongPic,
|
||||
callback: () =>
|
||||
picArr[index].safeWidth <= picArr[index].safeHeight,
|
||||
getPlaceHolder: () {
|
||||
return Container(
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface
|
||||
.withOpacity(0.4),
|
||||
borderRadius: borderRadius(index),
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/loading.png',
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
cacheWidth: imageWidth.cacheSize(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (picArr[index].isLivePhoto)
|
||||
const PBadge(
|
||||
text: 'Live',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
type: 'gray',
|
||||
)
|
||||
else if (picArr[index].isLongPic)
|
||||
const PBadge(
|
||||
text: '长图',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -22,8 +22,8 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3;
|
||||
///
|
||||
/// * [InteractiveViewer.builder], whose builder is of this type.
|
||||
/// * [WidgetBuilder], which is similar, but takes no viewport.
|
||||
typedef InteractiveViewerWidgetBuilder = Widget Function(
|
||||
BuildContext context, Quad viewport);
|
||||
typedef InteractiveViewerWidgetBuilder =
|
||||
Widget Function(BuildContext context, Quad viewport);
|
||||
|
||||
/// A widget that enables pan and zoom interactions with its child.
|
||||
///
|
||||
@@ -82,23 +82,23 @@ class InteractiveViewer extends StatefulWidget {
|
||||
this.onReset,
|
||||
this.isAnimating,
|
||||
required Widget this.child,
|
||||
}) : assert(minScale > 0),
|
||||
assert(interactionEndFrictionCoefficient > 0),
|
||||
assert(minScale.isFinite),
|
||||
assert(maxScale > 0),
|
||||
assert(!maxScale.isNaN),
|
||||
assert(maxScale >= minScale),
|
||||
// boundaryMargin must be either fully infinite or fully finite, but not
|
||||
// a mix of both.
|
||||
assert(
|
||||
(boundaryMargin.horizontal.isInfinite &&
|
||||
boundaryMargin.vertical.isInfinite) ||
|
||||
(boundaryMargin.top.isFinite &&
|
||||
boundaryMargin.right.isFinite &&
|
||||
boundaryMargin.bottom.isFinite &&
|
||||
boundaryMargin.left.isFinite),
|
||||
),
|
||||
builder = null;
|
||||
}) : assert(minScale > 0),
|
||||
assert(interactionEndFrictionCoefficient > 0),
|
||||
assert(minScale.isFinite),
|
||||
assert(maxScale > 0),
|
||||
assert(!maxScale.isNaN),
|
||||
assert(maxScale >= minScale),
|
||||
// boundaryMargin must be either fully infinite or fully finite, but not
|
||||
// a mix of both.
|
||||
assert(
|
||||
(boundaryMargin.horizontal.isInfinite &&
|
||||
boundaryMargin.vertical.isInfinite) ||
|
||||
(boundaryMargin.top.isFinite &&
|
||||
boundaryMargin.right.isFinite &&
|
||||
boundaryMargin.bottom.isFinite &&
|
||||
boundaryMargin.left.isFinite),
|
||||
),
|
||||
builder = null;
|
||||
|
||||
/// Creates an InteractiveViewer for a child that is created on demand.
|
||||
///
|
||||
@@ -132,24 +132,24 @@ class InteractiveViewer extends StatefulWidget {
|
||||
this.onReset,
|
||||
this.isAnimating,
|
||||
required InteractiveViewerWidgetBuilder this.builder,
|
||||
}) : assert(minScale > 0),
|
||||
assert(interactionEndFrictionCoefficient > 0),
|
||||
assert(minScale.isFinite),
|
||||
assert(maxScale > 0),
|
||||
assert(!maxScale.isNaN),
|
||||
assert(maxScale >= minScale),
|
||||
// boundaryMargin must be either fully infinite or fully finite, but not
|
||||
// a mix of both.
|
||||
assert(
|
||||
(boundaryMargin.horizontal.isInfinite &&
|
||||
boundaryMargin.vertical.isInfinite) ||
|
||||
(boundaryMargin.top.isFinite &&
|
||||
boundaryMargin.right.isFinite &&
|
||||
boundaryMargin.bottom.isFinite &&
|
||||
boundaryMargin.left.isFinite),
|
||||
),
|
||||
constrained = false,
|
||||
child = null;
|
||||
}) : assert(minScale > 0),
|
||||
assert(interactionEndFrictionCoefficient > 0),
|
||||
assert(minScale.isFinite),
|
||||
assert(maxScale > 0),
|
||||
assert(!maxScale.isNaN),
|
||||
assert(maxScale >= minScale),
|
||||
// boundaryMargin must be either fully infinite or fully finite, but not
|
||||
// a mix of both.
|
||||
assert(
|
||||
(boundaryMargin.horizontal.isInfinite &&
|
||||
boundaryMargin.vertical.isInfinite) ||
|
||||
(boundaryMargin.top.isFinite &&
|
||||
boundaryMargin.right.isFinite &&
|
||||
boundaryMargin.bottom.isFinite &&
|
||||
boundaryMargin.left.isFinite),
|
||||
),
|
||||
constrained = false,
|
||||
child = null;
|
||||
|
||||
final Function? isAnimating;
|
||||
final VoidCallback? onReset;
|
||||
@@ -402,7 +402,8 @@ class InteractiveViewer extends StatefulWidget {
|
||||
/// Returns the closest point to the given point on the given line segment.
|
||||
@visibleForTesting
|
||||
static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) {
|
||||
final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() +
|
||||
final double lengthSquared =
|
||||
math.pow(l2.x - l1.x, 2.0).toDouble() +
|
||||
math.pow(l2.y - l1.y, 2.0).toDouble();
|
||||
|
||||
// In this case, l1 == l2.
|
||||
@@ -414,8 +415,11 @@ class InteractiveViewer extends StatefulWidget {
|
||||
// the point.
|
||||
final Vector3 l1P = point - l1;
|
||||
final Vector3 l1L2 = l2 - l1;
|
||||
final double fraction =
|
||||
clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0);
|
||||
final double fraction = clampDouble(
|
||||
l1P.dot(l1L2) / lengthSquared,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
return l1 + l1L2 * fraction;
|
||||
}
|
||||
|
||||
@@ -558,8 +562,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
final RenderBox childRenderBox =
|
||||
_childKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
final Size childSize = childRenderBox.size;
|
||||
final Rect boundaryRect =
|
||||
widget.boundaryMargin.inflateRect(Offset.zero & childSize);
|
||||
final Rect boundaryRect = widget.boundaryMargin.inflateRect(
|
||||
Offset.zero & childSize,
|
||||
);
|
||||
assert(
|
||||
!boundaryRect.isEmpty,
|
||||
"InteractiveViewer's child must have nonzero dimensions.",
|
||||
@@ -631,8 +636,10 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
);
|
||||
|
||||
// If the given translation fits completely within the boundaries, allow it.
|
||||
final Offset offendingDistance =
|
||||
_exceedsBy(boundariesAabbQuad, nextViewport);
|
||||
final Offset offendingDistance = _exceedsBy(
|
||||
boundariesAabbQuad,
|
||||
nextViewport,
|
||||
);
|
||||
if (offendingDistance == Offset.zero) {
|
||||
return nextMatrix;
|
||||
}
|
||||
@@ -651,17 +658,23 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
// complicated than this when rotated.
|
||||
// https://github.com/flutter/flutter/issues/57698
|
||||
final Matrix4 correctedMatrix = matrix.clone()
|
||||
..setTranslation(Vector3(
|
||||
correctedTotalTranslation.dx,
|
||||
correctedTotalTranslation.dy,
|
||||
0.0,
|
||||
));
|
||||
..setTranslation(
|
||||
Vector3(
|
||||
correctedTotalTranslation.dx,
|
||||
correctedTotalTranslation.dy,
|
||||
0.0,
|
||||
),
|
||||
);
|
||||
|
||||
// Double check that the corrected translation fits.
|
||||
final Quad correctedViewport =
|
||||
_transformViewport(correctedMatrix, _viewport);
|
||||
final Offset offendingCorrectedDistance =
|
||||
_exceedsBy(boundariesAabbQuad, correctedViewport);
|
||||
final Quad correctedViewport = _transformViewport(
|
||||
correctedMatrix,
|
||||
_viewport,
|
||||
);
|
||||
final Offset offendingCorrectedDistance = _exceedsBy(
|
||||
boundariesAabbQuad,
|
||||
correctedViewport,
|
||||
);
|
||||
if (offendingCorrectedDistance == Offset.zero) {
|
||||
return correctedMatrix;
|
||||
}
|
||||
@@ -680,12 +693,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
|
||||
offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
|
||||
);
|
||||
return matrix.clone()
|
||||
..setTranslation(Vector3(
|
||||
return matrix.clone()..setTranslation(
|
||||
Vector3(
|
||||
unidirectionalCorrectedTotalTranslation.dx,
|
||||
unidirectionalCorrectedTotalTranslation.dy,
|
||||
0.0,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Return a new matrix representing the given matrix after applying the given
|
||||
@@ -698,8 +712,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
|
||||
// Don't allow a scale that results in an overall scale beyond min/max
|
||||
// scale.
|
||||
final double currentScale =
|
||||
_transformationController!.value.getMaxScaleOnAxis();
|
||||
final double currentScale = _transformationController!.value
|
||||
.getMaxScaleOnAxis();
|
||||
final double totalScale = math.max(
|
||||
currentScale * scale,
|
||||
// Ensure that the scale cannot make the child so big that it can't fit
|
||||
@@ -771,14 +785,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;
|
||||
}
|
||||
@@ -931,10 +947,12 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
_currentAxis = null;
|
||||
return;
|
||||
}
|
||||
final Vector3 translationVector =
|
||||
_transformationController!.value.getTranslation();
|
||||
final Offset translation =
|
||||
Offset(translationVector.x, translationVector.y);
|
||||
final Vector3 translationVector = _transformationController!.value
|
||||
.getTranslation();
|
||||
final Offset translation = Offset(
|
||||
translationVector.x,
|
||||
translationVector.y,
|
||||
);
|
||||
final FrictionSimulation frictionSimulationX = FrictionSimulation(
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
translation.dx,
|
||||
@@ -949,13 +967,19 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
details.velocity.pixelsPerSecond.distance,
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
);
|
||||
_animation = Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.decelerate,
|
||||
));
|
||||
_animation =
|
||||
Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(
|
||||
frictionSimulationX.finalX,
|
||||
frictionSimulationY.finalX,
|
||||
),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.decelerate,
|
||||
),
|
||||
);
|
||||
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
|
||||
_animation!.addListener(_onAnimate);
|
||||
_controller.forward();
|
||||
@@ -964,21 +988,31 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
_currentAxis = null;
|
||||
return;
|
||||
}
|
||||
final double scale =
|
||||
_transformationController!.value.getMaxScaleOnAxis();
|
||||
final double scale = _transformationController!.value
|
||||
.getMaxScaleOnAxis();
|
||||
final FrictionSimulation frictionSimulation = FrictionSimulation(
|
||||
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
|
||||
scale,
|
||||
details.scaleVelocity / 10);
|
||||
final double tFinal = _getFinalTime(details.scaleVelocity.abs(),
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
effectivelyMotionless: 0.1);
|
||||
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
|
||||
scale,
|
||||
details.scaleVelocity / 10,
|
||||
);
|
||||
final double tFinal = _getFinalTime(
|
||||
details.scaleVelocity.abs(),
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
effectivelyMotionless: 0.1,
|
||||
);
|
||||
_scaleAnimation =
|
||||
Tween<double>(begin: scale, end: frictionSimulation.x(tFinal))
|
||||
.animate(CurvedAnimation(
|
||||
parent: _scaleController, curve: Curves.decelerate));
|
||||
_scaleController.duration =
|
||||
Duration(milliseconds: (tFinal * 1000).round());
|
||||
Tween<double>(
|
||||
begin: scale,
|
||||
end: frictionSimulation.x(tFinal),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.decelerate,
|
||||
),
|
||||
);
|
||||
_scaleController.duration = Duration(
|
||||
milliseconds: (tFinal * 1000).round(),
|
||||
);
|
||||
_scaleAnimation!.addListener(_onScaleAnimate);
|
||||
_scaleController.forward();
|
||||
case _GestureType.rotate || null:
|
||||
@@ -1007,11 +1041,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
);
|
||||
|
||||
if (!_gestureIsSupported(_GestureType.pan)) {
|
||||
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
|
||||
focalPoint: event.position - event.scrollDelta,
|
||||
localFocalPoint: event.localPosition - event.scrollDelta,
|
||||
focalPointDelta: -localDelta,
|
||||
));
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: event.position - event.scrollDelta,
|
||||
localFocalPoint: event.localPosition - event.scrollDelta,
|
||||
focalPointDelta: -localDelta,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
return;
|
||||
}
|
||||
@@ -1025,13 +1061,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
);
|
||||
|
||||
_transformationController!.value = _matrixTranslate(
|
||||
_transformationController!.value,
|
||||
newFocalPointScene - focalPointScene);
|
||||
_transformationController!.value,
|
||||
newFocalPointScene - focalPointScene,
|
||||
);
|
||||
|
||||
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: event.position - event.scrollDelta,
|
||||
localFocalPoint: event.localPosition - localDelta,
|
||||
focalPointDelta: -localDelta));
|
||||
focalPointDelta: -localDelta,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
return;
|
||||
}
|
||||
@@ -1053,11 +1093,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
);
|
||||
|
||||
if (!_gestureIsSupported(_GestureType.scale)) {
|
||||
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
|
||||
focalPoint: event.position,
|
||||
localFocalPoint: event.localPosition,
|
||||
scale: scaleChange,
|
||||
));
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: event.position,
|
||||
localFocalPoint: event.localPosition,
|
||||
scale: scaleChange,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
return;
|
||||
}
|
||||
@@ -1081,11 +1123,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
focalPointSceneScaled - focalPointScene,
|
||||
);
|
||||
|
||||
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
|
||||
focalPoint: event.position,
|
||||
localFocalPoint: event.localPosition,
|
||||
scale: scaleChange,
|
||||
));
|
||||
widget.onInteractionUpdate?.call(
|
||||
ScaleUpdateDetails(
|
||||
focalPoint: event.position,
|
||||
localFocalPoint: event.localPosition,
|
||||
scale: scaleChange,
|
||||
),
|
||||
);
|
||||
widget.onInteractionEnd?.call(ScaleEndDetails());
|
||||
}
|
||||
|
||||
@@ -1099,8 +1143,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
return;
|
||||
}
|
||||
// Translate such that the resulting translation is _animation.value.
|
||||
final Vector3 translationVector =
|
||||
_transformationController!.value.getTranslation();
|
||||
final Vector3 translationVector = _transformationController!.value
|
||||
.getTranslation();
|
||||
final Offset translation = Offset(translationVector.x, translationVector.y);
|
||||
final Offset translationScene = _transformationController!.toScene(
|
||||
translation,
|
||||
@@ -1174,27 +1218,33 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
// transformationControllers.
|
||||
if (oldWidget.transformationController == null) {
|
||||
if (widget.transformationController != null) {
|
||||
_transformationController!
|
||||
.removeListener(_onTransformationControllerChange);
|
||||
_transformationController!.removeListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
_transformationController!.dispose();
|
||||
_transformationController = widget.transformationController;
|
||||
_transformationController!
|
||||
.addListener(_onTransformationControllerChange);
|
||||
_transformationController!.addListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (widget.transformationController == null) {
|
||||
_transformationController!
|
||||
.removeListener(_onTransformationControllerChange);
|
||||
_transformationController!.removeListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
_transformationController = TransformationController();
|
||||
_transformationController!
|
||||
.addListener(_onTransformationControllerChange);
|
||||
_transformationController!.addListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
} else if (widget.transformationController !=
|
||||
oldWidget.transformationController) {
|
||||
_transformationController!
|
||||
.removeListener(_onTransformationControllerChange);
|
||||
_transformationController!.removeListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
_transformationController = widget.transformationController;
|
||||
_transformationController!
|
||||
.addListener(_onTransformationControllerChange);
|
||||
_transformationController!.addListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1203,8 +1253,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_scaleController.dispose();
|
||||
_transformationController!
|
||||
.removeListener(_onTransformationControllerChange);
|
||||
_transformationController!.removeListener(
|
||||
_onTransformationControllerChange,
|
||||
);
|
||||
if (widget.transformationController == null) {
|
||||
_transformationController!.dispose();
|
||||
}
|
||||
@@ -1327,7 +1378,7 @@ class TransformationController extends ValueNotifier<Matrix4> {
|
||||
/// The [value] defaults to the identity matrix, which corresponds to no
|
||||
/// transformation.
|
||||
TransformationController([Matrix4? value])
|
||||
: super(value ?? Matrix4.identity());
|
||||
: super(value ?? Matrix4.identity());
|
||||
|
||||
/// Return the scene point at the given viewport point.
|
||||
///
|
||||
@@ -1363,11 +1414,13 @@ class TransformationController extends ValueNotifier<Matrix4> {
|
||||
// On viewportPoint, perform the inverse transformation of the scene to get
|
||||
// where the point would be in the scene before the transformation.
|
||||
final Matrix4 inverseMatrix = Matrix4.inverted(value);
|
||||
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
|
||||
viewportPoint.dx,
|
||||
viewportPoint.dy,
|
||||
0,
|
||||
));
|
||||
final Vector3 untransformed = inverseMatrix.transform3(
|
||||
Vector3(
|
||||
viewportPoint.dx,
|
||||
viewportPoint.dy,
|
||||
0,
|
||||
),
|
||||
);
|
||||
return Offset(untransformed.x, untransformed.y);
|
||||
}
|
||||
}
|
||||
@@ -1382,8 +1435,11 @@ enum _GestureType {
|
||||
|
||||
// Given a velocity and drag, calculate the time at which motion will come to
|
||||
// a stop, within the margin of effectivelyMotionless.
|
||||
double _getFinalTime(double velocity, double drag,
|
||||
{double effectivelyMotionless = 10}) {
|
||||
double _getFinalTime(
|
||||
double velocity,
|
||||
double drag, {
|
||||
double effectivelyMotionless = 10,
|
||||
}) {
|
||||
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
|
||||
}
|
||||
|
||||
@@ -1400,26 +1456,34 @@ Offset _getMatrixTranslation(Matrix4 matrix) {
|
||||
Quad _transformViewport(Matrix4 matrix, Rect viewport) {
|
||||
final Matrix4 inverseMatrix = matrix.clone()..invert();
|
||||
return Quad.points(
|
||||
inverseMatrix.transform3(Vector3(
|
||||
viewport.topLeft.dx,
|
||||
viewport.topLeft.dy,
|
||||
0.0,
|
||||
)),
|
||||
inverseMatrix.transform3(Vector3(
|
||||
viewport.topRight.dx,
|
||||
viewport.topRight.dy,
|
||||
0.0,
|
||||
)),
|
||||
inverseMatrix.transform3(Vector3(
|
||||
viewport.bottomRight.dx,
|
||||
viewport.bottomRight.dy,
|
||||
0.0,
|
||||
)),
|
||||
inverseMatrix.transform3(Vector3(
|
||||
viewport.bottomLeft.dx,
|
||||
viewport.bottomLeft.dy,
|
||||
0.0,
|
||||
)),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(
|
||||
viewport.topLeft.dx,
|
||||
viewport.topLeft.dy,
|
||||
0.0,
|
||||
),
|
||||
),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(
|
||||
viewport.topRight.dx,
|
||||
viewport.topRight.dy,
|
||||
0.0,
|
||||
),
|
||||
),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(
|
||||
viewport.bottomRight.dx,
|
||||
viewport.bottomRight.dy,
|
||||
0.0,
|
||||
),
|
||||
),
|
||||
inverseMatrix.transform3(
|
||||
Vector3(
|
||||
viewport.bottomLeft.dx,
|
||||
viewport.bottomLeft.dy,
|
||||
0.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1451,8 +1515,10 @@ Offset _exceedsBy(Quad boundary, Quad viewport) {
|
||||
];
|
||||
Offset largestExcess = Offset.zero;
|
||||
for (final Vector3 point in viewportPoints) {
|
||||
final Vector3 pointInside =
|
||||
InteractiveViewer.getNearestPointInside(point, boundary);
|
||||
final Vector3 pointInside = InteractiveViewer.getNearestPointInside(
|
||||
point,
|
||||
boundary,
|
||||
);
|
||||
final Offset excess = Offset(
|
||||
pointInside.x - point.x,
|
||||
pointInside.y - point.y,
|
||||
|
||||
@@ -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
|
||||
@@ -212,15 +213,15 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
}
|
||||
|
||||
Widget get content => DecoratedBoxTransition(
|
||||
decoration: _opacityAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
decoration: _opacityAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
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/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/image_util.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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
|
||||
|
||||
@@ -26,30 +28,17 @@ import 'interactive_viewer.dart' as custom;
|
||||
/// source is hit after zooming in to disable or enable the swiping gesture of
|
||||
/// the [PageView].
|
||||
///
|
||||
typedef IndexedFocusedWidgetBuilder = Widget Function(
|
||||
BuildContext context, int index, bool isFocus, bool enablePageView);
|
||||
typedef IndexedFocusedWidgetBuilder =
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
int index,
|
||||
bool isFocus,
|
||||
bool enablePageView,
|
||||
);
|
||||
|
||||
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 {
|
||||
class InteractiveviewerGallery extends StatefulWidget {
|
||||
const InteractiveviewerGallery({
|
||||
super.key,
|
||||
required this.sources,
|
||||
@@ -59,13 +48,16 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
this.minScale = 1.0,
|
||||
this.onPageChanged,
|
||||
this.onDismissed,
|
||||
this.setStatusBar,
|
||||
this.setStatusBar = true,
|
||||
this.onClose,
|
||||
required this.quality,
|
||||
});
|
||||
|
||||
final ValueChanged? onClose;
|
||||
final int quality;
|
||||
|
||||
final bool? setStatusBar;
|
||||
final ValueChanged<bool>? onClose;
|
||||
|
||||
final bool setStatusBar;
|
||||
|
||||
/// The sources to show.
|
||||
final List<SourceModel> sources;
|
||||
@@ -107,7 +99,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
|
||||
late final RxInt currentIndex = widget.initIndex.obs;
|
||||
|
||||
late final int _quality = GStorage.previewQ;
|
||||
late final int _quality = Pref.previewQ;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -122,12 +114,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
duration: const Duration(milliseconds: 300),
|
||||
)..addListener(listener);
|
||||
|
||||
if (widget.setStatusBar != false) {
|
||||
if (widget.setStatusBar) {
|
||||
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,14 +129,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
}
|
||||
|
||||
SystemUiMode? mode;
|
||||
setStatusBar() async {
|
||||
Future<void> setStatusBar() async {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.immersiveSticky,
|
||||
);
|
||||
}
|
||||
if (Platform.isAndroid &&
|
||||
(await DeviceInfoPlugin().androidInfo).version.sdkInt < 29) {
|
||||
if (Platform.isAndroid && (await Utils.sdkInt < 29)) {
|
||||
mode = SystemUiMode.manual;
|
||||
}
|
||||
}
|
||||
@@ -153,9 +145,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
widget.onClose?.call(true);
|
||||
_player?.dispose();
|
||||
_pageController?.dispose();
|
||||
_animationController.removeListener(listener);
|
||||
_animationController.dispose();
|
||||
if (widget.setStatusBar != false) {
|
||||
_animationController
|
||||
..removeListener(listener)
|
||||
..dispose();
|
||||
if (widget.setStatusBar) {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
mode ?? SystemUiMode.edgeToEdge,
|
||||
@@ -163,9 +156,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 +217,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,28 +230,30 @@ 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()) {
|
||||
// animate the reset for the transformation of the interactive viewer
|
||||
|
||||
_animation = Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: Matrix4.identity(),
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animation =
|
||||
Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: Matrix4.identity(),
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
|
||||
_animationController.forward(from: 0);
|
||||
}
|
||||
}
|
||||
|
||||
String _getActualUrl(int index) {
|
||||
String _getActualUrl(String url) {
|
||||
return _quality != 100
|
||||
? Utils.thumbnailImgUrl(widget.sources[index].url, _quality)
|
||||
: widget.sources[index].url.http2https;
|
||||
? ImageUtil.thumbnailUrl(url, _quality)
|
||||
: url.http2https;
|
||||
}
|
||||
|
||||
void onClose() {
|
||||
@@ -280,7 +275,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
children: [
|
||||
InteractiveViewerBoundary(
|
||||
controller: _transformationController,
|
||||
boundaryWidth: MediaQuery.of(context).size.width,
|
||||
boundaryWidth: MediaQuery.sizeOf(context).width,
|
||||
onScaleChanged: _onScaleChanged,
|
||||
onLeftBoundaryHit: _onLeftBoundaryHit,
|
||||
onRightBoundaryHit: _onRightBoundaryHit,
|
||||
@@ -298,21 +293,30 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
child: PageView.builder(
|
||||
onPageChanged: _onPageChanged,
|
||||
controller: _pageController,
|
||||
physics:
|
||||
_enablePageView ? null : const NeverScrollableScrollPhysics(),
|
||||
physics: _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,
|
||||
onTap: () => EasyThrottle.throttle(
|
||||
'preview',
|
||||
const Duration(milliseconds: 555),
|
||||
onClose,
|
||||
),
|
||||
onDoubleTapDown: (TapDownDetails details) {
|
||||
_doubleTapLocalPosition = details.localPosition;
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress:
|
||||
widget.sources[index].sourceType == SourceType.fileImage
|
||||
? null
|
||||
: onLongPress,
|
||||
onDoubleTap: () => EasyThrottle.throttle(
|
||||
'preview',
|
||||
const Duration(milliseconds: 555),
|
||||
onDoubleTap,
|
||||
),
|
||||
onLongPress: item.sourceType == SourceType.fileImage
|
||||
? null
|
||||
: () => onLongPress(item),
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
context,
|
||||
@@ -320,7 +324,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
index == currentIndex.value,
|
||||
_enablePageView,
|
||||
)
|
||||
: _itemBuilder(index),
|
||||
: _itemBuilder(index, item),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -330,7 +334,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: MediaQuery.paddingOf(context) +
|
||||
padding:
|
||||
MediaQuery.viewPaddingOf(context) +
|
||||
const EdgeInsets.fromLTRB(12, 8, 20, 8),
|
||||
decoration: _enablePageView
|
||||
? BoxDecoration(
|
||||
@@ -339,7 +344,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.3)
|
||||
Colors.black.withValues(alpha: 0.3),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -371,53 +376,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: () => ImageUtil.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(
|
||||
this.context,
|
||||
[widget.sources[currentIndex.value].url],
|
||||
);
|
||||
},
|
||||
onTap: () => ImageUtil.downloadImg(
|
||||
this.context,
|
||||
[item.url],
|
||||
),
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
this.context,
|
||||
widget.sources
|
||||
.map((item) => item.url)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
onTap: () => ImageUtil.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(
|
||||
ImageUtil.downloadLivePhoto(
|
||||
context: this.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!,
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width!,
|
||||
height: item.height!,
|
||||
);
|
||||
},
|
||||
child: const Text("保存 Live Photo"),
|
||||
@@ -435,42 +427,44 @@ 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)),
|
||||
),
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(item.url)),
|
||||
),
|
||||
SourceType.networkImage => CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: _getActualUrl(index),
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
placeholder: (context, url) {
|
||||
return CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: Utils.thumbnailImgUrl(widget.sources[index].url),
|
||||
);
|
||||
},
|
||||
),
|
||||
SourceType.livePhoto => Obx(() => currentIndex.value == index
|
||||
? IgnorePointer(
|
||||
child: Video(
|
||||
controller: _videoController!,
|
||||
fill: Colors.transparent,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: _getActualUrl(item.url),
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
placeholder: (context, url) {
|
||||
return CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: ImageUtil.thumbnailUrl(item.url, widget.quality),
|
||||
);
|
||||
},
|
||||
),
|
||||
SourceType.livePhoto => Obx(
|
||||
() => currentIndex.value == index
|
||||
? IgnorePointer(
|
||||
child: Video(
|
||||
controller: _videoController!,
|
||||
fill: Colors.transparent,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
void onDoubleTap() {
|
||||
Matrix4 matrix = _transformationController!.value.clone();
|
||||
double currentScale = matrix.row0.x;
|
||||
|
||||
@@ -503,35 +497,35 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
offSetX,
|
||||
offSetY,
|
||||
matrix.row2.w,
|
||||
matrix.row3.w
|
||||
matrix.row3.w,
|
||||
]);
|
||||
|
||||
_animation = Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: matrix,
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animation =
|
||||
Matrix4Tween(
|
||||
begin: _transformationController!.value,
|
||||
end: matrix,
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animationController
|
||||
.forward(from: 0)
|
||||
.whenComplete(() => _onScaleChanged(targetScale));
|
||||
}
|
||||
|
||||
onLongPress() {
|
||||
void onLongPress(SourceModel item) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
DownloadUtils.onShareImg(
|
||||
widget.sources[currentIndex.value].url);
|
||||
Get.back();
|
||||
ImageUtil.onShareImg(item.url);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('分享', style: TextStyle(fontSize: 14)),
|
||||
@@ -539,7 +533,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)),
|
||||
@@ -547,9 +541,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
ImageUtil.downloadImg(
|
||||
this.context,
|
||||
[widget.sources[currentIndex.value].url],
|
||||
[item.url],
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
@@ -559,7 +553,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
ImageUtil.downloadImg(
|
||||
this.context,
|
||||
widget.sources.map((item) => item.url).toList(),
|
||||
);
|
||||
@@ -567,17 +561,16 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
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(
|
||||
ImageUtil.downloadLivePhoto(
|
||||
context: this.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!,
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width!,
|
||||
height: item.height!,
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
|
||||
@@ -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, onReload}) => HttpError(
|
||||
isSliver: false,
|
||||
errMsg: errMsg,
|
||||
onReload: onReload,
|
||||
);
|
||||
|
||||
Widget scrollErrorWidget({errMsg, onReload, controller}) => CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: errMsg,
|
||||
onReload: onReload,
|
||||
)
|
||||
],
|
||||
);
|
||||
@@ -51,16 +51,16 @@ class HttpError extends StatelessWidget {
|
||||
FilledButton.tonal(
|
||||
onPressed: onReload,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return theme.colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.primary.withAlpha(20),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
btnText ?? '点击重试',
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
|
||||
SizedBox(height: 40 + MediaQuery.viewPaddingOf(context).bottom),
|
||||
],
|
||||
);
|
||||
}
|
||||
23
lib/common/widgets/loading_widget/loading_widget.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget get loadingWidget => const Center(child: CircularProgressIndicator());
|
||||
|
||||
Widget get linearLoading =>
|
||||
const SliverToBoxAdapter(child: LinearProgressIndicator());
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
359
lib/common/widgets/marquee.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class MarqueeText extends StatelessWidget {
|
||||
final double maxWidth;
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
final int? count;
|
||||
final bool bounce;
|
||||
final double spacing;
|
||||
|
||||
const MarqueeText(
|
||||
this.text, {
|
||||
super.key,
|
||||
required this.maxWidth,
|
||||
this.style,
|
||||
this.count,
|
||||
this.bounce = true,
|
||||
this.spacing = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: style,
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout();
|
||||
final width = textPainter.width;
|
||||
final child = Text(
|
||||
text,
|
||||
style: style,
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
if (width > maxWidth) {
|
||||
return SingleWidgetMarquee(
|
||||
child,
|
||||
duration: Duration(milliseconds: (width / 50 * 1000).round()),
|
||||
bounce: bounce,
|
||||
count: count,
|
||||
spacing: spacing,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SingleWidgetMarquee extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration? duration;
|
||||
final bool bounce;
|
||||
final double spacing;
|
||||
final int? count;
|
||||
|
||||
const SingleWidgetMarquee(
|
||||
this.child, {
|
||||
super.key,
|
||||
this.duration,
|
||||
this.bounce = false,
|
||||
this.spacing = 0,
|
||||
this.count,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SingleWidgetMarqueeState();
|
||||
}
|
||||
|
||||
class _SingleWidgetMarqueeState extends State<SingleWidgetMarquee>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final _controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
reverseDuration: widget.duration,
|
||||
)..repeat(reverse: widget.bounce, count: widget.count);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.bounce
|
||||
? BounceMarquee(
|
||||
animation: _controller,
|
||||
spacing: widget.spacing,
|
||||
child: widget.child,
|
||||
)
|
||||
: NormalMarquee(
|
||||
animation: _controller,
|
||||
spacing: widget.spacing,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Marquee extends SingleChildRenderObjectWidget {
|
||||
final Axis direction;
|
||||
final Clip clipBehavior;
|
||||
final double spacing;
|
||||
final Animation<double> animation;
|
||||
|
||||
const Marquee({
|
||||
super.key,
|
||||
required this.animation,
|
||||
required super.child,
|
||||
this.direction = Axis.horizontal,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.spacing = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant MarqueeRender renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..direction = direction
|
||||
..clipBehavior = clipBehavior
|
||||
..animation = animation
|
||||
..spacing = spacing;
|
||||
}
|
||||
}
|
||||
|
||||
class NormalMarquee extends Marquee {
|
||||
const NormalMarquee({
|
||||
super.key,
|
||||
required super.animation,
|
||||
required super.child,
|
||||
super.direction,
|
||||
super.clipBehavior,
|
||||
super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender(
|
||||
direction: direction,
|
||||
animation: animation,
|
||||
clipBehavior: clipBehavior,
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
class BounceMarquee extends Marquee {
|
||||
const BounceMarquee({
|
||||
super.key,
|
||||
required super.animation,
|
||||
required super.child,
|
||||
super.direction,
|
||||
super.clipBehavior,
|
||||
super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender(
|
||||
direction: direction,
|
||||
animation: animation,
|
||||
clipBehavior: clipBehavior,
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class MarqueeRender extends RenderBox
|
||||
with RenderObjectWithChildMixin<RenderBox> {
|
||||
MarqueeRender({
|
||||
required Axis direction,
|
||||
required Animation<double> animation,
|
||||
required this.clipBehavior,
|
||||
required this.spacing,
|
||||
}) : _direction = direction,
|
||||
_animation = animation,
|
||||
assert(spacing.isFinite && !spacing.isNaN);
|
||||
|
||||
Clip clipBehavior;
|
||||
double spacing;
|
||||
|
||||
Axis _direction;
|
||||
Axis get direction => _direction;
|
||||
set direction(Axis value) {
|
||||
if (_direction == value) return;
|
||||
_direction = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
Animation<double> _animation;
|
||||
Animation<double> get animation => _animation;
|
||||
set animation(Animation<double> value) {
|
||||
if (_animation == value) return;
|
||||
if (_listened) {
|
||||
_animation.removeListener(markNeedsPaint);
|
||||
value.addListener(markNeedsPaint);
|
||||
}
|
||||
_animation = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_removeListener();
|
||||
super.detach();
|
||||
}
|
||||
|
||||
bool _listened = false;
|
||||
void _addListener() {
|
||||
if (!_listened) {
|
||||
_animation.addListener(markNeedsPaint);
|
||||
_listened = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _removeListener() {
|
||||
if (_listened) {
|
||||
_animation.removeListener(markNeedsPaint);
|
||||
_listened = false;
|
||||
}
|
||||
}
|
||||
|
||||
late double _distance;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final child = this.child;
|
||||
if (child == null) {
|
||||
size = constraints.smallest;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_direction == Axis.horizontal) {
|
||||
child.layout(
|
||||
BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
size = constraints.constrain(child.size);
|
||||
_distance = child.size.width - size.width;
|
||||
if (spacing.isNegative) spacing *= -size.width;
|
||||
} else {
|
||||
child.layout(
|
||||
BoxConstraints(maxWidth: constraints.maxWidth),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
size = constraints.constrain(child.size);
|
||||
_distance = child.size.height - size.height;
|
||||
if (spacing.isNegative) spacing *= -size.height;
|
||||
}
|
||||
if (_distance > 0) {
|
||||
_addListener();
|
||||
} else {
|
||||
_removeListener();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
|
||||
void paintCenter(PaintingContext context, Offset offset) {
|
||||
if (_direction == Axis.horizontal) {
|
||||
context.paintChild(child!, Offset(offset.dx - _distance / 2, offset.dy));
|
||||
} else {
|
||||
context.paintChild(child!, Offset(offset.dx, offset.dy - _distance / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BounceMarqueeRender extends MarqueeRender {
|
||||
_BounceMarqueeRender({
|
||||
required super.direction,
|
||||
required super.animation,
|
||||
required super.clipBehavior,
|
||||
required super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child == null) return;
|
||||
|
||||
final tick = _animation.value;
|
||||
|
||||
if (_distance > 0) {
|
||||
final helfSpacing = spacing / 2.0;
|
||||
void paintChild() {
|
||||
if (_direction == Axis.horizontal) {
|
||||
context.paintChild(
|
||||
child!,
|
||||
Offset(
|
||||
offset.dx + helfSpacing - tick * (_distance + spacing),
|
||||
offset.dy,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.paintChild(
|
||||
child!,
|
||||
Offset(
|
||||
offset.dx,
|
||||
offset.dy + helfSpacing - tick * (_distance + spacing),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (clipBehavior == Clip.none) {
|
||||
paintChild();
|
||||
} else {
|
||||
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
|
||||
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
|
||||
}
|
||||
} else {
|
||||
paintCenter(context, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _NormalMarqueeRender extends MarqueeRender {
|
||||
_NormalMarqueeRender({
|
||||
required super.direction,
|
||||
required super.animation,
|
||||
required super.clipBehavior,
|
||||
required super.spacing,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final child = this.child;
|
||||
if (child == null) return;
|
||||
|
||||
final tick = _animation.value;
|
||||
|
||||
if (_distance > 0) {
|
||||
void paintChild() {
|
||||
if (_direction == Axis.horizontal) {
|
||||
final w = child.size.width + spacing;
|
||||
final dx = tick * w;
|
||||
context.paintChild(child, Offset(offset.dx - dx, offset.dy));
|
||||
if (dx > _distance) {
|
||||
context.paintChild(child, Offset(offset.dx + w - dx, offset.dy));
|
||||
}
|
||||
} else {
|
||||
final h = child.size.height + spacing;
|
||||
final dy = tick * h;
|
||||
context.paintChild(child, Offset(offset.dx, offset.dy - dy));
|
||||
if (dy > _distance) {
|
||||
context.paintChild(child, Offset(offset.dx, offset.dy + h - dy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clipBehavior == Clip.none) {
|
||||
paintChild();
|
||||
} else {
|
||||
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
|
||||
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
|
||||
}
|
||||
} else {
|
||||
paintCenter(context, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
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,
|
||||
this.height,
|
||||
this.type,
|
||||
this.fadeOutDuration,
|
||||
this.fadeInDuration,
|
||||
// 图片质量 默认1%
|
||||
this.quality,
|
||||
this.semanticsLabel,
|
||||
this.radius,
|
||||
this.imageBuilder,
|
||||
this.isLongPic,
|
||||
this.callback,
|
||||
this.getPlaceHolder,
|
||||
this.boxFit,
|
||||
});
|
||||
|
||||
final String? src;
|
||||
final double width;
|
||||
final double? height;
|
||||
final String? type;
|
||||
final Duration? fadeOutDuration;
|
||||
final Duration? fadeInDuration;
|
||||
final int? quality;
|
||||
final String? semanticsLabel;
|
||||
final double? radius;
|
||||
final ImageWidgetBuilder? imageBuilder;
|
||||
final Function? isLongPic;
|
||||
final Function? callback;
|
||||
final Function? getPlaceHolder;
|
||||
final BoxFit? boxFit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return src.isNullOrEmpty.not
|
||||
? type == 'avatar'
|
||||
? ClipOval(child: _buildImage(context))
|
||||
: radius == 0 || type == 'emote'
|
||||
? _buildImage(context)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
radius ?? StyleString.imgRadius.x,
|
||||
),
|
||||
child: _buildImage(context),
|
||||
)
|
||||
: getPlaceHolder?.call() ?? placeholder(context);
|
||||
}
|
||||
|
||||
Widget _buildImage(context) {
|
||||
int? memCacheWidth, memCacheHeight;
|
||||
if (height == null || callback?.call() == true || width <= height!) {
|
||||
memCacheWidth = width.cacheSize(context);
|
||||
} else {
|
||||
memCacheHeight = height.cacheSize(context);
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
imageUrl: Utils.thumbnailImgUrl(src, quality),
|
||||
width: width,
|
||||
height: height,
|
||||
memCacheWidth: memCacheWidth,
|
||||
memCacheHeight: memCacheHeight,
|
||||
fit: boxFit ?? BoxFit.cover,
|
||||
alignment:
|
||||
isLongPic?.call() == true ? Alignment.topCenter : Alignment.center,
|
||||
fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120),
|
||||
fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120),
|
||||
filterQuality: FilterQuality.low,
|
||||
placeholder: (BuildContext context, String url) =>
|
||||
getPlaceHolder?.call() ?? placeholder(context),
|
||||
imageBuilder: imageBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
Widget placeholder(BuildContext 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,
|
||||
),
|
||||
),
|
||||
child: type == 'bg'
|
||||
? const SizedBox.shrink()
|
||||
: Center(
|
||||
child: Image.asset(
|
||||
type == 'avatar'
|
||||
? 'assets/images/noface.jpeg'
|
||||
: 'assets/images/loading.png',
|
||||
width: width,
|
||||
height: height,
|
||||
cacheWidth: width.cacheSize(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
461
lib/common/widgets/page/page_view.dart
Normal file
@@ -0,0 +1,461 @@
|
||||
// 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',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2201
lib/common/widgets/page/scrollable.dart
Normal file
378
lib/common/widgets/page/tabs.dart
Normal file
@@ -0,0 +1,378 @@
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
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:PiliPlus/utils/image_util.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
class Avatar extends StatelessWidget {
|
||||
final _BadgeType _badgeType;
|
||||
final String avatar;
|
||||
class PendantAvatar extends StatelessWidget {
|
||||
final BadgeType _badgeType;
|
||||
final String? avatar;
|
||||
final double size;
|
||||
final double badgeSize;
|
||||
final String? garbPendantImage;
|
||||
final dynamic roomId;
|
||||
final int? roomId;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const Avatar({
|
||||
const PendantAvatar({
|
||||
super.key,
|
||||
required this.avatar,
|
||||
this.size = 80,
|
||||
@@ -26,42 +27,44 @@ class Avatar extends StatelessWidget {
|
||||
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;
|
||||
}) : _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;
|
||||
static bool showDynDecorate = Pref.showDynDecorate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isMemberAvatar = size == 80;
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
onTap == null
|
||||
? _buildAvatar(colorScheme)
|
||||
? _buildAvatar(colorScheme, isMemberAvatar)
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: _buildAvatar(colorScheme),
|
||||
child: _buildAvatar(colorScheme, isMemberAvatar),
|
||||
),
|
||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty)
|
||||
Positioned(
|
||||
top: -0.375 *
|
||||
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),
|
||||
imageUrl: ImageUtil.thumbnailUrl(garbPendantImage),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -69,14 +72,12 @@ class Avatar extends StatelessWidget {
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.toNamed('/liveRoom?roomid=$roomId');
|
||||
},
|
||||
onTap: () => PageUtils.toLiveRoom(roomId),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(36)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -99,14 +100,15 @@ class Avatar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_badgeType != _BadgeType.none)
|
||||
_buildBadge(colorScheme),
|
||||
else if (_badgeType != BadgeType.none)
|
||||
_buildBadge(colorScheme, isMemberAvatar),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(ColorScheme colorScheme) => size == 80
|
||||
? Container(
|
||||
Widget _buildAvatar(ColorScheme colorScheme, bool isMemberAvatar) =>
|
||||
isMemberAvatar
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
@@ -114,56 +116,50 @@ class Avatar extends StatelessWidget {
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: NetworkImgLayer(
|
||||
src: avatar,
|
||||
width: size,
|
||||
height: size,
|
||||
type: 'avatar',
|
||||
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: 'avatar',
|
||||
type: ImageType.avatar,
|
||||
);
|
||||
|
||||
Widget _buildBadge(ColorScheme colorScheme) {
|
||||
Widget _buildBadge(ColorScheme colorScheme, bool isMemberAvatar) {
|
||||
final child = switch (_badgeType) {
|
||||
_BadgeType.vip => Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
height: badgeSize,
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
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,
|
||||
),
|
||||
Icons.offline_bolt,
|
||||
color: _badgeType.color,
|
||||
size: badgeSize,
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
};
|
||||
final offset = isMemberAvatar ? 2.0 : 0.0;
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: child),
|
||||
));
|
||||
right: offset,
|
||||
bottom: offset,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _BadgeType { none, vip, person, institution }
|
||||
|
||||
extension _BadgeTypeExt on _BadgeType {
|
||||
String get desc => const ['', '大会员', '认证个人', '认证机构'][index];
|
||||
Color get color => const [
|
||||
Colors.transparent,
|
||||
Color(0xFFFF6699),
|
||||
Color(0xFFFFCC00),
|
||||
Colors.lightBlueAccent
|
||||
][index];
|
||||
}
|
||||
@@ -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,60 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +404,8 @@ class ThumbDragDetails {
|
||||
final Offset localPosition;
|
||||
|
||||
@override
|
||||
String toString() => '${objectRuntimeType(this, 'ThumbDragDetails')}('
|
||||
String toString() =>
|
||||
'${objectRuntimeType(this, 'ThumbDragDetails')}('
|
||||
'time: $timeStamp, '
|
||||
'global: $globalPosition, '
|
||||
'local: $localPosition)';
|
||||
@@ -430,27 +450,27 @@ class _RenderProgressBar extends RenderBox {
|
||||
TextStyle? timeLabelTextStyle,
|
||||
double timeLabelPadding = 0.0,
|
||||
double textScaleFactor = 1.0,
|
||||
}) : _total = total,
|
||||
_buffered = buffered,
|
||||
_onSeek = onSeek,
|
||||
_onDragStartUserCallback = onDragStart,
|
||||
_onDragUpdateUserCallback = onDragUpdate,
|
||||
_onDragEndUserCallback = onDragEnd,
|
||||
_barHeight = barHeight,
|
||||
_baseBarColor = baseBarColor,
|
||||
_progressBarColor = progressBarColor,
|
||||
_bufferedBarColor = bufferedBarColor,
|
||||
_barCapShape = barCapShape,
|
||||
_thumbRadius = thumbRadius,
|
||||
_thumbColor = thumbColor,
|
||||
_thumbGlowColor = thumbGlowColor,
|
||||
_thumbGlowRadius = thumbGlowRadius,
|
||||
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
|
||||
_timeLabelLocation = timeLabelLocation,
|
||||
_timeLabelType = timeLabelType,
|
||||
_timeLabelTextStyle = timeLabelTextStyle,
|
||||
_timeLabelPadding = timeLabelPadding,
|
||||
_textScaleFactor = textScaleFactor {
|
||||
}) : _total = total,
|
||||
_buffered = buffered,
|
||||
_onSeek = onSeek,
|
||||
_onDragStartUserCallback = onDragStart,
|
||||
_onDragUpdateUserCallback = onDragUpdate,
|
||||
_onDragEndUserCallback = onDragEnd,
|
||||
_barHeight = barHeight,
|
||||
_baseBarColor = baseBarColor,
|
||||
_progressBarColor = progressBarColor,
|
||||
_bufferedBarColor = bufferedBarColor,
|
||||
_barCapShape = barCapShape,
|
||||
_thumbRadius = thumbRadius,
|
||||
_thumbColor = thumbColor,
|
||||
_thumbGlowColor = thumbGlowColor,
|
||||
_thumbGlowRadius = thumbGlowRadius,
|
||||
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
|
||||
_timeLabelLocation = timeLabelLocation,
|
||||
_timeLabelType = timeLabelType,
|
||||
_timeLabelTextStyle = timeLabelTextStyle,
|
||||
_timeLabelPadding = timeLabelPadding,
|
||||
_textScaleFactor = textScaleFactor {
|
||||
_drag = _EagerHorizontalDragGestureRecognizer()
|
||||
..onStart = _onDragStart
|
||||
..onUpdate = _onDragUpdate
|
||||
@@ -489,11 +509,13 @@ class _RenderProgressBar extends RenderBox {
|
||||
}
|
||||
_userIsDraggingThumb = true;
|
||||
_updateThumbPosition(details.localPosition);
|
||||
onDragStart?.call(ThumbDragDetails(
|
||||
timeStamp: _currentThumbDuration(),
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
));
|
||||
onDragStart?.call(
|
||||
ThumbDragDetails(
|
||||
timeStamp: _currentThumbDuration(),
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
@@ -501,11 +523,13 @@ class _RenderProgressBar extends RenderBox {
|
||||
return;
|
||||
}
|
||||
_updateThumbPosition(details.localPosition);
|
||||
onDragUpdate?.call(ThumbDragDetails(
|
||||
timeStamp: _currentThumbDuration(),
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
));
|
||||
onDragUpdate?.call(
|
||||
ThumbDragDetails(
|
||||
timeStamp: _currentThumbDuration(),
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
@@ -621,9 +645,8 @@ class _RenderProgressBar extends RenderBox {
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: _timeLabelTextStyle),
|
||||
textDirection: TextDirection.ltr,
|
||||
textScaleFactor: textScaleFactor,
|
||||
);
|
||||
textPainter.layout(minWidth: 0, maxWidth: double.infinity);
|
||||
textScaler: TextScaler.linear(textScaleFactor),
|
||||
)..layout(minWidth: 0, maxWidth: double.infinity);
|
||||
return textPainter;
|
||||
}
|
||||
|
||||
@@ -919,9 +942,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:
|
||||
@@ -966,8 +989,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
_rightTimeLabel().paint(canvas, rightLabelOffset);
|
||||
|
||||
// progress bar
|
||||
final barDy =
|
||||
(isLabelBelow) ? 0.0 : _leftLabelSize.height + _timeLabelPadding;
|
||||
final barDy = (isLabelBelow)
|
||||
? 0.0
|
||||
: _leftLabelSize.height + _timeLabelPadding;
|
||||
_drawProgressBar(canvas, Offset(0, barDy), Size(barWidth, barHeight));
|
||||
}
|
||||
|
||||
@@ -992,7 +1016,8 @@ class _RenderProgressBar extends RenderBox {
|
||||
// progress bar
|
||||
final leftLabelWidth = leftLabelSize.width;
|
||||
final barHeight = _heightWhenNoLabels();
|
||||
final barWidth = size.width -
|
||||
final barWidth =
|
||||
size.width -
|
||||
2 * _defaultSidePadding -
|
||||
2 * _timeLabelPadding -
|
||||
leftLabelWidth -
|
||||
@@ -1013,8 +1038,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);
|
||||
@@ -1049,11 +1075,12 @@ class _RenderProgressBar extends RenderBox {
|
||||
);
|
||||
}
|
||||
|
||||
void _drawBar(
|
||||
{required Canvas canvas,
|
||||
required Size availableSize,
|
||||
required double widthProportion,
|
||||
required Color color}) {
|
||||
void _drawBar({
|
||||
required Canvas canvas,
|
||||
required Size availableSize,
|
||||
required double widthProportion,
|
||||
required Color color,
|
||||
}) {
|
||||
final strokeCap = (_barCapShape == BarCapShape.round)
|
||||
? StrokeCap.round
|
||||
: StrokeCap.square;
|
||||
@@ -1093,8 +1120,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
}
|
||||
|
||||
String _getTimeString(Duration time) {
|
||||
final minutes =
|
||||
time.inMinutes.remainder(Duration.minutesPerHour).toString();
|
||||
final minutes = time.inMinutes
|
||||
.remainder(Duration.minutesPerHour)
|
||||
.toString();
|
||||
final seconds = time.inSeconds
|
||||
.remainder(Duration.secondsPerMinute)
|
||||
.toString()
|
||||
@@ -1109,17 +1137,18 @@ class _RenderProgressBar extends RenderBox {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
|
||||
// description
|
||||
config.textDirection = TextDirection.ltr;
|
||||
config.label = '进度条'; //'Progress bar';
|
||||
config.value = '${(_thumbValue * 100).round()}%';
|
||||
|
||||
// increase action
|
||||
config.onIncrease = increaseAction;
|
||||
config
|
||||
..textDirection = TextDirection.ltr
|
||||
..label =
|
||||
'进度条' //'Progress bar';
|
||||
..value = '${(_thumbValue * 100).round()}%'
|
||||
// increase action
|
||||
..onIncrease = increaseAction;
|
||||
final increased = _thumbValue + _semanticActionUnit;
|
||||
config.increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%';
|
||||
|
||||
// decrease action
|
||||
config.onDecrease = decreaseAction;
|
||||
config
|
||||
..increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%'
|
||||
// decrease action
|
||||
..onDecrease = decreaseAction;
|
||||
final decreased = _thumbValue - _semanticActionUnit;
|
||||
config.decreasedValue = '${((decreased).clamp(0.0, 1.0) * 100).round()}%';
|
||||
}
|
||||
@@ -33,40 +33,40 @@ class SegmentProgressBar extends CustomPainter {
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
for (int i = 0; i < segmentColors.length; i++) {
|
||||
paint.color = segmentColors[i].color;
|
||||
final segmentStart = segmentColors[i].start * size.width;
|
||||
final segmentEnd = segmentColors[i].end * size.width;
|
||||
final item = segmentColors[i];
|
||||
paint.color = item.color;
|
||||
final segmentStart = item.start * size.width;
|
||||
final segmentEnd = item.end * size.width;
|
||||
|
||||
if (segmentEnd > segmentStart ||
|
||||
(segmentEnd == segmentStart && segmentStart > 0)) {
|
||||
if (segmentColors[i].title != null) {
|
||||
if (item.title != null) {
|
||||
double fontSize = 10;
|
||||
|
||||
_defHeight ??= (TextPainter(
|
||||
_defHeight ??=
|
||||
(TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
text: item.title,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout())
|
||||
.height +
|
||||
)..layout()).height +
|
||||
2;
|
||||
|
||||
TextPainter getTextPainter() => TextPainter(
|
||||
text: TextSpan(
|
||||
text: segmentColors[i].title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
strutStyle:
|
||||
StrutStyle(leading: 0, height: 1, fontSize: fontSize),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
text: TextSpan(
|
||||
text: item.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1, fontSize: fontSize),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
TextPainter textPainter = getTextPainter();
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,8 +106,8 @@ class SegmentProgressBar extends CustomPainter {
|
||||
double textX = i == 0
|
||||
? (segmentStart - textPainter.width) / 2
|
||||
: (segmentStart - prevStart - textPainter.width) / 2 +
|
||||
prevStart +
|
||||
1;
|
||||
prevStart +
|
||||
1;
|
||||
double textY = (-_defHeight! - textPainter.height) / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
} else {
|
||||
@@ -2,18 +2,19 @@ import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget videoProgressIndicator(double progress) => ClipRect(
|
||||
clipper: ProgressClipper(),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: StyleString.imgRadius,
|
||||
bottomRight: StyleString.imgRadius,
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10,
|
||||
value: progress,
|
||||
),
|
||||
),
|
||||
);
|
||||
clipper: ProgressClipper(),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: StyleString.imgRadius,
|
||||
bottomRight: StyleString.imgRadius,
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10,
|
||||
value: progress,
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
class ProgressClipper extends CustomClipper<Rect> {
|
||||
@override
|
||||
@@ -17,16 +17,16 @@ class RadioWidget<T> extends StatelessWidget {
|
||||
});
|
||||
|
||||
Widget _child() => Row(
|
||||
children: [
|
||||
Radio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
Text(title),
|
||||
],
|
||||
);
|
||||
children: [
|
||||
Radio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
Text(title),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/material.dart' hide RefreshIndicator;
|
||||
@@ -23,8 +24,8 @@ Widget refreshIndicator({
|
||||
// The over-scroll distance that moves the indicator to its maximum
|
||||
// displacement, as a percentage of the scrollable's container extent.
|
||||
|
||||
double displacement = 20;
|
||||
double kDragContainerExtentPercentage = 0.25;
|
||||
double displacement = Pref.refreshDisplacement;
|
||||
double kDragContainerExtentPercentage = Pref.refreshDragPercentage;
|
||||
|
||||
// How much the scroll's drag gesture can overshoot the RefreshIndicator's
|
||||
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
|
||||
@@ -46,15 +47,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 +79,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 +107,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 +166,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 +200,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 +268,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 +318,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,38 +338,50 @@ 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;
|
||||
late Color _effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
|
||||
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> _oneToZeroTween =
|
||||
Tween<double>(begin: 1.0, end: 0.0);
|
||||
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> _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 +390,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
_positionController.dispose();
|
||||
@@ -343,9 +413,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
begin: color.withAlpha(0),
|
||||
end: color.withAlpha(color.alpha),
|
||||
).chain(
|
||||
CurveTween(
|
||||
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit),
|
||||
),
|
||||
CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -364,7 +432,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,23 +442,24 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
if (_shouldStart(notification)) {
|
||||
setState(() {
|
||||
_mode = _RefreshIndicatorMode.drag;
|
||||
_status = RefreshIndicatorStatus.drag;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
final bool? indicatorAtTopNow =
|
||||
switch (notification.metrics.axisDirection) {
|
||||
AxisDirection.down || AxisDirection.up => true,
|
||||
AxisDirection.left || AxisDirection.right => null,
|
||||
};
|
||||
AxisDirection.down || AxisDirection.up => true,
|
||||
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 +467,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 +475,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 +485,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;
|
||||
@@ -438,11 +507,12 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
|
||||
bool _handleIndicatorNotification(
|
||||
OverscrollIndicatorNotification notification) {
|
||||
OverscrollIndicatorNotification notification,
|
||||
) {
|
||||
if (notification.depth != 0 || !notification.leading) {
|
||||
return false;
|
||||
}
|
||||
if (_mode == _RefreshIndicatorMode.drag) {
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
notification.disallowIndicator();
|
||||
return true;
|
||||
}
|
||||
@@ -450,7 +520,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,79 +540,94 @@ 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 &&
|
||||
_positionController.value = 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:
|
||||
await _scaleController.animateTo(1.0,
|
||||
duration: _kIndicatorScaleDuration);
|
||||
case _RefreshIndicatorMode.canceled:
|
||||
await _positionController.animateTo(0.0,
|
||||
duration: _kIndicatorScaleDuration);
|
||||
case _RefreshIndicatorMode.armed:
|
||||
case _RefreshIndicatorMode.drag:
|
||||
case _RefreshIndicatorMode.refresh:
|
||||
case _RefreshIndicatorMode.snap:
|
||||
switch (_status!) {
|
||||
case RefreshIndicatorStatus.done:
|
||||
await _scaleController.animateTo(
|
||||
1.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
await _positionController.animateTo(
|
||||
0.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
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) {
|
||||
setState(() {
|
||||
// Show the indeterminate progress indicator.
|
||||
_mode = _RefreshIndicatorMode.refresh;
|
||||
});
|
||||
.animateTo(
|
||||
1.0 / _kDragSizeFactorLimit,
|
||||
duration: _kIndicatorSnapDuration,
|
||||
)
|
||||
.whenComplete(() {
|
||||
if (mounted && _status == RefreshIndicatorStatus.snap) {
|
||||
setState(() {
|
||||
// Show the indeterminate progress indicator.
|
||||
_status = RefreshIndicatorStatus.refresh;
|
||||
});
|
||||
|
||||
final Future<void> refreshResult = widget.onRefresh();
|
||||
refreshResult.whenComplete(() {
|
||||
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
|
||||
completer.complete();
|
||||
_dismiss(_RefreshIndicatorMode.done);
|
||||
widget.onRefresh().whenComplete(() {
|
||||
if (mounted && _status == RefreshIndicatorStatus.refresh) {
|
||||
completer.complete();
|
||||
_dismiss(RefreshIndicatorStatus.done);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Show the refresh indicator and run the refresh callback as if it had
|
||||
@@ -562,9 +647,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 +657,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
return _pendingRefreshFuture;
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
@@ -583,7 +669,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
),
|
||||
);
|
||||
assert(() {
|
||||
if (_mode == null) {
|
||||
if (_status == null) {
|
||||
assert(_dragOffset == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
} else {
|
||||
@@ -594,14 +680,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,
|
||||
@@ -609,41 +695,47 @@ 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:
|
||||
@@ -655,9 +747,12 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
case TargetPlatform.macOS:
|
||||
return cupertinoIndicator;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
case _IndicatorType.noSpinner:
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/bangumi/introduction/controller.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pretty_qr_code/pretty_qr_code.dart';
|
||||
import 'package:saver_gallery/saver_gallery.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class SavePanel extends StatefulWidget {
|
||||
const SavePanel({
|
||||
required this.item,
|
||||
// reply
|
||||
this.upMid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final dynamic upMid;
|
||||
final dynamic item;
|
||||
|
||||
@override
|
||||
State<SavePanel> createState() => _SavePanelState();
|
||||
|
||||
static void toSavePanel({upMid, item}) {
|
||||
Get.generalDialog(
|
||||
barrierLabel: '',
|
||||
barrierDismissible: true,
|
||||
pageBuilder: (context, animation, secondaryAnimation) {
|
||||
return SavePanel(upMid: upMid, item: item);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 255),
|
||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||
var tween = Tween<double>(begin: 0, end: 1)
|
||||
.chain(CurveTween(curve: Curves.easeInOut));
|
||||
return FadeTransition(
|
||||
opacity: animation.drive(tween),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
routeSettings: RouteSettings(arguments: Get.arguments),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SavePanelState extends State<SavePanel> {
|
||||
final boundaryKey = GlobalKey();
|
||||
|
||||
bool showBottom = true;
|
||||
|
||||
// item
|
||||
dynamic get _item => widget.item;
|
||||
late String viewType = '查看';
|
||||
late String itemType = '内容';
|
||||
|
||||
//reply
|
||||
String? cover;
|
||||
String? title;
|
||||
int? pubdate;
|
||||
String? uname;
|
||||
|
||||
String uri = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_item is ReplyInfo) {
|
||||
itemType = '评论';
|
||||
final currentRoute = Get.currentRoute;
|
||||
late final hasRoot = _item.hasRoot();
|
||||
|
||||
if (currentRoute.startsWith('/video')) {
|
||||
try {
|
||||
final heroTag = Get.arguments?['heroTag'];
|
||||
late final ctr = Get.find<VideoIntroController>(tag: heroTag);
|
||||
cover = ctr.videoDetail.value.pic;
|
||||
title = ctr.videoDetail.value.title;
|
||||
pubdate = ctr.videoDetail.value.pubdate;
|
||||
uname = ctr.videoDetail.value.owner?.name;
|
||||
} catch (_) {}
|
||||
uri =
|
||||
'bilibili://video/${_item.oid}?comment_root_id=${hasRoot ? _item.root : _item.id}${hasRoot ? '&comment_secondary_id=${_item.id}' : ''}';
|
||||
|
||||
try {
|
||||
final heroTag = Get.arguments?['heroTag'];
|
||||
late final ctr = Get.find<BangumiIntroController>(tag: heroTag);
|
||||
final type = _item.type.toInt();
|
||||
late final oid = _item.oid;
|
||||
late final rootId = hasRoot ? _item.root : _item.id;
|
||||
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
|
||||
uri =
|
||||
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=bilibili://pgc/season/ep/${ctr.epId}';
|
||||
} catch (_) {}
|
||||
} else if (currentRoute.startsWith('/dynamicDetail')) {
|
||||
try {
|
||||
DynamicItemModel dynItem = Get.arguments['item'];
|
||||
uname = dynItem.modules.moduleAuthor?.name;
|
||||
final type = _item.type.toInt();
|
||||
late final oid = dynItem.idStr;
|
||||
late final rootId = hasRoot ? _item.root : _item.id;
|
||||
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
|
||||
late final enterUri = parseDyn(dynItem);
|
||||
viewType = '查看';
|
||||
itemType = '评论';
|
||||
uri = switch (type) {
|
||||
1 ||
|
||||
11 ||
|
||||
12 =>
|
||||
'bilibili://comment/detail/$type/${dynItem.basic!.ridStr}/$rootId/?${anchor}enterUri=$enterUri',
|
||||
_ =>
|
||||
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
|
||||
};
|
||||
} catch (_) {}
|
||||
} else if (currentRoute.startsWith('/Scaffold')) {
|
||||
try {
|
||||
final type = _item.type.toInt();
|
||||
late final oid = Get.arguments['oid'];
|
||||
late final rootId = hasRoot ? _item.root : _item.id;
|
||||
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
|
||||
late final enterUri = 'bilibili://following/detail/$oid';
|
||||
uri = switch (type) {
|
||||
1 ||
|
||||
11 ||
|
||||
12 =>
|
||||
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=${Get.arguments['enterUri']}',
|
||||
_ =>
|
||||
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
|
||||
};
|
||||
} catch (_) {}
|
||||
} else if (currentRoute.startsWith('/articlePage')) {
|
||||
try {
|
||||
final type = _item.type.toInt();
|
||||
late final oid = _item.oid;
|
||||
late final rootId = hasRoot ? _item.root : _item.id;
|
||||
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
|
||||
late final enterUri =
|
||||
'bilibili://following/detail/${Get.parameters['id'] ?? Get.arguments?['id']}';
|
||||
uri =
|
||||
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
debugPrint(uri);
|
||||
} else if (_item is DynamicItemModel) {
|
||||
uri = parseDyn(_item);
|
||||
|
||||
debugPrint(uri);
|
||||
}
|
||||
}
|
||||
|
||||
String parseDyn(item) {
|
||||
String uri = '';
|
||||
try {
|
||||
switch (item.type) {
|
||||
case 'DYNAMIC_TYPE_AV':
|
||||
viewType = '观看';
|
||||
itemType = '视频';
|
||||
uri = 'bilibili://video/${item.basic.commentIdStr}';
|
||||
break;
|
||||
|
||||
case 'DYNAMIC_TYPE_ARTICLE':
|
||||
itemType = '专栏';
|
||||
uri = 'bilibili://following/detail/${item.idStr}';
|
||||
break;
|
||||
|
||||
case 'DYNAMIC_TYPE_LIVE_RCMD':
|
||||
viewType = '观看';
|
||||
itemType = '直播';
|
||||
final roomId = item.modules.moduleDynamic.major.liveRcmd.roomId;
|
||||
uri = 'bilibili://live/$roomId';
|
||||
break;
|
||||
|
||||
case 'DYNAMIC_TYPE_UGC_SEASON':
|
||||
viewType = '观看';
|
||||
itemType = '合集';
|
||||
int aid = item.modules.moduleDynamic.major.ugcSeason.aid;
|
||||
uri = 'bilibili://video/$aid';
|
||||
break;
|
||||
|
||||
case 'DYNAMIC_TYPE_PGC':
|
||||
case 'DYNAMIC_TYPE_PGC_UNION':
|
||||
viewType = '观看';
|
||||
itemType =
|
||||
item?.modules?.moduleDynamic?.major?.pgc?.badge?['text'] ?? '番剧';
|
||||
final epid = item.modules.moduleDynamic.major.pgc.epid;
|
||||
uri = 'bilibili://pgc/season/ep/$epid';
|
||||
break;
|
||||
|
||||
// https://www.bilibili.com/medialist/detail/ml12345678
|
||||
case 'DYNAMIC_TYPE_MEDIALIST':
|
||||
itemType = '收藏夹';
|
||||
final mediaId = item.modules.moduleDynamic.major.medialist!['id'];
|
||||
uri = 'bilibili://medialist/detail/$mediaId';
|
||||
break;
|
||||
|
||||
// 纯文字动态查看
|
||||
// case 'DYNAMIC_TYPE_WORD':
|
||||
// # 装扮/剧集点评/普通分享
|
||||
// case 'DYNAMIC_TYPE_COMMON_SQUARE':
|
||||
// 转发的动态
|
||||
// case 'DYNAMIC_TYPE_FORWARD':
|
||||
// 图文动态查看
|
||||
// case 'DYNAMIC_TYPE_DRAW':
|
||||
default:
|
||||
itemType = '动态';
|
||||
uri = 'bilibili://following/detail/${item.idStr}';
|
||||
break;
|
||||
}
|
||||
} catch (_) {}
|
||||
return uri;
|
||||
}
|
||||
|
||||
void _onSaveOrSharePic([bool isShare = false]) async {
|
||||
if (!isShare) {
|
||||
if (mounted &&
|
||||
!await DownloadUtils.checkPermissionDependOnSdkInt(context)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
RenderRepaintBoundary boundary = boundaryKey.currentContext!
|
||||
.findRenderObject() as RenderRepaintBoundary;
|
||||
var image = await boundary.toImage(pixelRatio: 3);
|
||||
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
|
||||
Uint8List pngBytes = byteData!.buffer.asUint8List();
|
||||
String picName =
|
||||
"plpl_reply_${DateTime.now().toString().substring(0, 19).replaceAll(RegExp(r'[- :]'), '')}";
|
||||
if (isShare) {
|
||||
Get.back();
|
||||
SmartDialog.dismiss();
|
||||
Share.shareXFiles(
|
||||
[
|
||||
XFile.fromData(
|
||||
pngBytes,
|
||||
name: picName,
|
||||
mimeType: 'image/png',
|
||||
)
|
||||
],
|
||||
sharePositionOrigin: await Utils.isIpad()
|
||||
? Rect.fromLTWH(0, 0, Get.width, Get.height / 2)
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
final result = await SaverGallery.saveImage(
|
||||
pngBytes,
|
||||
fileName: '$picName.png',
|
||||
androidRelativePath: "Pictures/PiliPlus",
|
||||
skipIfExists: false,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (result.isSuccess) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('保存成功');
|
||||
} else if (result.errorMessage?.isNotEmpty == true) {
|
||||
SmartDialog.showToast(result.errorMessage!);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('on save/share reply: $e');
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: Get.back,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 80),
|
||||
child: SafeArea(
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: min(Get.width, Get.height),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: RepaintBoundary(
|
||||
key: boundaryKey,
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: AnimatedSize(
|
||||
curve: Curves.easeInOut,
|
||||
alignment: Alignment.topCenter,
|
||||
duration: const Duration(milliseconds: 255),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_item is ReplyInfo)
|
||||
IgnorePointer(
|
||||
child: ReplyItemGrpc(
|
||||
replyItem: _item,
|
||||
replyLevel: '',
|
||||
needDivider: false,
|
||||
upMid: widget.upMid,
|
||||
),
|
||||
)
|
||||
else if (_item is DynamicItemModel)
|
||||
IgnorePointer(
|
||||
child: DynamicPanel(
|
||||
item: _item,
|
||||
source: 'detail',
|
||||
isSave: true,
|
||||
),
|
||||
),
|
||||
if (cover?.isNotEmpty == true &&
|
||||
title?.isNotEmpty == true)
|
||||
Container(
|
||||
height: 81,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
radius: 6,
|
||||
src: cover!,
|
||||
height: MediaQuery.textScalerOf(context)
|
||||
.scale(65),
|
||||
width: MediaQuery.textScalerOf(context)
|
||||
.scale(65) *
|
||||
16 /
|
||||
9,
|
||||
quality: 100,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$title\n',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (pubdate != null) ...[
|
||||
const Spacer(),
|
||||
Text(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
pubdate! * 1000)
|
||||
.toString()
|
||||
.substring(0, 19),
|
||||
style: TextStyle(
|
||||
color:
|
||||
theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
showBottom
|
||||
? Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
if (uri.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (uname?.isNotEmpty ==
|
||||
true) ...[
|
||||
Text(
|
||||
'@$uname',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow
|
||||
.ellipsis,
|
||||
style: TextStyle(
|
||||
color: theme
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
Text(
|
||||
'识别二维码,$viewType$itemType',
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateTime.now()
|
||||
.toString()
|
||||
.split('.')
|
||||
.first,
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
padding:
|
||||
const EdgeInsets.all(12),
|
||||
child: Container(
|
||||
color: Get.isDarkMode
|
||||
? Colors.white
|
||||
: theme
|
||||
.colorScheme.surface,
|
||||
padding:
|
||||
const EdgeInsets.all(3),
|
||||
child: PrettyQrView.data(
|
||||
data: uri,
|
||||
decoration:
|
||||
const PrettyQrDecoration(
|
||||
shape:
|
||||
PrettyQrRoundedSymbol(
|
||||
borderRadius:
|
||||
BorderRadius.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Image.asset(
|
||||
'assets/images/logo/logo_2.png',
|
||||
width: 100,
|
||||
color: theme
|
||||
.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black54,
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: 25, top: 10),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
iconButton(
|
||||
size: 42,
|
||||
tooltip: '关闭',
|
||||
context: context,
|
||||
icon: Icons.clear,
|
||||
onPressed: Get.back,
|
||||
bgColor: theme.colorScheme.onInverseSurface,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 40),
|
||||
iconButton(
|
||||
size: 42,
|
||||
tooltip: showBottom ? '隐藏' : '显示',
|
||||
context: context,
|
||||
icon: showBottom
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
onPressed: () => setState(() {
|
||||
showBottom = !showBottom;
|
||||
})),
|
||||
const SizedBox(width: 40),
|
||||
iconButton(
|
||||
size: 42,
|
||||
tooltip: '分享',
|
||||
context: context,
|
||||
icon: Icons.share,
|
||||
onPressed: () => _onSaveOrSharePic(true),
|
||||
),
|
||||
const SizedBox(width: 40),
|
||||
iconButton(
|
||||
size: 42,
|
||||
tooltip: '保存',
|
||||
context: context,
|
||||
icon: Icons.save_alt,
|
||||
onPressed: _onSaveOrSharePic,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,23 @@
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget videoTabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) =>
|
||||
TabBarView(
|
||||
physics: const CustomTabBarViewClampingScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
}) => TabBarView(
|
||||
physics: const CustomTabBarViewClampingScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
|
||||
Widget tabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) =>
|
||||
TabBarView(
|
||||
physics: const CustomTabBarViewScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
}) => TabBarView(
|
||||
physics: const CustomTabBarViewScrollPhysics(),
|
||||
controller: controller,
|
||||
children: children,
|
||||
);
|
||||
|
||||
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
|
||||
const CustomTabBarViewScrollPhysics({super.parent});
|
||||
@@ -45,14 +43,21 @@ class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
SpringDescription get spring => CustomSpringDescription();
|
||||
}
|
||||
|
||||
class PositionRetainedScrollPhysics extends AlwaysScrollableScrollPhysics {
|
||||
const PositionRetainedScrollPhysics({super.parent, this.shouldRetain = true});
|
||||
mixin ReloadMixin {
|
||||
late bool reload = false;
|
||||
}
|
||||
|
||||
final bool shouldRetain;
|
||||
class ReloadScrollPhysics extends AlwaysScrollableScrollPhysics {
|
||||
const ReloadScrollPhysics({super.parent, required this.controller});
|
||||
|
||||
final ReloadMixin controller;
|
||||
|
||||
@override
|
||||
PositionRetainedScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return PositionRetainedScrollPhysics(parent: buildParent(ancestor));
|
||||
ReloadScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return ReloadScrollPhysics(
|
||||
parent: buildParent(ancestor),
|
||||
controller: controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -62,36 +67,42 @@ class PositionRetainedScrollPhysics extends AlwaysScrollableScrollPhysics {
|
||||
required bool isScrolling,
|
||||
required double velocity,
|
||||
}) {
|
||||
final position = super.adjustPositionForNewDimensions(
|
||||
if (controller.reload) {
|
||||
controller.reload = false;
|
||||
return 0;
|
||||
}
|
||||
return super.adjustPositionForNewDimensions(
|
||||
oldPosition: oldPosition,
|
||||
newPosition: newPosition,
|
||||
isScrolling: isScrolling,
|
||||
velocity: velocity,
|
||||
);
|
||||
|
||||
late final diff = newPosition.maxScrollExtent - oldPosition.maxScrollExtent;
|
||||
|
||||
if (shouldRetain && oldPosition.pixels == 0 && diff > 0) {
|
||||
return position + diff;
|
||||
} else {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSpringDescription implements SpringDescription {
|
||||
@override
|
||||
final mass = GStorage.springDescription[0];
|
||||
static final List<double> springDescription = Pref.springDescription;
|
||||
|
||||
@override
|
||||
final stiffness = GStorage.springDescription[1];
|
||||
final mass = springDescription[0];
|
||||
|
||||
@override
|
||||
final damping = GStorage.springDescription[2];
|
||||
final stiffness = springDescription[1];
|
||||
|
||||
@override
|
||||
final damping = 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);
|
||||
}
|
||||
|
||||
38
lib/common/widgets/select_mask.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget selectMask(
|
||||
ThemeData theme,
|
||||
bool checked, {
|
||||
BorderRadiusGeometry borderRadius = StyleString.mdRadius,
|
||||
}) {
|
||||
return AnimatedOpacity(
|
||||
opacity: checked ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
),
|
||||
child: AnimatedScale(
|
||||
scale: checked ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface.withValues(alpha: 0.8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.done_all_outlined,
|
||||
color: theme.colorScheme.primary,
|
||||
semanticLabel: '取消选择',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,14 @@ 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) {
|
||||
@@ -42,10 +50,13 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
|
||||
}
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +67,7 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
|
||||
padding: widget.padding,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.itemCount,
|
||||
itemBuilder: (c, i) => widget.childBuilder.call(i),
|
||||
itemBuilder: (c, i) => widget.childBuilder(i),
|
||||
separatorBuilder: (c, i) => SizedBox(width: widget.gapSize),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,90 +1,41 @@
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/models/common/stat_type.dart';
|
||||
import 'package:PiliPlus/utils/num_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class _StatItemBase extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
final Object value;
|
||||
final String? theme;
|
||||
final Color? textColor;
|
||||
class StatWidget extends StatelessWidget {
|
||||
final StatType type;
|
||||
final dynamic value;
|
||||
final Color? color;
|
||||
final double iconSize;
|
||||
|
||||
const _StatItemBase({
|
||||
required this.context,
|
||||
const StatWidget({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.value,
|
||||
this.theme,
|
||||
this.textColor,
|
||||
this.color,
|
||||
this.iconSize = 13,
|
||||
});
|
||||
|
||||
IconData get iconData;
|
||||
String get semanticsLabel;
|
||||
|
||||
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),
|
||||
_ => Colors.white,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color color =
|
||||
this.color ??
|
||||
Theme.of(context).colorScheme.outline.withValues(alpha: 0.8);
|
||||
return Row(
|
||||
spacing: 2,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
iconData,
|
||||
type.iconData,
|
||||
semanticLabel: type.label,
|
||||
size: iconSize,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
Utils.numFormat(value),
|
||||
NumUtil.numFormat(value),
|
||||
style: TextStyle(fontSize: 12, color: color),
|
||||
overflow: TextOverflow.clip,
|
||||
semanticsLabel: semanticsLabel,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatView extends _StatItemBase {
|
||||
final String? goto;
|
||||
|
||||
const StatView({
|
||||
required super.context,
|
||||
required super.value,
|
||||
this.goto,
|
||||
super.theme,
|
||||
super.textColor,
|
||||
}) : super(iconSize: 13);
|
||||
|
||||
@override
|
||||
IconData get iconData => switch (goto) {
|
||||
'picture' => Icons.remove_red_eye_outlined,
|
||||
'like' => Icons.thumb_up_outlined,
|
||||
'reply' => Icons.comment_outlined,
|
||||
'follow' => Icons.favorite_border,
|
||||
_ => Icons.play_circle_outlined,
|
||||
};
|
||||
|
||||
@override
|
||||
String get semanticsLabel =>
|
||||
'${Utils.numFormat(value)}次${goto == "picture" ? "浏览" : "播放"}';
|
||||
}
|
||||
|
||||
class StatDanMu extends _StatItemBase {
|
||||
const StatDanMu({
|
||||
required super.context,
|
||||
required super.value,
|
||||
super.theme,
|
||||
super.textColor,
|
||||
}) : super(iconSize: 14);
|
||||
|
||||
@override
|
||||
IconData get iconData => Icons.subtitles_outlined;
|
||||
|
||||
@override
|
||||
String get semanticsLabel => '${Utils.numFormat(value)}条弹幕';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -130,8 +132,11 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
required Curve curve,
|
||||
}) async {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController!
|
||||
.animateToPage(page, duration: duration, curve: curve);
|
||||
await _pageController!.animateToPage(
|
||||
page,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
@@ -190,7 +195,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() {
|
||||
@@ -222,13 +231,14 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(_currentIndex!,
|
||||
duration: duration, curve: Curves.ease);
|
||||
await _animateToPage(
|
||||
_currentIndex!,
|
||||
duration: duration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_updateChildren();
|
||||
});
|
||||
setState(_updateChildren);
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
@@ -260,20 +270,24 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(_currentIndex!,
|
||||
duration: duration, curve: Curves.ease);
|
||||
await _animateToPage(
|
||||
_currentIndex!,
|
||||
duration: duration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_updateChildren();
|
||||
});
|
||||
setState(_updateChildren);
|
||||
}
|
||||
}
|
||||
|
||||
void _syncControllerOffset() {
|
||||
_controller!.offset =
|
||||
clampDouble(_pageController!.page! - _controller!.index, -1.0, 1.0);
|
||||
_controller!.offset = clampDouble(
|
||||
_pageController!.page! - _controller!.index,
|
||||
-1.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
// Called when the PageView scrolls
|
||||
|
||||