Compare commits
792 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23813eb224 | ||
|
|
77e4a30bc5 | ||
|
|
15f4ae2567 | ||
|
|
b3f117d28e | ||
|
|
17a75da540 | ||
|
|
f8caa46eab | ||
|
|
8d4bbc1a1c | ||
|
|
b5f2510cce | ||
|
|
978f27c700 | ||
|
|
b4ca42e0c0 | ||
|
|
4abffeed32 | ||
|
|
9b5628cb65 | ||
|
|
85f06ed65d | ||
|
|
f6b5d358e0 | ||
|
|
a42881ba9f | ||
|
|
d5991b4354 | ||
|
|
101e49fe74 | ||
|
|
1cbeacbd0f | ||
|
|
4b6b3e8377 | ||
|
|
b3ab417c85 | ||
|
|
defc6911d6 | ||
|
|
6c757ec395 | ||
|
|
b876840d08 | ||
|
|
30bad3a066 | ||
|
|
ca993df0c6 | ||
|
|
451a84e696 | ||
|
|
e65ec1b0b9 | ||
|
|
aed45b08ac | ||
|
|
7f93b42a1b | ||
|
|
a831b41623 | ||
|
|
4d193a1f72 | ||
|
|
51750a4ad5 | ||
|
|
8fe6e3f4b7 | ||
|
|
6d7b0e8dd5 | ||
|
|
43409826f3 | ||
|
|
bb6bd95e9b | ||
|
|
d4d1602b45 | ||
|
|
bd3c76ef43 | ||
|
|
3722ff1f33 | ||
|
|
dc1cca0d4c | ||
|
|
3dad24e7b4 | ||
|
|
c591b57f22 | ||
|
|
91389f91d1 | ||
|
|
ec811f75e6 | ||
|
|
51e88939d6 | ||
|
|
f4470c383e | ||
|
|
ed99aee3fd | ||
|
|
40fb93f036 | ||
|
|
64f7ba2a1a | ||
|
|
6a45f993ae | ||
|
|
0bdf620c2f | ||
|
|
b8d2ff7e9b | ||
|
|
91142be3bd | ||
|
|
8159e1b1df | ||
|
|
27b05098cc | ||
|
|
1e851d34b6 | ||
|
|
f10aa38bfd | ||
|
|
9a1b15029e | ||
|
|
2063c366c2 | ||
|
|
afe812e2be | ||
|
|
738cd61825 | ||
|
|
c28729af5b | ||
|
|
4d7d9abc60 | ||
|
|
8c7001c801 | ||
|
|
039e1696dd | ||
|
|
636e083044 | ||
|
|
fcaba24cee | ||
|
|
33b8902375 | ||
|
|
65eecb8dcf | ||
|
|
e0fe16fd14 | ||
|
|
7bb0307e6a | ||
|
|
cba70c3507 | ||
|
|
f779ed63e8 | ||
|
|
07e34eb17b | ||
|
|
f220db96ed | ||
|
|
a0abd472e0 | ||
|
|
0d27d88719 | ||
|
|
e212144250 | ||
|
|
2f5a3d66fc | ||
|
|
ff0ff42222 | ||
|
|
0dc209d30a | ||
|
|
2aeecb05d3 | ||
|
|
65404ce356 | ||
|
|
246061c69e | ||
|
|
92f96c93f0 | ||
|
|
993c1f309a | ||
|
|
7856857cca | ||
|
|
1f2f00d49c | ||
|
|
3afdd9d3f3 | ||
|
|
42fa4a2fff | ||
|
|
3d4bcbc082 | ||
|
|
4c0443ec28 | ||
|
|
8b28a31d09 | ||
|
|
e6e9ce7d57 | ||
|
|
9ad57dccb0 | ||
|
|
95caf111ae | ||
|
|
abdde1f811 | ||
|
|
ae901c709d | ||
|
|
a2af297a84 | ||
|
|
f9e28d1de9 | ||
|
|
a2ef4e6f84 | ||
|
|
e5f3c3c922 | ||
|
|
6f4321ae14 | ||
|
|
a5c7ec0d60 | ||
|
|
6bc0a8b4aa | ||
|
|
538494b7ec | ||
|
|
ed60c274fc | ||
|
|
bbc498f882 | ||
|
|
0932b3d625 | ||
|
|
9d4d37f2e7 | ||
|
|
6fc7e47111 | ||
|
|
c05ad1e724 | ||
|
|
5ed86b9165 | ||
|
|
75cbd20f54 | ||
|
|
3c07b7347b | ||
|
|
d0ebedac0a | ||
|
|
d86caac189 | ||
|
|
c2b02b9b8d | ||
|
|
a4e8ea37aa | ||
|
|
f56ca9c082 | ||
|
|
e27476bc32 | ||
|
|
8ca4f7c8d3 | ||
|
|
1c4eb0766b | ||
|
|
87a812b7e0 | ||
|
|
f42a6200ed | ||
|
|
a252ee0655 | ||
|
|
498988c2e3 | ||
|
|
261922d73a | ||
|
|
ebe08c23e4 | ||
|
|
70edd4cc3a | ||
|
|
fa48a07970 | ||
|
|
0259ca963a | ||
|
|
8dc9f68584 | ||
|
|
4db7711a36 | ||
|
|
7b9e4b2f82 | ||
|
|
07c04a9e7e | ||
|
|
8427ebc36e | ||
|
|
a99fc8fa72 | ||
|
|
5959288491 | ||
|
|
0522dd5ad4 | ||
|
|
d886569dc3 | ||
|
|
12c711424b | ||
|
|
cb6ead96d1 | ||
|
|
c4e7263ed6 | ||
|
|
4972e64cad | ||
|
|
5ea8a7d313 | ||
|
|
296cd863d2 | ||
|
|
9ccf91659f | ||
|
|
f0e3b776bb | ||
|
|
3638d65008 | ||
|
|
2cc9324f08 | ||
|
|
bc8907b3ef | ||
|
|
14f8ec37c5 | ||
|
|
2b567e7cb3 | ||
|
|
b58a3ec044 | ||
|
|
2d0d578bb4 | ||
|
|
54ba05c4aa | ||
|
|
27b251b06e | ||
|
|
5643ebfe48 | ||
|
|
d9c2f6bf91 | ||
|
|
3eb404a9e2 | ||
|
|
bc9c20c509 | ||
|
|
7cc0c83df1 | ||
|
|
41daefa6c4 | ||
|
|
38fa8a10b7 | ||
|
|
07d37a1209 | ||
|
|
509f0d1266 | ||
|
|
7966bab62d | ||
|
|
a136c150ad | ||
|
|
a89fe6b026 | ||
|
|
56460c937d | ||
|
|
f2080bfb7b | ||
|
|
012d55452e | ||
|
|
6ac482ed5e | ||
|
|
68df173558 | ||
|
|
d9c6c31a4d | ||
|
|
d3d2715418 | ||
|
|
a93fbd4444 | ||
|
|
9fee9a4cf1 | ||
|
|
4bbc008788 | ||
|
|
671b6e1ef7 | ||
|
|
634bae915a | ||
|
|
a7bbfc983e | ||
|
|
17548e935e | ||
|
|
15f84712cd | ||
|
|
2f34ae7d45 | ||
|
|
16cbe7e43c | ||
|
|
8d633377ae | ||
|
|
0b867c254f | ||
|
|
08a47e6c1d | ||
|
|
6c9cd8b120 | ||
|
|
71e7219084 | ||
|
|
c13063b230 | ||
|
|
26ca69cb83 | ||
|
|
afc8c5f873 | ||
|
|
4d3f739a0c | ||
|
|
1781fdb7ca | ||
|
|
32aa37505c | ||
|
|
9f9ed7dd4b | ||
|
|
03e3b897cf | ||
|
|
3bc20ce1d4 | ||
|
|
9ce9940306 | ||
|
|
da35cf471e | ||
|
|
c517df2c09 | ||
|
|
02dee71670 | ||
|
|
1eadcd41f6 | ||
|
|
e8185535b0 | ||
|
|
b68bebfa2e | ||
|
|
3801bdf9d7 | ||
|
|
9a6ba82467 | ||
|
|
3a52c1199c | ||
|
|
ea5c0584cc | ||
|
|
01b30d942b | ||
|
|
5aa5308a50 | ||
|
|
de029b7043 | ||
|
|
a45da453ce | ||
|
|
e1b73f4766 | ||
|
|
99b19e7b03 | ||
|
|
37bd849a86 | ||
|
|
4eb6f78a38 | ||
|
|
68f03f2311 | ||
|
|
2a60a9b393 | ||
|
|
1d4b08672b | ||
|
|
b0d9a1dada | ||
|
|
796494e53f | ||
|
|
cef7bfd534 | ||
|
|
36ff4a0ed3 | ||
|
|
6a6894030b | ||
|
|
497d31ddf7 | ||
|
|
783218429c | ||
|
|
0ccd15047b | ||
|
|
fe2a6ec006 | ||
|
|
a3ecf59fae | ||
|
|
4f4f89a1d7 | ||
|
|
ece3bdd2e8 | ||
|
|
f403ed1a21 | ||
|
|
17e3a0206a | ||
|
|
5da86d85de | ||
|
|
d3cbc95235 | ||
|
|
a7eebcc209 | ||
|
|
fca22eb592 | ||
|
|
1202e5ec0f | ||
|
|
03830533eb | ||
|
|
850e5a199e | ||
|
|
2d11158ecd | ||
|
|
a34c18b262 | ||
|
|
560b1e40cc | ||
|
|
3cd512857c | ||
|
|
356adbef5c | ||
|
|
42d7445d83 | ||
|
|
3a0f32fce7 | ||
|
|
6bc128cfda | ||
|
|
6f2d697748 | ||
|
|
4de180c23a | ||
|
|
af289c533f | ||
|
|
82d615fbbf | ||
|
|
457f2ea6c7 | ||
|
|
41ad5c45ed | ||
|
|
e9da2e8d6b | ||
|
|
a8cfbb12fd | ||
|
|
6d89b7769e | ||
|
|
2d86daec83 | ||
|
|
a5e8594611 | ||
|
|
99810ef512 | ||
|
|
2317b831db | ||
|
|
e073086cf4 | ||
|
|
b14844f459 | ||
|
|
8719c8f639 | ||
|
|
d3cec0ec72 | ||
|
|
a8daf02610 | ||
|
|
f9b844fb1a | ||
|
|
6d1d6b575a | ||
|
|
0a5a094e54 | ||
|
|
754da4777a | ||
|
|
216e3e606e | ||
|
|
bb013a8fe6 | ||
|
|
6b6449f023 | ||
|
|
fcf3348371 | ||
|
|
f90f759667 | ||
|
|
b02e6c04b9 | ||
|
|
08dc04f874 | ||
|
|
4776b84c7c | ||
|
|
78d13b586a | ||
|
|
f522ecd42d | ||
|
|
44fa2a8c3e | ||
|
|
ff30c8c2bf | ||
|
|
4aaaffbcea | ||
|
|
21da122902 | ||
|
|
849904ad45 | ||
|
|
1c0bae600f | ||
|
|
f1433c6e9b | ||
|
|
2dc106adcb | ||
|
|
df6738f607 | ||
|
|
ee64f1e7f1 | ||
|
|
d921f6176b | ||
|
|
7009c3400a | ||
|
|
7bd481b090 | ||
|
|
7fafa88eb7 | ||
|
|
cb3e57feec | ||
|
|
9a7d73cb6b | ||
|
|
f5c2bd47d5 | ||
|
|
c154d25f7a | ||
|
|
8c259205f5 | ||
|
|
849329b66b | ||
|
|
f542565dc5 | ||
|
|
08aedbf0b0 | ||
|
|
09c8a41c52 | ||
|
|
6a7d14a3f8 | ||
|
|
5b171ec044 | ||
|
|
978d634cb3 | ||
|
|
7437d8c592 | ||
|
|
e190ca5868 | ||
|
|
64fc995f6b | ||
|
|
2d0e801a1a | ||
|
|
d409424871 | ||
|
|
b855ef9865 | ||
|
|
86abf006d0 | ||
|
|
d1a6798f2e | ||
|
|
f64d543ec7 | ||
|
|
2abf01362c | ||
|
|
9bbd934f8e | ||
|
|
6ab72b65aa | ||
|
|
c39de1e245 | ||
|
|
d112843a8a | ||
|
|
89df091542 | ||
|
|
d870c36a96 | ||
|
|
fc55bf33d0 | ||
|
|
f99740ef2d | ||
|
|
f9f30a5f13 | ||
|
|
f70cf05870 | ||
|
|
22866012ca | ||
|
|
cdb2718aeb | ||
|
|
e4f3203351 | ||
|
|
d6b388ad5c | ||
|
|
61819d9f27 | ||
|
|
3ccc7ef69d | ||
|
|
e834311664 | ||
|
|
fd8dff327c | ||
|
|
a9df8cd883 | ||
|
|
909394965e | ||
|
|
2d5991e0c5 | ||
|
|
e7ae66a3dc | ||
|
|
5929150047 | ||
|
|
acb6bc569e | ||
|
|
678db34c81 | ||
|
|
134bfd43ff | ||
|
|
8712248ef2 | ||
|
|
c97227e807 | ||
|
|
40429021be | ||
|
|
e89bd2fedf | ||
|
|
9da3a538fb | ||
|
|
a904414f3d | ||
|
|
f003e8bf35 | ||
|
|
7399915357 | ||
|
|
7aa0289c1f | ||
|
|
99d0b1c468 | ||
|
|
84a342a0e0 | ||
|
|
db1c836a3e | ||
|
|
6539457f83 | ||
|
|
82f9f48a8e | ||
|
|
2ddfea5cf3 | ||
|
|
79aee2fdd9 | ||
|
|
5dc8b8e54f | ||
|
|
72fa9c51f0 | ||
|
|
6ea8ffea7a | ||
|
|
eea5257da2 | ||
|
|
385cffefb8 | ||
|
|
347420c531 | ||
|
|
cc774015f9 | ||
|
|
da3f64feab | ||
|
|
76d031e8d1 | ||
|
|
25995b0ed6 | ||
|
|
3cdd40a710 | ||
|
|
f36f8d69fc | ||
|
|
5655e6ccdf | ||
|
|
49fff821b1 | ||
|
|
3b34cecdcd | ||
|
|
3693d6c350 | ||
|
|
54cb1a6fc0 | ||
|
|
ab7b1524b6 | ||
|
|
2b4a27076c | ||
|
|
63a7fa95f5 | ||
|
|
a15b932a69 | ||
|
|
3a6b6614a4 | ||
|
|
6ff83e34f3 | ||
|
|
e4cadc5a40 | ||
|
|
907d37dd4c | ||
|
|
00d30313af | ||
|
|
950dd82e3c | ||
|
|
c53c3a387c | ||
|
|
5d0b2dc8e3 | ||
|
|
f5e9375917 | ||
|
|
cdfab7a7db | ||
|
|
f3e6a59e4f | ||
|
|
239c9ca2a7 | ||
|
|
becb566ca8 | ||
|
|
aa378d924b | ||
|
|
812f351ddd | ||
|
|
b9adf26ee0 | ||
|
|
018cd058ca | ||
|
|
cd3385be63 | ||
|
|
d4e4813c78 | ||
|
|
8030912087 | ||
|
|
4879701008 | ||
|
|
6ab8e5925e | ||
|
|
52dda9964c | ||
|
|
1825329236 | ||
|
|
60a650f798 | ||
|
|
0f78669faf | ||
|
|
015309b3dc | ||
|
|
a74edd22c1 | ||
|
|
7a6085e923 | ||
|
|
bf464994df | ||
|
|
a611a88f69 | ||
|
|
025b5c8e6d | ||
|
|
d37685f7cf | ||
|
|
10a22b5186 | ||
|
|
d9a74c43dc | ||
|
|
90c8aeb05d | ||
|
|
34f63612a4 | ||
|
|
edfa9a8dd1 | ||
|
|
95fa19f121 | ||
|
|
79d0d314f5 | ||
|
|
ddbf168c87 | ||
|
|
0eee8bbac2 | ||
|
|
92f02b5943 | ||
|
|
f110c2a55f | ||
|
|
8ddf42fff1 | ||
|
|
d2c34d64c3 | ||
|
|
2341027972 | ||
|
|
006c9301d9 | ||
|
|
1f8955d0b3 | ||
|
|
5a758ebb3a | ||
|
|
97bef56006 | ||
|
|
96ba36ed67 | ||
|
|
edf84fcc8f | ||
|
|
a8428e52d2 | ||
|
|
fd8559228e | ||
|
|
d6587cf3b6 | ||
|
|
7c3e3cb1f8 | ||
|
|
9d0ac30fad | ||
|
|
68d11d7638 | ||
|
|
3881b3dc74 | ||
|
|
99b14d0f0e | ||
|
|
066f3d4132 | ||
|
|
b15fdfa2ff | ||
|
|
94fa0652ac | ||
|
|
5c54e131ba | ||
|
|
1fc85fd618 | ||
|
|
e1c561b613 | ||
|
|
950620bf9e | ||
|
|
ae7a1e2373 | ||
|
|
bddeb72d9b | ||
|
|
b99cf4f629 | ||
|
|
b07cf62bdd | ||
|
|
57d2d3f5d9 | ||
|
|
7854c5e6b9 | ||
|
|
0b8e95477c | ||
|
|
c2e9a7deb3 | ||
|
|
361a6a4c1d | ||
|
|
8eca9a6644 | ||
|
|
0770f325ab | ||
|
|
98d52760b3 | ||
|
|
0ebe976b8a | ||
|
|
21fe0ef288 | ||
|
|
28ef1890d1 | ||
|
|
d6f238c720 | ||
|
|
44bf9dd9e1 | ||
|
|
e357da5162 | ||
|
|
c296aa036a | ||
|
|
12c46f938d | ||
|
|
b4412f5b37 | ||
|
|
5f2ac0d59b | ||
|
|
2a73725455 | ||
|
|
a8725e64ee | ||
|
|
727ae8cd2b | ||
|
|
714f288170 | ||
|
|
3da64d2641 | ||
|
|
2556290a6e | ||
|
|
66b547a904 | ||
|
|
c1ab273478 | ||
|
|
4aa3d5f273 | ||
|
|
a6a1de169b | ||
|
|
af6188be77 | ||
|
|
bd39de2109 | ||
|
|
33375aeb7d | ||
|
|
fafe6c1e91 | ||
|
|
68b072bf44 | ||
|
|
99cdec62a1 | ||
|
|
59797a2f5f | ||
|
|
5cc661e314 | ||
|
|
2ce79d21b5 | ||
|
|
b75fda3596 | ||
|
|
2efa6f4ace | ||
|
|
95e50e436b | ||
|
|
35a53bc8ac | ||
|
|
58c16ef52e | ||
|
|
847ac80d5f | ||
|
|
0408b27ca5 | ||
|
|
2949adbbfd | ||
|
|
2f616ba237 | ||
|
|
b50ead327c | ||
|
|
2fe0f43cb6 | ||
|
|
b85413be9b | ||
|
|
c88776c4a0 | ||
|
|
b7cb977f2b | ||
|
|
3048e36d2f | ||
|
|
64f37fa743 | ||
|
|
737b7d0507 | ||
|
|
973dad4176 | ||
|
|
46110adb8f | ||
|
|
b5c7ed1c34 | ||
|
|
86678ec15a | ||
|
|
893fb63a72 | ||
|
|
3b717cfc58 | ||
|
|
ed40a91a52 | ||
|
|
8b1bec6ed2 | ||
|
|
a6a3476cb2 | ||
|
|
fac3c19d3f | ||
|
|
dc1451c3af | ||
|
|
08b0a93064 | ||
|
|
72dd0b9e81 | ||
|
|
8236b93717 | ||
|
|
c4c5eee2eb | ||
|
|
5cc9c59c76 | ||
|
|
bf4ecc85dd | ||
|
|
da5c2148ad | ||
|
|
bfcea11320 | ||
|
|
ed19e13630 | ||
|
|
6497fb6cd0 | ||
|
|
9c21f03df8 | ||
|
|
7667e73d9d | ||
|
|
ff2ed0421c | ||
|
|
56c5ad360a | ||
|
|
ef644d2837 | ||
|
|
4642eda98d | ||
|
|
8ef163dd38 | ||
|
|
5986add7dd | ||
|
|
c990cf1660 | ||
|
|
76c16c035e | ||
|
|
d5a244ce7f | ||
|
|
432c5133e6 | ||
|
|
a9f9b324a9 | ||
|
|
4735297285 | ||
|
|
3abff4b9da | ||
|
|
c32b98fa7f | ||
|
|
a605c0fcfb | ||
|
|
1e83b4557f | ||
|
|
c3d729fc77 | ||
|
|
00ea891784 | ||
|
|
c98dbccbd7 | ||
|
|
4a68122c31 | ||
|
|
9c4a52de87 | ||
|
|
6c11140f43 | ||
|
|
11398ca64b | ||
|
|
312ce6e639 | ||
|
|
139b48c457 | ||
|
|
f1f478e193 | ||
|
|
1abaf3db3f | ||
|
|
f6bfbc3ed6 | ||
|
|
d2890d72e5 | ||
|
|
5c029c8f64 | ||
|
|
c37a631df2 | ||
|
|
82030b8d06 | ||
|
|
e362f75dac | ||
|
|
3fecf7c0a4 | ||
|
|
801043468d | ||
|
|
1b4f588671 | ||
|
|
7ad48570f0 | ||
|
|
5b8c68303f | ||
|
|
87d3d0ca14 | ||
|
|
b330440371 | ||
|
|
2a173ef804 | ||
|
|
bceabae06f | ||
|
|
69667c135d | ||
|
|
587870ad71 | ||
|
|
609fab345a | ||
|
|
29c47cee78 | ||
|
|
6a9795f561 | ||
|
|
72e7f0aa9f | ||
|
|
acfa384c0c | ||
|
|
c2d27ddd04 | ||
|
|
0a6950e34a | ||
|
|
1c3d77b95d | ||
|
|
fb11208bbe | ||
|
|
94f05127b6 | ||
|
|
25a3046c3c | ||
|
|
f479fc37ba | ||
|
|
3ee19a8f08 | ||
|
|
b8d2ad68dd | ||
|
|
8434c488da | ||
|
|
41f251ad50 | ||
|
|
8e99ff1173 | ||
|
|
a921b983f5 | ||
|
|
81eeda0a68 | ||
|
|
1a54f61355 | ||
|
|
382cd5b73d | ||
|
|
e236485bc7 | ||
|
|
0e69e23606 | ||
|
|
0ef85f2551 | ||
|
|
8d3990124e | ||
|
|
7f912a1781 | ||
|
|
d9ae1dd97a | ||
|
|
307db51aec | ||
|
|
347a704b54 | ||
|
|
9e242fb902 | ||
|
|
192cd60a4f | ||
|
|
a98d8511d6 | ||
|
|
811b79610c | ||
|
|
14129e8f21 | ||
|
|
16de044d3d | ||
|
|
e573a8a9c0 | ||
|
|
108648cabf | ||
|
|
8e4ce07d19 | ||
|
|
09cebd70ae | ||
|
|
6a615c408b | ||
|
|
9ebc054c8c | ||
|
|
b2c520bd91 | ||
|
|
6506afa732 | ||
|
|
d1c74b9389 | ||
|
|
61ca7bc1cb | ||
|
|
f94cb2a4b5 | ||
|
|
4c56fcd6a8 | ||
|
|
d5bb2ec165 | ||
|
|
27bc68f264 | ||
|
|
516eed76b7 | ||
|
|
4190c17cdc | ||
|
|
3d0fedfb61 | ||
|
|
9d57deffb4 | ||
|
|
cc1951c721 | ||
|
|
1cd8d4913d | ||
|
|
19890e29e9 | ||
|
|
f759dba7da | ||
|
|
fb6f92a70b | ||
|
|
f22cad42d7 | ||
|
|
cfb6c674ea | ||
|
|
415c68a570 | ||
|
|
15b949bb9c | ||
|
|
316a9809e4 | ||
|
|
3f5aa03056 | ||
|
|
6bc33795a3 | ||
|
|
3191ae27a5 | ||
|
|
b25de52b9e | ||
|
|
a08b4648d5 | ||
|
|
e7a7c945de | ||
|
|
571f358280 | ||
|
|
7ddc3adfaa | ||
|
|
957c326148 | ||
|
|
0b246d03a6 | ||
|
|
5dd3ff32b6 | ||
|
|
a48d262637 | ||
|
|
b5d17b5161 | ||
|
|
980733ba22 | ||
|
|
7043fdc35d | ||
|
|
81713a6bc4 | ||
|
|
959bcfaa30 | ||
|
|
fa465f792d | ||
|
|
74bf78b9cd | ||
|
|
8c408e59f6 | ||
|
|
25d27e42ed | ||
|
|
0f2b0cc5f2 | ||
|
|
00ea34f45d | ||
|
|
ec936c1821 | ||
|
|
2ff84857e7 | ||
|
|
84ed34f3a7 | ||
|
|
f0508e1bc2 | ||
|
|
8ea7bf36d7 | ||
|
|
8819461eed | ||
|
|
7c30668c87 | ||
|
|
a3424950ca | ||
|
|
ebc42eb05e | ||
|
|
fc6ff44471 | ||
|
|
be03377449 | ||
|
|
e52934093a | ||
|
|
ebfd98488e | ||
|
|
6a68af77dc | ||
|
|
e5c0fb7cb2 | ||
|
|
d9611cce80 | ||
|
|
4b48aba2ae | ||
|
|
47fbb6cd0e | ||
|
|
dae71d427c | ||
|
|
46bc2ceb78 | ||
|
|
6f98200179 | ||
|
|
a57b4c56a5 | ||
|
|
6c3062ba2d | ||
|
|
064c8a9dfe | ||
|
|
7dd47736fb | ||
|
|
84cc65489f | ||
|
|
2b9cb54d91 | ||
|
|
54c7fef217 | ||
|
|
ba74cb8c01 | ||
|
|
675932aa69 | ||
|
|
d996e0a7dd | ||
|
|
b6279f702a | ||
|
|
695a89b91a | ||
|
|
09753b6bbd | ||
|
|
6502b97388 | ||
|
|
95d84647b7 | ||
|
|
8f5065332e | ||
|
|
71c8cbb8da | ||
|
|
3217731486 | ||
|
|
a4e63fe0e8 | ||
|
|
cdb8f6845c | ||
|
|
0a7d286c47 | ||
|
|
e17fd0071d | ||
|
|
a9ba30b9b9 | ||
|
|
4267a3b8e0 | ||
|
|
50022ae635 | ||
|
|
0991621152 | ||
|
|
192f8924c8 | ||
|
|
51a12d7266 | ||
|
|
1417fcda6e | ||
|
|
6114e6f033 | ||
|
|
bc2dbc59ce | ||
|
|
7c5075413e | ||
|
|
52175b0b69 | ||
|
|
f0a3515279 | ||
|
|
3c2ccf7d40 | ||
|
|
abd01e1a27 | ||
|
|
0f63976a00 | ||
|
|
6817eb6e56 | ||
|
|
a951d42623 | ||
|
|
8f5c2bf3ba | ||
|
|
7744217d17 | ||
|
|
a84c153bdd | ||
|
|
31a0a90ba4 | ||
|
|
383ce777e3 | ||
|
|
e7ac88ffb1 | ||
|
|
9657c77999 | ||
|
|
afd508f28b | ||
|
|
634612c1a2 | ||
|
|
76545397d4 | ||
|
|
d2f586a7f1 | ||
|
|
7cfebcb6ed | ||
|
|
9a3766e7b7 | ||
|
|
588a06bece | ||
|
|
e45a126862 | ||
|
|
a581945c9e | ||
|
|
331fd0d619 | ||
|
|
c6e229d571 | ||
|
|
b2c3b1ff95 | ||
|
|
3fc12fcc09 | ||
|
|
e098631553 | ||
|
|
0fcd55755e | ||
|
|
65e7c0c4f4 | ||
|
|
70aecd1e38 | ||
|
|
a40c773491 | ||
|
|
b4abb58a41 | ||
|
|
e368436bc6 | ||
|
|
6c96b3a7f5 | ||
|
|
149f0c082d | ||
|
|
994199b5a2 | ||
|
|
8db3d80151 | ||
|
|
93af1e7c44 | ||
|
|
54e90bd986 | ||
|
|
ca16551917 | ||
|
|
f4977d2855 | ||
|
|
bd91fb7c6d | ||
|
|
e1805896f4 | ||
|
|
31a639400e | ||
|
|
d6b24561fa | ||
|
|
7ba9646d38 | ||
|
|
58a7cf1e75 | ||
|
|
1a327198f7 | ||
|
|
e4fe91ef92 | ||
|
|
afcf817c4f | ||
|
|
21550815db | ||
|
|
02af3a18ff | ||
|
|
a5a13b45cf | ||
|
|
0fd232ab3a | ||
|
|
8d83143ca6 | ||
|
|
74452cd622 | ||
|
|
cf2e8cec54 | ||
|
|
5231faf254 | ||
|
|
959d4de78a | ||
|
|
f5d7dc6b6a | ||
|
|
b761c35d10 | ||
|
|
7f3f7f6bdd | ||
|
|
c5877b7c5e | ||
|
|
9e4187ef17 | ||
|
|
bf7ce3e5a2 | ||
|
|
2c55314491 | ||
|
|
d28efef672 | ||
|
|
49b631d560 | ||
|
|
896510f852 | ||
|
|
1d8e469a46 | ||
|
|
caee40a5d9 | ||
|
|
7de051e6bb | ||
|
|
18cec3c752 | ||
|
|
3b46655051 | ||
|
|
f72ad572fb | ||
|
|
a57ea2adb6 |
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Bug 反馈
|
||||
about: 描述你所遇到的bug
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### 问题描述
|
||||
请提供一个清晰而简明的问题描述。
|
||||
|
||||
### 复现步骤
|
||||
请提供复现该问题所需的具体步骤。
|
||||
|
||||
### 预期行为
|
||||
请描述你期望的正确行为或结果。
|
||||
|
||||
### 错误日志
|
||||
请提供设置->关于->错误日志中的内容,粘贴在下方代码框中。如果没有,请提供您的app版本号、系统版本、设备型号等相关信息。
|
||||
|
||||
### 相关信息
|
||||
请补充截图、录屏、BV号等其他有助于解决问题的信息。
|
||||
66
.github/ISSUE_TEMPLATE/bug-反馈.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Bug 反馈
|
||||
description: 描述你所遇到的bug
|
||||
labels: [ "bug" ]
|
||||
title: "[Bug] "
|
||||
body:
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
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
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本号
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 请提供一个清晰而简明的问题描述。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 请提供复现该问题所需的具体步骤。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 预期行为
|
||||
description: 请描述你期望的正确行为或结果。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: 错误日志
|
||||
description: 请提供设置->关于->错误日志中的内容,粘贴在下方代码框中。如果没有,请提供您的app版本号、系统版本、设备型号等相关信息。
|
||||
|
||||
- type: textarea
|
||||
id: info
|
||||
attributes:
|
||||
label: 相关信息
|
||||
description: 请补充截图、录屏、BV号等其他有助于解决问题的信息。
|
||||
20
.github/ISSUE_TEMPLATE/功能请求.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 对于功能的一些建议
|
||||
title: "[FR] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### 功能描述
|
||||
请提供对所请求功能的清晰描述。
|
||||
|
||||
### 目标
|
||||
请描述你希望通过这个功能实现的目标。
|
||||
|
||||
### 解决方案
|
||||
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
|
||||
|
||||
### 其他
|
||||
请提供已实现该功能或类似功能的应用
|
||||
51
.github/ISSUE_TEMPLATE/功能请求.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: 功能请求
|
||||
description: 对于功能的一些建议
|
||||
labels: [ "enhancement" ]
|
||||
title: "[FR] "
|
||||
body:
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的功能请求。1
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。2
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。3
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。4
|
||||
required: true
|
||||
- label: 之前没有人提交过类似或相同的功能请求。5
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: 功能描述
|
||||
description: 请提供对所请求功能的清晰描述。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: propose
|
||||
attributes:
|
||||
label: 目标
|
||||
description: 请描述你希望通过这个功能实现的目标。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 解决方案
|
||||
description: 如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
|
||||
|
||||
- type: textarea
|
||||
id: addition
|
||||
attributes:
|
||||
label: 其他
|
||||
description: 请提供已实现该功能或类似功能的应用
|
||||
32
README.md
@@ -47,6 +47,30 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 分享视频至消息
|
||||
- [x] 创建/修改/删除关注分组
|
||||
- [x] 移除粉丝
|
||||
- [x] 直播弹幕发送表情
|
||||
- [x] 收藏夹排序
|
||||
- [x] 稍后再看`未看`/`未看完`/`已看完`分类
|
||||
- [x] WebDAV 备份/恢复设置
|
||||
- [x] 保存评论/动态
|
||||
- [x] 高级弹幕 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 取消/置顶评论
|
||||
- [x] 记笔记
|
||||
- [x] 多账号支持 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 屏蔽带货动态/评论
|
||||
- [x] 互动视频
|
||||
- [x] 发评/动态反诈
|
||||
- [x] 高能进度条
|
||||
- [x] 滑动跳转预览视频缩略图
|
||||
- [x] Live Photo
|
||||
- [x] 复制/移动/排序收藏夹/稍后再看视频
|
||||
- [x] 超分辨率
|
||||
- [x] 合并弹幕
|
||||
- [x] 会员彩色弹幕
|
||||
- [x] 播放全部/继续播放/倒序播放
|
||||
- [x] Cookie登录
|
||||
- [x] 显示视频分段信息
|
||||
- [x] 调节字幕大小
|
||||
- [x] 调节全屏弹幕大小
|
||||
@@ -59,7 +83,6 @@
|
||||
- [x] 评论楼中楼定位点击查看的评论
|
||||
- [x] 评论楼中楼按热度/时间排序
|
||||
- [x] 评论点踩
|
||||
- [x] 显示ops专栏
|
||||
- [x] 私信发图
|
||||
- [x] 投币动画
|
||||
- [x] 取消/追番,更新追番状态
|
||||
@@ -73,9 +96,9 @@
|
||||
- [x] 筛选搜索
|
||||
- [x] 转发动态
|
||||
- [x] 合集图片
|
||||
- [x] 删除/置顶私信
|
||||
- [x] 举报用户/评论/视频
|
||||
- [x] 删除/发布文本/图片动态
|
||||
- [x] 删除/置顶/撤回私信
|
||||
- [x] 举报用户/评论/视频/动态
|
||||
- [x] 删除/发布/置顶文本/图片动态
|
||||
- [x] 其他
|
||||
|
||||
## opt
|
||||
@@ -144,7 +167,6 @@
|
||||
- [x] 音质选择(视视频而定)
|
||||
- [x] 解码格式选择(视视频而定)
|
||||
- [x] 弹幕
|
||||
- [ ] 直播弹幕
|
||||
- [x] 字幕
|
||||
- [x] 记忆播放
|
||||
- [x] 视频比例:高度/宽度适应、填充、包含等
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.example.piliplus">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission
|
||||
android:name="android.permission.INTERNET"
|
||||
/>
|
||||
|
||||
<application
|
||||
android:label="PiliPlus Debug"
|
||||
tools:replace="android:label">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:resizeableActivity="true"
|
||||
>
|
||||
<!-- 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
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPlus Debug">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="*.bilibili.com"/>
|
||||
<data android:host="*.bilibili.cn"/>
|
||||
<data android:host="*.bilibili.tv"/>
|
||||
<data android:host="bilibili.com"/>
|
||||
<data android:host="bilibili.cn"/>
|
||||
<data android:host="bilibili.tv"/>
|
||||
<data android:host="b23.tv" />
|
||||
<!--<data android:host="live.bilibili.com"/>-->
|
||||
<!--<data android:host="www.bilibili.com"/>-->
|
||||
<!--<data android:host="www.bilibili.tv"/>-->
|
||||
<!--<data android:host="www.bilibili.cn"/>-->
|
||||
<!--<data android:host="m.bilibili.cn"/>-->
|
||||
<!--<data android:host="m.bilibili.com"/>-->
|
||||
<!--<data android:host="bilibili.cn"/>-->
|
||||
<!--<data android:host="bilibili.com"/>-->
|
||||
<!--<data android:host="bangumi.bilibili.com"/>-->
|
||||
<!--<data android:host="space.bilibili.com"/>-->
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPlus Debug">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bilibili"/>
|
||||
<data android:host="forward" />
|
||||
<data android:host="comment"
|
||||
android:pathPattern="/detail/.*/.*/.*" />
|
||||
<data android:host="uper" />
|
||||
<data android:host="article"
|
||||
android:pathPattern="/readlist" />
|
||||
<data android:host="advertise" android:path="/home" />
|
||||
<data android:host="clip" />
|
||||
<data android:host="search" />
|
||||
<data android:host="stardust-search" />
|
||||
<data android:host="music" />
|
||||
<data android:host="bangumi"
|
||||
android:pathPattern="/season.*" />
|
||||
<data android:host="bangumi" android:pathPattern="/.*" />
|
||||
<data android:host="pictureshow"
|
||||
android:pathPrefix="/creative_center" />
|
||||
<data android:host="cliparea" />
|
||||
<data android:host="im" />
|
||||
<data android:host="im" android:path="/notifications" />
|
||||
<data android:host="following" />
|
||||
<data android:host="following"
|
||||
android:pathPattern="/detail/.*" />
|
||||
<data android:host="following"
|
||||
android:path="/publishInfo/" />
|
||||
<data android:host="laser" android:pathPattern="/.*" />
|
||||
<data android:host="livearea" />
|
||||
<data android:host="live" />
|
||||
<data android:host="catalog" />
|
||||
<data android:host="browser" />
|
||||
<data android:host="user_center" />
|
||||
<data android:host="login" />
|
||||
<data android:host="space" />
|
||||
<data android:host="author" />
|
||||
<data android:host="tag" />
|
||||
<data android:host="rank" />
|
||||
<data android:host="external" />
|
||||
<data android:host="blank" />
|
||||
<data android:host="home" />
|
||||
<data android:host="root" />
|
||||
<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" />
|
||||
<data android:host="m.bilibili.com"
|
||||
android:path="/topic-detail" />
|
||||
<data android:host="article" />
|
||||
<data android:host="pegasus"
|
||||
android:pathPattern="/channel/v2/.*" />
|
||||
<data android:host="feed" android:pathPattern="/channel" />
|
||||
<data android:host="vip" />
|
||||
<data android:host="user_center" android:path="/vip" />
|
||||
<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>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="512dp"
|
||||
android:width="512dp"
|
||||
android:viewportWidth="512.0"
|
||||
android:viewportHeight="512.0">
|
||||
<path
|
||||
android:fillColor="#FF5CB67B"
|
||||
android:pathData="M456.65,256C456.65,366.81 366.81,456.65 256,456.65 145.19,456.65 55.35,366.81 55.35,256 55.35,145.18 145.19,55.35 256,55.35 366.81,55.35 456.65,145.18 456.65,256Z"
|
||||
android:strokeWidth="0.783784"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M270.04,256L156.1,256l15.61,-76.8h98.32c21.21,0 38.4,17.19 38.4,38.4 0,21.21 -17.19,38.4 -38.4,38.4zM270.04,128L202.46,128l-50.1,256h52.76l15.18,-76.8h49.73c49.49,0 89.6,-40.12 89.6,-89.6 0,-49.49 -40.11,-89.6 -89.6,-89.6z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FF5CB67B</color>
|
||||
</resources>
|
||||
@@ -4,10 +4,13 @@ import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import com.ryanheise.audioservice.AudioServiceActivity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@@ -21,6 +24,55 @@ class MainActivity : AudioServiceActivity() {
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
if (call.method == "back") {
|
||||
back()
|
||||
} else if (call.method == "biliSendCommAntifraud") {
|
||||
try {
|
||||
val action = call.argument<Int>("action") ?: 0
|
||||
val oid = call.argument<Number>("oid") ?: 0L
|
||||
val type = call.argument<Int>("type") ?: 0
|
||||
val rpid = call.argument<Number>("rpid") ?: 0L
|
||||
val root = call.argument<Number>("root") ?: 0L
|
||||
val parent = call.argument<Number>("parent") ?: 0L
|
||||
val ctime = call.argument<Number>("ctime") ?: 0L
|
||||
val commentText = call.argument<String>("comment_text") ?: ""
|
||||
val pictures = call.argument<String?>("pictures")
|
||||
val sourceId = call.argument<String>("source_id") ?: ""
|
||||
val uid = call.argument<Number>("uid") ?: 0L
|
||||
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
|
||||
|
||||
val intent = Intent().apply {
|
||||
component = ComponentName("icu.freedomIntrovert.biliSendCommAntifraud", "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity")
|
||||
putExtra("action", action)
|
||||
putExtra("oid", oid.toLong())
|
||||
putExtra("type", type)
|
||||
putExtra("rpid", rpid.toLong())
|
||||
putExtra("root", root.toLong())
|
||||
putExtra("parent", parent.toLong())
|
||||
putExtra("ctime", ctime.toLong())
|
||||
putExtra("comment_text", commentText)
|
||||
if(pictures != null)
|
||||
putExtra("pictures", pictures)
|
||||
putExtra("source_id", sourceId)
|
||||
putExtra("uid", uid.toLong())
|
||||
putStringArrayListExtra("cookies", ArrayList(cookies))
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {}
|
||||
} else if (call.method == "linkVerifySettings") {
|
||||
try {
|
||||
val intent = Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:" + context.packageName))
|
||||
context.startActivity(intent)
|
||||
} catch (t: Throwable) {
|
||||
try {
|
||||
val intent = Intent("android.intent.action.MAIN", Uri.parse("package:" + context.packageName))
|
||||
intent.setClassName("com.android.settings", "com.android.settings.applications.InstalledAppOpenByDefaultActivity")
|
||||
context.startActivity(intent)
|
||||
} catch (t2: Throwable) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.parse("package:" + context.packageName))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -1,16 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="512dp"
|
||||
android:width="512dp"
|
||||
android:viewportWidth="512.0"
|
||||
android:viewportHeight="512.0">
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportWidth="108.0"
|
||||
android:viewportHeight="108.0">
|
||||
<path
|
||||
android:fillColor="#FF5CB67B"
|
||||
android:pathData="M456.65,256C456.65,366.81 366.81,456.65 256,456.65 145.19,456.65 55.35,366.81 55.35,256 55.35,145.18 145.19,55.35 256,55.35 366.81,55.35 456.65,145.18 456.65,256Z"
|
||||
android:strokeWidth="0.783784"
|
||||
android:fillColor="@color/ic_launcher_foreground"
|
||||
android:pathData="M56,54L39.78,54l2.22,-10.94h14c3.02,0 5.47,2.45 5.47,5.47 0,3.02 -2.45,5.47 -5.47,5.47zM56,35.77h-9.62l-7.13,36.45h7.51L48.92,61.29h7.08c7.05,0 12.76,-5.71 12.76,-12.76 0,-7.05 -5.71,-12.76 -12.76,-12.76z"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M270.04,256L156.1,256l15.61,-76.8h98.32c21.21,0 38.4,17.19 38.4,38.4 0,21.21 -17.19,38.4 -38.4,38.4zM270.04,128L202.46,128l-50.1,256h52.76l15.18,-76.8h49.73c49.49,0 89.6,-40.12 89.6,-89.6 0,-49.49 -40.11,-89.6 -89.6,-89.6z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
</vector>
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="512dp"
|
||||
android:width="512dp"
|
||||
android:viewportWidth="512.0"
|
||||
android:viewportHeight="512.0">
|
||||
<path
|
||||
android:fillColor="#FF5CB67B"
|
||||
android:pathData="M456.65,256C456.65,366.81 366.81,456.65 256,456.65 145.19,456.65 55.35,366.81 55.35,256 55.35,145.18 145.19,55.35 256,55.35 366.81,55.35 456.65,145.18 456.65,256Z"
|
||||
android:strokeWidth="0.783784"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M270.04,256L156.1,256l15.61,-76.8h98.32c21.21,0 38.4,17.19 38.4,38.4 0,21.21 -17.19,38.4 -38.4,38.4zM270.04,128L202.46,128l-50.1,256h52.76l15.18,-76.8h49.73c49.49,0 89.6,-40.12 89.6,-89.6 0,-49.49 -40.11,-89.6 -89.6,-89.6z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
@@ -2,15 +2,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="512.0"
|
||||
android:viewportHeight="512.0">
|
||||
android:viewportWidth="108.0"
|
||||
android:viewportHeight="108.0">
|
||||
<path
|
||||
android:fillColor="#FF5CB67B"
|
||||
android:pathData="M456.65,256C456.65,366.81 366.81,456.65 256,456.65 145.19,456.65 55.35,366.81 55.35,256 55.35,145.18 145.19,55.35 256,55.35 366.81,55.35 456.65,145.18 456.65,256Z"
|
||||
android:strokeWidth="0.783784"
|
||||
android:fillType="evenOdd" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M270.04,256L156.1,256l15.61,-76.8h98.32c21.21,0 38.4,17.19 38.4,38.4 0,21.21 -17.19,38.4 -38.4,38.4zM270.04,128L202.46,128l-50.1,256h52.76l15.18,-76.8h49.73c49.49,0 89.6,-40.12 89.6,-89.6 0,-49.49 -40.11,-89.6 -89.6,-89.6z"
|
||||
android:pathData="M57.54,54L28.82,54l3.93,-19.36h24.78c5.35,0 9.68,4.33 9.68,9.68 0,5.35 -4.33,9.68 -9.68,9.68zM57.54,21.73L40.5,21.73L27.88,86.27h13.3l3.83,-19.36h12.54c12.48,0 22.59,-10.11 22.59,-22.59 0,-12.48 -10.11,-22.59 -22.59,-22.59z"
|
||||
android:strokeWidth="0.252073"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_monochrome"
|
||||
android:inset="16%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 914 B |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 3.4 KiB |
3
android/app/src/main/res/raw/keep.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/*" />
|
||||
5
android/app/src/main/res/values-night-v31/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_foreground">@android:color/system_accent1_100</color>
|
||||
<color name="ic_launcher_background">@android:color/system_neutral1_800</color>
|
||||
</resources>
|
||||
5
android/app/src/main/res/values-v31/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_foreground">@android:color/system_neutral2_700</color>
|
||||
<color name="ic_launcher_background">@android:color/system_accent1_100</color>
|
||||
</resources>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FF5CB67B</color>
|
||||
<color name="ic_launcher_foreground">#FF5CB67B</color>
|
||||
<color name="ic_launcher_background">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -18,19 +18,33 @@ subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.extensions.findByName("android") != null) {
|
||||
Integer pluginCompileSdk = project.android.compileSdk
|
||||
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)."
|
||||
+ "\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 != 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
assets/fonts/digital_id_num.ttf
Normal file
BIN
assets/images/live/live.gif
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/images/logo/logo_2.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
assets/images/logo/logo_3.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 915 B |
|
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 876 B |
|
Before Width: | Height: | Size: 518 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 991 B |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 912 B |
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 1.1 KiB |
BIN
assets/images/lv/lv6_s.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/paycoins/ic_panel_close.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
class StyleString {
|
||||
static const double cardSpace = 8;
|
||||
static const double safeSpace = 12;
|
||||
static BorderRadius mdRadius = BorderRadius.circular(10);
|
||||
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
||||
static const Radius imgRadius = Radius.circular(10);
|
||||
static const double aspectRatio = 16 / 10;
|
||||
}
|
||||
@@ -23,246 +23,251 @@ class Constants {
|
||||
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';
|
||||
static const String statistics =
|
||||
'%7B%22appId%22%3A5%2C%22platform%22%3A3%2C%22version%22%3A%221.46.2%22%2C%22abtest%22%3A%22%22%7D';
|
||||
//Uri.encodeComponent('{"appId": 5,"platform": 3,"version": "1.46.2","abtest": ""}');
|
||||
'{"appId":5,"platform":3,"version":"1.46.2","abtest":""}';
|
||||
// 请求时会自动encodeComponent
|
||||
|
||||
// 超分辨率滤镜
|
||||
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 const urlPattern =
|
||||
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]';
|
||||
|
||||
static get 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> 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 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'
|
||||
];
|
||||
|
||||
//内容来自 https://passport.bilibili.com/web/generic/country/list
|
||||
static const List<Map<String, dynamic>> 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"}
|
||||
];
|
||||
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"}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final color = theme.colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||
@@ -13,7 +15,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 8,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.05),
|
||||
color: theme.dividerColor.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -25,7 +27,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
@@ -34,13 +36,13 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 50,
|
||||
height: 11,
|
||||
),
|
||||
@@ -55,31 +57,31 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: double.infinity,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: double.infinity,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 300,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 250,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 7),
|
||||
@@ -87,6 +89,7 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
@@ -99,10 +102,8 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.2),
|
||||
foregroundColor:
|
||||
theme.colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
label: Text(
|
||||
i == 0
|
||||
|
||||
67
lib/common/skeleton/fav_pgc_item.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class FavPgcItemSkeleton extends StatelessWidget {
|
||||
const FavPgcItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
width: boxConstraints.maxWidth,
|
||||
height: boxConstraints.maxHeight,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 175,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: 55,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Container(
|
||||
width: 35,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -35,25 +35,25 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 200,
|
||||
height: 20,
|
||||
margin: const EdgeInsets.only(bottom: 15),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 150,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 150,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
@@ -64,7 +64,7 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: bgColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
53
lib/common/skeleton/msg_feed_sys_msg_.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class MsgFeedSysMsgSkeleton extends StatelessWidget {
|
||||
const MsgFeedSysMsgSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 125,
|
||||
height: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 12,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 10,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/common/skeleton/msg_feed_top.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class MsgFeedTopSkeleton extends StatelessWidget {
|
||||
const MsgFeedTopSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
title: UnconstrainedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
subtitle: Container(
|
||||
color: color,
|
||||
width: 125,
|
||||
height: 11,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ class Skeleton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
|
||||
var shimmerGradient = LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Theme.of(context).colorScheme.surface.withAlpha(10),
|
||||
Theme.of(context).colorScheme.surface.withAlpha(10),
|
||||
color,
|
||||
color,
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [
|
||||
@@ -99,7 +100,7 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child ?? const SizedBox();
|
||||
return widget.child ?? const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ class _ShimmerLoadingState extends State<ShimmerLoading> {
|
||||
|
||||
final shimmer = Shimmer.of(context)!;
|
||||
if (!shimmer.isSized) {
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final shimmerSize = shimmer.size;
|
||||
final gradient = shimmer.gradient;
|
||||
|
||||
@@ -7,87 +7,74 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
),
|
||||
// VideoContent(videoItem: videoItem)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
color: color,
|
||||
width: 200,
|
||||
height: 11,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Container(
|
||||
color: color,
|
||||
width: 150,
|
||||
height: 13,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
color: color,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 200,
|
||||
height: 11,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
color: color,
|
||||
width: 40,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 150,
|
||||
color: color,
|
||||
width: 40,
|
||||
height: 13,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 100,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
width: 40,
|
||||
height: 13,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -16,7 +17,7 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
builder: (context, boxConstraints) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
);
|
||||
@@ -37,24 +38,24 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
width: 200,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
Container(
|
||||
width: 150,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
Container(
|
||||
width: 110,
|
||||
height: 13,
|
||||
margin: const EdgeInsets.only(bottom: 5),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
Container(
|
||||
width: 75,
|
||||
height: 13,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
41
lib/common/skeleton/whisper_item.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'skeleton.dart';
|
||||
|
||||
class WhisperItemSkeleton extends StatelessWidget {
|
||||
const WhisperItemSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
title: UnconstrainedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 11,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
subtitle: Container(
|
||||
color: color,
|
||||
width: 125,
|
||||
height: 11,
|
||||
),
|
||||
trailing: Container(
|
||||
color: color,
|
||||
width: 50,
|
||||
height: 11,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/no_splash_factory.dart';
|
||||
import 'package:PiliPlus/common/widgets/overlay_pop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedDialog extends StatefulWidget {
|
||||
const AnimatedDialog({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
required this.closeFn,
|
||||
});
|
||||
|
||||
final dynamic videoItem;
|
||||
final Function closeFn;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AnimatedDialogState();
|
||||
}
|
||||
|
||||
class AnimatedDialogState extends State<AnimatedDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController controller;
|
||||
late Animation<double> opacityAnimation;
|
||||
late Animation<double> scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 255));
|
||||
opacityAnimation = Tween<double>(begin: 0.0, end: 0.6)
|
||||
.animate(CurvedAnimation(parent: controller, curve: Curves.linear));
|
||||
scaleAnimation = CurvedAnimation(parent: controller, curve: Curves.linear);
|
||||
controller.addListener(() => setState(() {}));
|
||||
controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.removeListener(() {});
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void closeFn() async {
|
||||
await controller.reverse();
|
||||
widget.closeFn();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.black.withOpacity(opacityAnimation.value),
|
||||
child: InkWell(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
splashFactory: NoSplashFactory(),
|
||||
onTap: closeFn,
|
||||
child: Center(
|
||||
child: FadeTransition(
|
||||
opacity: scaleAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: scaleAnimation,
|
||||
child: OverlayPop(
|
||||
videoItem: widget.videoItem,
|
||||
closeFn: closeFn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
|
||||
|
||||
class _SaltedKey<S, V> extends LocalKey {
|
||||
const _SaltedKey(this.salt, this.value);
|
||||
|
||||
final S salt;
|
||||
final V value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is _SaltedKey<S, V> &&
|
||||
other.salt == salt &&
|
||||
other.value == value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, salt, value);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String saltString = S == String ? "<'$salt'>" : '<$salt>';
|
||||
final String valueString = V == String ? "<'$value'>" : '<$value>';
|
||||
return '[$saltString $valueString]';
|
||||
}
|
||||
}
|
||||
|
||||
class AppExpansionPanelList extends StatefulWidget {
|
||||
/// Creates an expansion panel list widget. The [expansionCallback] is
|
||||
/// triggered when an expansion panel expand/collapse button is pushed.
|
||||
///
|
||||
/// The [children] and [animationDuration] arguments must not be null.
|
||||
const AppExpansionPanelList({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.expansionCallback,
|
||||
this.animationDuration = kThemeAnimationDuration,
|
||||
this.expandedHeaderPadding = EdgeInsets.zero,
|
||||
this.dividerColor,
|
||||
this.elevation = 2,
|
||||
}) : _allowOnlyOnePanelOpen = false,
|
||||
initialOpenPanelValue = null;
|
||||
|
||||
/// The children of the expansion panel list. They are laid out in a similar
|
||||
/// fashion to [ListBody].
|
||||
final List<AppExpansionPanel> children;
|
||||
|
||||
/// The callback that gets called whenever one of the expand/collapse buttons
|
||||
/// is pressed. The arguments passed to the callback are the index of the
|
||||
/// pressed panel and whether the panel is currently expanded or not.
|
||||
///
|
||||
/// If AppExpansionPanelList.radio is used, the callback may be called a
|
||||
/// second time if a different panel was previously open. The arguments
|
||||
/// passed to the second callback are the index of the panel that will close
|
||||
/// and false, marking that it will be closed.
|
||||
///
|
||||
/// For AppExpansionPanelList, the callback needs to setState when it's notified
|
||||
/// about the closing/opening panel. On the other hand, the callback for
|
||||
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of
|
||||
/// changes, as the radio panels' open/close states are managed internally.
|
||||
///
|
||||
/// This callback is useful in order to keep track of the expanded/collapsed
|
||||
/// panels in a parent widget that may need to react to these changes.
|
||||
final ExpansionPanelCallback? expansionCallback;
|
||||
|
||||
/// The duration of the expansion animation.
|
||||
final Duration animationDuration;
|
||||
|
||||
// Whether multiple panels can be open simultaneously
|
||||
final bool _allowOnlyOnePanelOpen;
|
||||
|
||||
/// The value of the panel that initially begins open. (This value is
|
||||
/// only used when initializing with the [AppExpansionPanelList.radio]
|
||||
/// constructor.)
|
||||
final Object? initialOpenPanelValue;
|
||||
|
||||
/// The padding that surrounds the panel header when expanded.
|
||||
///
|
||||
/// By default, 16px of space is added to the header vertically (above and below)
|
||||
/// during expansion.
|
||||
final EdgeInsets expandedHeaderPadding;
|
||||
|
||||
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.
|
||||
///
|
||||
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
|
||||
/// is null, then [ThemeData.dividerColor] is used.
|
||||
final Color? dividerColor;
|
||||
|
||||
/// Defines elevation for the [AppExpansionPanel] while it's expanded.
|
||||
///
|
||||
/// By default, the value of elevation is 2.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();
|
||||
}
|
||||
|
||||
class _AppExpansionPanelListState extends State<AppExpansionPanelList> {
|
||||
ExpansionPanelRadio? _currentOpenPanel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
assert(_allIdentifiersUnique(),
|
||||
'All ExpansionPanelRadio identifier values must be unique.');
|
||||
if (widget.initialOpenPanelValue != null) {
|
||||
_currentOpenPanel = searchPanelByValue(
|
||||
widget.children.cast<ExpansionPanelRadio>(),
|
||||
widget.initialOpenPanelValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AppExpansionPanelList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
assert(_allIdentifiersUnique(),
|
||||
'All ExpansionPanelRadio identifier values must be unique.');
|
||||
// If the previous widget was non-radio AppExpansionPanelList, initialize the
|
||||
// open panel to widget.initialOpenPanelValue
|
||||
if (!oldWidget._allowOnlyOnePanelOpen) {
|
||||
_currentOpenPanel = searchPanelByValue(
|
||||
widget.children.cast<ExpansionPanelRadio>(),
|
||||
widget.initialOpenPanelValue);
|
||||
}
|
||||
} else {
|
||||
_currentOpenPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _allIdentifiersUnique() {
|
||||
final Map<Object, bool> identifierMap = <Object, bool>{};
|
||||
for (final ExpansionPanelRadio child
|
||||
in widget.children.cast<ExpansionPanelRadio>()) {
|
||||
identifierMap[child.value] = true;
|
||||
}
|
||||
return identifierMap.length == widget.children.length;
|
||||
}
|
||||
|
||||
bool _isChildExpanded(int index) {
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
final ExpansionPanelRadio radioWidget =
|
||||
widget.children[index] as ExpansionPanelRadio;
|
||||
return _currentOpenPanel?.value == radioWidget.value;
|
||||
}
|
||||
return widget.children[index].isExpanded;
|
||||
}
|
||||
|
||||
void _handlePressed(bool isExpanded, int index) {
|
||||
widget.expansionCallback?.call(index, isExpanded);
|
||||
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
final ExpansionPanelRadio pressedChild =
|
||||
widget.children[index] as ExpansionPanelRadio;
|
||||
|
||||
// If another ExpansionPanelRadio was already open, apply its
|
||||
// expansionCallback (if any) to false, because it's closing.
|
||||
for (int childIndex = 0;
|
||||
childIndex < widget.children.length;
|
||||
childIndex += 1) {
|
||||
final ExpansionPanelRadio child =
|
||||
widget.children[childIndex] as ExpansionPanelRadio;
|
||||
if (widget.expansionCallback != null &&
|
||||
childIndex != index &&
|
||||
child.value == _currentOpenPanel?.value) {
|
||||
widget.expansionCallback?.call(childIndex, false);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentOpenPanel = isExpanded ? null : pressedChild;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ExpansionPanelRadio? searchPanelByValue(
|
||||
List<ExpansionPanelRadio> panels, Object? value) {
|
||||
for (final ExpansionPanelRadio panel in panels) {
|
||||
if (panel.value == value) return panel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
kElevationToShadow.containsKey(widget.elevation),
|
||||
'Invalid value for elevation. See the kElevationToShadow constant for'
|
||||
' possible elevation values.',
|
||||
);
|
||||
|
||||
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
|
||||
|
||||
for (int index = 0; index < widget.children.length; index += 1) {
|
||||
//todo: Uncomment to add gap between selected panels
|
||||
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
|
||||
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/
|
||||
|
||||
final AppExpansionPanel child = widget.children[index];
|
||||
final Widget headerWidget = child.headerBuilder(
|
||||
context,
|
||||
_isChildExpanded(index),
|
||||
);
|
||||
|
||||
Widget? expandIconContainer = ExpandIcon(
|
||||
isExpanded: _isChildExpanded(index),
|
||||
onPressed: !child.canTapOnHeader
|
||||
? (bool isExpanded) => _handlePressed(isExpanded, index)
|
||||
: null,
|
||||
);
|
||||
if (!child.canTapOnHeader) {
|
||||
final MaterialLocalizations localizations =
|
||||
MaterialLocalizations.of(context);
|
||||
expandIconContainer = Semantics(
|
||||
label: _isChildExpanded(index)
|
||||
? localizations.expandedIconTapHint
|
||||
: localizations.collapsedIconTapHint,
|
||||
container: true,
|
||||
child: expandIconContainer,
|
||||
);
|
||||
}
|
||||
|
||||
final iconContainer = child.iconBuilder;
|
||||
if (iconContainer != null) {
|
||||
expandIconContainer = iconContainer(
|
||||
expandIconContainer,
|
||||
_isChildExpanded(index),
|
||||
);
|
||||
}
|
||||
|
||||
Widget header = Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: widget.animationDuration,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
margin: _isChildExpanded(index)
|
||||
? widget.expandedHeaderPadding
|
||||
: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: _kPanelHeaderCollapsedHeight),
|
||||
child: headerWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (expandIconContainer != null) expandIconContainer,
|
||||
],
|
||||
);
|
||||
if (child.canTapOnHeader) {
|
||||
header = MergeSemantics(
|
||||
child: InkWell(
|
||||
onTap: () => _handlePressed(_isChildExpanded(index), index),
|
||||
child: header,
|
||||
),
|
||||
);
|
||||
}
|
||||
items.add(
|
||||
MaterialSlice(
|
||||
key: _SaltedKey<BuildContext, int>(context, index * 2),
|
||||
color: child.backgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
header,
|
||||
AnimatedCrossFade(
|
||||
firstChild: Container(height: 0.0),
|
||||
secondChild: child.body,
|
||||
firstCurve:
|
||||
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
|
||||
secondCurve:
|
||||
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
|
||||
sizeCurve: Curves.fastOutSlowIn,
|
||||
crossFadeState: _isChildExpanded(index)
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: widget.animationDuration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (_isChildExpanded(index) && index != widget.children.length - 1) {
|
||||
items.add(MaterialGap(
|
||||
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
return MergeableMaterial(
|
||||
hasDividers: true,
|
||||
dividerColor: widget.dividerColor,
|
||||
elevation: widget.elevation,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef ExpansionPanelIconBuilder = Widget? Function(
|
||||
Widget child,
|
||||
bool isExpanded,
|
||||
);
|
||||
|
||||
class AppExpansionPanel {
|
||||
/// Creates an expansion panel to be used as a child for [ExpansionPanelList].
|
||||
/// See [ExpansionPanelList] for an example on how to use this widget.
|
||||
///
|
||||
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
|
||||
AppExpansionPanel({
|
||||
required this.headerBuilder,
|
||||
required this.body,
|
||||
this.iconBuilder,
|
||||
this.isExpanded = false,
|
||||
this.canTapOnHeader = false,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
/// The widget builder that builds the expansion panels' header.
|
||||
final ExpansionPanelHeaderBuilder headerBuilder;
|
||||
|
||||
/// The widget builder that builds the expansion panels' icon.
|
||||
///
|
||||
/// If not pass any function, then default icon will be displayed.
|
||||
///
|
||||
/// If builder function return null, then icon will not displayed.
|
||||
final ExpansionPanelIconBuilder? iconBuilder;
|
||||
|
||||
/// The body of the expansion panel that's displayed below the header.
|
||||
///
|
||||
/// This widget is visible only when the panel is expanded.
|
||||
final Widget body;
|
||||
|
||||
/// Whether the panel is expanded.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool isExpanded;
|
||||
|
||||
/// Whether tapping on the panel's header will expand/collapse it.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool canTapOnHeader;
|
||||
|
||||
/// Defines the background color of the panel.
|
||||
///
|
||||
/// Defaults to [ThemeData.cardColor].
|
||||
final Color? backgroundColor;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
|
||||
const AppBarWidget({
|
||||
required this.child,
|
||||
required this.controller,
|
||||
required this.visible,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PreferredSizeWidget child;
|
||||
final AnimationController controller;
|
||||
final bool visible;
|
||||
|
||||
@override
|
||||
Size get preferredSize => child.preferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
visible ? controller.reverse() : controller.forward();
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0, -1),
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOutBack,
|
||||
)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
|
||||
Widget articleContent({
|
||||
required BuildContext context,
|
||||
required List<ArticleContentModel> list,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
List<String>? imgList = list
|
||||
.where((item) => item.pic != null)
|
||||
.toList()
|
||||
.map((item) => item.pic?.pics?.first.url ?? '')
|
||||
.toList();
|
||||
return SliverList.separated(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
ArticleContentModel item = list[index];
|
||||
if (item.text != null) {
|
||||
List<InlineSpan> spanList = [];
|
||||
item.text?.nodes?.forEach((item) {
|
||||
spanList.add(TextSpan(
|
||||
text: item.word?.words,
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.3,
|
||||
fontSize: 17,
|
||||
height: LineHeight.percent(125).size,
|
||||
fontStyle:
|
||||
item.word?.style?.italic == true ? FontStyle.italic : null,
|
||||
color: item.word?.color != null
|
||||
? Color(int.parse(
|
||||
item.word!.color!.replaceFirst('#', 'FF'),
|
||||
radix: 16,
|
||||
))
|
||||
: null,
|
||||
decoration: item.word?.style?.strikethrough == true
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
fontWeight:
|
||||
item.word?.style?.bold == true ? FontWeight.bold : null,
|
||||
),
|
||||
));
|
||||
});
|
||||
return SelectableText.rich(TextSpan(children: spanList));
|
||||
} else if (item.line != null) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: item.line?.pic?.url?.http2https ?? '',
|
||||
height: item.line?.pic?.height?.toDouble(),
|
||||
),
|
||||
);
|
||||
} else if (item.pic != null) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => Hero(
|
||||
tag: item.pic!.pics!.first.url!,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback(
|
||||
imgList,
|
||||
imgList.indexOf(item.pic!.pics!.first.url!),
|
||||
);
|
||||
} else {
|
||||
context.imageView(
|
||||
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
|
||||
imgList: imgList,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth *
|
||||
item.pic!.pics!.first.height! /
|
||||
item.pic!.pics!.first.width!,
|
||||
src: item.pic!.pics!.first.url,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
// return Text('unsupported content');
|
||||
}
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 10),
|
||||
);
|
||||
}
|
||||
@@ -484,6 +484,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
}
|
||||
|
||||
void _onDragStart(DragStartDetails details) {
|
||||
if (onDragStart == null) {
|
||||
return;
|
||||
}
|
||||
_userIsDraggingThumb = true;
|
||||
_updateThumbPosition(details.localPosition);
|
||||
onDragStart?.call(ThumbDragDetails(
|
||||
@@ -494,6 +497,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
if (onDragUpdate == null) {
|
||||
return;
|
||||
}
|
||||
_updateThumbPosition(details.localPosition);
|
||||
onDragUpdate?.call(ThumbDragDetails(
|
||||
timeStamp: _currentThumbDuration(),
|
||||
@@ -503,6 +509,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
if (onSeek == null) {
|
||||
return;
|
||||
}
|
||||
onDragEnd?.call();
|
||||
onSeek?.call(_currentThumbDuration());
|
||||
_finishDrag();
|
||||
|
||||
169
lib/common/widgets/avatar.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
class Avatar extends StatelessWidget {
|
||||
final _BadgeType _badgeType;
|
||||
final String avatar;
|
||||
final double size;
|
||||
final double badgeSize;
|
||||
final String? garbPendantImage;
|
||||
final dynamic roomId;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const Avatar({
|
||||
super.key,
|
||||
required this.avatar,
|
||||
this.size = 80,
|
||||
double? badgeSize,
|
||||
bool? isVip,
|
||||
int? officialType,
|
||||
this.garbPendantImage,
|
||||
this.roomId,
|
||||
this.onTap,
|
||||
}) : _badgeType = officialType == null || officialType < 0
|
||||
? isVip == true
|
||||
? _BadgeType.vip
|
||||
: _BadgeType.none
|
||||
: officialType == 0
|
||||
? _BadgeType.person
|
||||
: officialType == 1
|
||||
? _BadgeType.institution
|
||||
: _BadgeType.none,
|
||||
badgeSize = badgeSize ?? size / 3;
|
||||
|
||||
static bool showDynDecorate = GStorage.showDynDecorate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
onTap == null
|
||||
? _buildAvatar(colorScheme)
|
||||
: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: _buildAvatar(colorScheme),
|
||||
),
|
||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty)
|
||||
Positioned(
|
||||
top: -0.375 *
|
||||
(size == 80 ? size - 4 : size), // -(size * 1.75 - size) / 2
|
||||
child: IgnorePointer(
|
||||
child: CachedNetworkImage(
|
||||
width: size * 1.75,
|
||||
height: size * 1.75,
|
||||
imageUrl: Utils.thumbnailImgUrl(garbPendantImage),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (roomId != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.toNamed('/liveRoom?roomid=$roomId');
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.equalizer_rounded,
|
||||
size: MediaQuery.textScalerOf(context).scale(16),
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
Text(
|
||||
'直播中',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_badgeType != _BadgeType.none)
|
||||
_buildBadge(colorScheme),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(ColorScheme colorScheme) => size == 80
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: NetworkImgLayer(
|
||||
src: avatar,
|
||||
width: size,
|
||||
height: size,
|
||||
type: 'avatar',
|
||||
),
|
||||
)
|
||||
: NetworkImgLayer(
|
||||
src: avatar,
|
||||
width: size,
|
||||
height: size,
|
||||
type: 'avatar',
|
||||
);
|
||||
|
||||
Widget _buildBadge(ColorScheme colorScheme) {
|
||||
final child = switch (_badgeType) {
|
||||
_BadgeType.vip => Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
height: badgeSize,
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
_ => Icon(
|
||||
Icons.offline_bolt,
|
||||
color: _badgeType.color,
|
||||
size: badgeSize,
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
};
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: child),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PBadge extends StatelessWidget {
|
||||
@@ -12,10 +13,12 @@ class PBadge extends StatelessWidget {
|
||||
final double? fs;
|
||||
final String? semanticsLabel;
|
||||
final bool bold;
|
||||
final double? textScaleFactor;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const PBadge({
|
||||
super.key,
|
||||
this.text,
|
||||
required this.text,
|
||||
this.top,
|
||||
this.right,
|
||||
this.bottom,
|
||||
@@ -26,32 +29,39 @@ class PBadge extends StatelessWidget {
|
||||
this.fs = 11,
|
||||
this.semanticsLabel,
|
||||
this.bold = true,
|
||||
this.textScaleFactor,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ColorScheme t = Theme.of(context).colorScheme;
|
||||
if (text.isNullOrEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
ColorScheme theme = Theme.of(context).colorScheme;
|
||||
// 背景色
|
||||
Color bgColor = t.primary;
|
||||
Color bgColor = theme.primary;
|
||||
// 前景色
|
||||
Color color = t.onPrimary;
|
||||
Color color = theme.onPrimary;
|
||||
// 边框色
|
||||
Color borderColor = Colors.transparent;
|
||||
if (type == 'gray') {
|
||||
bgColor = Colors.black54.withOpacity(0.4);
|
||||
bgColor = Colors.black45;
|
||||
color = Colors.white;
|
||||
}
|
||||
if (type == 'color') {
|
||||
bgColor = t.secondaryContainer.withOpacity(0.5);
|
||||
color = t.onSecondaryContainer;
|
||||
}
|
||||
if (type == 'line') {
|
||||
} else if (type == 'color') {
|
||||
bgColor = theme.secondaryContainer.withOpacity(0.5);
|
||||
color = theme.onSecondaryContainer;
|
||||
} else if (type == 'line') {
|
||||
bgColor = Colors.transparent;
|
||||
color = t.primary;
|
||||
borderColor = t.primary;
|
||||
color = theme.primary;
|
||||
borderColor = theme.primary;
|
||||
} else if (type == 'error') {
|
||||
bgColor = theme.error;
|
||||
color = theme.onError;
|
||||
}
|
||||
|
||||
EdgeInsets paddingStyle =
|
||||
late EdgeInsets paddingStyle =
|
||||
const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
|
||||
double fontSize = 11;
|
||||
BorderRadius br = BorderRadius.circular(4);
|
||||
@@ -63,14 +73,17 @@ class PBadge extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget content = Container(
|
||||
padding: paddingStyle,
|
||||
padding: padding ?? paddingStyle,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: br,
|
||||
color: bgColor,
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
child: Text(
|
||||
text ?? "",
|
||||
text!,
|
||||
textScaler: textScaleFactor != null
|
||||
? TextScaler.linear(textScaleFactor!)
|
||||
: null,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: fs ?? fontSize,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ContentContainer extends StatelessWidget {
|
||||
final Widget? contentWidget;
|
||||
final Widget? bottomWidget;
|
||||
final bool isScrollable;
|
||||
final Clip? childClipBehavior;
|
||||
|
||||
const ContentContainer({
|
||||
super.key,
|
||||
this.contentWidget,
|
||||
this.bottomWidget,
|
||||
this.isScrollable = true,
|
||||
this.childClipBehavior,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return SingleChildScrollView(
|
||||
clipBehavior: childClipBehavior ?? Clip.hardEdge,
|
||||
physics: isScrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: constraints.copyWith(
|
||||
minHeight: constraints.maxHeight,
|
||||
maxHeight: double.infinity,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (contentWidget != null)
|
||||
Expanded(
|
||||
child: contentWidget!,
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
if (bottomWidget != null) bottomWidget!,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,16 @@ class CustomSliverPersistentHeaderDelegate
|
||||
//创建child子组件
|
||||
//shrinkOffset:child偏移值minExtent~maxExtent
|
||||
//overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false
|
||||
return ColoredBox(
|
||||
color: bgColor,
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: bgColor,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ class CustomToast extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final double toastOpacity = GStorage.setting
|
||||
.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0) as double;
|
||||
return Container(
|
||||
@@ -15,19 +16,49 @@ class CustomToast extends StatelessWidget {
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 30),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(toastOpacity),
|
||||
color: theme.colorScheme.primaryContainer.withOpacity(toastOpacity),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingWidget extends StatelessWidget {
|
||||
const LoadingWidget({super.key, required this.msg});
|
||||
|
||||
///loading msg
|
||||
final String msg;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dialogBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
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)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
106
lib/common/widgets/dialog.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
void showConfirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
dynamic content,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showPgcFollowDialog({
|
||||
required BuildContext context,
|
||||
required String type,
|
||||
required int followStatus,
|
||||
required ValueChanged<int> onUpdateStatus,
|
||||
}) {
|
||||
Widget statusItem({
|
||||
required bool enabled,
|
||||
required String text,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
enabled: enabled,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
'标记为 $text',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
trailing: !enabled ? const Icon(size: 22, Icons.check) : null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
101
lib/common/widgets/disabled_icon.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
|
||||
final Color? color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
const DisabledIcon({
|
||||
super.key,
|
||||
required T child,
|
||||
this.color,
|
||||
double? lineLengthScale,
|
||||
StrokeCap? strokeCap,
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderMaskedIcon(
|
||||
color ??
|
||||
(child is Icon
|
||||
? (child as Icon).color ?? IconTheme.of(context).color!
|
||||
: IconTheme.of(context).color!),
|
||||
lineLengthScale,
|
||||
strokeCap,
|
||||
);
|
||||
}
|
||||
|
||||
T enable() => child as T;
|
||||
}
|
||||
|
||||
class RenderMaskedIcon extends RenderProxyBox {
|
||||
final Color color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
RenderMaskedIcon(this.color, this.lineLengthScale, this.strokeCap);
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final strokeWidth = size.width / 12;
|
||||
|
||||
final canvas = context.canvas;
|
||||
var rect = offset & size;
|
||||
|
||||
final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4
|
||||
|
||||
// final path = Path.combine(
|
||||
// PathOperation.difference,
|
||||
// Path()..addRect(rect),
|
||||
// Path()..moveTo(rect.left, rect.top)
|
||||
// ..relativeLineTo(sqrt2Width, 0)
|
||||
// ..lineTo(rect.right, rect.bottom - sqrt2Width)
|
||||
// ..lineTo(rect.right, rect.bottom)
|
||||
// ..close(),
|
||||
// );
|
||||
|
||||
final path = Path.combine(
|
||||
PathOperation.union,
|
||||
Path() // bottom
|
||||
..moveTo(rect.left, rect.bottom)
|
||||
..lineTo(rect.left, rect.top + sqrt2Width)
|
||||
..lineTo(rect.right - sqrt2Width, rect.bottom)
|
||||
..close(),
|
||||
Path() // top
|
||||
..moveTo(rect.right, rect.top)
|
||||
..lineTo(rect.right, rect.bottom - sqrt2Width)
|
||||
..lineTo(rect.left + sqrt2Width, rect.top));
|
||||
|
||||
canvas.save();
|
||||
|
||||
canvas.clipPath(path, doAntiAlias: false);
|
||||
super.paint(context, offset);
|
||||
|
||||
context.canvas.restore();
|
||||
|
||||
final linePaint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = strokeCap;
|
||||
|
||||
final strokeOffset = strokeWidth * sqrt1_2 / 2;
|
||||
rect = rect
|
||||
.translate(-strokeOffset, strokeOffset)
|
||||
.deflate(size.width * lineLengthScale);
|
||||
canvas.drawLine(
|
||||
rect.topLeft,
|
||||
rect.bottomRight,
|
||||
linePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DisabledIconExt on Icon {
|
||||
DisabledIcon<Icon> disable([double? lineLengthScale]) =>
|
||||
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class DynamicSliverAppBar extends StatefulWidget {
|
||||
this.stretchTriggerOffset = 100.0,
|
||||
this.onStretchTrigger,
|
||||
this.shape,
|
||||
this.toolbarHeight = kToolbarHeight + 20,
|
||||
this.toolbarHeight = kToolbarHeight,
|
||||
this.leadingWidth,
|
||||
this.toolbarTextStyle,
|
||||
this.titleTextStyle,
|
||||
@@ -43,8 +43,10 @@ class DynamicSliverAppBar extends StatefulWidget {
|
||||
this.forceMaterialTransparency = false,
|
||||
this.clipBehavior,
|
||||
this.appBarClipper,
|
||||
this.callback,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? callback;
|
||||
final Widget? flexibleSpace;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
@@ -95,7 +97,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
|
||||
// to calculate dynamically the size for the sliver app bar
|
||||
double _height = 0;
|
||||
Orientation? _orientation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -103,13 +104,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
_updateHeight();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DynamicSliverAppBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
_updateHeight();
|
||||
}
|
||||
|
||||
void _updateHeight() {
|
||||
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
|
||||
// otherwise this will throw an error
|
||||
@@ -119,27 +113,32 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
|
||||
.size
|
||||
.height;
|
||||
widget.callback?.call(_height);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
|
||||
Orientation orientation = MediaQuery.orientationOf(context);
|
||||
if (_orientation != orientation) {
|
||||
_orientation = orientation;
|
||||
_height = 0;
|
||||
}
|
||||
if (_height == 0) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
child: widget.flexibleSpace ?? SizedBox(height: kToolbarHeight),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MediaQuery.orientationOf(context);
|
||||
|
||||
return SliverAppBar(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
|
||||
656
lib/common/widgets/episode_panel.dart
Normal file
@@ -0,0 +1,656 @@
|
||||
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,128 +0,0 @@
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
Widget htmlRender({
|
||||
required BuildContext context,
|
||||
String? htmlContent,
|
||||
int? imgCount,
|
||||
List<String>? imgList,
|
||||
required double constrainedWidth,
|
||||
Function(List<String>, int)? callback,
|
||||
}) {
|
||||
return SelectionArea(
|
||||
child: Html(
|
||||
data: htmlContent,
|
||||
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
|
||||
extensions: [
|
||||
TagExtension(
|
||||
tagsToExtend: <String>{'img'},
|
||||
builder: (ExtensionContext extensionContext) {
|
||||
try {
|
||||
final Map<String, dynamic> attributes = extensionContext.attributes;
|
||||
final List<dynamic> key = attributes.keys.toList();
|
||||
String imgUrl = key.contains('src')
|
||||
? attributes['src'] as String
|
||||
: attributes['data-src'] as String;
|
||||
if (imgUrl.startsWith('//')) {
|
||||
imgUrl = 'https:$imgUrl';
|
||||
}
|
||||
if (imgUrl.startsWith('http://')) {
|
||||
imgUrl = imgUrl.replaceAll('http://', 'https://');
|
||||
}
|
||||
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
|
||||
final bool isEmote = imgUrl.contains('/emote/');
|
||||
final bool isMall = imgUrl.contains('/mall/');
|
||||
if (isMall) {
|
||||
return const SizedBox();
|
||||
}
|
||||
// bool inTable =
|
||||
// extensionContext.element!.previousElementSibling == null ||
|
||||
// extensionContext.element!.nextElementSibling == null;
|
||||
// imgUrl = Utils().imageUrl(imgUrl!);
|
||||
// return CachedNetworkImage(
|
||||
// imageUrl: imgUrl,
|
||||
// width: isEmote ? 22 : null,
|
||||
// height: isEmote ? 22 : null,
|
||||
// );
|
||||
return Hero(
|
||||
tag: imgUrl,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback([imgUrl], 0);
|
||||
} else {
|
||||
context.imageView(
|
||||
imgList: [imgUrl],
|
||||
);
|
||||
}
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: isEmote ? 22 : constrainedWidth,
|
||||
height: isEmote ? 22 : 200,
|
||||
src: imgUrl,
|
||||
ignoreHeight: !isEmote,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
style: {
|
||||
'html': Style(
|
||||
fontSize: FontSize(16),
|
||||
lineHeight: LineHeight.percent(160),
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
// 'br': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||
'a': Style(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textDecoration: TextDecoration.none,
|
||||
),
|
||||
'br': Style(
|
||||
lineHeight: LineHeight.percent(-1),
|
||||
),
|
||||
'p': Style(
|
||||
margin: Margins.only(bottom: 4),
|
||||
// margin: Margins.zero,
|
||||
),
|
||||
'span': Style(
|
||||
fontSize: FontSize.large,
|
||||
height: Height(1.8),
|
||||
),
|
||||
'div': Style(height: Height.auto()),
|
||||
'li > p': Style(
|
||||
display: Display.inline,
|
||||
),
|
||||
'li': Style(
|
||||
padding: HtmlPaddings.only(bottom: 4),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
'img': Style(margin: Margins.only(top: 4, bottom: 4)),
|
||||
'h1,h2': Style(
|
||||
fontSize: FontSize.xLarge,
|
||||
fontWeight: FontWeight.bold,
|
||||
margin: Margins.only(bottom: 8),
|
||||
),
|
||||
'h3,h4,h5': Style(
|
||||
fontSize: FontSize(16),
|
||||
fontWeight: FontWeight.bold,
|
||||
margin: Margins.only(bottom: 4),
|
||||
),
|
||||
'figcaption': Style(
|
||||
fontSize: FontSize.large,
|
||||
textAlign: TextAlign.center,
|
||||
// margin: Margins.only(top: 4),
|
||||
),
|
||||
'strong': Style(fontWeight: FontWeight.bold),
|
||||
'figure': Style(
|
||||
margin: Margins.zero,
|
||||
),
|
||||
},
|
||||
));
|
||||
}
|
||||
@@ -5,17 +5,15 @@ class HttpError extends StatelessWidget {
|
||||
const HttpError({
|
||||
this.isSliver = true,
|
||||
this.errMsg,
|
||||
this.callback,
|
||||
this.onReload,
|
||||
this.btnText,
|
||||
this.extraWidget,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final bool isSliver;
|
||||
final String? errMsg;
|
||||
final Function()? callback;
|
||||
final VoidCallback? onReload;
|
||||
final String? btnText;
|
||||
final Widget? extraWidget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -27,46 +25,43 @@ class HttpError extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget content(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
SvgPicture.asset(
|
||||
"assets/images/error.svg",
|
||||
height: 200,
|
||||
Widget content(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
SvgPicture.asset(
|
||||
"assets/images/error.svg",
|
||||
height: 200,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
child: SelectableText(
|
||||
errMsg ?? '没有数据',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleSmall,
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SelectableText(
|
||||
errMsg ?? '没有数据',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
if (onReload != null)
|
||||
FilledButton.tonal(
|
||||
onPressed: onReload,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return theme.colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
),
|
||||
child: Text(
|
||||
btnText ?? '点击重试',
|
||||
style: TextStyle(color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
if (extraWidget != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
extraWidget!,
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
if (callback != null) ...[
|
||||
if (extraWidget == null) const SizedBox(height: 20),
|
||||
FilledButton.tonal(
|
||||
onPressed: callback,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return Theme.of(context).colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
),
|
||||
child: Text(
|
||||
btnText ?? '点击重试',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
);
|
||||
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ Widget iconButton({
|
||||
Color? bgColor,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
late final theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -19,12 +20,11 @@ Widget iconButton({
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: iconSize ?? size / 2,
|
||||
color: iconColor ?? Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
color: iconColor ?? theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.all(0),
|
||||
backgroundColor:
|
||||
bgColor ?? Theme.of(context).colorScheme.secondaryContainer,
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: bgColor ?? theme.colorScheme.secondaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,89 +9,109 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
void imageSaveDialog({
|
||||
required BuildContext context,
|
||||
required String? title,
|
||||
required String? cover,
|
||||
}) {
|
||||
final double imgWidth = min(Get.width, Get.height) - 8 * 2;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (context) => Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: SmartDialog.dismiss,
|
||||
child: NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: cover,
|
||||
quality: 100,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title ?? '',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
GestureDetector(
|
||||
onTap: SmartDialog.dismiss,
|
||||
child: NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: cover,
|
||||
quality: 100,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[cover ?? ''],
|
||||
);
|
||||
// 保存成功,自动关闭弹窗
|
||||
if (saveStatus) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.download, size: 20),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
title ?? '',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
if (cover?.isNotEmpty == true) ...[
|
||||
const SizedBox(width: 4),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '分享',
|
||||
onPressed: () {
|
||||
SmartDialog.dismiss();
|
||||
DownloadUtils.onShareImg(cover!);
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.share,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
iconButton(
|
||||
context: context,
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[cover!],
|
||||
);
|
||||
if (saveStatus) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
},
|
||||
iconSize: 20,
|
||||
icon: Icons.download,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ 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 {
|
||||
@@ -12,19 +15,23 @@ class 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(
|
||||
Widget imageView(
|
||||
double maxWidth,
|
||||
List<ImageModel> picArr, {
|
||||
VoidCallback? onViewImage,
|
||||
@@ -83,6 +90,17 @@ Widget imageview(
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -102,12 +120,25 @@ Widget imageview(
|
||||
onViewImage?.call();
|
||||
context.imageView(
|
||||
initialPage: index,
|
||||
imgList: picArr.map((item) => item.url).toList(),
|
||||
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(
|
||||
@@ -143,7 +174,14 @@ Widget imageview(
|
||||
},
|
||||
),
|
||||
),
|
||||
if (picArr[index].isLongPic)
|
||||
if (picArr[index].isLivePhoto)
|
||||
const PBadge(
|
||||
text: 'Live',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
type: 'gray',
|
||||
)
|
||||
else if (picArr[index].isLongPic)
|
||||
const PBadge(
|
||||
text: '长图',
|
||||
right: 8,
|
||||
@@ -1,18 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/utils/download.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:status_bar_control/status_bar_control.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;
|
||||
|
||||
@@ -33,6 +31,24 @@ typedef IndexedFocusedWidgetBuilder = Widget Function(
|
||||
|
||||
typedef IndexedTagStringBuilder = String Function(int index);
|
||||
|
||||
enum SourceType { fileImage, networkImage, livePhoto }
|
||||
|
||||
class SourceModel {
|
||||
final SourceType sourceType;
|
||||
final String url;
|
||||
final String? liveUrl;
|
||||
final int? width;
|
||||
final int? height;
|
||||
|
||||
const SourceModel({
|
||||
this.sourceType = SourceType.networkImage,
|
||||
required this.url,
|
||||
this.liveUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
}
|
||||
|
||||
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
const InteractiveviewerGallery({
|
||||
super.key,
|
||||
@@ -45,17 +61,14 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||
this.onDismissed,
|
||||
this.setStatusBar,
|
||||
this.onClose,
|
||||
this.isFile,
|
||||
});
|
||||
|
||||
final bool? isFile;
|
||||
|
||||
final VoidCallback? onClose;
|
||||
final ValueChanged? onClose;
|
||||
|
||||
final bool? setStatusBar;
|
||||
|
||||
/// The sources to show.
|
||||
final List<String> sources;
|
||||
final List<SourceModel> sources;
|
||||
|
||||
/// The index of the first source in [sources] to show.
|
||||
final int initIndex;
|
||||
@@ -92,17 +105,14 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
|
||||
late Offset _doubleTapLocalPosition;
|
||||
|
||||
int? currentIndex;
|
||||
late final RxInt currentIndex = widget.initIndex.obs;
|
||||
|
||||
late List<bool> _thumbList;
|
||||
late final int _quality = GStorage.previewQ;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_thumbList = List.generate(widget.sources.length, (_) => true);
|
||||
|
||||
_pageController = PageController(initialPage: widget.initIndex);
|
||||
|
||||
_transformationController = custom.TransformationController();
|
||||
@@ -110,38 +120,51 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
)..addListener(() {
|
||||
_transformationController!.value =
|
||||
_animation?.value ?? Matrix4.identity();
|
||||
});
|
||||
)..addListener(listener);
|
||||
|
||||
currentIndex = widget.initIndex;
|
||||
if (widget.setStatusBar != false) {
|
||||
setStatusBar();
|
||||
}
|
||||
|
||||
if (widget.sources[currentIndex.value].sourceType == SourceType.livePhoto) {
|
||||
_onPlay(currentIndex.value);
|
||||
}
|
||||
}
|
||||
|
||||
void listener() {
|
||||
_transformationController!.value = _animation?.value ?? Matrix4.identity();
|
||||
}
|
||||
|
||||
SystemUiMode? mode;
|
||||
setStatusBar() async {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
await StatusBarControl.setHidden(
|
||||
true,
|
||||
animation: StatusBarAnimation.FADE,
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.immersiveSticky,
|
||||
);
|
||||
}
|
||||
if (Platform.isAndroid &&
|
||||
(await DeviceInfoPlugin().androidInfo).version.sdkInt < 29) {
|
||||
mode = SystemUiMode.manual;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
void dispose() {
|
||||
widget.onClose?.call(true);
|
||||
_player?.dispose();
|
||||
_pageController?.dispose();
|
||||
_animationController.removeListener(() {});
|
||||
_animationController.removeListener(listener);
|
||||
_animationController.dispose();
|
||||
if (widget.setStatusBar != false) {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
mode ?? SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (widget.isFile != true) {
|
||||
for (int index = 0; index < widget.sources.length; index++) {
|
||||
for (int index = 0; index < widget.sources.length; index++) {
|
||||
if (widget.sources[index].sourceType == SourceType.networkImage) {
|
||||
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
||||
}
|
||||
}
|
||||
@@ -201,14 +224,22 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
}
|
||||
}
|
||||
|
||||
void _onPlay(int index) {
|
||||
_player ??= Player();
|
||||
_videoController ??= VideoController(_player!);
|
||||
_player!.open(Media(widget.sources[index].liveUrl!));
|
||||
}
|
||||
|
||||
/// When the page view changed its page, the source will animate back into the
|
||||
/// original scale if it was scaled up.
|
||||
///
|
||||
/// Additionally the swipe up / down to dismiss gets enabled.
|
||||
void _onPageChanged(int page) {
|
||||
setState(() {
|
||||
currentIndex = page;
|
||||
});
|
||||
_player?.pause();
|
||||
currentIndex.value = page;
|
||||
if (widget.sources[page].sourceType == SourceType.livePhoto) {
|
||||
_onPlay(page);
|
||||
}
|
||||
widget.onPageChanged?.call(page);
|
||||
if (_transformationController!.value != Matrix4.identity()) {
|
||||
// animate the reset for the transformation of the interactive viewer
|
||||
@@ -224,22 +255,28 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
}
|
||||
}
|
||||
|
||||
String _getActualUrl(int index) => _thumbList[index] && _quality != 100
|
||||
? '${widget.sources[index]}@${_quality}q.webp'.http2https
|
||||
: widget.sources[index].http2https;
|
||||
String _getActualUrl(int index) {
|
||||
return _quality != 100
|
||||
? Utils.thumbnailImgUrl(widget.sources[index].url, _quality)
|
||||
: widget.sources[index].url.http2https;
|
||||
}
|
||||
|
||||
void onClose() {
|
||||
if (widget.onClose != null) {
|
||||
widget.onClose!();
|
||||
widget.onClose!(false);
|
||||
} else {
|
||||
Get.back();
|
||||
widget.onDismissed?.call(_pageController!.page!.floor());
|
||||
}
|
||||
}
|
||||
|
||||
Player? _player;
|
||||
VideoController? _videoController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
InteractiveViewerBoundary(
|
||||
controller: _transformationController,
|
||||
@@ -272,12 +309,15 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
_doubleTapLocalPosition = details.localPosition;
|
||||
},
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: widget.isFile == true ? null : onLongPress,
|
||||
onLongPress:
|
||||
widget.sources[index].sourceType == SourceType.fileImage
|
||||
? null
|
||||
: onLongPress,
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
context,
|
||||
index,
|
||||
index == currentIndex,
|
||||
index == currentIndex.value,
|
||||
_enablePageView,
|
||||
)
|
||||
: _itemBuilder(index),
|
||||
@@ -290,12 +330,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
12,
|
||||
8,
|
||||
20,
|
||||
MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
padding: MediaQuery.paddingOf(context) +
|
||||
const EdgeInsets.fromLTRB(12, 8, 20, 8),
|
||||
decoration: _enablePageView
|
||||
? BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -309,6 +345,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Align(
|
||||
@@ -321,51 +358,70 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
if (widget.sources.length > 1)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"${currentIndex! + 1}/${widget.sources.length}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Obx(
|
||||
() => Text(
|
||||
"${currentIndex.value + 1}/${widget.sources.length}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.isFile != true)
|
||||
if (widget.sources[currentIndex.value].sourceType !=
|
||||
SourceType.fileImage)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
onTap: () =>
|
||||
onShareImg(widget.sources[currentIndex!]),
|
||||
onTap: () => DownloadUtils.onShareImg(
|
||||
widget.sources[currentIndex.value].url),
|
||||
child: const Text("分享图片"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
onTap: () {
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
Utils.copyText(
|
||||
widget.sources[currentIndex.value].url);
|
||||
},
|
||||
child: const Text("复制链接"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
this.context,
|
||||
[widget.sources[currentIndex.value].url],
|
||||
);
|
||||
},
|
||||
child: const Text("保存图片"),
|
||||
),
|
||||
if (widget.sources.length > 1)
|
||||
PopupMenuItem(
|
||||
value: 3,
|
||||
onTap: () {
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources,
|
||||
this.context,
|
||||
widget.sources
|
||||
.map((item) => item.url)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
child: const Text("保存全部图片"),
|
||||
),
|
||||
if (widget.sources[currentIndex.value].sourceType ==
|
||||
SourceType.livePhoto)
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
DownloadUtils.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!,
|
||||
);
|
||||
},
|
||||
child: const Text("保存 Live Photo"),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: const Icon(Icons.more_horiz, color: Colors.white),
|
||||
@@ -379,51 +435,37 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
);
|
||||
}
|
||||
|
||||
// 图片分享
|
||||
void onShareImg(String imgUrl) async {
|
||||
SmartDialog.showLoading();
|
||||
var response = await Request()
|
||||
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
|
||||
final temp = await getTemporaryDirectory();
|
||||
SmartDialog.dismiss();
|
||||
String imgName =
|
||||
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
|
||||
var path = '${temp.path}/$imgName';
|
||||
File(path).writeAsBytesSync(response.data);
|
||||
Share.shareXFiles([XFile(path)], subject: imgUrl);
|
||||
}
|
||||
|
||||
Widget _itemBuilder(index) {
|
||||
return Center(
|
||||
child: Hero(
|
||||
tag: widget.sources[index],
|
||||
child: widget.isFile == true
|
||||
? Image(
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(widget.sources[index])),
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 0),
|
||||
imageUrl: _getActualUrl(index),
|
||||
// fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(
|
||||
value: progress.progress ?? 0),
|
||||
),
|
||||
);
|
||||
},
|
||||
// errorListener: (value) {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// setState(() {
|
||||
// _thumbList[index] = false;
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
),
|
||||
tag: widget.sources[index].url,
|
||||
child: switch (widget.sources[index].sourceType) {
|
||||
SourceType.fileImage => Image(
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(widget.sources[index].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()),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -487,7 +529,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
onShareImg(widget.sources[currentIndex!]);
|
||||
DownloadUtils.onShareImg(
|
||||
widget.sources[currentIndex.value].url);
|
||||
Get.back();
|
||||
},
|
||||
dense: true,
|
||||
@@ -496,7 +539,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
Utils.copyText(widget.sources[currentIndex!]);
|
||||
Utils.copyText(widget.sources[currentIndex.value].url);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
||||
@@ -505,8 +548,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
[widget.sources[currentIndex!]],
|
||||
this.context,
|
||||
[widget.sources[currentIndex.value].url],
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
@@ -517,13 +560,32 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.downloadImg(
|
||||
context,
|
||||
widget.sources,
|
||||
this.context,
|
||||
widget.sources.map((item) => item.url).toList(),
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (widget.sources[currentIndex.value].sourceType ==
|
||||
SourceType.livePhoto)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
DownloadUtils.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!,
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'保存 Live Photo',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
27
lib/common/widgets/keep_alive_wrapper.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KeepAliveWrapper extends StatefulWidget {
|
||||
const KeepAliveWrapper({
|
||||
super.key,
|
||||
required this.builder,
|
||||
this.wantKeepAlive = true,
|
||||
});
|
||||
|
||||
final WidgetBuilder builder;
|
||||
final bool wantKeepAlive;
|
||||
|
||||
@override
|
||||
State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
|
||||
}
|
||||
|
||||
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.builder(context);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => widget.wantKeepAlive;
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
import 'dart:async';
|
||||
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/http/video.dart';
|
||||
import 'package:PiliPlus/models/bangumi/info.dart' as bangumi;
|
||||
import 'package:PiliPlus/models/video_detail_res.dart' as video;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import '../../utils/storage.dart';
|
||||
import '../../utils/utils.dart';
|
||||
|
||||
class ListSheetContent extends StatefulWidget {
|
||||
const ListSheetContent({
|
||||
super.key,
|
||||
this.index, // tab index
|
||||
this.season,
|
||||
this.episodes,
|
||||
this.bvid,
|
||||
this.aid,
|
||||
required this.currentCid,
|
||||
required this.changeFucCall,
|
||||
this.onClose,
|
||||
this.onReverse,
|
||||
this.showTitle,
|
||||
this.isSupportReverse,
|
||||
this.isReversed,
|
||||
});
|
||||
|
||||
final dynamic index;
|
||||
final dynamic season;
|
||||
final dynamic episodes;
|
||||
final String? bvid;
|
||||
final int? aid;
|
||||
final int currentCid;
|
||||
final Function changeFucCall;
|
||||
final VoidCallback? onClose;
|
||||
final VoidCallback? onReverse;
|
||||
final bool? showTitle;
|
||||
final bool? isSupportReverse;
|
||||
final bool? isReversed;
|
||||
|
||||
@override
|
||||
State<ListSheetContent> createState() => _ListSheetContentState();
|
||||
}
|
||||
|
||||
class _ListSheetContentState extends State<ListSheetContent>
|
||||
with TickerProviderStateMixin {
|
||||
late List<ItemScrollController> itemScrollController = [];
|
||||
late int currentIndex = _currentIndex;
|
||||
late List<bool> reverse;
|
||||
|
||||
int get _index => widget.index ?? 0;
|
||||
late final bool _isList = widget.season != null &&
|
||||
widget.season?.sections is List &&
|
||||
widget.season.sections.length > 1;
|
||||
dynamic get episodes =>
|
||||
widget.episodes ?? widget.season?.sections[_index].episodes;
|
||||
TabController? _ctr;
|
||||
StreamController? _indexStream;
|
||||
int? _seasonFav;
|
||||
StreamController? _favStream;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ListSheetContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.showTitle != false) {
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = _currentIndex;
|
||||
|
||||
void jumpToCurrent() {
|
||||
if (this.currentIndex != currentIndex) {
|
||||
this.currentIndex = currentIndex;
|
||||
try {
|
||||
itemScrollController[_index].jumpTo(
|
||||
index: currentIndex,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// jump to current
|
||||
if (_ctr != null && widget.index != _ctr?.index) {
|
||||
_ctr?.animateTo(_index, duration: const Duration(milliseconds: 200));
|
||||
Future.delayed(const Duration(milliseconds: 300)).then((_) {
|
||||
jumpToCurrent();
|
||||
});
|
||||
} else {
|
||||
jumpToCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
int get _currentIndex => max(
|
||||
0,
|
||||
_isList
|
||||
? widget.season.sections[_index].episodes
|
||||
.indexWhere((e) => e.cid == widget.currentCid)
|
||||
: episodes.indexWhere((e) => e.cid == widget.currentCid));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isList) {
|
||||
_indexStream ??= StreamController<int>.broadcast();
|
||||
_ctr = TabController(
|
||||
vsync: this,
|
||||
length: widget.season.sections.length,
|
||||
initialIndex: _index,
|
||||
)..addListener(() {
|
||||
_indexStream?.add(_ctr?.index);
|
||||
});
|
||||
}
|
||||
itemScrollController = _isList
|
||||
? List.generate(
|
||||
widget.season.sections.length, (_) => ItemScrollController())
|
||||
: [ItemScrollController()];
|
||||
reverse = _isList
|
||||
? List.generate(widget.season.sections.length, (_) => false)
|
||||
: [false];
|
||||
if (widget.bvid != null && widget.season != null) {
|
||||
_favStream ??= StreamController<int>();
|
||||
() async {
|
||||
dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid);
|
||||
if (result['status']) {
|
||||
_seasonFav = result['data']['season_fav'] ? 1 : 0;
|
||||
_favStream?.add(_seasonFav);
|
||||
}
|
||||
}();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
try {
|
||||
itemScrollController[_index].jumpTo(index: currentIndex);
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_favStream?.close();
|
||||
_favStream = null;
|
||||
_indexStream?.close();
|
||||
_indexStream = null;
|
||||
_ctr?.removeListener(() {});
|
||||
_ctr?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget buildEpisodeListItem(
|
||||
dynamic episode,
|
||||
int index,
|
||||
int length,
|
||||
bool isCurrentIndex,
|
||||
) {
|
||||
Color primary = Theme.of(context).colorScheme.primary;
|
||||
late String title;
|
||||
if (episode.runtimeType.toString() == "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!;
|
||||
}
|
||||
} else if (episode.runtimeType.toString() == "PageItem") {
|
||||
title = episode.pagePart!;
|
||||
} else if (episode.runtimeType.toString() == "Part") {
|
||||
title = episode.pagePart!;
|
||||
// debugPrint("未知类型:${episode.runtimeType}");
|
||||
}
|
||||
return ListTile(
|
||||
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();
|
||||
currentIndex = 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,
|
||||
episode is video.EpisodeItem
|
||||
? episode.arc?.pic
|
||||
: episode is bangumi.EpisodeItem
|
||||
? episode.cover
|
||||
: null,
|
||||
);
|
||||
},
|
||||
dense: false,
|
||||
leading: (episode is video.EpisodeItem && episode.arc?.pic != null) ||
|
||||
(episode is video.Part && episode.firstFrame != null) ||
|
||||
(episode is bangumi.EpisodeItem && episode.cover != null)
|
||||
? Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: isCurrentIndex
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
width: 1.8,
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => NetworkImgLayer(
|
||||
radius: 6,
|
||||
src: episode is video.EpisodeItem
|
||||
? episode.arc?.pic
|
||||
: episode is bangumi.EpisodeItem
|
||||
? episode.cover
|
||||
: episode.firstFrame,
|
||||
width: constraints.maxHeight * StyleString.aspectRatio,
|
||||
height: constraints.maxHeight,
|
||||
),
|
||||
),
|
||||
)
|
||||
: isCurrentIndex
|
||||
? Image.asset(
|
||||
'assets/images/live.png',
|
||||
color: primary,
|
||||
height: 12,
|
||||
semanticLabel: "正在播放:",
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isCurrentIndex ? FontWeight.bold : null,
|
||||
color: isCurrentIndex
|
||||
? primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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),
|
||||
],
|
||||
if (episode is! bangumi.EpisodeItem) Text('${index + 1}/$length'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: Utils.getSheetHeight(context),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 45,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.showTitle != false ? 14 : 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.showTitle != false)
|
||||
Text(
|
||||
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: _favStream?.stream,
|
||||
builder: (context, snapshot) => snapshot.hasData
|
||||
? mediumButton(
|
||||
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
|
||||
icon: _seasonFav == 1
|
||||
? Icons.notifications_off_outlined
|
||||
: Icons.notifications_active_outlined,
|
||||
onPressed: () async {
|
||||
dynamic result = await VideoHttp.seasonFav(
|
||||
isFav: _seasonFav == 1,
|
||||
seasonId: widget.season.id,
|
||||
);
|
||||
if (result['status']) {
|
||||
SmartDialog.showToast(
|
||||
'${_seasonFav == 1 ? '取消' : ''}订阅成功');
|
||||
_seasonFav = _seasonFav == 1 ? 0 : 1;
|
||||
_favStream?.add(_seasonFav);
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
mediumButton(
|
||||
tooltip: '跳至顶部',
|
||||
icon: Icons.vertical_align_top,
|
||||
onPressed: () {
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: !reverse[_ctr?.index ?? 0]
|
||||
? 0
|
||||
: _isList
|
||||
? widget.season.sections[_ctr?.index].episodes
|
||||
.length -
|
||||
1
|
||||
: episodes.length - 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
mediumButton(
|
||||
tooltip: '跳至底部',
|
||||
icon: Icons.vertical_align_bottom,
|
||||
onPressed: () {
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: !reverse[_ctr?.index ?? 0]
|
||||
? _isList
|
||||
? widget.season.sections[_ctr?.index].episodes
|
||||
.length -
|
||||
1
|
||||
: episodes.length - 1
|
||||
: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
mediumButton(
|
||||
tooltip: '跳至当前',
|
||||
icon: Icons.my_location,
|
||||
onPressed: () async {
|
||||
if (_ctr != null && _ctr?.index != (_index)) {
|
||||
_ctr?.animateTo(_index);
|
||||
await Future.delayed(const Duration(milliseconds: 225));
|
||||
}
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: currentIndex,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
if (widget.isSupportReverse == true)
|
||||
if (!_isList)
|
||||
_reverseButton
|
||||
else
|
||||
StreamBuilder(
|
||||
stream: _indexStream?.stream,
|
||||
initialData: _index,
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.data == _index
|
||||
? _reverseButton
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
StreamBuilder(
|
||||
stream: _indexStream?.stream,
|
||||
initialData: _index,
|
||||
builder: (context, snapshot) => mediumButton(
|
||||
tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
|
||||
icon: !reverse[snapshot.data]
|
||||
? MdiIcons.sortNumericAscending
|
||||
: MdiIcons.sortNumericDescending,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.onClose != null)
|
||||
mediumButton(
|
||||
tooltip: '关闭',
|
||||
icon: Icons.close,
|
||||
onPressed: widget.onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
if (_isList)
|
||||
Material(
|
||||
child: TabBar(
|
||||
controller: _ctr,
|
||||
padding: const EdgeInsets.only(right: 60),
|
||||
isScrollable: true,
|
||||
tabs: (widget.season.sections as List)
|
||||
.map((item) => Tab(text: item.title))
|
||||
.toList(),
|
||||
dividerHeight: 1,
|
||||
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _isList
|
||||
? TabBarView(
|
||||
controller: _ctr,
|
||||
children: List.generate(
|
||||
widget.season.sections.length,
|
||||
(index) => _buildBody(
|
||||
index, widget.season.sections[index].episodes),
|
||||
),
|
||||
)
|
||||
: _buildBody(null, episodes),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _reverseButton => mediumButton(
|
||||
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',
|
||||
icon: widget.isReversed == true
|
||||
? MdiIcons.sortDescending
|
||||
: MdiIcons.sortAscending,
|
||||
onPressed: () async {
|
||||
if (widget.showTitle == false) {
|
||||
// jump to current
|
||||
if (_ctr != null && _ctr?.index != (_index)) {
|
||||
_ctr?.animateTo(_index);
|
||||
await Future.delayed(const Duration(milliseconds: 225));
|
||||
}
|
||||
try {
|
||||
itemScrollController[_ctr?.index ?? 0].scrollTo(
|
||||
index: currentIndex,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
widget.onReverse?.call();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildBody(i, episodes) => Material(
|
||||
child: ScrollablePositionedList.separated(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
reverse: reverse[i ?? 0],
|
||||
itemCount: episodes.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return buildEpisodeListItem(
|
||||
episodes[index],
|
||||
index,
|
||||
episodes.length,
|
||||
i != null
|
||||
? i == (_index)
|
||||
? currentIndex == index
|
||||
: false
|
||||
: currentIndex == index,
|
||||
);
|
||||
},
|
||||
itemScrollController: itemScrollController[i ?? 0],
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../utils/utils.dart';
|
||||
import '../constants.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
class LiveCard extends StatelessWidget {
|
||||
final dynamic liveItem;
|
||||
|
||||
const LiveCard({
|
||||
super.key,
|
||||
required this.liveItem,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String heroTag = Utils.makeHeroTag(liveItem.roomid);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.08),
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(builder:
|
||||
(BuildContext context, BoxConstraints boxConstraints) {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: liveItem.cover as String,
|
||||
type: 'emote',
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedOpacity(
|
||||
opacity: 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: liveStat(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
liveContent(context)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget liveContent(context) {
|
||||
return Padding(
|
||||
// 多列
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 6, 7),
|
||||
// 单列
|
||||
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
liveItem.title as String,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
liveItem.uname as String,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget liveStat(context) {
|
||||
return Container(
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(top: 22, left: 8, right: 8),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black54,
|
||||
],
|
||||
tileMode: TileMode.mirror,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
// Row(
|
||||
// children: [
|
||||
// StatView(
|
||||
// theme: 'white',
|
||||
// view: view,
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
// StatDanMu(
|
||||
// theme: 'white',
|
||||
// danmu: danmaku,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
Text(
|
||||
liveItem.online.toString(),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,20 @@
|
||||
import 'package:PiliPlus/common/widgets/http_error.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget get loadingWidget => Center(child: CircularProgressIndicator());
|
||||
|
||||
Widget errorWidget({errMsg, callback}) => HttpError(
|
||||
Widget errorWidget({errMsg, onReload}) => HttpError(
|
||||
isSliver: false,
|
||||
errMsg: errMsg,
|
||||
callback: callback,
|
||||
onReload: onReload,
|
||||
);
|
||||
|
||||
Widget scrollErrorWidget({errMsg, callback}) => CustomScrollView(
|
||||
Widget scrollErrorWidget({errMsg, onReload, controller}) => CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
HttpError(
|
||||
errMsg: errMsg,
|
||||
callback: callback,
|
||||
onReload: onReload,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
Widget replyErrorWidget(context, isSliver, errMsg, onReload) => HttpError(
|
||||
isSliver: isSliver,
|
||||
errMsg:
|
||||
'${errMsg.startsWith('gRPC Error') ? '如无法加载评论:\n关闭代理\n或设置中关闭使用gRPC加载评论\n\n' : ''}$errMsg',
|
||||
callback: onReload,
|
||||
extraWidget: errMsg.startsWith('gRPC Error') && GlobalData().grpcReply
|
||||
? FilledButton.tonal(
|
||||
onPressed: () {
|
||||
GlobalData().grpcReply = false;
|
||||
onReload();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
return Theme.of(context).colorScheme.primary.withAlpha(20);
|
||||
}),
|
||||
),
|
||||
child: Text(
|
||||
'暂时关闭gRPC加载评论',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 'package:PiliPlus/utils/global_data.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
class NetworkImgLayer extends StatelessWidget {
|
||||
@@ -9,35 +9,35 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
super.key,
|
||||
this.src,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.height,
|
||||
this.type,
|
||||
this.fadeOutDuration,
|
||||
this.fadeInDuration,
|
||||
// 图片质量 默认1%
|
||||
this.quality,
|
||||
this.semanticsLabel,
|
||||
this.ignoreHeight,
|
||||
this.radius,
|
||||
this.imageBuilder,
|
||||
this.isLongPic,
|
||||
this.callback,
|
||||
this.getPlaceHolder,
|
||||
this.boxFit,
|
||||
});
|
||||
|
||||
final String? src;
|
||||
final double width;
|
||||
final double height;
|
||||
final double? height;
|
||||
final String? type;
|
||||
final Duration? fadeOutDuration;
|
||||
final Duration? fadeInDuration;
|
||||
final int? quality;
|
||||
final String? semanticsLabel;
|
||||
final bool? ignoreHeight;
|
||||
final double? radius;
|
||||
final ImageWidgetBuilder? imageBuilder;
|
||||
final Function? isLongPic;
|
||||
final Function? callback;
|
||||
final Function? getPlaceHolder;
|
||||
final BoxFit? boxFit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -56,43 +56,31 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildImage(context) {
|
||||
late final int defaultImgQuality = GlobalData().imgQuality;
|
||||
bool thumbnail = true;
|
||||
int? memCacheWidth, memCacheHeight;
|
||||
if (callback?.call() == true || width <= height) {
|
||||
if (height == null || callback?.call() == true || width <= height!) {
|
||||
memCacheWidth = width.cacheSize(context);
|
||||
} else {
|
||||
memCacheHeight = height.cacheSize(context);
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
imageUrl:
|
||||
'${src?.startsWith('//') == true ? 'https:$src' : src?.http2https}${type != 'emote' && type != 'cover' && thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}',
|
||||
imageUrl: Utils.thumbnailImgUrl(src, quality),
|
||||
width: width,
|
||||
height: ignoreHeight == null || ignoreHeight == false ? height : null,
|
||||
height: height,
|
||||
memCacheWidth: memCacheWidth,
|
||||
memCacheHeight: memCacheHeight,
|
||||
fit: BoxFit.cover,
|
||||
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,
|
||||
// errorWidget: (BuildContext context, String url, Object error) =>
|
||||
// placeholder(context),
|
||||
placeholder: (BuildContext context, String url) =>
|
||||
getPlaceHolder?.call() ?? placeholder(context),
|
||||
imageBuilder: imageBuilder,
|
||||
// errorListener: (value) {
|
||||
// thumbnail = false;
|
||||
// if (context.mounted) {
|
||||
// (context as Element).markNeedsBuild();
|
||||
// }
|
||||
// },
|
||||
);
|
||||
}
|
||||
|
||||
Widget placeholder(BuildContext context) {
|
||||
int cacheWidth = width.cacheSize(context);
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
@@ -107,7 +95,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: type == 'bg'
|
||||
? const SizedBox()
|
||||
? const SizedBox.shrink()
|
||||
: Center(
|
||||
child: Image.asset(
|
||||
type == 'avatar'
|
||||
@@ -115,8 +103,7 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
: 'assets/images/loading.png',
|
||||
width: width,
|
||||
height: height,
|
||||
cacheWidth: cacheWidth == 0 ? null : cacheWidth,
|
||||
// cacheHeight: height.cacheSize(context),
|
||||
cacheWidth: width.cacheSize(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -172,6 +172,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
)));
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: list,
|
||||
);
|
||||
}
|
||||
@@ -260,6 +261,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
)));
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: list,
|
||||
);
|
||||
}
|
||||
@@ -286,6 +288,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
}
|
||||
return ClipOval(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
@@ -372,7 +375,10 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
return Stack(children: children);
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
/// double is zero.
|
||||
@@ -410,7 +416,7 @@ class _NineGridViewState extends State<NineGridView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget? child = Container();
|
||||
Widget? child;
|
||||
double? realWidth = widget.width;
|
||||
double? realHeight = widget.height;
|
||||
switch (widget.type) {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NoSplashFactory extends InteractiveInkFeatureFactory {
|
||||
@override
|
||||
InteractiveInkFeature create(
|
||||
{required MaterialInkController controller,
|
||||
required RenderBox referenceBox,
|
||||
required Offset position,
|
||||
required Color color,
|
||||
required TextDirection textDirection,
|
||||
bool containedInkWell = false,
|
||||
RectCallback? rectCallback,
|
||||
BorderRadius? borderRadius,
|
||||
ShapeBorder? customBorder,
|
||||
double? radius,
|
||||
VoidCallback? onRemoved}) {
|
||||
return _NoInteractiveInkFeature(
|
||||
controller: controller,
|
||||
referenceBox: referenceBox,
|
||||
color: color,
|
||||
onRemoved: onRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoInteractiveInkFeature extends InteractiveInkFeature {
|
||||
@override
|
||||
void paintFeature(Canvas canvas, Matrix4 transform) {}
|
||||
_NoInteractiveInkFeature({
|
||||
required super.controller,
|
||||
required super.referenceBox,
|
||||
required super.color,
|
||||
super.onRemoved,
|
||||
});
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../utils/download.dart';
|
||||
import '../constants.dart';
|
||||
import 'network_img_layer.dart';
|
||||
|
||||
class OverlayPop extends StatelessWidget {
|
||||
const OverlayPop({super.key, this.videoItem, this.closeFn});
|
||||
|
||||
final dynamic videoItem;
|
||||
final Function? closeFn;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double imgWidth = min(Get.height, Get.width) - 8 * 2;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
width: imgWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: videoItem is card.Card
|
||||
? (videoItem as card.Card).smallCoverV5.base.cover
|
||||
: videoItem.pic,
|
||||
quality: 100,
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20))),
|
||||
child: IconButton(
|
||||
tooltip: '关闭',
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => closeFn?.call(),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
videoItem is card.Card
|
||||
? (videoItem as card.Card).smallCoverV5.base.title
|
||||
: videoItem.title,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
await DownloadUtils.downloadImg(
|
||||
context,
|
||||
[
|
||||
videoItem is card.Card
|
||||
? (videoItem as card.Card).smallCoverV5.base.cover
|
||||
: videoItem.pic ?? videoItem.cover
|
||||
],
|
||||
);
|
||||
closeFn?.call();
|
||||
},
|
||||
icon: const Icon(Icons.download, size: 20),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,14 @@ class Pair<T, R> {
|
||||
T first;
|
||||
R second;
|
||||
}
|
||||
|
||||
class Triple<T, R, S> {
|
||||
Triple({
|
||||
required this.first,
|
||||
required this.second,
|
||||
required this.third,
|
||||
});
|
||||
T first;
|
||||
R second;
|
||||
S third;
|
||||
}
|
||||
|
||||