Compare commits
577 Commits
1.1.5.1
...
50070997de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50070997de | ||
|
|
88f0ab1dea | ||
|
|
038285cbe6 | ||
|
|
295a587df5 | ||
|
|
73d7d78080 | ||
|
|
cd367a8649 | ||
|
|
f637771527 | ||
|
|
7b01c33657 | ||
|
|
af1cd30ed7 | ||
|
|
508384c016 | ||
|
|
3446484d97 | ||
|
|
bed533eaeb | ||
|
|
31f8435511 | ||
|
|
6bcd143362 | ||
|
|
e5f2a1b0e5 | ||
|
|
66a191b018 | ||
|
|
e619946b9c | ||
|
|
6d7ae5d830 | ||
|
|
2de59b0c9b | ||
|
|
9ac37d6fb3 | ||
|
|
fb9568a628 | ||
|
|
90590faaaf | ||
|
|
bee04fb2b0 | ||
|
|
b3e2dcf2c5 | ||
|
|
d4d9fc3405 | ||
|
|
6a75061dbb | ||
|
|
2e7c80422c | ||
|
|
60f800b671 | ||
|
|
1ba6983f39 | ||
|
|
6c61ff59ff | ||
|
|
6464987437 | ||
|
|
41027f7019 | ||
|
|
41c8e1a538 | ||
|
|
56756c5c85 | ||
|
|
3b68fe467d | ||
|
|
f14aacd664 | ||
|
|
c2ceb9ec73 | ||
|
|
99a950b049 | ||
|
|
87914a267a | ||
|
|
dd85aba8a9 | ||
|
|
8309be9ca9 | ||
|
|
5fbc86afa3 | ||
|
|
be5889ee33 | ||
|
|
7c342032de | ||
|
|
46baad899c | ||
|
|
66f15f532f | ||
|
|
89522e2059 | ||
|
|
0cac46ee26 | ||
|
|
1fcc26464f | ||
|
|
ad1583706a | ||
|
|
cc169db12b | ||
|
|
6a1db5d26b | ||
|
|
b04c3d878d | ||
|
|
68e302b5d7 | ||
|
|
ed39624476 | ||
|
|
8683658046 | ||
|
|
0c308a7001 | ||
|
|
cd82668435 | ||
|
|
e142cb6800 | ||
|
|
200329df4a | ||
|
|
859f14423e | ||
|
|
9d21be1048 | ||
|
|
d5293fbc72 | ||
|
|
ad6c0e0d15 | ||
|
|
ce706d4732 | ||
|
|
2c511d1a2c | ||
|
|
3ac21d95f9 | ||
|
|
b33fdf14af | ||
|
|
24769e144f | ||
|
|
50875421a5 | ||
|
|
0581d8f7d4 | ||
|
|
be9f39d043 | ||
|
|
034482362d | ||
|
|
1a45b5cb4a | ||
|
|
fab34df973 | ||
|
|
dbb865d723 | ||
|
|
23db485183 | ||
|
|
d63116542b | ||
|
|
f5dbfcec79 | ||
|
|
b7b40c557e | ||
|
|
d0373da5ca | ||
|
|
31aeab3516 | ||
|
|
4228f918d7 | ||
|
|
0f07dccc4b | ||
|
|
9b0c9e9276 | ||
|
|
ac9631462a | ||
|
|
d5bf3487f8 | ||
|
|
9259e84d5c | ||
|
|
583092735f | ||
|
|
d41f69e95e | ||
|
|
c4847caa27 | ||
|
|
cdd9289e6d | ||
|
|
cd1fdd42d3 | ||
|
|
74ed06157a | ||
|
|
293f046b35 | ||
|
|
b00b9e2816 | ||
|
|
68e9afd72b | ||
|
|
09c73d3c87 | ||
|
|
a91a90b7da | ||
|
|
d434f54ddf | ||
|
|
dbbdb61e34 | ||
|
|
d85dd864fe | ||
|
|
84bbbd19fa | ||
|
|
3f34d1a251 | ||
|
|
7b6d8bef99 | ||
|
|
a2b54e50d0 | ||
|
|
887c3525a2 | ||
|
|
3a46ce2429 | ||
|
|
c0b5876d5b | ||
|
|
74c389be8b | ||
|
|
5d87361693 | ||
|
|
9752ae2f33 | ||
|
|
3a9cafaf24 | ||
|
|
5937d83aa9 | ||
|
|
6feb1c15ee | ||
|
|
57c683d5d8 | ||
|
|
cd26cf6d98 | ||
|
|
c7864ff4a3 | ||
|
|
7b51b6900f | ||
|
|
11b6f241b0 | ||
|
|
88c01ffca3 | ||
|
|
9ed8b14d1b | ||
|
|
f7a2be1923 | ||
|
|
47e3a47e00 | ||
|
|
524b6bca1c | ||
|
|
80ecd35784 | ||
|
|
89fdc28150 | ||
|
|
b406a00c8d | ||
|
|
6755b7fae6 | ||
|
|
60801f9846 | ||
|
|
fae0cfe800 | ||
|
|
dccb5d4bf5 | ||
|
|
b9ce4bad67 | ||
|
|
baef1655d6 | ||
|
|
8fd5f353e4 | ||
|
|
326291de23 | ||
|
|
1761a9abc9 | ||
|
|
d372142d62 | ||
|
|
91f00173bb | ||
|
|
73484f1f72 | ||
|
|
490a08fa79 | ||
|
|
5b5983ed50 | ||
|
|
efc202c10f | ||
|
|
24ab7505e6 | ||
|
|
d6bce6aa63 | ||
|
|
354282930c | ||
|
|
79c9849c80 | ||
|
|
d7b4ceabbf | ||
|
|
dd5ccd11b8 | ||
|
|
26a5d00718 | ||
|
|
d32456fae0 | ||
|
|
bffdfae1f6 | ||
|
|
44b7cced09 | ||
|
|
91936a9f44 | ||
|
|
0fb394cc36 | ||
|
|
4229024adf | ||
|
|
add633bc66 | ||
|
|
54a88fa2ff | ||
|
|
9543f3b457 | ||
|
|
e88769308e | ||
|
|
bac0769933 | ||
|
|
24f2cfa4e9 | ||
|
|
970ee679f1 | ||
|
|
db7ad269b2 | ||
|
|
2232bc009d | ||
|
|
e778f2b463 | ||
|
|
7e9618d712 | ||
|
|
62880a3769 | ||
|
|
e566a358dd | ||
|
|
1b0b966e76 | ||
|
|
dd2492e04d | ||
|
|
0b4ed25891 | ||
|
|
372c677e8f | ||
|
|
9a6f335e48 | ||
|
|
f9441db232 | ||
|
|
4de43faa2e | ||
|
|
ba372a101b | ||
|
|
a2ff54af70 | ||
|
|
cbc4f58323 | ||
|
|
b553e7554d | ||
|
|
68724c8a9e | ||
|
|
85baf8e0e6 | ||
|
|
222c9d01a0 | ||
|
|
db30aa8041 | ||
|
|
6f95456d20 | ||
|
|
de6e402d97 | ||
|
|
6341660788 | ||
|
|
a1dbcae93e | ||
|
|
1526137a64 | ||
|
|
3097b56816 | ||
|
|
db74eccf77 | ||
|
|
14890d342a | ||
|
|
51163dd985 | ||
|
|
f0d9b3a9a7 | ||
|
|
8f3707fbf1 | ||
|
|
f52bbe9804 | ||
|
|
3ec54868d0 | ||
|
|
c0b55f9af3 | ||
|
|
279f21857d | ||
|
|
b897103af0 | ||
|
|
353664fbd4 | ||
|
|
de3505ce07 | ||
|
|
cdc1720358 | ||
|
|
904d210ba2 | ||
|
|
db8dd85b63 | ||
|
|
8ad130567e | ||
|
|
7eb21bc5a2 | ||
|
|
ea4316a847 | ||
|
|
2bbc97a950 | ||
|
|
0178d105ba | ||
|
|
771fa75f48 | ||
|
|
82483b33fc | ||
|
|
886c53c7d8 | ||
|
|
f0050dd6e6 | ||
|
|
e6a2f65b4e | ||
|
|
2fc3f9864f | ||
|
|
64c05a1b06 | ||
|
|
7c4e20f96c | ||
|
|
ace286753c | ||
|
|
f0430eba9f | ||
|
|
bbcceb72a7 | ||
|
|
be4fa6ad2c | ||
|
|
50e1f77e10 | ||
|
|
ba56b45038 | ||
|
|
b4b3764e5f | ||
|
|
2220372e4f | ||
|
|
0957dfc66e | ||
|
|
9578f948b4 | ||
|
|
1724f0d202 | ||
|
|
2bebf200df | ||
|
|
fc7fc18b14 | ||
|
|
8f00ca5680 | ||
|
|
236b524445 | ||
|
|
ae59d257c3 | ||
|
|
662ccfcf0a | ||
|
|
b7ab3655c4 | ||
|
|
eda04b32a4 | ||
|
|
9b1ae39922 | ||
|
|
d1497115da | ||
|
|
7f2682bb7b | ||
|
|
d6579b29ae | ||
|
|
8a8aa6c1e0 | ||
|
|
ed66a4655b | ||
|
|
e04affd0fe | ||
|
|
e293083492 | ||
|
|
7f39f36c75 | ||
|
|
565819febe | ||
|
|
af150118a1 | ||
|
|
470e519a2b | ||
|
|
d73588f1fd | ||
|
|
ffbbd8e702 | ||
|
|
a1815c4cc7 | ||
|
|
b9e543f26b | ||
|
|
0788a4de2d | ||
|
|
b0c6e2f5cd | ||
|
|
9489d8a7ca | ||
|
|
aee4424dbf | ||
|
|
96f9972895 | ||
|
|
6ddf282555 | ||
|
|
e98b2b69bb | ||
|
|
448192b635 | ||
|
|
6cda3a1880 | ||
|
|
99128b2641 | ||
|
|
b8098fe067 | ||
|
|
9fef3284db | ||
|
|
f2b0a3a5ed | ||
|
|
3090cfc6f9 | ||
|
|
98ce99202e | ||
|
|
fddf46a90a | ||
|
|
a5231a55b8 | ||
|
|
b8cae015d7 | ||
|
|
3b09534320 | ||
|
|
702cf988d3 | ||
|
|
5586d12b1f | ||
|
|
4683939364 | ||
|
|
f825f87dc1 | ||
|
|
4ad422c3ea | ||
|
|
c01318c066 | ||
|
|
01a74e191a | ||
|
|
a1f15b5da5 | ||
|
|
1e83a23c5c | ||
|
|
2d69c05f33 | ||
|
|
7a2dbe68c7 | ||
|
|
db08af6ca5 | ||
|
|
fefb5c837b | ||
|
|
a88429d6d7 | ||
|
|
cbe99a32eb | ||
|
|
b65d10ac5f | ||
|
|
868f7f5055 | ||
|
|
e843684109 | ||
|
|
631197e3b9 | ||
|
|
381c385726 | ||
|
|
077255e776 | ||
|
|
0bcc1a7f12 | ||
|
|
9b145b525a | ||
|
|
b61a54bf9b | ||
|
|
cf103a09c1 | ||
|
|
a802bc1cdf | ||
|
|
8d312d8cf1 | ||
|
|
6738142ac0 | ||
|
|
3d99e6c761 | ||
|
|
f9f52e918a | ||
|
|
6108290b4b | ||
|
|
8bae275120 | ||
|
|
0504011ba0 | ||
|
|
dc9d4f9eed | ||
|
|
187c92d691 | ||
|
|
9c7b18710c | ||
|
|
1dbc54f063 | ||
|
|
348bc8b920 | ||
|
|
a375d8525f | ||
|
|
e3e423f9b1 | ||
|
|
62048992be | ||
|
|
ec9498a2ca | ||
|
|
1d35abef63 | ||
|
|
889f6d01c2 | ||
|
|
d9c47be2a9 | ||
|
|
cf44036589 | ||
|
|
7276cde48a | ||
|
|
6782bee11a | ||
|
|
b55e102dc3 | ||
|
|
65ad8a0fdc | ||
|
|
fdb3bf3edc | ||
|
|
95506ad896 | ||
|
|
348b2533dc | ||
|
|
2bdab71138 | ||
|
|
e707764f84 | ||
|
|
4a3d827f7a | ||
|
|
e88cd12dfa | ||
|
|
ee04978e0c | ||
|
|
d15ad4911d | ||
|
|
14b6c115b5 | ||
|
|
ee188da6b0 | ||
|
|
998b70cd87 | ||
|
|
7563a52bed | ||
|
|
7e81fae2bc | ||
|
|
639dfac8af | ||
|
|
d8950adb64 | ||
|
|
9092db86ca | ||
|
|
d7d9655f81 | ||
|
|
a63ca93762 | ||
|
|
243178c112 | ||
|
|
dcb3a02da8 | ||
|
|
b1c0eca328 | ||
|
|
e3a1eb5c87 | ||
|
|
736478b1c5 | ||
|
|
12919804dc | ||
|
|
888b3d8173 | ||
|
|
1e6b0f0b53 | ||
|
|
aa3e5a4737 | ||
|
|
3f3d54fd27 | ||
|
|
a142b15344 | ||
|
|
651e79ce26 | ||
|
|
9b93ce84ab | ||
|
|
dfa258b9e6 | ||
|
|
a5efca4e1f | ||
|
|
1fe84d1d34 | ||
|
|
b978ff5649 | ||
|
|
fa85ae47ac | ||
|
|
3209ecd0ba | ||
|
|
807de41ff0 | ||
|
|
d273e72a44 | ||
|
|
2c0597175d | ||
|
|
85292a3df2 | ||
|
|
9c7c6f9e4e | ||
|
|
511ff71f5f | ||
|
|
e104982246 | ||
|
|
e7e79eb62a | ||
|
|
352e314ee1 | ||
|
|
e9dafbc227 | ||
|
|
96727469ac | ||
|
|
c70c9829c0 | ||
|
|
beb7eb1aea | ||
|
|
8e726f49b2 | ||
|
|
007375371e | ||
|
|
6d79551566 | ||
|
|
483953cf56 | ||
|
|
fbf7116edf | ||
|
|
6c164d81e3 | ||
|
|
d0789734ec | ||
|
|
f3bd305337 | ||
|
|
5ab7000716 | ||
|
|
dc1c33f086 | ||
|
|
920c51100a | ||
|
|
05a385d69e | ||
|
|
9411785d26 | ||
|
|
ed2bd069ee | ||
|
|
0460030a2b | ||
|
|
7e570d11d8 | ||
|
|
32cd3209d0 | ||
|
|
0cb07aef1c | ||
|
|
0c65605ac0 | ||
|
|
8234b7ac92 | ||
|
|
4ac855d393 | ||
|
|
7381939c0f | ||
|
|
a380bcd96a | ||
|
|
d253ef468b | ||
|
|
e8145ef65a | ||
|
|
0c175abc0b | ||
|
|
946a5a1e47 | ||
|
|
29e7e0e556 | ||
|
|
cc1704a021 | ||
|
|
7ab2cf973f | ||
|
|
32386bf146 | ||
|
|
40269da391 | ||
|
|
42e082bbc6 | ||
|
|
1ad710c1cf | ||
|
|
cfa925549e | ||
|
|
ca387787b3 | ||
|
|
29a9b22c29 | ||
|
|
672375b925 | ||
|
|
c099738802 | ||
|
|
50561b8dc1 | ||
|
|
2596859778 | ||
|
|
3d453bafdb | ||
|
|
18e0b93ca7 | ||
|
|
7260a387f9 | ||
|
|
37fa165f59 | ||
|
|
8f08104f37 | ||
|
|
6ee4deab05 | ||
|
|
77fff92939 | ||
|
|
8964197b73 | ||
|
|
dbc7bcd0dd | ||
|
|
207ad2753c | ||
|
|
d6e6e52df2 | ||
|
|
9442b17d63 | ||
|
|
058ff44e39 | ||
|
|
48c7dc0eed | ||
|
|
99634a66ab | ||
|
|
21fad89cde | ||
|
|
5979ddb60c | ||
|
|
bcbfe5c849 | ||
|
|
1640732f5d | ||
|
|
9567910611 | ||
|
|
d1713504a0 | ||
|
|
bce73d9f16 | ||
|
|
6f30d2e331 | ||
|
|
556bda0d68 | ||
|
|
9d5eb55e26 | ||
|
|
110469961d | ||
|
|
fa348db7c5 | ||
|
|
3eac565b5e | ||
|
|
af40e489bc | ||
|
|
361eb4c614 | ||
|
|
7ace981f24 | ||
|
|
bfb2becb2d | ||
|
|
038f03a4e7 | ||
|
|
219228f8b5 | ||
|
|
1f64de5954 | ||
|
|
e9b5cffa91 | ||
|
|
68872f7b14 | ||
|
|
bd158619a4 | ||
|
|
310f497c30 | ||
|
|
30ee413852 | ||
|
|
0ab07a713e | ||
|
|
7eaf05839a | ||
|
|
777c3c2278 | ||
|
|
b9b54ce4f7 | ||
|
|
92e5fae29c | ||
|
|
05e8ded86a | ||
|
|
7a65b777c9 | ||
|
|
0b1f6c4d0e | ||
|
|
923af32c96 | ||
|
|
4eae7e698f | ||
|
|
5a61dbe30c | ||
|
|
036dbcaf21 | ||
|
|
bd97f9a500 | ||
|
|
33278a74b2 | ||
|
|
397f887b91 | ||
|
|
ebe793ccfc | ||
|
|
68464e4e34 | ||
|
|
395893fc7d | ||
|
|
f5657d2d4c | ||
|
|
a3ddc83430 | ||
|
|
d2f8aff421 | ||
|
|
25148509d2 | ||
|
|
2879d0dc00 | ||
|
|
90349189ee | ||
|
|
bdc524e486 | ||
|
|
cb58822009 | ||
|
|
4a2679a589 | ||
|
|
09bd1edeb3 | ||
|
|
00da3c4a0e | ||
|
|
c40d794180 | ||
|
|
34a839d9e2 | ||
|
|
f06d0605ce | ||
|
|
ef975de624 | ||
|
|
d10c737a38 | ||
|
|
28b69a06fa | ||
|
|
069cf555ea | ||
|
|
836ab311d6 | ||
|
|
dbc11c36df | ||
|
|
fffce10b31 | ||
|
|
de85e82bfa | ||
|
|
9855b35b65 | ||
|
|
5a0b045a1f | ||
|
|
c226f8f6df | ||
|
|
fd06fa9cc4 | ||
|
|
2b5f111fb1 | ||
|
|
9f5ce5ae37 | ||
|
|
3d95165d46 | ||
|
|
cfb72f27ac | ||
|
|
bcacc41db3 | ||
|
|
b2da99e334 | ||
|
|
041af37bb0 | ||
|
|
80e007bac6 | ||
|
|
87c7699324 | ||
|
|
11912c5f62 | ||
|
|
236a8b3023 | ||
|
|
63e4bac204 | ||
|
|
2e11247af4 | ||
|
|
13f377f680 | ||
|
|
b9d594bc8b | ||
|
|
2a52157c3f | ||
|
|
a037d8e793 | ||
|
|
49b7ea14c3 | ||
|
|
0a40d11133 | ||
|
|
dff6b6486d | ||
|
|
b51c646415 | ||
|
|
25acf3a9bb | ||
|
|
7ec90e9a22 | ||
|
|
645ce0b7b3 | ||
|
|
864fef5881 | ||
|
|
eea232c6db | ||
|
|
25fca498fc | ||
|
|
c9a02f9c74 | ||
|
|
99602eea95 | ||
|
|
b5fe0faeec | ||
|
|
20a36e8f9a | ||
|
|
161bf2eedb | ||
|
|
fcf4e72d8e | ||
|
|
b46cb69df4 | ||
|
|
43c7620b4c | ||
|
|
1a8f65b075 | ||
|
|
259e7080f8 | ||
|
|
7da6f05a50 | ||
|
|
521ca3ad18 | ||
|
|
31e5692dff | ||
|
|
191bcbc525 | ||
|
|
a0f3b3e442 | ||
|
|
5bcd822251 | ||
|
|
d80324655e | ||
|
|
952d168022 | ||
|
|
af723e161c | ||
|
|
3ff521e103 | ||
|
|
b4a5d985f5 | ||
|
|
1e0e2d2d6e | ||
|
|
d7f7611af4 | ||
|
|
11cdb67050 | ||
|
|
53cf9d54c4 | ||
|
|
2e73688688 | ||
|
|
ce5e85e64b | ||
|
|
02e0d34127 | ||
|
|
830f3b60e0 | ||
|
|
b4fb7d14d4 | ||
|
|
ab1e5cb62a | ||
|
|
348a9e014e | ||
|
|
0baf3fcd36 | ||
|
|
13818533a7 | ||
|
|
0dd3689d65 | ||
|
|
23b6850778 | ||
|
|
d8ca89ac8f | ||
|
|
ae06d5f7f2 | ||
|
|
62506d3eb5 | ||
|
|
f7c61d63a0 | ||
|
|
f46437f891 | ||
|
|
1cd949c365 | ||
|
|
bc5ce11449 | ||
|
|
cef4beaa0c | ||
|
|
02bd68f697 | ||
|
|
2bc3275c1f | ||
|
|
ec107063c3 | ||
|
|
4c2fd38d6c | ||
|
|
1a6653ba93 | ||
|
|
74d5e03a34 | ||
|
|
2b4b1debe6 | ||
|
|
17883eb77e |
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
50
.github/workflows/build.yml
vendored
@@ -38,7 +38,7 @@ on:
|
||||
build_linux_x64:
|
||||
description: "Build Linux-x64"
|
||||
required: false
|
||||
default: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
tag:
|
||||
@@ -49,7 +49,7 @@ on:
|
||||
|
||||
jobs:
|
||||
android:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_android == 'true' }}
|
||||
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_android == 'true' }}
|
||||
name: Release Android
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
@@ -78,9 +78,9 @@ jobs:
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: apply bottom sheet patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 android
|
||||
continue-on-error: true
|
||||
|
||||
- name: Write key
|
||||
@@ -95,13 +95,20 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set and Extract version
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1 android
|
||||
|
||||
- name: flutter build apk
|
||||
- name: Flutter Build Release Apk
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub
|
||||
|
||||
- name: rename
|
||||
- name: Flutter Build Dev Apk
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
flutter build apk --release --split-per-abi --android-project-arg dev=1 --pub
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
||||
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
|
||||
@@ -115,53 +122,52 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_android_*.apk
|
||||
files: PiliPlus_android_*.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Android_arm64-v8a
|
||||
path: |
|
||||
PiliPlus_android_*_arm64-v8a.apk
|
||||
path: PiliPlus_android_*_arm64-v8a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Android_armeabi-v7a
|
||||
path: |
|
||||
PiliPlus_android_*_armeabi-v7a.apk
|
||||
path: PiliPlus_android_*_armeabi-v7a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Android_x86_64
|
||||
path: |
|
||||
PiliPlus_android_*_x86_64.apk
|
||||
path: PiliPlus_android_*_x86_64.apk
|
||||
|
||||
ios:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_ios == 'true' }}
|
||||
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_ios == 'true' }}
|
||||
uses: ./.github/workflows/ios.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
mac:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_mac == 'true' }}
|
||||
if: ${{ github.event.inputs.build_mac == 'true' }}
|
||||
uses: ./.github/workflows/mac.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
win_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
|
||||
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_win_x64 == 'true' }}
|
||||
uses: ./.github/workflows/win_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
linux_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_linux_x64 == 'true' }}
|
||||
if: ${{ github.event.inputs.build_linux_x64 == 'true' }}
|
||||
uses: ./.github/workflows/linux_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
|
||||
14
.github/workflows/ios.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
build-macos-app:
|
||||
name: Release IOS
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -30,11 +30,18 @@ jobs:
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 iOS
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build iOS
|
||||
run: |
|
||||
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
|
||||
# make AltSign happy...
|
||||
find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \;
|
||||
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/Runner.app
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
@@ -46,7 +53,8 @@ jobs:
|
||||
PiliPlus_ios_*.ipa
|
||||
|
||||
- name: Upload ios release
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: iOS-release
|
||||
path: PiliPlus_ios_*.ipa
|
||||
|
||||
33
.github/workflows/linux_x64.yml
vendored
@@ -51,6 +51,11 @@ jobs:
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 Linux
|
||||
continue-on-error: true
|
||||
|
||||
#TODO: deb and rpm packages need to be build
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
|
||||
@@ -70,7 +75,7 @@ jobs:
|
||||
printf "复制文件...\n"
|
||||
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
||||
cp -r ../assets/linux/DEBIAN .
|
||||
cp ../assets/linux/piliplus.desktop usr/share/applications
|
||||
cp ../assets/linux/com.example.piliplus.desktop usr/share/applications
|
||||
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
printf "修改控制文件...\n"
|
||||
@@ -112,7 +117,7 @@ jobs:
|
||||
SRC_DIR="$PWD/piliplus-${{ env.version }}"
|
||||
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
|
||||
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
|
||||
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
|
||||
cp assets/linux/com.example.piliplus.desktop "$SRC_DIR/assets/com.example.piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
|
||||
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
|
||||
|
||||
@@ -145,7 +150,7 @@ jobs:
|
||||
|
||||
# 桌面集成
|
||||
mkdir -p %{buildroot}/usr/share/applications
|
||||
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
|
||||
install -m 644 assets/com.example.piliplus.desktop %{buildroot}/usr/share/applications/com.example.piliplus.desktop
|
||||
|
||||
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
|
||||
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
@@ -161,7 +166,7 @@ jobs:
|
||||
%files
|
||||
/opt/PiliPlus
|
||||
/usr/bin/piliplus
|
||||
/usr/share/applications/piliplus.desktop
|
||||
/usr/share/applications/com.example.piliplus.desktop
|
||||
/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
%changelog
|
||||
@@ -197,8 +202,8 @@ jobs:
|
||||
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
|
||||
|
||||
printf "复制桌面文件和图标...\n"
|
||||
cp assets/linux/piliplus.desktop "$APPDIR/piliplus.desktop"
|
||||
cp assets/linux/piliplus.desktop "$APPDIR/usr/share/applications/piliplus.desktop"
|
||||
cp assets/linux/com.example.piliplus.desktop "$APPDIR/com.example.piliplus.desktop"
|
||||
cp assets/linux/com.example.piliplus.desktop "$APPDIR/usr/share/applications/com.example.piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$APPDIR/piliplus.png"
|
||||
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
|
||||
|
||||
@@ -214,8 +219,8 @@ jobs:
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
printf "修改桌面文件中的 Exec 路径...\n"
|
||||
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
|
||||
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
|
||||
|
||||
printf "打包 AppImage...\n"
|
||||
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
|
||||
@@ -236,25 +241,29 @@ jobs:
|
||||
PiliPlus_linux_*.AppImage
|
||||
|
||||
- name: Upload linux targz package
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_targz_amd64_packege
|
||||
path: PiliPlus_linux_*.tar.gz
|
||||
|
||||
- name: Upload linux deb package
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_deb_amd64_package
|
||||
path: PiliPlus_linux_*.deb
|
||||
|
||||
- name: Upload linux rpm package
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_rpm_amd64_package
|
||||
path: PiliPlus_linux_*.rpm
|
||||
|
||||
- name: Upload linux AppImage package
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_AppImage_amd64_package
|
||||
path: PiliPlus_linux_*.AppImage
|
||||
|
||||
12
.github/workflows/mac.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
build-mac-app:
|
||||
name: Release Mac
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
@@ -30,13 +30,18 @@ jobs:
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 macOS
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build Mac
|
||||
run: flutter build macos --release --dart-define-from-file=pili_release.json
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
npm install --global create-dmg
|
||||
create-dmg build/macos/Build/Products/Release/PiliPlus.app
|
||||
create-dmg build/macos/Build/Products/Release/PiliPlus.app || true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Rename DMG
|
||||
@@ -52,7 +57,8 @@ jobs:
|
||||
PiliPlus_macos_*.dmg
|
||||
|
||||
- name: Upload macos release
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: macOS-release
|
||||
path: PiliPlus_macos_*.dmg
|
||||
|
||||
18
.github/workflows/win_x64.yml
vendored
@@ -26,6 +26,11 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 windows
|
||||
continue-on-error: true
|
||||
|
||||
- name: Add fastforge and Inno Setup
|
||||
run: |
|
||||
dart pub global activate fastforge
|
||||
@@ -52,9 +57,8 @@ jobs:
|
||||
mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe
|
||||
|
||||
- name: Compress
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
run: |
|
||||
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64.zip"
|
||||
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64_portable.zip"
|
||||
shell: pwsh
|
||||
|
||||
- name: Release
|
||||
@@ -68,13 +72,15 @@ jobs:
|
||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
- name: Upload windows file release
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Windows-file-x64-release
|
||||
path: Release
|
||||
path: PiliPlus_windows_*.zip
|
||||
|
||||
- name: Upload windows setup release
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Windows-setup-x64-release
|
||||
path: PiliPlus-Win-Setup
|
||||
path: PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
5
.gitignore
vendored
@@ -5,10 +5,13 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
Package.resolved
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
@@ -146,4 +149,4 @@ pili_release.json
|
||||
|
||||
dist
|
||||
|
||||
test.dart
|
||||
test*
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 编辑动态
|
||||
- [x] DLNA 投屏
|
||||
- [x] 离线缓存/播放
|
||||
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
@@ -218,8 +219,8 @@
|
||||
|
||||
## 声明
|
||||
|
||||
此项目(PiliPlus)是个人为了兴趣而开发, 仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
||||
此项目(PiliPlus)是个人为了兴趣而开发,仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集,不提供任何破解内容。
|
||||
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
|
||||
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
|
||||
本仓库做了更激进的修改,感谢原作者的开源精神。
|
||||
|
||||
@@ -12,7 +12,6 @@ include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/grpc/bilibili/**
|
||||
# - lib/grpc/google/**
|
||||
# - lib/common/widgets/flutter/**
|
||||
|
||||
formatter:
|
||||
@@ -72,5 +71,11 @@ linter:
|
||||
- use_truncating_division
|
||||
- use_string_buffers
|
||||
- unnecessary_statements
|
||||
- unnecessary_nullable_for_final_variable_declarations
|
||||
- tighten_type_of_initializing_formals
|
||||
- prefer_void_to_null
|
||||
- prefer_spread_collections
|
||||
- unnecessary_to_list_in_spreads
|
||||
- prefer_for_elements_to_map_fromIterable
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@@ -3,7 +3,6 @@ import org.jetbrains.kotlin.konan.properties.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
@@ -18,10 +17,6 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.piliplus"
|
||||
minSdk = flutter.minSdkVersion
|
||||
@@ -49,15 +44,29 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
if (project.hasProperty("dev")) {
|
||||
resValues = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
all {
|
||||
signingConfig = config ?: signingConfigs["debug"]
|
||||
}
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
if (project.hasProperty("dev")) {
|
||||
applicationIdSuffix = ".dev"
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "app_name",
|
||||
value = "PiliPlus dev",
|
||||
)
|
||||
}
|
||||
// proguardFiles(
|
||||
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
// "proguard-rules.pro"
|
||||
// )
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
@@ -72,6 +81,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.piliplus">
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -16,8 +17,7 @@
|
||||
</queries>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name=
|
||||
"android.support.customtabs.action.CustomTabsService" />
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
@@ -35,56 +35,62 @@
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
<application xmlns:tools="http://schemas.android.com/tools"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
tools:replace="android:allowBackup">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
<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:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
>
|
||||
android:supportsPictureInPicture="true"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutter_deeplinking_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
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"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPlus">
|
||||
<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: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"/>-->
|
||||
@@ -100,36 +106,56 @@
|
||||
<intent-filter android:label="PiliPlus">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.example.piliplus.SHORTCUT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bilibili"/>
|
||||
|
||||
<data android:scheme="bilibili" />
|
||||
<data android:host="download" />
|
||||
<data android:host="forward" />
|
||||
<data android:host="comment"
|
||||
<data
|
||||
android:host="comment"
|
||||
android:pathPattern="/detail/.*/.*/.*" />
|
||||
<data android:host="uper" />
|
||||
<data android:host="article"
|
||||
<data
|
||||
android:host="article"
|
||||
android:pathPattern="/readlist" />
|
||||
<data android:host="opus" />
|
||||
<data android:host="advertise" android:path="/home" />
|
||||
<data
|
||||
android:host="advertise"
|
||||
android:path="/home" />
|
||||
<data android:host="clip" />
|
||||
<data android:host="search" android:pathPattern=".*" />
|
||||
<data
|
||||
android:host="search"
|
||||
android:pathPattern=".*" />
|
||||
<data android:host="stardust-search" />
|
||||
<data android:host="music" />
|
||||
<data android:host="cheese" />
|
||||
<data android:host="bangumi"
|
||||
<data
|
||||
android:host="bangumi"
|
||||
android:pathPattern="/season.*" />
|
||||
<data android:host="bangumi" android:pathPattern="/.*" />
|
||||
<data android:host="pictureshow"
|
||||
<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="im"
|
||||
android:path="/notifications" />
|
||||
<data android:host="following" />
|
||||
<data android:host="following"
|
||||
<data
|
||||
android:host="following"
|
||||
android:pathPattern="/detail/.*" />
|
||||
<data android:host="following"
|
||||
<data
|
||||
android:host="following"
|
||||
android:path="/publishInfo/" />
|
||||
<data android:host="laser" android:pathPattern="/.*" />
|
||||
<data
|
||||
android:host="laser"
|
||||
android:pathPattern="/.*" />
|
||||
<data android:host="livearea" />
|
||||
<data android:host="live" />
|
||||
<data android:host="catalog" />
|
||||
@@ -147,28 +173,44 @@
|
||||
<data android:host="video" />
|
||||
<data android:host="story" />
|
||||
<data android:host="podcast" />
|
||||
<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"
|
||||
<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"
|
||||
<data
|
||||
android:host="pegasus"
|
||||
android:pathPattern="/channel/v2/.*" />
|
||||
<data android:host="feed" android:pathPattern="/channel" />
|
||||
<data
|
||||
android:host="feed"
|
||||
android:pathPattern="/channel" />
|
||||
<data android:host="vip" />
|
||||
<data android:host="user_center" android:path="/vip" />
|
||||
<data
|
||||
android:host="user_center"
|
||||
android:path="/vip" />
|
||||
<data android:host="history" />
|
||||
<data android:host="charge" android:path="/rank" />
|
||||
<data
|
||||
android:host="charge"
|
||||
android:path="/rank" />
|
||||
<data android:host="assistant" />
|
||||
<data android:host="feedback" />
|
||||
<data android:host="auth" android:path="/launch" />
|
||||
<data
|
||||
android:host="auth"
|
||||
android:path="/launch" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
@@ -177,32 +219,37 @@
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Ucrop.CropTheme"/>
|
||||
android:theme="@style/Ucrop.CropTheme" />
|
||||
|
||||
<receiver
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</receiver>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!--
|
||||
Media access permissions.
|
||||
Android 13 or higher.
|
||||
@@ -210,5 +257,5 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.example.piliplus;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.app.RemoteAction;
|
||||
import android.app.SearchManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.util.Rational;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.github.dart_lang.jni_flutter.JniFlutterPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
@Keep
|
||||
public final class AndroidHelper {
|
||||
public static final boolean isFoldable;
|
||||
|
||||
public static final boolean isPipAvailable;
|
||||
|
||||
public static volatile boolean isPipMode = false;
|
||||
|
||||
static {
|
||||
PackageManager pm = getContext().getPackageManager();
|
||||
isFoldable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE);
|
||||
isPipAvailable = pm.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
}
|
||||
|
||||
private AndroidHelper() {
|
||||
}
|
||||
|
||||
private static Context getContext() {
|
||||
return JniFlutterPlugin.getApplicationContext();
|
||||
}
|
||||
|
||||
public static int sdkInt() {
|
||||
return Build.VERSION.SDK_INT;
|
||||
}
|
||||
|
||||
public static void back() {
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_HOME);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
}
|
||||
|
||||
public static void biliSendCommAntifraud(
|
||||
int action, long oid, int type, long rpId, long root, long parent, long ctime, @NonNull String commentText,
|
||||
String pictures, @NonNull String sourceId, long uid, @NonNull String cookie
|
||||
) {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setComponent(new ComponentName(
|
||||
"icu.freedomIntrovert.biliSendCommAntifraud",
|
||||
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
|
||||
));
|
||||
intent.putExtra("action", action);
|
||||
intent.putExtra("oid", oid);
|
||||
intent.putExtra("type", type);
|
||||
intent.putExtra("rpid", rpId);
|
||||
intent.putExtra("root", root);
|
||||
intent.putExtra("parent", parent);
|
||||
intent.putExtra("ctime", ctime);
|
||||
intent.putExtra("comment_text", commentText);
|
||||
if (pictures != null) {
|
||||
intent.putExtra("pictures", pictures);
|
||||
}
|
||||
intent.putExtra("source_id", sourceId);
|
||||
intent.putExtra("uid", uid);
|
||||
ArrayList<String> cookiesList = new ArrayList<>(1);
|
||||
cookiesList.add(cookie);
|
||||
intent.putStringArrayListExtra("cookies", cookiesList);
|
||||
getContext().startActivity(intent);
|
||||
}
|
||||
|
||||
public static void openLinkVerifySettings() {
|
||||
Context context = getContext();
|
||||
Uri uri = Uri.parse("package:" + context.getPackageName());
|
||||
try {
|
||||
Intent intent;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri);
|
||||
} else {
|
||||
intent = new Intent(Intent.ACTION_MAIN, uri);
|
||||
intent.setClassName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
|
||||
);
|
||||
}
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
} catch (Exception ignored) {
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean openMusic(@NonNull String title, String artist, String album) {
|
||||
Intent intent = new Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(SearchManager.QUERY, title);
|
||||
intent.putExtra(MediaStore.EXTRA_MEDIA_TITLE, title);
|
||||
if (artist != null) {
|
||||
intent.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist);
|
||||
}
|
||||
if (album != null) {
|
||||
intent.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album);
|
||||
}
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
|
||||
Context context = getContext();
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
try {
|
||||
if (pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
context.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
intent.setAction(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH);
|
||||
if (pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
context.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void enterPip(long engineId, int width, int height, boolean autoEnter, boolean isLive, boolean isPlaying) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Activity activity = JniFlutterPlugin.getActivity(engineId);
|
||||
assert activity != null;
|
||||
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(width, height));
|
||||
setPipActions(activity, builder, isLive, isPlaying);
|
||||
if (autoEnter) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(true);
|
||||
activity.setPictureInPictureParams(builder.build());
|
||||
}
|
||||
} else {
|
||||
activity.enterPictureInPictureMode(builder.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public static void updatePipActions(long engineId, boolean isLive, boolean isPlaying) {
|
||||
Activity activity = JniFlutterPlugin.getActivity(engineId);
|
||||
assert activity != null;
|
||||
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
|
||||
setPipActions(activity, builder, isLive, isPlaying);
|
||||
activity.setPictureInPictureParams(builder.build());
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private static void setPipActions(Activity activity, PictureInPictureParams.Builder builder, boolean isLive, boolean isPlaying) {
|
||||
ComponentName mbrComponent = MediaHelper.getMediaButtonReceiverComponent(activity);
|
||||
if (mbrComponent == null) return;
|
||||
ArrayList<RemoteAction> actionList = new ArrayList<>(3);
|
||||
if (!isLive) {
|
||||
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_rewind_10s, "ACTION_REWIND", (int) PlaybackState.ACTION_REWIND));
|
||||
}
|
||||
if (isPlaying) {
|
||||
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_pause, "ACTION_PAUSE", (int) PlaybackState.ACTION_PAUSE));
|
||||
} else {
|
||||
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_play, "ACTION_PLAY", (int) PlaybackState.ACTION_PLAY));
|
||||
}
|
||||
if (!isLive) {
|
||||
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_fast_forward_10s, "ACTION_FAST_FORWARD", (int) PlaybackState.ACTION_FAST_FORWARD));
|
||||
}
|
||||
builder.setActions(actionList);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private static RemoteAction getRemoteAction(@NonNull ComponentName mbrComponent, Activity activity, @DrawableRes int resId, String title, int action) {
|
||||
return new RemoteAction(
|
||||
Icon.createWithResource(activity, resId),
|
||||
title,
|
||||
title,
|
||||
Objects.requireNonNull(MediaHelper.buildMediaButtonPendingIntent(activity, mbrComponent, action))
|
||||
);
|
||||
}
|
||||
|
||||
public static void disableAutoEnterPip(long engineId) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Activity activity = JniFlutterPlugin.getActivity(engineId);
|
||||
if (activity != null) {
|
||||
activity.setPictureInPictureParams(new PictureInPictureParams.Builder()
|
||||
.setAutoEnterEnabled(false)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int[] maxScreenSize() {
|
||||
Context context = getContext();
|
||||
WindowManager wm = context.getSystemService(WindowManager.class);
|
||||
try {
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Rect maxBounds = wm.getMaximumWindowMetrics().getBounds();
|
||||
return new int[]{Math.round(maxBounds.width() / density), Math.round(maxBounds.height() / density)};
|
||||
} else {
|
||||
Point realSize = new Point();
|
||||
wm.getDefaultDisplay().getRealSize(realSize);
|
||||
return new int[]{Math.round(realSize.x / density), Math.round(realSize.y / density)};
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void createShortcut(@NonNull String id, @NonNull String uri, @NonNull String label, @NonNull String icon) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Context context = getContext();
|
||||
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager != null && shortcutManager.isRequestPinShortcutSupported()) {
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(icon);
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id)
|
||||
.setShortLabel(label)
|
||||
.setIcon(Icon.createWithAdaptiveBitmap(bitmap))
|
||||
.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(uri)))
|
||||
.build();
|
||||
// TODO: WorkerThread
|
||||
Intent pinIntent = shortcutManager.createShortcutResultIntent(shortcut);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||
context, 0, pinIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
shortcutManager.requestPinShortcut(shortcut, pendingIntent.getIntentSender());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static final class ToDart {
|
||||
public static volatile Runnable onUserLeaveHint;
|
||||
public static Runnable onConfigurationChanged;
|
||||
|
||||
private ToDart() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.piliplus;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class MediaHelper {
|
||||
private static final String TAG = "MediaButtonReceiver";
|
||||
|
||||
static PendingIntent buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, int action) {
|
||||
if (mbrComponent == null) {
|
||||
Log.w(TAG, "The component name of media button receiver should be provided.");
|
||||
return null;
|
||||
}
|
||||
int keyCode = PlaybackStateCompat_toKeyCode(action);
|
||||
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
Log.w(TAG,
|
||||
"Cannot build a media button pending intent with the given action: " + action);
|
||||
return null;
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||
intent.setComponent(mbrComponent);
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
|
||||
return PendingIntent.getBroadcast(context, keyCode, intent,
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);
|
||||
}
|
||||
|
||||
private static int PlaybackStateCompat_toKeyCode(int action) {
|
||||
return switch (action) {
|
||||
case (int) PlaybackState.ACTION_STOP -> KeyEvent.KEYCODE_MEDIA_STOP;
|
||||
case (int) PlaybackState.ACTION_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE;
|
||||
case (int) PlaybackState.ACTION_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY;
|
||||
case (int) PlaybackState.ACTION_REWIND -> KeyEvent.KEYCODE_MEDIA_REWIND;
|
||||
case (int) PlaybackState.ACTION_SKIP_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS;
|
||||
case (int) PlaybackState.ACTION_SKIP_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT;
|
||||
case (int) PlaybackState.ACTION_FAST_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
|
||||
case (int) PlaybackState.ACTION_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
|
||||
default -> KeyEvent.KEYCODE_UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
static ComponentName getMediaButtonReceiverComponent(Context context) {
|
||||
Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||
queryIntent.setPackage(context.getPackageName());
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
|
||||
if (resolveInfos.size() == 1) {
|
||||
ResolveInfo resolveInfo = resolveInfos.get(0);
|
||||
return new ComponentName(resolveInfo.activityInfo.packageName,
|
||||
resolveInfo.activityInfo.name);
|
||||
} else if (resolveInfos.size() > 1) {
|
||||
Log.w(TAG, "More than one BroadcastReceiver that handles "
|
||||
+ Intent.ACTION_MEDIA_BUTTON + " was found, returning null.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,20 @@
|
||||
package com.example.piliplus
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.SearchManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import androidx.core.net.toUri
|
||||
import com.ryanheise.audioservice.AudioServiceActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MainActivity : AudioServiceActivity() {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "PiliPlus")
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"back" -> back();
|
||||
"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 (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
"linkVerifySettings" -> {
|
||||
val uri = ("package:" + context.packageName).toUri()
|
||||
try {
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
|
||||
} else {
|
||||
Intent("android.intent.action.MAIN", uri).setClassName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
|
||||
)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (_: Throwable) {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
"music" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, title)
|
||||
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
|
||||
call.argument<String?>("artist")
|
||||
?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
||||
call.argument<String?>("album")
|
||||
?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
||||
|
||||
addCategory(Intent.CATEGORY_DEFAULT)
|
||||
}
|
||||
try {
|
||||
if (packageManager.resolveActivity(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
) != null
|
||||
) {
|
||||
startActivity(intent)
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
|
||||
if (packageManager.resolveActivity(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
) != null
|
||||
) {
|
||||
startActivity(intent)
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(false)
|
||||
}
|
||||
|
||||
"setPipAutoEnterEnabled" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAutoEnterEnabled(call.argument<Boolean>("autoEnable") ?: false)
|
||||
.build()
|
||||
setPictureInPictureParams(params)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (AndroidHelper.isFoldable) {
|
||||
AndroidHelper.ToDart.onConfigurationChanged?.run()
|
||||
}
|
||||
}
|
||||
|
||||
private fun back() {
|
||||
val intent = Intent(Intent.ACTION_MAIN).apply {
|
||||
addCategory(Intent.CATEGORY_HOME)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
@@ -157,23 +26,15 @@ class MainActivity : AudioServiceActivity() {
|
||||
override fun onDestroy() {
|
||||
stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java))
|
||||
super.onDestroy()
|
||||
android.os.Process.killProcess(android.os.Process.myPid())
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
methodChannel.invokeMethod("onUserLeaveHint", null)
|
||||
AndroidHelper.ToDart.onUserLeaveHint?.run()
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration?
|
||||
) {
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel(
|
||||
flutterEngine!!.dartExecutor.binaryMessenger,
|
||||
"floating"
|
||||
).invokeMethod("onPipChanged", isInPictureInPictureMode)
|
||||
AndroidHelper.isPipMode = isInPictureInPictureMode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
|
||||
</vector>
|
||||
@@ -1,7 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
|
||||
</vector>
|
||||
@@ -6,7 +6,6 @@
|
||||
android:viewportHeight="108.0">
|
||||
<path
|
||||
android:fillColor="#FF5CB67B"
|
||||
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:pathData="m58.52,54h-36.71l5.03,-24.75h31.68c6.83,0 12.37,5.54 12.37,12.37 0,6.83 -5.54,12.37 -12.37,12.37zM58.52,12.76h-21.77l-16.14,82.49h17l4.89,-24.75h16.02c15.95,0 28.87,-12.93 28.87,-28.87 0,-15.95 -12.92,-28.87 -28.87,-28.87z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="30.0"
|
||||
android:viewportHeight="31.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M23.5,11C23.423,10.894 23.325,10.803 23.213,10.734C23.101,10.666 22.976,10.62 22.846,10.599C22.716,10.579 22.583,10.584 22.455,10.615C22.327,10.646 22.206,10.702 22.1,10.78C21.89,10.929 21.745,11.154 21.697,11.408C21.648,11.661 21.699,11.924 21.84,12.14C22.788,13.456 23.296,15.038 23.29,16.66C23.278,17.713 23.059,18.754 22.644,19.722C22.229,20.69 21.627,21.567 20.873,22.302C20.118,23.037 19.226,23.615 18.248,24.004C17.269,24.394 16.223,24.586 15.17,24.57C14.112,24.593 13.06,24.405 12.075,24.018C11.09,23.631 10.191,23.053 9.431,22.316C8.671,21.579 8.065,20.699 7.648,19.727C7.231,18.754 7.01,17.708 7,16.65C7.021,14.61 7.82,12.655 9.234,11.184C10.649,9.714 12.572,8.84 14.61,8.74V10.62C14.607,10.732 14.634,10.843 14.69,10.94C14.738,11.02 14.812,11.08 14.9,11.11C14.969,11.144 15.044,11.161 15.12,11.161C15.196,11.161 15.271,11.144 15.34,11.11L20,8.3C20.102,8.244 20.187,8.161 20.247,8.061C20.307,7.961 20.339,7.847 20.34,7.73C20.342,7.613 20.311,7.497 20.251,7.397C20.191,7.296 20.104,7.214 20,7.16L15.34,4.38C15.209,4.298 15.052,4.269 14.9,4.3C14.815,4.319 14.738,4.365 14.68,4.43C14.62,4.511 14.588,4.609 14.59,4.71V6.79C12.03,6.895 9.608,7.978 7.824,9.817C6.039,11.655 5.029,14.108 5,16.67C5.037,19.33 6.127,21.868 8.032,23.725C9.936,25.583 12.5,26.61 15.16,26.58C17.82,26.61 20.384,25.583 22.288,23.725C24.193,21.868 25.283,19.33 25.32,16.67C25.316,14.637 24.68,12.656 23.5,11ZM11.77,20.77H12.67C12.71,20.771 12.749,20.765 12.786,20.75C12.823,20.736 12.857,20.713 12.885,20.685C12.913,20.657 12.935,20.624 12.95,20.587C12.965,20.55 12.971,20.51 12.97,20.47V13.14C12.971,13.1 12.965,13.061 12.95,13.024C12.935,12.987 12.913,12.953 12.885,12.925C12.857,12.897 12.823,12.875 12.786,12.86C12.749,12.846 12.71,12.839 12.67,12.84H12.08C11.963,12.838 11.847,12.862 11.74,12.91C11.495,13.029 11.049,13.285 10.63,13.526C10.52,13.59 10.412,13.652 10.31,13.71L9.81,14.01C9.764,14.031 9.725,14.066 9.7,14.11C9.681,14.189 9.681,14.271 9.7,14.35V15.3C9.693,15.33 9.693,15.361 9.7,15.39L9.76,15.44H9.82H9.92C10.158,15.327 10.348,15.229 10.525,15.139L10.525,15.139L10.525,15.139C10.659,15.07 10.786,15.005 10.92,14.94L11.33,14.72C11.349,14.71 11.369,14.705 11.39,14.705C11.411,14.705 11.432,14.71 11.45,14.72C11.459,14.737 11.463,14.756 11.463,14.775C11.463,14.794 11.459,14.813 11.45,14.83V20.47C11.45,20.511 11.458,20.552 11.475,20.59C11.491,20.628 11.515,20.662 11.545,20.69C11.575,20.718 11.61,20.74 11.649,20.753C11.688,20.767 11.729,20.773 11.77,20.77ZM15.952,12.977C16.339,12.772 16.772,12.67 17.21,12.68C17.66,12.661 18.108,12.759 18.509,12.964C18.91,13.169 19.252,13.474 19.5,13.85C20.039,14.737 20.3,15.764 20.25,16.8C20.302,17.837 20.041,18.865 19.5,19.75C19.255,20.12 18.92,20.422 18.527,20.627C18.133,20.831 17.694,20.933 17.25,20.92C16.806,20.937 16.365,20.838 15.97,20.633C15.575,20.428 15.241,20.124 15,19.75C14.437,18.872 14.158,17.842 14.2,16.8C14.16,15.758 14.439,14.729 15,13.85C15.238,13.482 15.566,13.182 15.952,12.977ZM17.94,19.4C18.176,19.215 18.353,18.965 18.45,18.68V18.69C18.654,18.081 18.752,17.442 18.74,16.8C18.763,16.163 18.675,15.527 18.48,14.92C18.38,14.637 18.208,14.385 17.98,14.19C17.76,14.04 17.501,13.96 17.235,13.96C16.969,13.96 16.709,14.04 16.49,14.19C16.259,14.381 16.089,14.634 16,14.92C15.791,15.521 15.693,16.154 15.71,16.79C15.683,17.431 15.771,18.071 15.97,18.68C16.056,18.964 16.227,19.216 16.46,19.4C16.674,19.565 16.94,19.65 17.21,19.64C17.474,19.648 17.732,19.563 17.94,19.4Z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
11
android/app/src/main/res/drawable/ic_player_pause.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="36.0"
|
||||
android:viewportHeight="36.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M13.402,8.296C13.402,6.887 12.259,5.744 10.849,5.744C9.439,5.744 8.296,6.887 8.296,8.296V27.442C8.296,28.852 9.439,29.995 10.849,29.995C12.259,29.995 13.402,28.852 13.402,27.442V8.296ZM27.442,8.296C27.442,6.887 26.299,5.744 24.889,5.744C23.479,5.744 22.337,6.887 22.337,8.296V27.442C22.337,28.852 23.479,29.995 24.889,29.995C26.299,29.995 27.442,28.852 27.442,27.442V8.296Z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_player_play.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="36.0"
|
||||
android:viewportHeight="36.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M29.436,21.035L13.449,31.028C12.545,31.592 11.356,31.318 10.791,30.414C10.599,30.108 10.498,29.754 10.498,29.392L10.498,8.317C10.498,7.252 11.361,6.389 12.427,6.389C12.788,6.389 13.142,6.49 13.449,6.682L29.436,16.674C30.641,17.427 31.007,19.013 30.254,20.218C30.047,20.549 29.767,20.829 29.436,21.035Z" />
|
||||
</vector>
|
||||
11
android/app/src/main/res/drawable/ic_player_rewind_10s.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="31.0"
|
||||
android:viewportHeight="31.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.375,6.79V4.73C16.379,4.627 16.343,4.527 16.275,4.45C16.225,4.377 16.15,4.324 16.065,4.3C15.92,4.277 15.772,4.305 15.645,4.38L10.955,7.16C10.846,7.208 10.753,7.287 10.688,7.386C10.622,7.485 10.587,7.601 10.585,7.72C10.59,7.84 10.625,7.956 10.688,8.058C10.752,8.16 10.84,8.243 10.945,8.3L15.645,11.06C15.716,11.093 15.792,11.11 15.87,11.11C15.948,11.11 16.025,11.093 16.095,11.06C16.182,11.028 16.255,10.968 16.305,10.89C16.361,10.793 16.388,10.682 16.385,10.57V8.74C18.416,8.849 20.329,9.728 21.735,11.197C23.142,12.667 23.935,14.616 23.955,16.65C23.944,17.703 23.724,18.744 23.309,19.712C22.894,20.68 22.292,21.557 21.538,22.292C20.784,23.027 19.891,23.605 18.913,23.994C17.934,24.384 16.888,24.576 15.835,24.56C14.776,24.585 13.723,24.4 12.736,24.015C11.749,23.63 10.849,23.053 10.087,22.317C9.325,21.581 8.717,20.701 8.298,19.728C7.879,18.756 7.657,17.709 7.645,16.65C7.651,15.024 8.172,13.441 9.135,12.13C9.276,11.914 9.327,11.651 9.279,11.398C9.23,11.144 9.086,10.92 8.875,10.77C8.768,10.691 8.646,10.635 8.517,10.604C8.387,10.573 8.253,10.568 8.122,10.589C7.991,10.61 7.865,10.658 7.752,10.728C7.639,10.799 7.542,10.891 7.465,11C6.283,12.655 5.646,14.637 5.645,16.67C5.682,19.33 6.772,21.868 8.677,23.725C10.581,25.583 13.145,26.61 15.805,26.58C18.465,26.61 21.029,25.583 22.934,23.725C24.838,21.868 25.928,19.33 25.965,16.67C25.937,14.108 24.926,11.655 23.141,9.817C21.357,7.978 18.935,6.895 16.375,6.79ZM15.645,19.75C15.886,20.124 16.221,20.428 16.615,20.633C17.01,20.838 17.451,20.937 17.895,20.92C18.337,20.934 18.776,20.833 19.168,20.628C19.56,20.423 19.893,20.121 20.135,19.75C20.675,18.864 20.936,17.836 20.885,16.8C20.935,15.764 20.674,14.737 20.135,13.85C19.89,13.48 19.556,13.178 19.162,12.974C18.768,12.769 18.329,12.668 17.885,12.68C17.442,12.665 17.003,12.764 16.611,12.969C16.218,13.175 15.885,13.478 15.645,13.85C15.084,14.729 14.805,15.758 14.845,16.8C14.804,17.842 15.083,18.872 15.645,19.75ZM16.655,14.88C16.739,14.592 16.91,14.337 17.145,14.15C17.36,13.985 17.625,13.9 17.895,13.91C18.159,13.902 18.417,13.987 18.625,14.15C18.859,14.342 19.036,14.594 19.135,14.88C19.33,15.487 19.418,16.123 19.395,16.76C19.417,17.397 19.329,18.033 19.135,18.64C19.036,18.926 18.859,19.178 18.625,19.37C18.417,19.533 18.159,19.618 17.895,19.61C17.625,19.62 17.36,19.535 17.145,19.37C16.917,19.181 16.747,18.932 16.655,18.65C16.453,18.041 16.362,17.401 16.385,16.76C16.361,16.136 16.449,15.513 16.645,14.92L16.655,14.88ZM10.455,15.43H10.385L10.345,15.39C10.338,15.361 10.338,15.33 10.345,15.3V14.34C10.325,14.261 10.325,14.179 10.345,14.1C10.356,14.077 10.371,14.056 10.39,14.039C10.409,14.022 10.431,14.009 10.455,14L10.955,13.71C11.445,13.42 12.075,13.05 12.385,12.91C12.491,12.859 12.607,12.832 12.725,12.83H13.285C13.365,12.83 13.441,12.862 13.497,12.918C13.554,12.974 13.585,13.051 13.585,13.13V20.46C13.587,20.5 13.58,20.54 13.565,20.577C13.551,20.614 13.529,20.647 13.5,20.675C13.472,20.704 13.439,20.726 13.402,20.74C13.365,20.755 13.325,20.762 13.285,20.76H12.385C12.346,20.76 12.307,20.752 12.27,20.737C12.234,20.722 12.201,20.7 12.173,20.672C12.145,20.645 12.123,20.611 12.108,20.575C12.093,20.539 12.085,20.5 12.085,20.46V14.82C12.094,14.803 12.099,14.784 12.099,14.765C12.099,14.746 12.094,14.727 12.085,14.71C12.067,14.701 12.046,14.696 12.025,14.696C12.004,14.696 11.984,14.701 11.965,14.71L11.555,14.93L10.555,15.43H10.455Z"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
BIN
android/app/src/main/res/drawable/ic_shortcut_download.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/drawable/ic_shortcut_search.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">PiliPlus</string>
|
||||
<string name="search">搜索</string>
|
||||
<string name="offline_video">离线视频</string>
|
||||
</resources>
|
||||
20
android/app/src/main/res/xml-v25/shortcuts.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:icon="@drawable/ic_shortcut_search"
|
||||
android:shortcutId="search"
|
||||
android:shortcutLongLabel="@string/search"
|
||||
android:shortcutShortLabel="@string/search">
|
||||
<intent
|
||||
android:action="com.example.piliplus.SHORTCUT"
|
||||
android:data="bilibili://search" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:icon="@drawable/ic_shortcut_download"
|
||||
android:shortcutId="offline_video"
|
||||
android:shortcutLongLabel="@string/offline_video"
|
||||
android:shortcutShortLabel="@string/offline_video">
|
||||
<intent
|
||||
android:action="com.example.piliplus.SHORTCUT"
|
||||
android:data="bilibili://download" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -43,14 +43,14 @@ subprojects {
|
||||
val pluginCompileSdk = pluginCompileSdkStr
|
||||
?.removePrefix("android-")
|
||||
?.toIntOrNull()
|
||||
if (pluginCompileSdk != null && pluginCompileSdk < 31) {
|
||||
if (pluginCompileSdk != null && pluginCompileSdk < 36) {
|
||||
project.logger.error(
|
||||
"Warning: Overriding compileSdk version in Flutter plugin: ${project.name} " +
|
||||
"from $pluginCompileSdk to 31 (to work around https://issuetracker.google.com/issues/199180389).\n" +
|
||||
"from $pluginCompileSdk to 36 (to work around https://issuetracker.google.com/issues/199180389).\n" +
|
||||
"If there is not a new version of ${project.name}, consider filing an issue against ${project.name} " +
|
||||
"to increase their compileSdk to the latest (otherwise try updating to the latest version)."
|
||||
)
|
||||
androidExtension.setCompileSdkVersion(31)
|
||||
androidExtension.setCompileSdkVersion(36)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=true
|
||||
android.builtInKotlin=false
|
||||
android.newDsl=false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -19,8 +19,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.12.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 4.0625C7.85812 4.0625 5.98983 4.1725 4.67735 4.2798C3.77861 4.35327 3.08174 5.04067 3.00119 5.93221C2.90388 7.00924 2.8125 8.43727 2.8125 10C2.8125 11.5627 2.90388 12.9908 3.00119 14.0678C3.08174 14.9593 3.77861 15.6467 4.67735 15.7202C5.98983 15.8275 7.85812 15.9375 10 15.9375C12.1421 15.9375 14.0105 15.8275 15.323 15.7202C16.2216 15.6467 16.9184 14.9595 16.9989 14.0682C17.0962 12.9916 17.1875 11.5639 17.1875 10C17.1875 8.43614 17.0962 7.00837 16.9989 5.9318C16.9184 5.04049 16.2216 4.3533 15.323 4.27983C14.0105 4.17252 12.1421 4.0625 10 4.0625ZM4.5755 3.03395C5.9136 2.92456 7.81674 2.8125 10 2.8125C12.1835 2.8125 14.0868 2.92458 15.4249 3.03398C16.9228 3.15645 18.108 4.31588 18.2438 5.81931C18.3435 6.92296 18.4375 8.38948 18.4375 10C18.4375 11.6105 18.3435 13.077 18.2438 14.1807C18.108 15.6841 16.9228 16.8436 15.4249 16.966C14.0868 17.0754 12.1835 17.1875 10 17.1875C7.81674 17.1875 5.9136 17.0754 4.5755 16.966C3.07738 16.8436 1.8921 15.6838 1.75626 14.1803C1.65651 13.0762 1.5625 11.6094 1.5625 10C1.5625 8.39058 1.65651 6.92379 1.75626 5.81973C1.8921 4.31616 3.07738 3.15643 4.5755 3.03395ZM5 8.4375C5 8.09232 5.27982 7.8125 5.625 7.8125H5.9375C6.28268 7.8125 6.5625 8.09232 6.5625 8.4375C6.5625 8.78268 6.28268 9.0625 5.9375 9.0625H5.625C5.27982 9.0625 5 8.78268 5 8.4375ZM7.5 8.4375C7.5 8.09232 7.77982 7.8125 8.125 7.8125H13.125C13.4702 7.8125 13.75 8.09232 13.75 8.4375C13.75 8.78268 13.4702 9.0625 13.125 9.0625H8.125C7.77982 9.0625 7.5 8.78268 7.5 8.4375ZM6.875 10.9375C6.52982 10.9375 6.25 11.2173 6.25 11.5625C6.25 11.9077 6.52982 12.1875 6.875 12.1875H7.1875C7.53268 12.1875 7.8125 11.9077 7.8125 11.5625C7.8125 11.2173 7.53268 10.9375 7.1875 10.9375H6.875ZM9.375 10.9375C9.02982 10.9375 8.75 11.2173 8.75 11.5625C8.75 11.9077 9.02982 12.1875 9.375 12.1875H14.375C14.7202 12.1875 15 11.9077 15 11.5625C15 11.2173 14.7202 10.9375 14.375 10.9375H9.375Z" fill="#999"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="M17.982 9.275L8.06 3.27A2.013 2.013 0 005 4.994v12.011a2.017 2.017 0 003.06 1.725l9.922-6.005a2.017 2.017 0 000-3.45z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 201 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="M16.118 3.667h.382a3.667 3.667 0 013.667 3.667v7.333a3.667 3.667 0 01-3.667 3.667h-11a3.667 3.667 0 01-3.667-3.667V7.333A3.667 3.667 0 015.5 3.666h.382L4.95 2.053a1.1 1.1 0 011.906-1.1l1.567 2.714h5.156L15.146.953a1.101 1.101 0 011.906 1.1l-.934 1.614z" fill="#333" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#c3beb7;"></path><path d="M5.561 5.194h10.878a2.2 2.2 0 012.2 2.2v7.211a2.2 2.2 0 01-2.2 2.2H5.561a2.2 2.2 0 01-2.2-2.2V7.394a2.2 2.2 0 012.2-2.2z" fill="#fff" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#e1ded9;"></path><path d="M6.967 8.556a1.1 1.1 0 011.1 1.1v2.689a1.1 1.1 0 11-2.2 0V9.656a1.1 1.1 0 011.1-1.1zM15.033 8.556a1.1 1.1 0 011.1 1.1v2.689a1.1 1.1 0 11-2.2 0V9.656a1.1 1.1 0 011.1-1.1z" fill="#333" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#c3beb7;"></path></svg>
|
||||
|
Before Width: | Height: | Size: 916 B |
@@ -1 +0,0 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 2.0625C6.85812 2.0625 4.98983 2.1725 3.67735 2.2798C2.77861 2.35327 2.08174 3.04067 2.00119 3.93221C1.90388 5.00924 1.8125 6.43727 1.8125 8C1.8125 9.56273 1.90388 10.9908 2.00119 12.0678C2.08174 12.9593 2.77861 13.6467 3.67735 13.7202C4.98983 13.8275 6.85812 13.9375 9 13.9375C11.1421 13.9375 13.0105 13.8275 14.323 13.7202C15.2216 13.6467 15.9184 12.9595 15.9989 12.0682C16.0962 10.9916 16.1875 9.56386 16.1875 8C16.1875 6.43614 16.0962 5.00837 15.9989 3.9318C15.9184 3.04049 15.2216 2.3533 14.323 2.27983C13.0105 2.17252 11.1421 2.0625 9 2.0625ZM3.5755 1.03395C4.9136 0.924562 6.81674 0.8125 9 0.8125C11.1835 0.8125 13.0868 0.924583 14.4249 1.03398C15.9228 1.15645 17.108 2.31588 17.2438 3.81931C17.3435 4.92296 17.4375 6.38948 17.4375 8C17.4375 9.61052 17.3435 11.077 17.2438 12.1807C17.108 13.6841 15.9228 14.8436 14.4249 14.966C13.0868 15.0754 11.1835 15.1875 9 15.1875C6.81674 15.1875 4.9136 15.0754 3.5755 14.966C2.07738 14.8436 0.892104 13.6838 0.756256 12.1803C0.656505 11.0762 0.5625 9.60942 0.5625 8C0.5625 6.39058 0.656505 4.92379 0.756257 3.81973C0.892104 2.31616 2.07738 1.15643 3.5755 1.03395ZM4.41663 4.93726C4.72729 4.93726 4.97913 5.1891 4.97913 5.49976V8.62476C4.97913 9.34963 5.56675 9.93726 6.29163 9.93726C7.0165 9.93726 7.60413 9.34963 7.60413 8.62476V5.49976C7.60413 5.1891 7.85597 4.93726 8.16663 4.93726C8.47729 4.93726 8.72913 5.1891 8.72913 5.49976V8.62476C8.72913 9.97095 7.63782 11.0623 6.29163 11.0623C4.94543 11.0623 3.85413 9.97095 3.85413 8.62476V5.49976C3.85413 5.1891 4.10597 4.93726 4.41663 4.93726ZM10.2501 4.93726C9.9394 4.93726 9.68756 5.1891 9.68756 5.49976V10.4998C9.68756 10.8104 9.9394 11.0623 10.2501 11.0623C10.5607 11.0623 10.8126 10.8104 10.8126 10.4998V9.60392H12.2292C13.5179 9.60392 14.5626 8.55925 14.5626 7.27059C14.5626 5.98193 13.5179 4.93726 12.2292 4.93726H10.2501ZM12.2292 8.47892H10.8126V6.06226H12.2292C12.8966 6.06226 13.4376 6.60325 13.4376 7.27059C13.4376 7.93793 12.8966 8.47892 12.2292 8.47892Z" fill="#999"></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24"><path fill-rule="evenodd" d="m8.085 4.891-.999-1.499a1.008 1.008 0 0 1 1.679-1.118l1.709 2.566c.54-.008 1.045-.012 1.515-.012h.13c.345 0 .707.003 1.088.007l1.862-2.59a1.008 1.008 0 0 1 1.637 1.177l-1.049 1.46c.788.02 1.631.046 2.53.078 1.958.069 3.468 1.6 3.74 3.507.088.613.13 2.158.16 3.276l.001.027c.01.333.017.63.025.856a.987.987 0 0 1-1.974.069c-.008-.23-.016-.539-.025-.881v-.002c-.028-1.103-.066-2.541-.142-3.065-.143-1.004-.895-1.78-1.854-1.813-2.444-.087-4.466-.13-6.064-.131-1.598 0-3.619.044-6.063.13a2.037 2.037 0 0 0-1.945 1.748c-.15 1.04-.225 2.341-.225 3.904 0 1.874.11 3.474.325 4.798.154.949.95 1.66 1.91 1.708a97.58 97.58 0 0 0 5.416.139.988.988 0 0 1 0 1.975c-2.196 0-3.61-.047-5.513-.141A4.012 4.012 0 0 1 2.197 17.7c-.236-1.446-.351-3.151-.351-5.116 0-1.64.08-3.035.245-4.184A4.013 4.013 0 0 1 5.92 4.96c.761-.027 1.483-.05 2.164-.069Zm4.436 4.707h-1.32v4.63h2.222v.848h-2.618v1.078h2.431a5.01 5.01 0 0 1 3.575-3.115V9.598h-1.276a8.59 8.59 0 0 0 .748-1.42l-1.089-.384a14.232 14.232 0 0 1-.814 1.804h-1.518l.693-.308a8.862 8.862 0 0 0-.814-1.408l-1.045.352c.297.396.572.847.825 1.364Zm-4.18 3.564.154-1.485h1.98V8.289h-3.2v.979h2.067v1.43H7.483l-.308 3.454h2.277c0 1.166-.044 1.925-.12 2.277-.078.352-.386.528-.936.528-.308 0-.616-.022-.902-.055l.297 1.067.062.004c.285.02.551.04.818.04 1.001-.066 1.562-.418 1.694-1.056.11-.638.176-1.903.176-3.795h-2.2Zm7.458.11v-.858h-1.254v.858H15.8Zm-2.376-.858v.858h-1.199v-.858h1.2Zm-1.199-.946h1.2v-.902h-1.2v.902Zm2.321 0v-.902H15.8v.902h-1.254Zm3.517 10.594a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-.002-1.502a2.5 2.5 0 0 1-2.217-3.657l3.326 3.398a2.49 2.49 0 0 1-1.109.259Zm2.5-2.5c0 .42-.103.815-.286 1.162l-3.328-3.401a2.5 2.5 0 0 1 3.614 2.239Z" clip-rule="evenodd"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24" id="svgcontent" overflow="visible" width="24" height="24" x="597" y="286"><g class="layer" style="pointer-events:all"><title style="pointer-events:inherit">Layer 1</title><path fill-rule="evenodd" d="M11.99,4.83C11.52,4.83 11.01,4.83 10.47,4.84L8.76,2.27A1.01,1.01 0 0 0 7.09,3.39L8.08,4.89C7.4,4.91 6.68,4.93 5.92,4.96A4.01,4.01 0 0 0 2.09,8.4C1.93,9.55 1.85,10.94 1.85,12.58C1.85,14.55 1.96,16.25 2.2,17.7A4.01,4.01 0 0 0 5.96,21.06L6.86,21.11C8.07,21.17 8.67,21.2 10.47,21.2A0.99,0.99 0 0 0 10.47,19.23C8.71,19.23 8.13,19.2 6.97,19.14L6.06,19.09A2.04,2.04 0 0 1 4.15,17.38C3.93,16.06 3.82,14.46 3.82,12.58C3.82,11.02 3.9,9.72 4.05,8.68C4.19,7.7 5.01,6.97 5.99,6.93C8.43,6.85 10.46,6.8 12.05,6.8C13.65,6.8 15.67,6.85 18.12,6.93C19.08,6.97 19.83,7.74 19.97,8.75C20.05,9.27 20.09,10.71 20.11,11.81L20.11,11.81C20.12,12.16 20.13,12.46 20.14,12.69A0.99,0.99 0 1 0 22.11,12.63C22.1,12.4 22.1,12.1 22.09,11.77L22.09,11.74C22.06,10.62 22.01,9.08 21.93,8.47C21.65,6.56 20.14,5.03 18.19,4.96C17.29,4.93 16.44,4.9 15.66,4.88L16.71,3.42A1.01,1.01 0 0 0 15.07,2.24L13.21,4.83C12.83,4.83 12.46,4.83 12.12,4.83L11.99,4.83zM12.51,9.6L11.19,9.6L11.19,14.23L13.41,14.23L13.41,15.08L10.79,15.08L10.79,16.16L13.41,16.16L13.42,16.84C13.78,16.86 14.13,17 14.43,17.24L14.54,17.24L14.54,16.16L17.23,16.16L17.23,15.08L14.53,15.08L14.53,14.23L16.8,14.23L16.8,9.6L15.52,9.6A8.59,8.59 0 0 0 16.27,8.18L15.18,7.8A14.23,14.23 0 0 1 14.37,9.6L12.85,9.6L13.54,9.3A8.86,8.86 0 0 0 12.73,7.89L11.68,8.24C11.98,8.64 12.26,9.09 12.51,9.6zM8.33,13.17L8.48,11.68L10.46,11.68L10.46,8.29L7.26,8.29L7.26,9.27L9.33,9.27L9.33,10.7L7.47,10.7L7.16,14.16L9.44,14.16C9.44,15.32 9.4,16.08 9.32,16.43C9.24,16.79 8.94,16.96 8.39,16.96C8.08,16.96 7.77,16.94 7.48,16.91L7.78,17.97L7.84,17.98C8.13,18 8.39,18.02 8.66,18.02C9.66,17.95 10.22,17.6 10.35,16.96C10.46,16.32 10.53,15.06 10.53,13.17L8.33,13.17zM15.79,13.28L15.79,12.42L14.53,12.42L14.53,13.28L15.79,13.28zM13.41,12.42L13.41,13.28L12.21,13.28L12.21,12.42L13.41,12.42zM12.21,11.47L13.41,11.47L13.41,10.57L12.21,10.57L12.21,11.47zM14.53,11.47L14.53,10.57L15.79,10.57L15.79,11.47L14.53,11.47z" clip-rule="evenodd" id="svg_1"></path><path fill="#000000" fill-rule="evenodd" d="M22.85,14.63A1,1 0 0 0 21.43,14.7L16.34,20.41L14.13,18.13L14.03,18.04L14.02,18.04A1,1 0 0 0 12.7,19.53L15.66,22.57L15.76,22.66L15.76,22.66C16.17,22.98 16.76,22.93 17.12,22.54L22.93,16.03L23.01,15.93L23.01,15.92A1,1 0 0 0 22.85,14.63z" clip-rule="evenodd" id="svg_2"></path></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.67735 4.2798C5.98983 4.1725 7.85812 4.0625 10 4.0625C12.1421 4.0625 14.0105 4.17252 15.323 4.27983C16.2216 4.3533 16.9184 5.04049 16.9989 5.9318C17.0962 7.00837 17.1875 8.43614 17.1875 10C17.1875 11.5639 17.0962 12.9916 16.9989 14.0682C16.9184 14.9595 16.2216 15.6467 15.323 15.7202C14.0105 15.8275 12.1421 15.9375 10 15.9375C7.85812 15.9375 5.98983 15.8275 4.67735 15.7202C3.77861 15.6467 3.08174 14.9593 3.00119 14.0678C2.90388 12.9908 2.8125 11.5627 2.8125 10C2.8125 8.43727 2.90388 7.00924 3.00119 5.93221C3.08174 5.04067 3.77861 4.35327 4.67735 4.2798ZM10 2.8125C7.81674 2.8125 5.9136 2.92456 4.5755 3.03395C3.07738 3.15643 1.8921 4.31616 1.75626 5.81973C1.65651 6.92379 1.5625 8.39058 1.5625 10C1.5625 11.6094 1.65651 13.0762 1.75626 14.1803C1.8921 15.6838 3.07738 16.8436 4.5755 16.966C5.9136 17.0754 7.81674 17.1875 10 17.1875C12.1835 17.1875 14.0868 17.0754 15.4249 16.966C16.9228 16.8436 18.108 15.6841 18.2438 14.1807C18.3435 13.077 18.4375 11.6105 18.4375 10C18.4375 8.38948 18.3435 6.92296 18.2438 5.81931C18.108 4.31588 16.9228 3.15645 15.4249 3.03398C14.0868 2.92458 12.1835 2.8125 10 2.8125ZM12.1876 10.722C12.7431 10.4013 12.7431 9.59941 12.1876 9.27866L9.06133 7.47373C8.50577 7.15298 7.81133 7.55392 7.81133 8.19542V11.8053C7.81133 12.4468 8.50577 12.8477 9.06133 12.527L12.1876 10.722Z" fill="#999"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -3,18 +3,18 @@
|
||||
ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus
|
||||
chmod +x /usr/bin/piliplus
|
||||
|
||||
if [ $1 == "config" ] && [ -x /usr/binupdate-mime-database ]; then
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/update-mime-database ]; then
|
||||
echo "updating mime database..."
|
||||
update-mime-database /usr/share/mime || true
|
||||
fi
|
||||
|
||||
if [ $1 == "config" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
echo "updating icon cache..."
|
||||
gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true
|
||||
fi
|
||||
|
||||
if [ $1 == "config" ] && [ -x /usr/bin/update-desktop-database ]; then
|
||||
echo "updating desktop database..."
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/update-desktop-database ]; then
|
||||
echo "configure desktop database..."
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
Exec=piliplus
|
||||
Icon=piliplus
|
||||
Terminal=false
|
||||
StartupWMClass=com.example.piliplus
|
||||
Categories=Video;AudioVideo;Player;
|
||||
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '13.0'
|
||||
platform :ios, '14.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
142
ios/Podfile.lock
@@ -6,7 +6,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- auto_orientation (0.0.1):
|
||||
- battery_plus (1.0.0):
|
||||
- Flutter
|
||||
- chat_bottom_container (0.0.1):
|
||||
- Flutter
|
||||
@@ -14,40 +14,9 @@ PODS:
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.9)
|
||||
- DKPhotoGallery (0.0.19):
|
||||
- DKPhotoGallery/Core (= 0.0.19)
|
||||
- DKPhotoGallery/Model (= 0.0.19)
|
||||
- DKPhotoGallery/Preview (= 0.0.19)
|
||||
- DKPhotoGallery/Resource (= 0.0.19)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Flutter (1.0.0)
|
||||
- flutter_inappwebview_ios (0.0.1):
|
||||
- Flutter
|
||||
@@ -67,10 +36,10 @@ PODS:
|
||||
- gt3_flutter_plugin (0.0.9):
|
||||
- Flutter
|
||||
- GT3Captcha-iOS
|
||||
- GT3Captcha-iOS (0.15.8.3)
|
||||
- image_cropper (0.0.4):
|
||||
- GT3Captcha-iOS (0.15.9)
|
||||
- image_cropper (0.0.5):
|
||||
- Flutter
|
||||
- TOCropViewController (~> 2.8.0)
|
||||
- TOCropViewController (~> 3.1.2)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- live_photo_maker (0.0.3):
|
||||
@@ -81,21 +50,17 @@ PODS:
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- native_device_orientation (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- saver_gallery (0.0.1):
|
||||
- Flutter
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -104,8 +69,7 @@ PODS:
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- TOCropViewController (2.8.0)
|
||||
- TOCropViewController (3.1.2)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
@@ -115,11 +79,11 @@ DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
|
||||
- battery_plus (from `.symlinks/plugins/battery_plus/ios`)
|
||||
- chat_bottom_container (from `.symlinks/plugins/chat_bottom_container/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/darwin`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
||||
@@ -133,8 +97,8 @@ DEPENDENCIES:
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
@@ -146,12 +110,8 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- GT3Captcha-iOS
|
||||
- OrderedSet
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- TOCropViewController
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -161,8 +121,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/audio_service/darwin"
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
auto_orientation:
|
||||
:path: ".symlinks/plugins/auto_orientation/ios"
|
||||
battery_plus:
|
||||
:path: ".symlinks/plugins/battery_plus/ios"
|
||||
chat_bottom_container:
|
||||
:path: ".symlinks/plugins/chat_bottom_container/ios"
|
||||
connectivity_plus:
|
||||
@@ -170,7 +130,7 @@ EXTERNAL SOURCES:
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
:path: ".symlinks/plugins/file_picker/darwin"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_inappwebview_ios:
|
||||
@@ -197,10 +157,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
native_device_orientation:
|
||||
:path: ".symlinks/plugins/native_device_orientation/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
saver_gallery:
|
||||
@@ -219,45 +179,41 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
|
||||
audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910
|
||||
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
|
||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||
chat_bottom_container: d8b077152c91b0ab90001e900748ea50353a5520
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
|
||||
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
|
||||
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||
battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977
|
||||
chat_bottom_container: f1eb8323db77a87db50f361142c679f11e892d1b
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
|
||||
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
||||
image_cropper: b8ef14d3fcff4040b0f9da2ca28d98219a5cba0e
|
||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
||||
live_photo_maker: 7d57bfc70a120b4673c10871f354f4b1b6fde5fd
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
gt3_flutter_plugin: 37090e5fa66ff2a52939eb9d208fc36fa49d36e5
|
||||
GT3Captcha-iOS: aeb6fed2e8594099821430a89208679e5a55b740
|
||||
image_cropper: fca51f94982730acae168c4b5d691e0f11aeb259
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
live_photo_maker: 29280ca88323bd5a33aafd00d98624d5cf522176
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
native_device_orientation: e3580675687d5034770da198f6839ebf2122ef94
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
|
||||
screen_brightness_ios: 6a6f7794b67f07c4f1e24f6374b2d8ad367ffb39
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
|
||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
screen_brightness_ios: 9953fd7da5bd480f1a93990daeec2eb42d4f3b52
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
TOCropViewController: a916930c465b5d9445a74d95e0c0da931771b4df
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: f62db4fb414ebdecb264109948f76dfef35fdc3d
|
||||
PODFILE CHECKSUM: 5e755568c318fde60f7b59d132a4ba634d53bf27
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -438,7 +438,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -487,7 +487,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -131,5 +131,13 @@
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>需要访问本地网络以发现和连接 DLNA 投屏设备</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_ssdp._udp</string>
|
||||
<string>_upnp._tcp</string>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class BuildConfig {
|
||||
abstract final class BuildConfig {
|
||||
static const int versionCode = int.fromEnvironment(
|
||||
'pili.code',
|
||||
defaultValue: 1,
|
||||
|
||||
57
lib/common/assets.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
abstract final class Assets {
|
||||
static const digitalNum = 'digital_id_num';
|
||||
|
||||
static const logo = 'assets/images/logo/logo.png';
|
||||
static const logo2 = 'assets/images/logo/logo_2.png';
|
||||
static const logoIco = 'assets/images/logo/ico/app_icon.ico';
|
||||
static const logoLarge = 'assets/images/logo/desktop/logo_large.png';
|
||||
|
||||
static const vipIcon = 'assets/images/big-vip.png';
|
||||
static const avatarPlaceHolder = 'assets/images/noface.jpeg';
|
||||
static const loading = 'assets/images/loading.png';
|
||||
static const buffering = 'assets/images/loading.webp';
|
||||
static const play = 'assets/images/play.png';
|
||||
static const topicHeader = 'assets/images/topic-header-bg.png';
|
||||
static const trendingBanner = 'assets/images/trending_banner.png';
|
||||
static const ai = 'assets/images/ai.png';
|
||||
static const error = 'assets/images/error.svg';
|
||||
|
||||
static const livingChart = 'assets/images/live.gif';
|
||||
static const livingStatic = 'assets/images/live.png';
|
||||
static const livingRect = 'assets/images/live/live.gif';
|
||||
static const livingBackground = 'assets/images/live/default_bg.webp';
|
||||
|
||||
static const thunder1 = 'assets/images/paycoins/ic_thunder_1.png';
|
||||
static const thunder2 = 'assets/images/paycoins/ic_thunder_2.png';
|
||||
static const thunder3 = 'assets/images/paycoins/ic_thunder_3.png';
|
||||
static const notEnough = 'assets/images/paycoins/ic_22_not_enough_pay.png';
|
||||
static const mario = 'assets/images/paycoins/ic_22_mario.png';
|
||||
static const gunSister = 'assets/images/paycoins/ic_22_gun_sister.png';
|
||||
static const payBox = 'assets/images/paycoins/ic_pay_coins_box.png';
|
||||
static const coinsOne = 'assets/images/paycoins/ic_coins_one.png';
|
||||
static const coinsTwo = 'assets/images/paycoins/ic_coins_two.png';
|
||||
static const left = 'assets/images/paycoins/ic_left.png';
|
||||
static const leftDisable = 'assets/images/paycoins/ic_left_disable.png';
|
||||
static const right = 'assets/images/paycoins/ic_right.png';
|
||||
static const rightDisable = 'assets/images/paycoins/ic_right_disable.png';
|
||||
static const panelClose = 'assets/images/paycoins/ic_panel_close.png';
|
||||
|
||||
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 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',
|
||||
];
|
||||
}
|
||||
@@ -1,17 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StyleString {
|
||||
static const double cardSpace = 8;
|
||||
static const double safeSpace = 12;
|
||||
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
||||
static const Radius imgRadius = Radius.circular(10);
|
||||
static const double aspectRatio = 16 / 10;
|
||||
static const bottomSheetRadius = BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
);
|
||||
}
|
||||
|
||||
class Constants {
|
||||
abstract final class Constants {
|
||||
static const appName = 'PiliPlus';
|
||||
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
|
||||
|
||||
@@ -20,9 +7,9 @@ class Constants {
|
||||
static const String appKey = 'dfca71928277209b';
|
||||
// 59b43e04ad6965f34319062b478f83dd TV端
|
||||
static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e';
|
||||
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
||||
static const String thirdApi =
|
||||
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
|
||||
// static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
||||
// static const String thirdApi =
|
||||
// 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
|
||||
|
||||
static const String traceId =
|
||||
'11111111111111111111111111111111:1111111111111111:0:0';
|
||||
@@ -52,243 +39,6 @@ class Constants {
|
||||
|
||||
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
|
||||
|
||||
// 超分辨率滤镜
|
||||
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 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 internationalDialingPrefix = [
|
||||
(id: 1, cname: "中国大陆", countryId: 86),
|
||||
(id: 5, cname: "中国香港特别行政区", countryId: 852),
|
||||
(id: 2, cname: "中国澳门特别行政区", countryId: 853),
|
||||
(id: 3, cname: "中国台湾", countryId: 886),
|
||||
(id: 4, cname: "美国", countryId: 1),
|
||||
(id: 6, cname: "比利时", countryId: 32),
|
||||
(id: 7, cname: "澳大利亚", countryId: 61),
|
||||
(id: 8, cname: "法国", countryId: 33),
|
||||
(id: 9, cname: "加拿大", countryId: 1),
|
||||
(id: 10, cname: "日本", countryId: 81),
|
||||
(id: 11, cname: "新加坡", countryId: 65),
|
||||
(id: 12, cname: "韩国", countryId: 82),
|
||||
(id: 13, cname: "马来西亚", countryId: 60),
|
||||
(id: 14, cname: "英国", countryId: 44),
|
||||
(id: 15, cname: "意大利", countryId: 39),
|
||||
(id: 16, cname: "德国", countryId: 49),
|
||||
(id: 18, cname: "俄罗斯", countryId: 7),
|
||||
(id: 19, cname: "新西兰", countryId: 64),
|
||||
(id: 153, cname: "瓦利斯群岛和富图纳群岛", countryId: 1681),
|
||||
(id: 152, cname: "葡萄牙", countryId: 351),
|
||||
(id: 151, cname: "帕劳", countryId: 680),
|
||||
(id: 150, cname: "诺福克岛", countryId: 672),
|
||||
(id: 149, cname: "挪威", countryId: 47),
|
||||
(id: 148, cname: "纽埃岛", countryId: 683),
|
||||
(id: 147, cname: "尼日利亚", countryId: 234),
|
||||
(id: 146, cname: "尼日尔", countryId: 227),
|
||||
(id: 145, cname: "尼加拉瓜", countryId: 505),
|
||||
(id: 144, cname: "尼泊尔", countryId: 977),
|
||||
(id: 143, cname: "瑙鲁", countryId: 674),
|
||||
(id: 154, cname: "格鲁吉亚", countryId: 995),
|
||||
(id: 155, cname: "瑞典", countryId: 46),
|
||||
(id: 165, cname: "沙特阿拉伯", countryId: 966),
|
||||
(id: 164, cname: "桑给巴尔岛", countryId: 259),
|
||||
(id: 163, cname: "塞舌尔共和国", countryId: 248),
|
||||
(id: 162, cname: "塞浦路斯", countryId: 357),
|
||||
(id: 161, cname: "塞内加尔", countryId: 221),
|
||||
(id: 160, cname: "塞拉利昂", countryId: 232),
|
||||
(id: 159, cname: "萨摩亚,东部", countryId: 684),
|
||||
(id: 158, cname: "萨摩亚,西部", countryId: 685),
|
||||
(id: 157, cname: "萨尔瓦多", countryId: 503),
|
||||
(id: 156, cname: "瑞士", countryId: 41),
|
||||
(id: 166, cname: "圣多美和普林西比", countryId: 239),
|
||||
(id: 142, cname: "塞尔维亚", countryId: 381),
|
||||
(id: 141, cname: "南非", countryId: 27),
|
||||
(id: 128, cname: "毛里塔尼亚", countryId: 222),
|
||||
(id: 127, cname: "毛里求斯", countryId: 230),
|
||||
(id: 126, cname: "马歇尔岛", countryId: 692),
|
||||
(id: 125, cname: "马提尼克岛", countryId: 596),
|
||||
(id: 124, cname: "马其顿", countryId: 389),
|
||||
(id: 123, cname: "马里亚纳岛", countryId: 1670),
|
||||
(id: 122, cname: "马里", countryId: 223),
|
||||
(id: 121, cname: "马拉维", countryId: 265),
|
||||
(id: 120, cname: "马耳他", countryId: 356),
|
||||
(id: 119, cname: "马尔代夫", countryId: 960),
|
||||
(id: 129, cname: "蒙古", countryId: 976),
|
||||
(id: 130, cname: "蒙特塞拉特岛", countryId: 1664),
|
||||
(id: 140, cname: "纳米比亚", countryId: 264),
|
||||
(id: 139, cname: "墨西哥", countryId: 52),
|
||||
(id: 138, cname: "莫桑比克", countryId: 258),
|
||||
(id: 137, cname: "摩纳哥", countryId: 377),
|
||||
(id: 136, cname: "摩洛哥", countryId: 212),
|
||||
(id: 135, cname: "摩尔多瓦", countryId: 373),
|
||||
(id: 134, cname: "缅甸", countryId: 95),
|
||||
(id: 133, cname: "密克罗尼西亚", countryId: 691),
|
||||
(id: 132, cname: "秘鲁", countryId: 51),
|
||||
(id: 131, cname: "孟加拉国", countryId: 880),
|
||||
(id: 118, cname: "马达加斯加", countryId: 261),
|
||||
(id: 167, cname: "圣卢西亚", countryId: 1784),
|
||||
(id: 216, cname: "智利", countryId: 56),
|
||||
(id: 203, cname: "牙买加", countryId: 1876),
|
||||
(id: 202, cname: "叙利亚", countryId: 963),
|
||||
(id: 201, cname: "匈牙利", countryId: 36),
|
||||
(id: 200, cname: "科特迪瓦", countryId: 225),
|
||||
(id: 199, cname: "希腊", countryId: 30),
|
||||
(id: 198, cname: "西班牙", countryId: 34),
|
||||
(id: 197, cname: "乌兹别克斯坦", countryId: 998),
|
||||
(id: 196, cname: "乌拉圭", countryId: 598),
|
||||
(id: 195, cname: "乌克兰", countryId: 380),
|
||||
(id: 194, cname: "乌干达", countryId: 256),
|
||||
(id: 204, cname: "亚美尼亚", countryId: 374),
|
||||
(id: 205, cname: "也门", countryId: 967),
|
||||
(id: 215, cname: "直布罗陀", countryId: 350),
|
||||
(id: 214, cname: "乍得", countryId: 235),
|
||||
(id: 213, cname: "赞比亚", countryId: 260),
|
||||
(id: 212, cname: "越南", countryId: 84),
|
||||
(id: 211, cname: "约旦", countryId: 962),
|
||||
(id: 210, cname: "印尼", countryId: 62),
|
||||
(id: 209, cname: "印度", countryId: 91),
|
||||
(id: 208, cname: "以色列", countryId: 972),
|
||||
(id: 207, cname: "伊朗", countryId: 98),
|
||||
(id: 206, cname: "伊拉克", countryId: 964),
|
||||
(id: 193, cname: "文莱", countryId: 673),
|
||||
(id: 192, cname: "委内瑞拉", countryId: 58),
|
||||
(id: 191, cname: "维珍群岛(英属)", countryId: 1284),
|
||||
(id: 178, cname: "泰国", countryId: 66),
|
||||
(id: 177, cname: "索马里", countryId: 252),
|
||||
(id: 176, cname: "所罗门群岛", countryId: 677),
|
||||
(id: 175, cname: "苏里南", countryId: 597),
|
||||
(id: 174, cname: "苏丹", countryId: 249),
|
||||
(id: 173, cname: "斯威士兰", countryId: 268),
|
||||
(id: 172, cname: "斯洛文尼亚", countryId: 386),
|
||||
(id: 171, cname: "斯洛伐克", countryId: 421),
|
||||
(id: 170, cname: "斯里兰卡", countryId: 94),
|
||||
(id: 169, cname: "圣皮埃尔和密克隆群岛", countryId: 508),
|
||||
(id: 179, cname: "坦桑尼亚", countryId: 255),
|
||||
(id: 180, cname: "汤加", countryId: 676),
|
||||
(id: 190, cname: "维珍群岛(美属)", countryId: 1340),
|
||||
(id: 189, cname: "瓦努阿图", countryId: 678),
|
||||
(id: 188, cname: "托克劳岛", countryId: 690),
|
||||
(id: 187, cname: "土库曼斯坦", countryId: 993),
|
||||
(id: 186, cname: "土耳其", countryId: 90),
|
||||
(id: 185, cname: "图瓦卢", countryId: 688),
|
||||
(id: 184, cname: "突尼斯", countryId: 216),
|
||||
(id: 183, cname: "阿森松岛", countryId: 247),
|
||||
(id: 182, cname: "特立尼达和多巴哥", countryId: 1868),
|
||||
(id: 181, cname: "特克斯和凯科斯", countryId: 1649),
|
||||
(id: 168, cname: "圣马力诺", countryId: 378),
|
||||
(id: 67, cname: "法属圭亚那", countryId: 594),
|
||||
(id: 54, cname: "不丹", countryId: 975),
|
||||
(id: 53, cname: "博茨瓦纳", countryId: 267),
|
||||
(id: 52, cname: "伯利兹", countryId: 501),
|
||||
(id: 51, cname: "玻利维亚", countryId: 591),
|
||||
(id: 50, cname: "波兰", countryId: 48),
|
||||
(id: 49, cname: "波黑", countryId: 387),
|
||||
(id: 48, cname: "波多黎各", countryId: 1787),
|
||||
(id: 47, cname: "冰岛", countryId: 354),
|
||||
(id: 46, cname: "贝宁", countryId: 229),
|
||||
(id: 45, cname: "保加利亚", countryId: 359),
|
||||
(id: 55, cname: "布基纳法索", countryId: 226),
|
||||
(id: 56, cname: "布隆迪", countryId: 257),
|
||||
(id: 66, cname: "法属波利尼西亚", countryId: 689),
|
||||
(id: 65, cname: "法罗岛", countryId: 298),
|
||||
(id: 64, cname: "厄立特里亚", countryId: 291),
|
||||
(id: 63, cname: "厄瓜多尔", countryId: 593),
|
||||
(id: 62, cname: "多米尼加代表", countryId: 1809),
|
||||
(id: 61, cname: "多米尼加", countryId: 1767),
|
||||
(id: 60, cname: "多哥", countryId: 228),
|
||||
(id: 59, cname: "迪戈加西亚岛", countryId: 246),
|
||||
(id: 58, cname: "丹麦", countryId: 45),
|
||||
(id: 57, cname: "赤道几内亚", countryId: 240),
|
||||
(id: 44, cname: "百慕大群岛", countryId: 1441),
|
||||
(id: 43, cname: "白俄罗斯", countryId: 375),
|
||||
(id: 42, cname: "巴西", countryId: 55),
|
||||
(id: 29, cname: "爱尔兰", countryId: 353),
|
||||
(id: 28, cname: "埃塞俄比亚", countryId: 251),
|
||||
(id: 27, cname: "埃及", countryId: 20),
|
||||
(id: 26, cname: "阿塞拜疆", countryId: 994),
|
||||
(id: 25, cname: "阿曼", countryId: 968),
|
||||
(id: 24, cname: "阿联酋", countryId: 971),
|
||||
(id: 23, cname: "阿根廷", countryId: 54),
|
||||
(id: 22, cname: "阿富汗", countryId: 93),
|
||||
(id: 21, cname: "阿尔及利亚", countryId: 213),
|
||||
(id: 20, cname: "阿尔巴尼亚", countryId: 355),
|
||||
(id: 30, cname: "爱沙尼亚", countryId: 372),
|
||||
(id: 31, cname: "安道尔", countryId: 376),
|
||||
(id: 41, cname: "巴拿马", countryId: 507),
|
||||
(id: 40, cname: "巴林", countryId: 973),
|
||||
(id: 39, cname: "巴拉圭", countryId: 595),
|
||||
(id: 38, cname: "巴基斯坦", countryId: 92),
|
||||
(id: 37, cname: "巴哈马群岛", countryId: 1242),
|
||||
(id: 36, cname: "巴布亚新几内亚", countryId: 675),
|
||||
(id: 35, cname: "巴巴多斯", countryId: 1246),
|
||||
(id: 34, cname: "奥地利", countryId: 43),
|
||||
(id: 33, cname: "安提瓜岛和巴布达", countryId: 1268),
|
||||
(id: 32, cname: "安哥拉", countryId: 244),
|
||||
(id: 68, cname: "非洲中部", countryId: 236),
|
||||
(id: 117, cname: "罗马尼亚", countryId: 40),
|
||||
(id: 104, cname: "科威特", countryId: 965),
|
||||
(id: 103, cname: "科摩罗", countryId: 269),
|
||||
(id: 102, cname: "开曼群岛", countryId: 1345),
|
||||
(id: 101, cname: "卡塔尔", countryId: 974),
|
||||
(id: 100, cname: "喀麦隆", countryId: 237),
|
||||
(id: 99, cname: "聚会岛", countryId: 262),
|
||||
(id: 98, cname: "津巴布韦", countryId: 263),
|
||||
(id: 97, cname: "捷克", countryId: 420),
|
||||
(id: 96, cname: "柬埔寨", countryId: 855),
|
||||
(id: 95, cname: "加蓬", countryId: 241),
|
||||
(id: 105, cname: "克罗地亚", countryId: 385),
|
||||
(id: 106, cname: "肯尼亚", countryId: 254),
|
||||
(id: 116, cname: "卢旺达", countryId: 250),
|
||||
(id: 115, cname: "卢森堡", countryId: 352),
|
||||
(id: 114, cname: "利比亚", countryId: 218),
|
||||
(id: 113, cname: "利比里亚", countryId: 231),
|
||||
(id: 112, cname: "立陶宛", countryId: 370),
|
||||
(id: 111, cname: "黎巴嫩", countryId: 961),
|
||||
(id: 110, cname: "老挝", countryId: 856),
|
||||
(id: 109, cname: "莱索托", countryId: 266),
|
||||
(id: 108, cname: "拉脱维亚", countryId: 371),
|
||||
(id: 107, cname: "库克岛", countryId: 682),
|
||||
(id: 94, cname: "加纳", countryId: 233),
|
||||
(id: 93, cname: "几内亚比绍", countryId: 245),
|
||||
(id: 92, cname: "几内亚", countryId: 224),
|
||||
(id: 79, cname: "格林纳达", countryId: 1473),
|
||||
(id: 78, cname: "哥斯达黎加", countryId: 506),
|
||||
(id: 77, cname: "哥伦比亚", countryId: 57),
|
||||
(id: 76, cname: "刚果(金)", countryId: 243),
|
||||
(id: 75, cname: "刚果", countryId: 242),
|
||||
(id: 74, cname: "冈比亚", countryId: 220),
|
||||
(id: 73, cname: "福克兰岛", countryId: 500),
|
||||
(id: 72, cname: "佛得角", countryId: 238),
|
||||
(id: 71, cname: "芬兰", countryId: 358),
|
||||
(id: 70, cname: "斐济", countryId: 679),
|
||||
(id: 80, cname: "格陵兰岛", countryId: 299),
|
||||
(id: 81, cname: "古巴", countryId: 53),
|
||||
(id: 91, cname: "吉尔吉斯斯坦", countryId: 996),
|
||||
(id: 90, cname: "吉布提", countryId: 253),
|
||||
(id: 89, cname: "基里巴斯", countryId: 686),
|
||||
(id: 88, cname: "维克岛", countryId: 1808),
|
||||
(id: 87, cname: "洪都拉斯", countryId: 504),
|
||||
(id: 86, cname: "荷兰", countryId: 31),
|
||||
(id: 85, cname: "朝鲜", countryId: 850),
|
||||
(id: 84, cname: "海地", countryId: 509),
|
||||
(id: 83, cname: "关岛", countryId: 1671),
|
||||
(id: 82, cname: "瓜德罗普岛", countryId: 590),
|
||||
(id: 69, cname: "菲律宾", countryId: 63),
|
||||
];
|
||||
// 'itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme,sunflowerStyle,cardsEnhance,eva3CardOpus,eva3CardVideo,eva3CardComment,eva3CardVote,eva3CardUser'
|
||||
static const dynFeatures = 'itemOpusStyle,listOnlyfans,onlyfansQaCard';
|
||||
}
|
||||
|
||||
220
lib/common/dial_prefix.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
abstract final class Login {
|
||||
//内容来自 https://passport.bilibili.com/web/generic/country/list
|
||||
static const dialPrefix = [
|
||||
(id: 1, cname: "中国大陆", countryId: 86),
|
||||
(id: 5, cname: "中国香港特别行政区", countryId: 852),
|
||||
(id: 2, cname: "中国澳门特别行政区", countryId: 853),
|
||||
(id: 3, cname: "中国台湾", countryId: 886),
|
||||
(id: 4, cname: "美国", countryId: 1),
|
||||
(id: 6, cname: "比利时", countryId: 32),
|
||||
(id: 7, cname: "澳大利亚", countryId: 61),
|
||||
(id: 8, cname: "法国", countryId: 33),
|
||||
(id: 9, cname: "加拿大", countryId: 1),
|
||||
(id: 10, cname: "日本", countryId: 81),
|
||||
(id: 11, cname: "新加坡", countryId: 65),
|
||||
(id: 12, cname: "韩国", countryId: 82),
|
||||
(id: 13, cname: "马来西亚", countryId: 60),
|
||||
(id: 14, cname: "英国", countryId: 44),
|
||||
(id: 15, cname: "意大利", countryId: 39),
|
||||
(id: 16, cname: "德国", countryId: 49),
|
||||
(id: 18, cname: "俄罗斯", countryId: 7),
|
||||
(id: 19, cname: "新西兰", countryId: 64),
|
||||
(id: 153, cname: "瓦利斯群岛和富图纳群岛", countryId: 1681),
|
||||
(id: 152, cname: "葡萄牙", countryId: 351),
|
||||
(id: 151, cname: "帕劳", countryId: 680),
|
||||
(id: 150, cname: "诺福克岛", countryId: 672),
|
||||
(id: 149, cname: "挪威", countryId: 47),
|
||||
(id: 148, cname: "纽埃岛", countryId: 683),
|
||||
(id: 147, cname: "尼日利亚", countryId: 234),
|
||||
(id: 146, cname: "尼日尔", countryId: 227),
|
||||
(id: 145, cname: "尼加拉瓜", countryId: 505),
|
||||
(id: 144, cname: "尼泊尔", countryId: 977),
|
||||
(id: 143, cname: "瑙鲁", countryId: 674),
|
||||
(id: 154, cname: "格鲁吉亚", countryId: 995),
|
||||
(id: 155, cname: "瑞典", countryId: 46),
|
||||
(id: 165, cname: "沙特阿拉伯", countryId: 966),
|
||||
(id: 164, cname: "桑给巴尔岛", countryId: 259),
|
||||
(id: 163, cname: "塞舌尔共和国", countryId: 248),
|
||||
(id: 162, cname: "塞浦路斯", countryId: 357),
|
||||
(id: 161, cname: "塞内加尔", countryId: 221),
|
||||
(id: 160, cname: "塞拉利昂", countryId: 232),
|
||||
(id: 159, cname: "萨摩亚,东部", countryId: 684),
|
||||
(id: 158, cname: "萨摩亚,西部", countryId: 685),
|
||||
(id: 157, cname: "萨尔瓦多", countryId: 503),
|
||||
(id: 156, cname: "瑞士", countryId: 41),
|
||||
(id: 166, cname: "圣多美和普林西比", countryId: 239),
|
||||
(id: 142, cname: "塞尔维亚", countryId: 381),
|
||||
(id: 141, cname: "南非", countryId: 27),
|
||||
(id: 128, cname: "毛里塔尼亚", countryId: 222),
|
||||
(id: 127, cname: "毛里求斯", countryId: 230),
|
||||
(id: 126, cname: "马歇尔岛", countryId: 692),
|
||||
(id: 125, cname: "马提尼克岛", countryId: 596),
|
||||
(id: 124, cname: "马其顿", countryId: 389),
|
||||
(id: 123, cname: "马里亚纳岛", countryId: 1670),
|
||||
(id: 122, cname: "马里", countryId: 223),
|
||||
(id: 121, cname: "马拉维", countryId: 265),
|
||||
(id: 120, cname: "马耳他", countryId: 356),
|
||||
(id: 119, cname: "马尔代夫", countryId: 960),
|
||||
(id: 129, cname: "蒙古", countryId: 976),
|
||||
(id: 130, cname: "蒙特塞拉特岛", countryId: 1664),
|
||||
(id: 140, cname: "纳米比亚", countryId: 264),
|
||||
(id: 139, cname: "墨西哥", countryId: 52),
|
||||
(id: 138, cname: "莫桑比克", countryId: 258),
|
||||
(id: 137, cname: "摩纳哥", countryId: 377),
|
||||
(id: 136, cname: "摩洛哥", countryId: 212),
|
||||
(id: 135, cname: "摩尔多瓦", countryId: 373),
|
||||
(id: 134, cname: "缅甸", countryId: 95),
|
||||
(id: 133, cname: "密克罗尼西亚", countryId: 691),
|
||||
(id: 132, cname: "秘鲁", countryId: 51),
|
||||
(id: 131, cname: "孟加拉国", countryId: 880),
|
||||
(id: 118, cname: "马达加斯加", countryId: 261),
|
||||
(id: 167, cname: "圣卢西亚", countryId: 1784),
|
||||
(id: 216, cname: "智利", countryId: 56),
|
||||
(id: 203, cname: "牙买加", countryId: 1876),
|
||||
(id: 202, cname: "叙利亚", countryId: 963),
|
||||
(id: 201, cname: "匈牙利", countryId: 36),
|
||||
(id: 200, cname: "科特迪瓦", countryId: 225),
|
||||
(id: 199, cname: "希腊", countryId: 30),
|
||||
(id: 198, cname: "西班牙", countryId: 34),
|
||||
(id: 197, cname: "乌兹别克斯坦", countryId: 998),
|
||||
(id: 196, cname: "乌拉圭", countryId: 598),
|
||||
(id: 195, cname: "乌克兰", countryId: 380),
|
||||
(id: 194, cname: "乌干达", countryId: 256),
|
||||
(id: 204, cname: "亚美尼亚", countryId: 374),
|
||||
(id: 205, cname: "也门", countryId: 967),
|
||||
(id: 215, cname: "直布罗陀", countryId: 350),
|
||||
(id: 214, cname: "乍得", countryId: 235),
|
||||
(id: 213, cname: "赞比亚", countryId: 260),
|
||||
(id: 212, cname: "越南", countryId: 84),
|
||||
(id: 211, cname: "约旦", countryId: 962),
|
||||
(id: 210, cname: "印尼", countryId: 62),
|
||||
(id: 209, cname: "印度", countryId: 91),
|
||||
(id: 208, cname: "以色列", countryId: 972),
|
||||
(id: 207, cname: "伊朗", countryId: 98),
|
||||
(id: 206, cname: "伊拉克", countryId: 964),
|
||||
(id: 193, cname: "文莱", countryId: 673),
|
||||
(id: 192, cname: "委内瑞拉", countryId: 58),
|
||||
(id: 191, cname: "维珍群岛(英属)", countryId: 1284),
|
||||
(id: 178, cname: "泰国", countryId: 66),
|
||||
(id: 177, cname: "索马里", countryId: 252),
|
||||
(id: 176, cname: "所罗门群岛", countryId: 677),
|
||||
(id: 175, cname: "苏里南", countryId: 597),
|
||||
(id: 174, cname: "苏丹", countryId: 249),
|
||||
(id: 173, cname: "斯威士兰", countryId: 268),
|
||||
(id: 172, cname: "斯洛文尼亚", countryId: 386),
|
||||
(id: 171, cname: "斯洛伐克", countryId: 421),
|
||||
(id: 170, cname: "斯里兰卡", countryId: 94),
|
||||
(id: 169, cname: "圣皮埃尔和密克隆群岛", countryId: 508),
|
||||
(id: 179, cname: "坦桑尼亚", countryId: 255),
|
||||
(id: 180, cname: "汤加", countryId: 676),
|
||||
(id: 190, cname: "维珍群岛(美属)", countryId: 1340),
|
||||
(id: 189, cname: "瓦努阿图", countryId: 678),
|
||||
(id: 188, cname: "托克劳岛", countryId: 690),
|
||||
(id: 187, cname: "土库曼斯坦", countryId: 993),
|
||||
(id: 186, cname: "土耳其", countryId: 90),
|
||||
(id: 185, cname: "图瓦卢", countryId: 688),
|
||||
(id: 184, cname: "突尼斯", countryId: 216),
|
||||
(id: 183, cname: "阿森松岛", countryId: 247),
|
||||
(id: 182, cname: "特立尼达和多巴哥", countryId: 1868),
|
||||
(id: 181, cname: "特克斯和凯科斯", countryId: 1649),
|
||||
(id: 168, cname: "圣马力诺", countryId: 378),
|
||||
(id: 67, cname: "法属圭亚那", countryId: 594),
|
||||
(id: 54, cname: "不丹", countryId: 975),
|
||||
(id: 53, cname: "博茨瓦纳", countryId: 267),
|
||||
(id: 52, cname: "伯利兹", countryId: 501),
|
||||
(id: 51, cname: "玻利维亚", countryId: 591),
|
||||
(id: 50, cname: "波兰", countryId: 48),
|
||||
(id: 49, cname: "波黑", countryId: 387),
|
||||
(id: 48, cname: "波多黎各", countryId: 1787),
|
||||
(id: 47, cname: "冰岛", countryId: 354),
|
||||
(id: 46, cname: "贝宁", countryId: 229),
|
||||
(id: 45, cname: "保加利亚", countryId: 359),
|
||||
(id: 55, cname: "布基纳法索", countryId: 226),
|
||||
(id: 56, cname: "布隆迪", countryId: 257),
|
||||
(id: 66, cname: "法属波利尼西亚", countryId: 689),
|
||||
(id: 65, cname: "法罗岛", countryId: 298),
|
||||
(id: 64, cname: "厄立特里亚", countryId: 291),
|
||||
(id: 63, cname: "厄瓜多尔", countryId: 593),
|
||||
(id: 62, cname: "多米尼加代表", countryId: 1809),
|
||||
(id: 61, cname: "多米尼加", countryId: 1767),
|
||||
(id: 60, cname: "多哥", countryId: 228),
|
||||
(id: 59, cname: "迪戈加西亚岛", countryId: 246),
|
||||
(id: 58, cname: "丹麦", countryId: 45),
|
||||
(id: 57, cname: "赤道几内亚", countryId: 240),
|
||||
(id: 44, cname: "百慕大群岛", countryId: 1441),
|
||||
(id: 43, cname: "白俄罗斯", countryId: 375),
|
||||
(id: 42, cname: "巴西", countryId: 55),
|
||||
(id: 29, cname: "爱尔兰", countryId: 353),
|
||||
(id: 28, cname: "埃塞俄比亚", countryId: 251),
|
||||
(id: 27, cname: "埃及", countryId: 20),
|
||||
(id: 26, cname: "阿塞拜疆", countryId: 994),
|
||||
(id: 25, cname: "阿曼", countryId: 968),
|
||||
(id: 24, cname: "阿联酋", countryId: 971),
|
||||
(id: 23, cname: "阿根廷", countryId: 54),
|
||||
(id: 22, cname: "阿富汗", countryId: 93),
|
||||
(id: 21, cname: "阿尔及利亚", countryId: 213),
|
||||
(id: 20, cname: "阿尔巴尼亚", countryId: 355),
|
||||
(id: 30, cname: "爱沙尼亚", countryId: 372),
|
||||
(id: 31, cname: "安道尔", countryId: 376),
|
||||
(id: 41, cname: "巴拿马", countryId: 507),
|
||||
(id: 40, cname: "巴林", countryId: 973),
|
||||
(id: 39, cname: "巴拉圭", countryId: 595),
|
||||
(id: 38, cname: "巴基斯坦", countryId: 92),
|
||||
(id: 37, cname: "巴哈马群岛", countryId: 1242),
|
||||
(id: 36, cname: "巴布亚新几内亚", countryId: 675),
|
||||
(id: 35, cname: "巴巴多斯", countryId: 1246),
|
||||
(id: 34, cname: "奥地利", countryId: 43),
|
||||
(id: 33, cname: "安提瓜岛和巴布达", countryId: 1268),
|
||||
(id: 32, cname: "安哥拉", countryId: 244),
|
||||
(id: 68, cname: "非洲中部", countryId: 236),
|
||||
(id: 117, cname: "罗马尼亚", countryId: 40),
|
||||
(id: 104, cname: "科威特", countryId: 965),
|
||||
(id: 103, cname: "科摩罗", countryId: 269),
|
||||
(id: 102, cname: "开曼群岛", countryId: 1345),
|
||||
(id: 101, cname: "卡塔尔", countryId: 974),
|
||||
(id: 100, cname: "喀麦隆", countryId: 237),
|
||||
(id: 99, cname: "聚会岛", countryId: 262),
|
||||
(id: 98, cname: "津巴布韦", countryId: 263),
|
||||
(id: 97, cname: "捷克", countryId: 420),
|
||||
(id: 96, cname: "柬埔寨", countryId: 855),
|
||||
(id: 95, cname: "加蓬", countryId: 241),
|
||||
(id: 105, cname: "克罗地亚", countryId: 385),
|
||||
(id: 106, cname: "肯尼亚", countryId: 254),
|
||||
(id: 116, cname: "卢旺达", countryId: 250),
|
||||
(id: 115, cname: "卢森堡", countryId: 352),
|
||||
(id: 114, cname: "利比亚", countryId: 218),
|
||||
(id: 113, cname: "利比里亚", countryId: 231),
|
||||
(id: 112, cname: "立陶宛", countryId: 370),
|
||||
(id: 111, cname: "黎巴嫩", countryId: 961),
|
||||
(id: 110, cname: "老挝", countryId: 856),
|
||||
(id: 109, cname: "莱索托", countryId: 266),
|
||||
(id: 108, cname: "拉脱维亚", countryId: 371),
|
||||
(id: 107, cname: "库克岛", countryId: 682),
|
||||
(id: 94, cname: "加纳", countryId: 233),
|
||||
(id: 93, cname: "几内亚比绍", countryId: 245),
|
||||
(id: 92, cname: "几内亚", countryId: 224),
|
||||
(id: 79, cname: "格林纳达", countryId: 1473),
|
||||
(id: 78, cname: "哥斯达黎加", countryId: 506),
|
||||
(id: 77, cname: "哥伦比亚", countryId: 57),
|
||||
(id: 76, cname: "刚果(金)", countryId: 243),
|
||||
(id: 75, cname: "刚果", countryId: 242),
|
||||
(id: 74, cname: "冈比亚", countryId: 220),
|
||||
(id: 73, cname: "福克兰岛", countryId: 500),
|
||||
(id: 72, cname: "佛得角", countryId: 238),
|
||||
(id: 71, cname: "芬兰", countryId: 358),
|
||||
(id: 70, cname: "斐济", countryId: 679),
|
||||
(id: 80, cname: "格陵兰岛", countryId: 299),
|
||||
(id: 81, cname: "古巴", countryId: 53),
|
||||
(id: 91, cname: "吉尔吉斯斯坦", countryId: 996),
|
||||
(id: 90, cname: "吉布提", countryId: 253),
|
||||
(id: 89, cname: "基里巴斯", countryId: 686),
|
||||
(id: 88, cname: "维克岛", countryId: 1808),
|
||||
(id: 87, cname: "洪都拉斯", countryId: 504),
|
||||
(id: 86, cname: "荷兰", countryId: 31),
|
||||
(id: 85, cname: "朝鲜", countryId: 850),
|
||||
(id: 84, cname: "海地", countryId: 509),
|
||||
(id: 83, cname: "关岛", countryId: 1671),
|
||||
(id: 82, cname: "瓜德罗普岛", countryId: 590),
|
||||
(id: 69, cname: "菲律宾", countryId: 63),
|
||||
];
|
||||
}
|
||||
@@ -9,6 +9,13 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final color = theme.colorScheme.onInverseSurface;
|
||||
final buttonStyle = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const .symmetric(horizontal: 15),
|
||||
foregroundColor: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.2,
|
||||
),
|
||||
);
|
||||
return Skeleton(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||
@@ -86,29 +93,19 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
for (var i = 0; i < 3; i++)
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.radio_button_unchecked_outlined,
|
||||
size: 20,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.2,
|
||||
children: const ['转发', '评论', '点赞']
|
||||
.map(
|
||||
(e) => TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.radio_button_unchecked_outlined,
|
||||
size: 20,
|
||||
),
|
||||
style: buttonStyle,
|
||||
label: Text(e),
|
||||
),
|
||||
label: Text(
|
||||
i == 0
|
||||
? '转发'
|
||||
: i == 1
|
||||
? '评论'
|
||||
: '点赞',
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FavPgcItemSkeleton extends StatelessWidget {
|
||||
@@ -11,7 +11,7 @@ class FavPgcItemSkeleton extends StatelessWidget {
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
horizontal: Style.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaPgcSkeleton extends StatefulWidget {
|
||||
@@ -15,11 +15,9 @@ class _MediaPgcSkeletonState extends State<MediaPgcSkeleton> {
|
||||
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace,
|
||||
7,
|
||||
StyleString.safeSpace,
|
||||
7,
|
||||
padding: const .symmetric(
|
||||
horizontal: Style.safeSpace,
|
||||
vertical: 7,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -1,189 +1,62 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Skeleton extends StatelessWidget {
|
||||
class Skeleton extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const Skeleton({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
const Skeleton({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
|
||||
var shimmerGradient = LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
color,
|
||||
color,
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [
|
||||
0.1,
|
||||
0.3,
|
||||
0.5,
|
||||
0.7,
|
||||
],
|
||||
begin: const Alignment(-1.0, -0.3),
|
||||
end: const Alignment(1.0, 0.9),
|
||||
tileMode: TileMode.clamp,
|
||||
);
|
||||
return Shimmer(
|
||||
linearGradient: shimmerGradient,
|
||||
child: ShimmerLoading(
|
||||
isLoading: true,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
State<Skeleton> createState() => _SkeletonState();
|
||||
}
|
||||
|
||||
class Shimmer extends StatefulWidget {
|
||||
static ShimmerState? of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<ShimmerState>();
|
||||
}
|
||||
|
||||
const Shimmer({
|
||||
super.key,
|
||||
required this.linearGradient,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final LinearGradient linearGradient;
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
ShimmerState createState() => ShimmerState();
|
||||
}
|
||||
|
||||
class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _shimmerController;
|
||||
class _SkeletonState extends State<Skeleton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late Color color;
|
||||
final matrix = Matrix4.identity();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_shimmerController = AnimationController.unbounded(vsync: this)
|
||||
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
|
||||
_controller = AnimationController.unbounded(vsync: this)
|
||||
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000))
|
||||
..addListener(_setState);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shimmerController.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
LinearGradient get gradient => LinearGradient(
|
||||
colors: widget.linearGradient.colors,
|
||||
stops: widget.linearGradient.stops,
|
||||
begin: widget.linearGradient.begin,
|
||||
end: widget.linearGradient.end,
|
||||
transform: _SlidingGradientTransform(
|
||||
slidePercent: _shimmerController.value,
|
||||
),
|
||||
);
|
||||
|
||||
bool get isSized =>
|
||||
(context.findRenderObject() as RenderBox?)?.hasSize ?? false;
|
||||
|
||||
Size get size => (context.findRenderObject() as RenderBox).size;
|
||||
|
||||
Offset getDescendantOffset({
|
||||
required RenderBox descendant,
|
||||
Offset offset = Offset.zero,
|
||||
}) {
|
||||
final shimmerBox = context.findRenderObject() as RenderBox;
|
||||
return descendant.localToGlobal(offset, ancestor: shimmerBox);
|
||||
void _setState() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Listenable get shimmerChanges => _shimmerController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child ?? const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _SlidingGradientTransform extends GradientTransform {
|
||||
const _SlidingGradientTransform({
|
||||
required this.slidePercent,
|
||||
});
|
||||
|
||||
final double slidePercent;
|
||||
|
||||
@override
|
||||
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
|
||||
return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
class ShimmerLoading extends StatefulWidget {
|
||||
const ShimmerLoading({
|
||||
super.key,
|
||||
required this.isLoading,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<ShimmerLoading> createState() => _ShimmerLoadingState();
|
||||
}
|
||||
|
||||
class _ShimmerLoadingState extends State<ShimmerLoading> {
|
||||
Listenable? _shimmerChanges;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_shimmerChanges != null) {
|
||||
_shimmerChanges!.removeListener(_onShimmerChange);
|
||||
}
|
||||
_shimmerChanges = Shimmer.of(context)?.shimmerChanges;
|
||||
if (_shimmerChanges != null) {
|
||||
_shimmerChanges!.addListener(_onShimmerChange);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shimmerChanges?.removeListener(_onShimmerChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onShimmerChange() {
|
||||
if (widget.isLoading) {
|
||||
setState(() {});
|
||||
}
|
||||
color = ColorScheme.of(context).surface.withAlpha(10);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.isLoading) {
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
final shimmer = Shimmer.of(context)!;
|
||||
if (!shimmer.isSized) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final shimmerSize = shimmer.size;
|
||||
final gradient = shimmer.gradient;
|
||||
final offsetWithinShimmer = shimmer.getDescendantOffset(
|
||||
descendant: context.findRenderObject() as RenderBox,
|
||||
);
|
||||
|
||||
final colors = [Colors.transparent, color, color, Colors.transparent];
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcATop,
|
||||
shaderCallback: (bounds) {
|
||||
return gradient.createShader(
|
||||
Rect.fromLTWH(
|
||||
-offsetWithinShimmer.dx,
|
||||
-offsetWithinShimmer.dy,
|
||||
shimmerSize.width,
|
||||
shimmerSize.height,
|
||||
),
|
||||
shaderCallback: (Rect bounds) {
|
||||
final width = bounds.width;
|
||||
final height = bounds.height;
|
||||
matrix[12] = width * _controller.value;
|
||||
return ui.Gradient.linear(
|
||||
Offset(0, 0.35 * height),
|
||||
Offset(width, 0.95 * height),
|
||||
colors,
|
||||
const [0.1, 0.3, 0.5, 0.7],
|
||||
TileMode.clamp,
|
||||
matrix.storage,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoCardHSkeleton extends StatelessWidget {
|
||||
@@ -10,25 +10,25 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
padding: const .symmetric(
|
||||
horizontal: Style.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
aspectRatio: Style.aspectRatio,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
borderRadius: Style.mdRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
padding: const .fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoCardVSkeleton extends StatelessWidget {
|
||||
@@ -13,11 +13,11 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
aspectRatio: Style.aspectRatio,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
borderRadius: Style.mdRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
24
lib/common/style.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart'
|
||||
show BorderRadius, Radius, BoxConstraints, ButtonStyle, VisualDensity;
|
||||
|
||||
abstract final class Style {
|
||||
static const cardSpace = 8.0;
|
||||
static const safeSpace = 12.0;
|
||||
static const mdRadius = BorderRadius.all(imgRadius);
|
||||
static const imgRadius = Radius.circular(10);
|
||||
static const aspectRatio = 16 / 10;
|
||||
static const aspectRatio16x9 = 16 / 9;
|
||||
static const imgMaxRatio = 2.6;
|
||||
static const bottomSheetRadius = BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
);
|
||||
static const dialogFixedConstraints = BoxConstraints(
|
||||
minWidth: 420,
|
||||
maxWidth: 420,
|
||||
);
|
||||
static const topBarHeight = 52.0;
|
||||
static const buttonStyle = ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -2, vertical: -1.25),
|
||||
tapTargetSize: .shrinkWrap,
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,8 @@ class MultiSelectAppBarWidget extends StatelessWidget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (visible ?? ctr.enableMultiSelect.value) {
|
||||
final style = TextButton.styleFrom(visualDensity: VisualDensity.compact);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return AppBar(
|
||||
bottom: child.bottom,
|
||||
leading: IconButton(
|
||||
@@ -30,17 +32,13 @@ class MultiSelectAppBarWidget extends StatelessWidget
|
||||
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
style: style,
|
||||
onPressed: () => ctr.handleSelect(checked: true),
|
||||
child: const Text('全选'),
|
||||
),
|
||||
...?actions,
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
style: style,
|
||||
onPressed: () {
|
||||
if (ctr.checkedCount == 0) {
|
||||
return;
|
||||
@@ -49,7 +47,7 @@ class MultiSelectAppBarWidget extends StatelessWidget
|
||||
},
|
||||
child: Text(
|
||||
'移除',
|
||||
style: TextStyle(color: Get.theme.colorScheme.error),
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
|
||||
56
lib/common/widgets/avatars.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/model_owner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget avatars({
|
||||
required ColorScheme colorScheme,
|
||||
required Iterable<Owner> users,
|
||||
double gap = 6.0,
|
||||
}) {
|
||||
const size = 22.0;
|
||||
const padding = 0.8;
|
||||
final offset = size - gap;
|
||||
const imgSize = size - 2 * padding;
|
||||
if (users.length == 1) {
|
||||
return NetworkImgLayer(
|
||||
src: users.first.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
);
|
||||
} else {
|
||||
final decoration = BoxDecoration(
|
||||
shape: .circle,
|
||||
border: Border.all(color: colorScheme.surface),
|
||||
);
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: offset * users.length + gap,
|
||||
child: Stack(
|
||||
clipBehavior: .none,
|
||||
children: users.indexed
|
||||
.map(
|
||||
(e) => Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: size,
|
||||
left: e.$1 * offset,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: Padding(
|
||||
padding: const .all(padding),
|
||||
child: NetworkImgLayer(
|
||||
src: e.$2.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/common/widgets/back_detector.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/gestures.dart' show kBackMouseButton;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show KeyDownEvent;
|
||||
|
||||
class BackDetector extends StatelessWidget {
|
||||
const BackDetector({
|
||||
super.key,
|
||||
required this.onBack,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final VoidCallback onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: _onKeyEvent,
|
||||
child: Listener(
|
||||
behavior: .translucent,
|
||||
onPointerDown: _onPointerDown,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event.logicalKey == .escape && event is KeyDownEvent) {
|
||||
onBack();
|
||||
return .handled;
|
||||
}
|
||||
return .ignored;
|
||||
}
|
||||
|
||||
void _onPointerDown(PointerDownEvent event) {
|
||||
if (event.buttons == kBackMouseButton) {
|
||||
onBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension/theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PBadge extends StatelessWidget {
|
||||
final String? text;
|
||||
@@ -59,7 +59,7 @@ class PBadge extends StatelessWidget {
|
||||
bgColor = Colors.black45;
|
||||
color = Colors.white;
|
||||
case PBadgeType.error:
|
||||
if (Get.isDarkMode) {
|
||||
if (theme.isDark) {
|
||||
bgColor = theme.errorContainer;
|
||||
color = theme.onErrorContainer;
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,7 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
@@ -24,14 +24,14 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
highlightColor: theme.colorScheme.secondaryContainer,
|
||||
highlightColor: colorScheme.secondaryContainer,
|
||||
color: selected
|
||||
? theme.colorScheme.onSecondaryContainer
|
||||
: theme.colorScheme.outline,
|
||||
? colorScheme.onSecondaryContainer
|
||||
: colorScheme.outline,
|
||||
style: ButtonStyle(
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
selected ? theme.colorScheme.secondaryContainer : null,
|
||||
selected ? colorScheme.secondaryContainer : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
class ColorPalette extends StatelessWidget {
|
||||
final Color color;
|
||||
final ColorScheme colorScheme;
|
||||
final bool selected;
|
||||
final bool showBgColor;
|
||||
|
||||
const ColorPalette({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.colorScheme,
|
||||
required this.selected,
|
||||
this.showBgColor = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final Hct hct = Hct.fromInt(color.toARGB32());
|
||||
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());
|
||||
final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
|
||||
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());
|
||||
Widget coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
final primary = colorScheme.primary;
|
||||
final tertiary = colorScheme.tertiary;
|
||||
final primaryContainer = colorScheme.primaryContainer;
|
||||
Widget child = ClipOval(
|
||||
child: Column(
|
||||
children: [
|
||||
coloredBox(primary),
|
||||
_coloredBox(primary),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
coloredBox(tertiary),
|
||||
coloredBox(primaryContainer),
|
||||
_coloredBox(tertiary),
|
||||
_coloredBox(primaryContainer),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -50,7 +43,7 @@ class ColorPalette extends StatelessWidget {
|
||||
width: 23,
|
||||
height: 23,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(Hct.from(hct.hue, 30.0, 40.0).toInt()),
|
||||
color: colorScheme.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
@@ -62,15 +55,25 @@ class ColorPalette extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
if (showBgColor) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onInverseSurface,
|
||||
borderRadius: Style.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
static Widget _coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
21
lib/common/widgets/colored_box_transition.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColoredBoxTransition extends AnimatedWidget {
|
||||
const ColoredBoxTransition({
|
||||
super.key,
|
||||
required this.color,
|
||||
this.child,
|
||||
}) : super(listenable: color);
|
||||
|
||||
final Animation<Color?> color;
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: color.value!,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
152
lib/common/widgets/cropped_image.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* This file is part of PiliPlus
|
||||
*
|
||||
* PiliPlus is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* PiliPlus is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CroppedImage extends LeafRenderObjectWidget {
|
||||
const CroppedImage({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.image,
|
||||
required this.srcRect,
|
||||
required this.dstRect,
|
||||
required this.rrect,
|
||||
required this.imgPaint,
|
||||
required this.borderPaint,
|
||||
});
|
||||
|
||||
final Size size;
|
||||
final ui.Image image;
|
||||
final Rect srcRect;
|
||||
final Rect dstRect;
|
||||
final RRect rrect;
|
||||
final Paint imgPaint;
|
||||
final Paint borderPaint;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderCroppedImage(
|
||||
preferredSize: size,
|
||||
image: image,
|
||||
srcRect: srcRect,
|
||||
dstRect: dstRect,
|
||||
rrect: rrect,
|
||||
imgPaint: imgPaint,
|
||||
borderPaint: borderPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderCroppedImage renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..preferredSize = size
|
||||
..image = image
|
||||
..srcRect = srcRect
|
||||
..dstRect = dstRect
|
||||
..rrect = rrect
|
||||
..imgPaint = imgPaint
|
||||
..borderPaint = borderPaint;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderCroppedImage extends RenderBox {
|
||||
RenderCroppedImage({
|
||||
required this._preferredSize,
|
||||
required this._image,
|
||||
required this._srcRect,
|
||||
required this._dstRect,
|
||||
required this._rrect,
|
||||
required this._imgPaint,
|
||||
required this._borderPaint,
|
||||
});
|
||||
|
||||
Size _preferredSize;
|
||||
Size get preferredSize => _preferredSize;
|
||||
set preferredSize(Size value) {
|
||||
if (_preferredSize == value) return;
|
||||
_preferredSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
ui.Image _image;
|
||||
ui.Image get image => _image;
|
||||
set image(ui.Image value) {
|
||||
if (_image == value) return;
|
||||
_image = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Rect _srcRect;
|
||||
Rect get srcRect => _srcRect;
|
||||
set srcRect(Rect value) {
|
||||
if (_srcRect == value) return;
|
||||
_srcRect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Rect _dstRect;
|
||||
Rect get dstRect => _dstRect;
|
||||
set dstRect(Rect value) {
|
||||
if (_dstRect == value) return;
|
||||
_dstRect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
RRect _rrect;
|
||||
RRect get rrect => _rrect;
|
||||
set rrect(RRect value) {
|
||||
if (_rrect == value) return;
|
||||
_rrect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Paint _imgPaint;
|
||||
Paint get imgPaint => _imgPaint;
|
||||
set imgPaint(Paint value) {
|
||||
if (_imgPaint == value) return;
|
||||
_imgPaint = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Paint _borderPaint;
|
||||
Paint get borderPaint => _borderPaint;
|
||||
set borderPaint(Paint value) {
|
||||
if (_borderPaint == value) return;
|
||||
_borderPaint = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrain(_preferredSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.canvas
|
||||
..drawImageRect(image, srcRect, dstRect, _imgPaint)
|
||||
..drawRRect(rrect, _borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
110
lib/common/widgets/custom_arc.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'dart:math' show pi;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class Arc extends LeafRenderObjectWidget {
|
||||
const Arc({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.color,
|
||||
required this.progress,
|
||||
this.strokeWidth = 2,
|
||||
});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
final double progress;
|
||||
final double strokeWidth;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderArc(
|
||||
preferredSize: size,
|
||||
color: color,
|
||||
progress: progress,
|
||||
strokeWidth: strokeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderArc renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..preferredSize = size
|
||||
..color = color
|
||||
..progress = progress
|
||||
..strokeWidth = strokeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderArc extends RenderBox {
|
||||
RenderArc({
|
||||
required this._preferredSize,
|
||||
required this._color,
|
||||
required this._progress,
|
||||
required this._strokeWidth,
|
||||
});
|
||||
|
||||
Color _color;
|
||||
Color get color => _color;
|
||||
set color(Color value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _progress;
|
||||
double get progress => _progress;
|
||||
set progress(double value) {
|
||||
if (_progress == value) return;
|
||||
_progress = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _strokeWidth;
|
||||
double get strokeWidth => _strokeWidth;
|
||||
set strokeWidth(double value) {
|
||||
if (_strokeWidth == value) return;
|
||||
_strokeWidth = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _preferredSize;
|
||||
double get preferredSize => _preferredSize;
|
||||
set preferredSize(double value) {
|
||||
if (_preferredSize == value) return;
|
||||
_preferredSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrainDimensions(_preferredSize, _preferredSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (progress == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final radius = size.width / 2;
|
||||
final rect = Rect.fromCircle(
|
||||
center: Offset(radius, radius),
|
||||
radius: radius,
|
||||
);
|
||||
|
||||
const startAngle = -pi / 2;
|
||||
context.canvas.drawArc(rect, startAngle, progress * 2 * pi, false, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
95
lib/common/widgets/custom_height_widget.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
|
||||
|
||||
class CustomHeightWidget extends SingleChildRenderObjectWidget {
|
||||
const CustomHeightWidget({
|
||||
super.key,
|
||||
this.height,
|
||||
this.offset = .zero,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
final double? height;
|
||||
|
||||
final Offset offset;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderCustomHeightWidget(
|
||||
height: height,
|
||||
offset: offset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderCustomHeightWidget renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..height = height
|
||||
..offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderCustomHeightWidget extends RenderProxyBox {
|
||||
RenderCustomHeightWidget({
|
||||
this._height,
|
||||
required this._offset,
|
||||
});
|
||||
|
||||
double? _height;
|
||||
double? get height => _height;
|
||||
set height(double? value) {
|
||||
if (_height == value) return;
|
||||
_height = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
Offset _offset;
|
||||
Offset get offset => _offset;
|
||||
set offset(Offset value) {
|
||||
if (_offset == value) return;
|
||||
_offset = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
if (height != null) {
|
||||
child!.layout(constraints.copyWith(maxHeight: .infinity));
|
||||
size = constraints.constrainDimensions(constraints.maxWidth, height!);
|
||||
} else {
|
||||
child!.layout(
|
||||
constraints.copyWith(maxHeight: .infinity),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
size = constraints.constrainDimensions(
|
||||
constraints.maxWidth,
|
||||
child!.size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.paintChild(child!, offset + _offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
return result.addWithPaintOffset(
|
||||
offset: _offset,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(transformed == position - _offset);
|
||||
return child!.hitTest(result, position: transformed);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
|
||||
transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,46 @@
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/widgets.dart' show IconData;
|
||||
|
||||
class CustomIcons {
|
||||
static const IconData coin = _CustomIconData(0xe800);
|
||||
static const IconData dm_off = _CustomIconData(0xe801);
|
||||
static const IconData dm_on = _CustomIconData(0xe802);
|
||||
static const IconData dm_settings = _CustomIconData(0xe803);
|
||||
static const IconData dyn = _CustomIconData(0xe804);
|
||||
static const IconData fav = _CustomIconData(0xe805);
|
||||
static const IconData live_reserve = _CustomIconData(0xe806);
|
||||
static const IconData player_dm_tip_back = _CustomIconData(0xe807);
|
||||
static const IconData player_dm_tip_copy = _CustomIconData(0xe808);
|
||||
static const IconData player_dm_tip_like = _CustomIconData(0xe809);
|
||||
static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a);
|
||||
static const IconData player_dm_tip_recall = _CustomIconData(0xe80b);
|
||||
static const IconData share = _CustomIconData(0xe80c);
|
||||
static const IconData share_line = _CustomIconData(0xe80d);
|
||||
static const IconData share_node = _CustomIconData(0xe80e);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80f);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe811);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe812);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe813);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe814);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe815);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe816);
|
||||
static const IconData topic_tag = _CustomIconData(0xe817);
|
||||
static const IconData watch_later = _CustomIconData(0xe818);
|
||||
}
|
||||
// dart format off
|
||||
abstract final class CustomIcons {
|
||||
static const _kFontFam = 'custom_icon';
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon');
|
||||
static const IconData ai_circle = IconData(0xe800, fontFamily: _kFontFam);
|
||||
static const IconData coin = IconData(0xe801, fontFamily: _kFontFam);
|
||||
static const IconData dm_off = IconData(0xe802, fontFamily: _kFontFam);
|
||||
static const IconData dm_on = IconData(0xe803, fontFamily: _kFontFam);
|
||||
static const IconData dm_settings = IconData(0xe804, fontFamily: _kFontFam);
|
||||
static const IconData dyn = IconData(0xe805, fontFamily: _kFontFam);
|
||||
static const IconData fav = IconData(0xe806, fontFamily: _kFontFam);
|
||||
static const IconData flip_rotate_90 = IconData(0xe807, fontFamily: _kFontFam);
|
||||
static const IconData identifier_circle = IconData(0xe808, fontFamily: _kFontFam);
|
||||
static const IconData live_reserve = IconData(0xe809, fontFamily: _kFontFam);
|
||||
static const IconData open_in_full_rotate_45 = IconData(0xe80a, fontFamily: _kFontFam);
|
||||
static const IconData player_dm_tip_back = IconData(0xe80b, fontFamily: _kFontFam);
|
||||
static const IconData player_dm_tip_copy = IconData(0xe80c, fontFamily: _kFontFam);
|
||||
static const IconData player_dm_tip_like = IconData(0xe80d, fontFamily: _kFontFam);
|
||||
static const IconData player_dm_tip_like_solid = IconData(0xe80e, fontFamily: _kFontFam);
|
||||
static const IconData player_dm_tip_recall = IconData(0xe80f, fontFamily: _kFontFam);
|
||||
static const IconData repeat_rounded_rotate_90 = IconData(0xe810, fontFamily: _kFontFam);
|
||||
static const IconData share = IconData(0xe811, fontFamily: _kFontFam);
|
||||
static const IconData share_line = IconData(0xe812, fontFamily: _kFontFam);
|
||||
static const IconData share_node = IconData(0xe813, fontFamily: _kFontFam);
|
||||
static const IconData shield_play_arrow = IconData(0xe814, fontFamily: _kFontFam);
|
||||
static const IconData shield_published = IconData(0xe815, fontFamily: _kFontFam);
|
||||
static const IconData shield_reply = IconData(0xe816, fontFamily: _kFontFam);
|
||||
static const IconData shopping_bag_not_interested = IconData(0xe817, fontFamily: _kFontFam);
|
||||
static const IconData splitscreen_rotate_90 = IconData(0xe818, fontFamily: _kFontFam);
|
||||
static const IconData star_favorite_line = IconData(0xe819, fontFamily: _kFontFam);
|
||||
static const IconData star_favorite_solid = IconData(0xe81a, fontFamily: _kFontFam);
|
||||
static const IconData thumbs_down = IconData(0xe81b, fontFamily: _kFontFam);
|
||||
static const IconData thumbs_down_outline = IconData(0xe81c, fontFamily: _kFontFam);
|
||||
static const IconData thumbs_up = IconData(0xe81d, fontFamily: _kFontFam);
|
||||
static const IconData thumbs_up_fill = IconData(0xe81e, fontFamily: _kFontFam);
|
||||
static const IconData thumbs_up_line = IconData(0xe81f, fontFamily: _kFontFam);
|
||||
static const IconData thumbs_up_outline = IconData(0xe820, fontFamily: _kFontFam);
|
||||
static const IconData topic_tag = IconData(0xe821, fontFamily: _kFontFam);
|
||||
static const IconData touch_app_rotate_270 = IconData(0xe822, fontFamily: _kFontFam);
|
||||
static const IconData view_headline_rotate_90 = IconData(0xe823, fontFamily: _kFontFam);
|
||||
static const IconData watch_later = IconData(0xe824, fontFamily: _kFontFam);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomSliverPersistentHeaderDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
const CustomSliverPersistentHeaderDelegate({
|
||||
required this.child,
|
||||
required this.bgColor,
|
||||
double extent = 45,
|
||||
this.needRebuild = false,
|
||||
}) : _minExtent = extent,
|
||||
_maxExtent = extent;
|
||||
final double _minExtent;
|
||||
final double _maxExtent;
|
||||
final Widget child;
|
||||
final Color? bgColor;
|
||||
final bool needRebuild;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
//创建child子组件
|
||||
//shrinkOffset:child偏移值minExtent~maxExtent
|
||||
//overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false
|
||||
return bgColor != null
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
boxShadow: Platform.isIOS
|
||||
? null
|
||||
: [
|
||||
BoxShadow(
|
||||
color: bgColor!,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
}
|
||||
|
||||
//SliverPersistentHeader最大高度
|
||||
@override
|
||||
double get maxExtent => _maxExtent;
|
||||
|
||||
//SliverPersistentHeader最小高度
|
||||
@override
|
||||
double get minExtent => _minExtent;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.bgColor != bgColor ||
|
||||
(needRebuild && oldDelegate.child != child);
|
||||
}
|
||||
}
|
||||
@@ -10,23 +10,21 @@ class CustomToast extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withValues(
|
||||
alpha: toastOpacity,
|
||||
),
|
||||
color: colorScheme.primaryContainer.withValues(alpha: toastOpacity),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Text(
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -41,7 +39,7 @@ class LoadingWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
@@ -50,6 +48,7 @@ class LoadingWidget extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//loading animation
|
||||
@@ -57,12 +56,8 @@ class LoadingWidget extends StatelessWidget {
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
||||
),
|
||||
|
||||
//msg
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,135 +1,63 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show clampDouble;
|
||||
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum TooltipType { top, right }
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
ContainerRenderObjectMixin,
|
||||
RenderBoxContainerDefaultsMixin,
|
||||
MultiChildLayoutParentData;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CustomTooltip extends StatefulWidget {
|
||||
const CustomTooltip({
|
||||
super.key,
|
||||
this.type = TooltipType.top,
|
||||
required this.overlayWidget,
|
||||
required this.child,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
final Widget child;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
|
||||
static final List<CustomTooltipState> _openedTooltips =
|
||||
<CustomTooltipState>[];
|
||||
|
||||
static bool dismissAllToolTips() {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
|
||||
for (final CustomTooltipState state in openedTooltips) {
|
||||
assert(state.mounted);
|
||||
state._scheduleDismissTooltip();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
State<CustomTooltip> createState() => CustomTooltipState();
|
||||
State<CustomTooltip> createState() => _CustomTooltipState();
|
||||
}
|
||||
|
||||
class CustomTooltipState extends State<CustomTooltip>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const Duration _fadeInDuration = Duration(milliseconds: 150);
|
||||
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
|
||||
|
||||
class _CustomTooltipState extends State<CustomTooltip> {
|
||||
final OverlayPortalController _overlayController = OverlayPortalController();
|
||||
|
||||
AnimationController? _backingController;
|
||||
AnimationController get _controller {
|
||||
return _backingController ??= AnimationController(
|
||||
duration: _fadeInDuration,
|
||||
reverseDuration: _fadeOutDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener(_handleStatusChanged);
|
||||
}
|
||||
|
||||
CurvedAnimation? _backingOverlayAnimation;
|
||||
CurvedAnimation get _overlayAnimation {
|
||||
return _backingOverlayAnimation ??= CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
}
|
||||
|
||||
LongPressGestureRecognizer? _longPressRecognizer;
|
||||
|
||||
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
assert(mounted);
|
||||
switch ((_animationStatus.isDismissed, status.isDismissed)) {
|
||||
case (false, true):
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_overlayController.hide();
|
||||
case (true, false):
|
||||
_overlayController.show();
|
||||
CustomTooltip._openedTooltips.add(this);
|
||||
case (true, true) || (false, false):
|
||||
break;
|
||||
}
|
||||
_animationStatus = status;
|
||||
}
|
||||
LongPressGestureRecognizer get longPressRecognizer =>
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer()
|
||||
..onLongPress = _scheduleShowTooltip;
|
||||
|
||||
void _scheduleShowTooltip() {
|
||||
_controller.forward();
|
||||
_overlayController.show();
|
||||
}
|
||||
|
||||
void _scheduleDismissTooltip() {
|
||||
_controller.reverse();
|
||||
_overlayController.hide();
|
||||
}
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
assert(mounted);
|
||||
const Set<PointerDeviceKind> triggerModeDeviceKinds = <PointerDeviceKind>{
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.unknown,
|
||||
PointerDeviceKind.trackpad,
|
||||
};
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer(
|
||||
debugOwner: this,
|
||||
supportedDevices: triggerModeDeviceKinds,
|
||||
);
|
||||
_longPressRecognizer!
|
||||
..onLongPress = _scheduleShowTooltip
|
||||
..addPointer(event);
|
||||
longPressRecognizer.addPointer(event);
|
||||
}
|
||||
|
||||
Widget _buildCustomTooltipOverlay(BuildContext context) {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
Widget _buildCustomTooltipOverlay(
|
||||
BuildContext context,
|
||||
OverlayChildLayoutInfo layoutInfo,
|
||||
) {
|
||||
final target = MatrixUtils.transformPoint(
|
||||
layoutInfo.childPaintTransform,
|
||||
layoutInfo.childSize.topCenter(Offset.zero),
|
||||
);
|
||||
final RenderBox box = this.context.findRenderObject()! as RenderBox;
|
||||
final Offset target = box.localToGlobal(
|
||||
box.size.center(Offset.zero),
|
||||
ancestor: overlayState.context.findRenderObject(),
|
||||
);
|
||||
|
||||
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
|
||||
verticalOffset: box.size.height / 2,
|
||||
horizontalOffset: box.size.width / 2,
|
||||
type: widget.type,
|
||||
animation: _overlayAnimation,
|
||||
target: target,
|
||||
onDismiss: _scheduleDismissTooltip,
|
||||
overlayWidget: widget.overlayWidget,
|
||||
indicator: widget.indicator,
|
||||
);
|
||||
|
||||
return SelectionContainer.maybeOf(context) == null
|
||||
? overlayChild
|
||||
: SelectionContainer.disabled(child: overlayChild);
|
||||
@@ -138,11 +66,10 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_longPressRecognizer?.onLongPressCancel = null;
|
||||
_longPressRecognizer?.dispose();
|
||||
_backingController?.dispose();
|
||||
_backingOverlayAnimation?.dispose();
|
||||
_longPressRecognizer
|
||||
?..onLongPress = null
|
||||
..dispose();
|
||||
_longPressRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -150,7 +77,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget result;
|
||||
if (Utils.isMobile) {
|
||||
if (PlatformUtils.isMobile) {
|
||||
result = Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -164,7 +91,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
return OverlayPortal(
|
||||
return OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildCustomTooltipOverlay,
|
||||
child: result,
|
||||
@@ -174,232 +101,227 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
|
||||
class _CustomTooltipOverlay extends StatelessWidget {
|
||||
const _CustomTooltipOverlay({
|
||||
required this.verticalOffset,
|
||||
required this.horizontalOffset,
|
||||
required this.type,
|
||||
required this.animation,
|
||||
required this.target,
|
||||
required this.onDismiss,
|
||||
required this.overlayWidget,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final double verticalOffset;
|
||||
final double horizontalOffset;
|
||||
final TooltipType type;
|
||||
final Animation<double> animation;
|
||||
final Offset target;
|
||||
final VoidCallback onDismiss;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = CustomMultiChildLayout(
|
||||
delegate: _CustomMultiTooltipPositionDelegate(
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontalOffset,
|
||||
preferBelow: false,
|
||||
),
|
||||
return _ToolTip(
|
||||
target: target,
|
||||
preferBelow: false,
|
||||
onTap: PlatformUtils.isMobile ? onDismiss : null,
|
||||
children: [
|
||||
LayoutId(
|
||||
id: 'overlay',
|
||||
child: overlayWidget(),
|
||||
),
|
||||
if (indicator != null)
|
||||
LayoutId(
|
||||
id: 'indicator',
|
||||
child: indicator!(),
|
||||
),
|
||||
indicator(),
|
||||
overlayWidget(),
|
||||
],
|
||||
);
|
||||
if (Utils.isMobile) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomMultiTooltipPositionDelegate({
|
||||
required this.type,
|
||||
class _ToolTip extends MultiChildRenderObjectWidget {
|
||||
const _ToolTip({
|
||||
super.children,
|
||||
this.onTap,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.preferBelow,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final Offset target;
|
||||
|
||||
final double verticalOffset;
|
||||
|
||||
final double horizontslOffset;
|
||||
|
||||
final bool preferBelow;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderToolTip(
|
||||
onTap: onTap,
|
||||
target: target,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
case TooltipType.right:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderToolTip renderObject) {
|
||||
renderObject
|
||||
..onTap = onTap
|
||||
..target = target
|
||||
..preferBelow = preferBelow;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
offset.dx - indicatorSize.width + 1,
|
||||
target.dy - indicatorSize.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
class _RenderToolTip extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
_RenderToolTip({
|
||||
VoidCallback? onTap,
|
||||
required this._target,
|
||||
required this._preferBelow,
|
||||
}) : _hitTestSelf = onTap != null {
|
||||
if (onTap != null) {
|
||||
_tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap;
|
||||
}
|
||||
}
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
set onTap(VoidCallback? value) {
|
||||
_tapGestureRecognizer?.onTap = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapGestureRecognizer
|
||||
?..onTap = null
|
||||
..dispose();
|
||||
_tapGestureRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final bool _hitTestSelf;
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => _hitTestSelf;
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry) {
|
||||
if (event is PointerDownEvent) {
|
||||
_tapGestureRecognizer?.addPointer(event);
|
||||
}
|
||||
}
|
||||
|
||||
Offset _target;
|
||||
Offset get target => _target;
|
||||
set target(Offset value) {
|
||||
if (_target == value) return;
|
||||
_target = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
bool _preferBelow;
|
||||
bool get preferBelow => _preferBelow;
|
||||
set preferBelow(bool value) {
|
||||
if (_preferBelow == value) return;
|
||||
_preferBelow = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) {
|
||||
return target != oldDelegate.target ||
|
||||
verticalOffset != oldDelegate.verticalOffset ||
|
||||
preferBelow != oldDelegate.preferBelow;
|
||||
void performLayout() {
|
||||
size = constraints.constrain(constraints.biggest);
|
||||
|
||||
final c = BoxConstraints.loose(size);
|
||||
RenderBox indicator = firstChild!..layout(c, parentUsesSize: true);
|
||||
RenderBox overlay = lastChild!..layout(c, parentUsesSize: true);
|
||||
|
||||
final indicatorSize = indicator.size;
|
||||
final overlaySize = overlay.size;
|
||||
|
||||
final indicatorParentData =
|
||||
indicator.parentData as MultiChildLayoutParentData;
|
||||
final overlayParentData = overlay.parentData as MultiChildLayoutParentData;
|
||||
|
||||
Offset offset = positionDependentBox(
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
overlayParentData.offset = offset;
|
||||
indicatorParentData.offset = Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
class TrianglePainter extends CustomPainter {
|
||||
TrianglePainter(this.color, {this.type = TooltipType.top});
|
||||
final TooltipType type;
|
||||
class Triangle extends LeafRenderObjectWidget {
|
||||
const Triangle({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderTriangle(
|
||||
color: color,
|
||||
preferredSize: size,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderTriangle renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..color = color
|
||||
..preferredSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderTriangle extends RenderBox {
|
||||
RenderTriangle({
|
||||
required this._color,
|
||||
required this._preferredSize,
|
||||
});
|
||||
|
||||
Color _color;
|
||||
Color get color => _color;
|
||||
set color(Color value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Size _preferredSize;
|
||||
set preferredSize(Size value) {
|
||||
if (_preferredSize == value) return;
|
||||
_preferredSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrain(_preferredSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final size = this.size;
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
Path path;
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
path = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width / 2, size.height)
|
||||
..close();
|
||||
case TooltipType.right:
|
||||
path = Path()
|
||||
..moveTo(0, size.height / 2)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width, size.height)
|
||||
..close();
|
||||
}
|
||||
final path = Path()
|
||||
..moveTo(offset.dx, offset.dy)
|
||||
..lineTo(offset.dx + size.width, offset.dy)
|
||||
..lineTo(offset.dx + size.width / 2, size.height + offset.dy)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color;
|
||||
}
|
||||
|
||||
Offset positionDependentBox({
|
||||
required TooltipType type,
|
||||
required Size size,
|
||||
required Size childSize,
|
||||
required Offset target,
|
||||
required bool preferBelow,
|
||||
double verticalOffset = 0.0,
|
||||
double horizontslOffset = 0.0,
|
||||
double margin = 10.0,
|
||||
}) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
// VERTICAL DIRECTION
|
||||
final bool fitsBelow =
|
||||
target.dy + verticalOffset + childSize.height <= size.height - margin;
|
||||
final bool fitsAbove =
|
||||
target.dy - verticalOffset - childSize.height >= margin;
|
||||
final bool tooltipBelow = fitsAbove == fitsBelow
|
||||
? preferBelow
|
||||
: fitsBelow;
|
||||
final double y;
|
||||
if (tooltipBelow) {
|
||||
y = math.min(target.dy + verticalOffset, size.height - margin);
|
||||
} else {
|
||||
y = math.max(target.dy - verticalOffset - childSize.height, margin);
|
||||
} // HORIZONTAL DIRECTION
|
||||
final double flexibleSpace = size.width - childSize.width;
|
||||
final double x = flexibleSpace <= 2 * margin
|
||||
// If there's not enough horizontal space for margin + child, center the
|
||||
// child.
|
||||
? flexibleSpace / 2.0
|
||||
: clampDouble(
|
||||
target.dx - childSize.width / 2,
|
||||
margin,
|
||||
flexibleSpace - margin,
|
||||
);
|
||||
return Offset(x, y);
|
||||
case TooltipType.right:
|
||||
final double dy = math.max(margin, target.dy - childSize.height / 2);
|
||||
final double dx = math.min(
|
||||
target.dx + horizontslOffset,
|
||||
size.width - childSize.width - margin,
|
||||
);
|
||||
return Offset(dx, dy);
|
||||
context.canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,42 +3,35 @@ import 'package:get/get.dart';
|
||||
|
||||
Future<bool> showConfirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
Object? content,
|
||||
required Widget title,
|
||||
Widget? content,
|
||||
// @Deprecated('use `bool result = await showConfirmDialog()` instead')
|
||||
VoidCallback? onConfirm,
|
||||
}) async {
|
||||
assert(content is String? || content is Widget);
|
||||
return await showDialog<bool>(
|
||||
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,
|
||||
),
|
||||
builder: (context) => AlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: true);
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: true);
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
272
lib/common/widgets/dialog/export_import.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'dart:async' show FutureOr;
|
||||
import 'dart:convert' show utf8, jsonDecode;
|
||||
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:PiliPlus/utils/extension/theme_ext.dart';
|
||||
import 'package:PiliPlus/utils/storage_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show Clipboard;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:re_highlight/languages/json.dart';
|
||||
import 'package:re_highlight/re_highlight.dart';
|
||||
import 'package:re_highlight/styles/base16/github.dart';
|
||||
import 'package:re_highlight/styles/github-dark.dart';
|
||||
|
||||
void exportToClipBoard({
|
||||
required ValueGetter<String> onExport,
|
||||
}) {
|
||||
Utils.copyText(onExport());
|
||||
}
|
||||
|
||||
void exportToLocalFile({
|
||||
required ValueGetter<String> onExport,
|
||||
required ValueGetter<String> localFileName,
|
||||
}) {
|
||||
final res = utf8.encode(onExport());
|
||||
StorageUtils.saveBytes2File(
|
||||
name:
|
||||
'piliplus_${localFileName()}_'
|
||||
'${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json',
|
||||
bytes: res,
|
||||
allowedExtensions: const ['json'],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> importFromClipBoard<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required ValueGetter<String> onExport,
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
bool showConfirmDialog = true,
|
||||
}) async {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
if (data?.text case final text? when (text.isNotEmpty)) {
|
||||
if (!context.mounted) return;
|
||||
final T json;
|
||||
final String formatText;
|
||||
try {
|
||||
json = jsonDecode(text);
|
||||
formatText = Utils.jsonEncoder.convert(json);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('解析json失败:$e');
|
||||
return;
|
||||
}
|
||||
bool? executeImport;
|
||||
if (showConfirmDialog) {
|
||||
final highlight = Highlight()..registerLanguage('json', langJson);
|
||||
final result = highlight.highlight(
|
||||
code: formatText,
|
||||
language: 'json',
|
||||
);
|
||||
late TextSpanRenderer renderer;
|
||||
bool? isDarkMode;
|
||||
executeImport = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
final isDark = colorScheme.isDark;
|
||||
if (isDark != isDarkMode) {
|
||||
isDarkMode = isDark;
|
||||
renderer = TextSpanRenderer(
|
||||
null,
|
||||
isDark ? githubDarkTheme : githubTheme,
|
||||
);
|
||||
result.render(renderer);
|
||||
}
|
||||
return AlertDialog(
|
||||
title: Text('是否导入如下$title?'),
|
||||
content: SingleChildScrollView(
|
||||
child: Text.rich(renderer.span!),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text('取消', style: TextStyle(color: colorScheme.outline)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
executeImport = true;
|
||||
}
|
||||
if (executeImport ?? false) {
|
||||
try {
|
||||
await onImport(json);
|
||||
SmartDialog.showToast('导入成功');
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('导入失败:$e');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SmartDialog.showToast('剪贴板无数据');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importFromLocalFile<T>({
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
}) async {
|
||||
final result = await FilePicker.pickFile(
|
||||
type: .custom,
|
||||
allowedExtensions: const ['json', 'txt'],
|
||||
);
|
||||
if (result != null) {
|
||||
final data = await result.xFile.readAsString();
|
||||
final T json;
|
||||
try {
|
||||
json = jsonDecode(data);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('解析json失败:$e');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await onImport(json);
|
||||
SmartDialog.showToast('导入成功');
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('导入失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void importFromInput<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
}) {
|
||||
final key = GlobalKey<FormFieldState<String>>();
|
||||
late T json;
|
||||
String? forceErrorText;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('输入$title'),
|
||||
constraints: Style.dialogFixedConstraints,
|
||||
content: TextFormField(
|
||||
key: key,
|
||||
minLines: 4,
|
||||
maxLines: 12,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
validator: (value) {
|
||||
if (forceErrorText != null) return forceErrorText;
|
||||
try {
|
||||
json = jsonDecode(value!) as T;
|
||||
return null;
|
||||
} catch (e) {
|
||||
return '解析json失败:$e';
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: ColorScheme.of(context).outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (key.currentState?.validate() == true) {
|
||||
try {
|
||||
await onImport(json);
|
||||
Get.back();
|
||||
SmartDialog.showToast('导入成功');
|
||||
return;
|
||||
} catch (e) {
|
||||
forceErrorText = '导入失败:$e';
|
||||
}
|
||||
key.currentState?.validate();
|
||||
forceErrorText = null;
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showImportExportDialog<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required ValueGetter<String> onExport,
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
required ValueGetter<String> localFileName,
|
||||
}) => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
const style = TextStyle(fontSize: 15);
|
||||
return SimpleDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
title: Text('导入/导出$title'),
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('导出至剪贴板', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
exportToClipBoard(onExport: onExport);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('导出文件至本地', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
exportToLocalFile(onExport: onExport, localFileName: localFileName);
|
||||
},
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: ColorScheme.of(context).outline.withValues(alpha: 0.1),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('输入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromInput<T>(context, title: title, onImport: onImport);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('从剪贴板导入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromClipBoard<T>(
|
||||
context,
|
||||
title: title,
|
||||
onExport: onExport,
|
||||
onImport: onImport,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('从本地文件导入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromLocalFile<T>(onImport: onImport);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@@ -8,93 +9,81 @@ import 'package:get/get.dart';
|
||||
Future<void> autoWrapReportDialog(
|
||||
BuildContext context,
|
||||
Map<String, Map<int, String>> options,
|
||||
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess,
|
||||
) {
|
||||
Future<LoadingState> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess, {
|
||||
bool ban = true,
|
||||
}) {
|
||||
int? reasonType;
|
||||
String? reasonDesc;
|
||||
bool banUid = false;
|
||||
late final key = GlobalKey<FormState>();
|
||||
late final key = GlobalKey<FormFieldState<String>>();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
constraints: const BoxConstraints(minWidth: 280, maxWidth: 420),
|
||||
title: const Text('举报'),
|
||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
||||
actionsPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 22,
|
||||
right: 22,
|
||||
bottom: 5,
|
||||
),
|
||||
child: Text('请选择举报的理由:'),
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('举报'),
|
||||
titlePadding: const .only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const .symmetric(vertical: 5),
|
||||
actionsPadding: const .only(left: 16, right: 16, bottom: 10),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: .only(left: 22, right: 22, bottom: 5),
|
||||
child: Text('请选择举报的理由:'),
|
||||
),
|
||||
RadioGroup(
|
||||
onChanged: (value) {
|
||||
reasonType = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonType,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.entries.map((entry) {
|
||||
return WrapRadioOptionsGroup<int>(
|
||||
groupTitle: entry.key,
|
||||
options: entry.value,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
RadioGroup(
|
||||
onChanged: (value) {
|
||||
reasonType = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonType,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.entries.map((entry) {
|
||||
return WrapRadioOptionsGroup<int>(
|
||||
groupTitle: entry.key,
|
||||
options: entry.value,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const .only(left: 22, top: 5, right: 22),
|
||||
child: TextFormField(
|
||||
key: key,
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
initialValue: reasonDesc,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: .all(10),
|
||||
labelStyle: TextStyle(fontSize: 14),
|
||||
floatingLabelStyle: TextStyle(fontSize: 14),
|
||||
),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
validator: (value) =>
|
||||
value.isNullOrEmpty ? '理由不能为空' : null,
|
||||
),
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 22,
|
||||
top: 5,
|
||||
right: 22,
|
||||
),
|
||||
child: Form(
|
||||
key: key,
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
initialValue: reasonDesc,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
validator: (value) =>
|
||||
value.isNullOrEmpty ? '理由不能为空' : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ban)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
@@ -102,43 +91,42 @@ Future<void> autoWrapReportDialog(
|
||||
onChanged: (value) => banUid = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (reasonType == null ||
|
||||
(reasonType == 0 && key.currentState?.validate() != true)) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final data = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (data['code'] == 0) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
SmartDialog.showToast(data['message'].toString());
|
||||
}
|
||||
} catch (e, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: ColorScheme.of(context).outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (reasonType == null ||
|
||||
(reasonType == 0 && key.currentState?.validate() != true)) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final res = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (res.isSuccess) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
} catch (e, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,7 +157,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -202,7 +190,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
|
||||
}
|
||||
}
|
||||
|
||||
class ReportOptions {
|
||||
abstract final class ReportOptions {
|
||||
// from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js
|
||||
static Map<String, Map<int, String>> get commentReport => const {
|
||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||
@@ -264,4 +252,16 @@ class ReportOptions {
|
||||
7: '其他', // avoid show form
|
||||
},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get imMsgReport => const {
|
||||
'': {
|
||||
1: '色情低俗',
|
||||
2: '政治敏感',
|
||||
3: '违法有害',
|
||||
4: '广告骚扰',
|
||||
5: '人身攻击',
|
||||
6: '诈骗',
|
||||
0: '其他问题',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,21 +146,16 @@ Future<void> showMemberReportDialog(
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
if (reason.isEmpty) {
|
||||
SmartDialog.showToast('至少选择一项作为举报内容');
|
||||
} else {
|
||||
Get.back();
|
||||
var result = await MemberHttp.reportMember(
|
||||
MemberHttp.reportMember(
|
||||
mid,
|
||||
reason: reason.join(','),
|
||||
reasonV2: reasonV2 != null ? reasonV2! + 1 : null,
|
||||
);
|
||||
if (result['msg'] is String && result['msg'].isNotEmpty) {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
} else {
|
||||
SmartDialog.showToast('举报失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
|
||||
@@ -3,62 +3,127 @@ 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;
|
||||
|
||||
class DisabledIcon extends SingleChildRenderObjectWidget {
|
||||
const DisabledIcon({
|
||||
super.key,
|
||||
required T child,
|
||||
required Widget super.child,
|
||||
this.disable = false,
|
||||
this.color,
|
||||
double? lineLengthScale,
|
||||
StrokeCap? strokeCap,
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
this.iconSize,
|
||||
this.lineLengthScale = 0.9,
|
||||
this.strokeCap = .butt,
|
||||
});
|
||||
|
||||
final bool disable;
|
||||
final Color? color;
|
||||
final double? iconSize;
|
||||
final StrokeCap strokeCap;
|
||||
final double lineLengthScale;
|
||||
|
||||
Icon? get _icon => child is Icon ? child as Icon : null;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
late final iconTheme = IconTheme.of(context);
|
||||
final icon = _icon;
|
||||
return RenderMaskedIcon(
|
||||
color ??
|
||||
(child is Icon
|
||||
? (child as Icon).color ?? IconTheme.of(context).color!
|
||||
: IconTheme.of(context).color!),
|
||||
lineLengthScale,
|
||||
strokeCap,
|
||||
disable: disable,
|
||||
iconSize: iconSize ?? icon?.size ?? iconTheme.size ?? 24.0,
|
||||
color: color ?? icon?.color ?? iconTheme.color!,
|
||||
strokeCap: strokeCap,
|
||||
lineLengthScale: lineLengthScale,
|
||||
);
|
||||
}
|
||||
|
||||
T enable() => child as T;
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) {
|
||||
late final iconTheme = IconTheme.of(context);
|
||||
final icon = _icon;
|
||||
renderObject
|
||||
..disable = disable
|
||||
..iconSize = iconSize ?? icon?.size ?? iconTheme.size ?? 24.0
|
||||
..color = color ?? icon?.color ?? iconTheme.color!
|
||||
..strokeCap = strokeCap
|
||||
..lineLengthScale = lineLengthScale;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderMaskedIcon extends RenderProxyBox {
|
||||
final Color color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
RenderMaskedIcon({
|
||||
required this._disable,
|
||||
required this._iconSize,
|
||||
required this._color,
|
||||
required this._strokeCap,
|
||||
required this._lineLengthScale,
|
||||
});
|
||||
|
||||
RenderMaskedIcon(this.color, this.lineLengthScale, this.strokeCap);
|
||||
bool _disable;
|
||||
bool get disable => _disable;
|
||||
set disable(bool value) {
|
||||
if (_disable == value) return;
|
||||
_disable = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _iconSize;
|
||||
double get iconSize => _iconSize;
|
||||
set iconSize(double value) {
|
||||
if (_iconSize == value) return;
|
||||
_iconSize = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Color _color;
|
||||
Color get color => _color;
|
||||
set color(Color value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
StrokeCap _strokeCap;
|
||||
StrokeCap get strokeCap => _strokeCap;
|
||||
set strokeCap(StrokeCap value) {
|
||||
if (_strokeCap == value) return;
|
||||
_strokeCap = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _lineLengthScale;
|
||||
double get lineLengthScale => _lineLengthScale;
|
||||
set lineLengthScale(double value) {
|
||||
if (_lineLengthScale == value) return;
|
||||
_lineLengthScale = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final strokeWidth = size.width / 12;
|
||||
if (!disable) {
|
||||
return super.paint(context, offset);
|
||||
}
|
||||
|
||||
final canvas = context.canvas;
|
||||
var rect = offset & size;
|
||||
|
||||
var rectOffset = offset;
|
||||
Size size = this.size;
|
||||
final exceedWidth = size.width > _iconSize;
|
||||
final exceedHeight = size.height > _iconSize;
|
||||
if (exceedWidth || exceedHeight) {
|
||||
final dx = exceedWidth ? (size.width - _iconSize) / 2.0 : 0.0;
|
||||
final dy = exceedHeight ? (size.height - _iconSize) / 2.0 : 0.0;
|
||||
size = Size.square(_iconSize);
|
||||
rectOffset += Offset(dx, dy);
|
||||
} else if (size.width < _iconSize && size.height < _iconSize) {
|
||||
size = Size.square(_iconSize);
|
||||
}
|
||||
|
||||
final strokeWidth = size.width / 12;
|
||||
|
||||
var rect = rectOffset & 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
|
||||
@@ -77,7 +142,7 @@ class RenderMaskedIcon extends RenderProxyBox {
|
||||
..clipPath(path, doAntiAlias: false);
|
||||
super.paint(context, offset);
|
||||
|
||||
context.canvas.restore();
|
||||
canvas.restore();
|
||||
|
||||
final linePaint = Paint()
|
||||
..color = color
|
||||
@@ -95,8 +160,3 @@ class RenderMaskedIcon extends RenderProxyBox {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DisabledIconExt on Icon {
|
||||
DisabledIcon<Icon> disable([double? lineLengthScale]) =>
|
||||
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
|
||||
}
|
||||
|
||||
91
lib/common/widgets/draggable_sheet/dyn.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
part of 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart';
|
||||
|
||||
class DynDraggableScrollableSheet extends DraggableScrollableSheet {
|
||||
const DynDraggableScrollableSheet({
|
||||
super.key,
|
||||
super.initialChildSize,
|
||||
super.minChildSize,
|
||||
super.maxChildSize,
|
||||
super.expand,
|
||||
super.snap,
|
||||
super.snapSizes,
|
||||
super.snapAnimationDuration,
|
||||
super.controller,
|
||||
super.shouldCloseOnMinExtent,
|
||||
required super.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DraggableScrollableSheet> createState() =>
|
||||
_DynDraggableScrollableSheetState();
|
||||
}
|
||||
|
||||
class _DynDraggableScrollableSheetState extends _DraggableScrollableSheetState {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_extent = _DraggableSheetExtent(
|
||||
minSize: widget.minChildSize,
|
||||
maxSize: widget.maxChildSize,
|
||||
snap: widget.snap,
|
||||
snapSizes: _impliedSnapSizes(),
|
||||
snapAnimationDuration: widget.snapAnimationDuration,
|
||||
initialSize: widget.initialChildSize,
|
||||
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
|
||||
);
|
||||
_scrollController = _DynDraggableScrollableSheetScrollController(
|
||||
extent: _extent,
|
||||
);
|
||||
widget.controller?._attach(_scrollController);
|
||||
}
|
||||
}
|
||||
|
||||
class _DynDraggableScrollableSheetScrollController
|
||||
extends _DraggableScrollableSheetScrollController {
|
||||
_DynDraggableScrollableSheetScrollController({
|
||||
required super.extent,
|
||||
});
|
||||
|
||||
@override
|
||||
_DraggableScrollableSheetScrollPosition createScrollPosition(
|
||||
ScrollPhysics physics,
|
||||
ScrollContext context,
|
||||
ScrollPosition? oldPosition,
|
||||
) {
|
||||
return _DynDraggableScrollableSheetScrollPosition(
|
||||
physics: physics.applyTo(const AlwaysScrollableScrollPhysics()),
|
||||
context: context,
|
||||
oldPosition: oldPosition,
|
||||
getExtent: () => extent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DynDraggableScrollableSheetScrollPosition
|
||||
extends _DraggableScrollableSheetScrollPosition {
|
||||
_DynDraggableScrollableSheetScrollPosition({
|
||||
required super.physics,
|
||||
required super.context,
|
||||
super.oldPosition,
|
||||
required super.getExtent,
|
||||
});
|
||||
|
||||
bool _isAtTop = true;
|
||||
|
||||
@override
|
||||
bool get listShouldScroll => !_isAtTop || super.listShouldScroll;
|
||||
|
||||
@override
|
||||
void applyUserOffset(double delta) {
|
||||
if (_isAtTop && pixels > 0) {
|
||||
_isAtTop = false;
|
||||
}
|
||||
super.applyUserOffset(delta);
|
||||
}
|
||||
|
||||
@override
|
||||
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
|
||||
_isAtTop = pixels == 0;
|
||||
return super.drag(details, dragCancelCallback);
|
||||
}
|
||||
}
|
||||
74
lib/common/widgets/draggable_sheet/topic.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
part of 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart';
|
||||
|
||||
class TopicDraggableScrollableSheet extends DraggableScrollableSheet {
|
||||
const TopicDraggableScrollableSheet({
|
||||
super.key,
|
||||
super.initialChildSize,
|
||||
super.minChildSize,
|
||||
super.maxChildSize,
|
||||
super.expand,
|
||||
super.snap,
|
||||
super.snapSizes,
|
||||
super.snapAnimationDuration,
|
||||
super.controller,
|
||||
super.shouldCloseOnMinExtent,
|
||||
required super.builder,
|
||||
this.initialScrollOffset = 0.0,
|
||||
});
|
||||
|
||||
final double initialScrollOffset;
|
||||
|
||||
@override
|
||||
State<DraggableScrollableSheet> createState() =>
|
||||
_TopicDraggableScrollableSheetState();
|
||||
}
|
||||
|
||||
class _TopicDraggableScrollableSheetState
|
||||
extends _DraggableScrollableSheetState {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_extent = _DraggableSheetExtent(
|
||||
minSize: widget.minChildSize,
|
||||
maxSize: widget.maxChildSize,
|
||||
snap: widget.snap,
|
||||
snapSizes: _impliedSnapSizes(),
|
||||
snapAnimationDuration: widget.snapAnimationDuration,
|
||||
initialSize: widget.initialChildSize,
|
||||
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
|
||||
);
|
||||
_scrollController = _TopicDraggableScrollableSheetScrollController(
|
||||
extent: _extent,
|
||||
initialScrollOffset:
|
||||
(widget as TopicDraggableScrollableSheet).initialScrollOffset,
|
||||
);
|
||||
widget.controller?._attach(_scrollController);
|
||||
}
|
||||
}
|
||||
|
||||
class _TopicDraggableScrollableSheetScrollController
|
||||
extends _DraggableScrollableSheetScrollController {
|
||||
_TopicDraggableScrollableSheetScrollController({
|
||||
required super.extent,
|
||||
this._initialScrollOffset = 0.0,
|
||||
});
|
||||
|
||||
@override
|
||||
double get initialScrollOffset => _initialScrollOffset;
|
||||
final double _initialScrollOffset;
|
||||
|
||||
@override
|
||||
_DraggableScrollableSheetScrollPosition createScrollPosition(
|
||||
ScrollPhysics physics,
|
||||
ScrollContext context,
|
||||
ScrollPosition? oldPosition,
|
||||
) {
|
||||
return _DraggableScrollableSheetScrollPosition(
|
||||
physics: physics.applyTo(const AlwaysScrollableScrollPhysics()),
|
||||
context: context,
|
||||
oldPosition: oldPosition,
|
||||
getExtent: () => extent,
|
||||
initialPixels: _initialScrollOffset,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
/*
|
||||
* This file is part of PiliPlus
|
||||
*
|
||||
* PiliPlus is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* PiliPlus is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/custom_height_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart';
|
||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
|
||||
import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
|
||||
show LayoutCallback;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
|
||||
import 'package:flutter/rendering.dart' show RenderOpacity, OpacityLayer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// ref [SliverAppBar]
|
||||
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
_SliverAppBarDelegate({
|
||||
required this.leading,
|
||||
required this.automaticallyImplyLeading,
|
||||
required this.title,
|
||||
required this.actions,
|
||||
required this.automaticallyImplyActions,
|
||||
required this.flexibleSpace,
|
||||
required this.bottom,
|
||||
required this.elevation,
|
||||
required this.scrolledUnderElevation,
|
||||
required this.shadowColor,
|
||||
required this.surfaceTintColor,
|
||||
required this.forceElevated,
|
||||
required this.backgroundColor,
|
||||
required this.foregroundColor,
|
||||
required this.iconTheme,
|
||||
required this.actionsIconTheme,
|
||||
required this.primary,
|
||||
required this.centerTitle,
|
||||
required this.excludeHeaderSemantics,
|
||||
required this.titleSpacing,
|
||||
required this.collapsedHeight,
|
||||
required this.topPadding,
|
||||
required this.shape,
|
||||
required this.toolbarHeight,
|
||||
required this.leadingWidth,
|
||||
required this.toolbarTextStyle,
|
||||
required this.titleTextStyle,
|
||||
required this.systemOverlayStyle,
|
||||
required this.forceMaterialTransparency,
|
||||
required this.useDefaultSemanticsOrder,
|
||||
required this.clipBehavior,
|
||||
required this.actionsPadding,
|
||||
}) : assert(primary || topPadding == 0.0),
|
||||
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
|
||||
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
final Widget title;
|
||||
final List<Widget>? actions;
|
||||
final bool automaticallyImplyActions;
|
||||
final Widget flexibleSpace;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final double? elevation;
|
||||
final double? scrolledUnderElevation;
|
||||
final Color? shadowColor;
|
||||
final Color? surfaceTintColor;
|
||||
final bool forceElevated;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final IconThemeData? iconTheme;
|
||||
final IconThemeData? actionsIconTheme;
|
||||
final bool primary;
|
||||
final bool? centerTitle;
|
||||
final bool excludeHeaderSemantics;
|
||||
final double? titleSpacing;
|
||||
final double collapsedHeight;
|
||||
final double topPadding;
|
||||
final ShapeBorder? shape;
|
||||
final double? toolbarHeight;
|
||||
final double? leadingWidth;
|
||||
final TextStyle? toolbarTextStyle;
|
||||
final TextStyle? titleTextStyle;
|
||||
final SystemUiOverlayStyle? systemOverlayStyle;
|
||||
final double _bottomHeight;
|
||||
final bool forceMaterialTransparency;
|
||||
final bool useDefaultSemanticsOrder;
|
||||
final Clip? clipBehavior;
|
||||
final EdgeInsetsGeometry? actionsPadding;
|
||||
|
||||
@override
|
||||
double get minExtent => collapsedHeight;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
double? maxExtent,
|
||||
) {
|
||||
maxExtent ??= double.infinity;
|
||||
final bool isScrolledUnder =
|
||||
overlapsContent ||
|
||||
forceElevated ||
|
||||
(shrinkOffset > maxExtent - minExtent);
|
||||
final effectiveTitle = AnimatedOpacity(
|
||||
opacity: isScrolledUnder ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: const Cubic(0.2, 0.0, 0.0, 1.0),
|
||||
child: title,
|
||||
);
|
||||
|
||||
return FlexibleSpaceBar.createSettings(
|
||||
minExtent: minExtent,
|
||||
maxExtent: maxExtent,
|
||||
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
|
||||
isScrolledUnder: isScrolledUnder,
|
||||
hasLeading: leading != null || automaticallyImplyLeading,
|
||||
child: AppBar(
|
||||
clipBehavior: clipBehavior,
|
||||
leading: leading,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
title: effectiveTitle,
|
||||
actions: actions,
|
||||
automaticallyImplyActions: automaticallyImplyActions,
|
||||
flexibleSpace: IgnorePointer(
|
||||
ignoring: isScrolledUnder,
|
||||
child: DynamicFlexibleSpaceBar(background: flexibleSpace),
|
||||
),
|
||||
bottom: bottom,
|
||||
elevation: isScrolledUnder ? elevation : 0.0,
|
||||
scrolledUnderElevation: scrolledUnderElevation,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
iconTheme: iconTheme,
|
||||
actionsIconTheme: actionsIconTheme,
|
||||
primary: primary,
|
||||
centerTitle: centerTitle,
|
||||
excludeHeaderSemantics: excludeHeaderSemantics,
|
||||
titleSpacing: titleSpacing,
|
||||
shape: shape,
|
||||
toolbarHeight: toolbarHeight,
|
||||
leadingWidth: leadingWidth,
|
||||
toolbarTextStyle: toolbarTextStyle,
|
||||
titleTextStyle: titleTextStyle,
|
||||
systemOverlayStyle: systemOverlayStyle,
|
||||
forceMaterialTransparency: forceMaterialTransparency,
|
||||
useDefaultSemanticsOrder: useDefaultSemanticsOrder,
|
||||
actionsPadding: actionsPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
|
||||
return leading != oldDelegate.leading ||
|
||||
automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading ||
|
||||
title != oldDelegate.title ||
|
||||
actions != oldDelegate.actions ||
|
||||
automaticallyImplyActions != oldDelegate.automaticallyImplyActions ||
|
||||
flexibleSpace != oldDelegate.flexibleSpace ||
|
||||
bottom != oldDelegate.bottom ||
|
||||
_bottomHeight != oldDelegate._bottomHeight ||
|
||||
elevation != oldDelegate.elevation ||
|
||||
shadowColor != oldDelegate.shadowColor ||
|
||||
backgroundColor != oldDelegate.backgroundColor ||
|
||||
foregroundColor != oldDelegate.foregroundColor ||
|
||||
iconTheme != oldDelegate.iconTheme ||
|
||||
actionsIconTheme != oldDelegate.actionsIconTheme ||
|
||||
primary != oldDelegate.primary ||
|
||||
centerTitle != oldDelegate.centerTitle ||
|
||||
titleSpacing != oldDelegate.titleSpacing ||
|
||||
topPadding != oldDelegate.topPadding ||
|
||||
forceElevated != oldDelegate.forceElevated ||
|
||||
toolbarHeight != oldDelegate.toolbarHeight ||
|
||||
leadingWidth != oldDelegate.leadingWidth ||
|
||||
toolbarTextStyle != oldDelegate.toolbarTextStyle ||
|
||||
titleTextStyle != oldDelegate.titleTextStyle ||
|
||||
systemOverlayStyle != oldDelegate.systemOverlayStyle ||
|
||||
forceMaterialTransparency != oldDelegate.forceMaterialTransparency ||
|
||||
useDefaultSemanticsOrder != oldDelegate.useDefaultSemanticsOrder ||
|
||||
actionsPadding != oldDelegate.actionsPadding;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicSliverAppBar extends StatelessWidget {
|
||||
const DynamicSliverAppBar.medium({
|
||||
super.key,
|
||||
this.leading,
|
||||
this.automaticallyImplyLeading = true,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.automaticallyImplyActions = true,
|
||||
required this.flexibleSpace,
|
||||
this.bottom,
|
||||
this.elevation,
|
||||
this.scrolledUnderElevation,
|
||||
this.shadowColor,
|
||||
this.surfaceTintColor,
|
||||
this.forceElevated = false,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.iconTheme,
|
||||
this.actionsIconTheme,
|
||||
this.primary = true,
|
||||
this.centerTitle,
|
||||
this.excludeHeaderSemantics = false,
|
||||
this.titleSpacing,
|
||||
this.shape,
|
||||
this.leadingWidth,
|
||||
this.toolbarTextStyle,
|
||||
this.titleTextStyle,
|
||||
this.systemOverlayStyle,
|
||||
this.forceMaterialTransparency = false,
|
||||
this.useDefaultSemanticsOrder = true,
|
||||
this.clipBehavior,
|
||||
this.actionsPadding,
|
||||
this.onPerformLayout,
|
||||
});
|
||||
|
||||
final LayoutCallback? onPerformLayout;
|
||||
|
||||
final Widget? leading;
|
||||
|
||||
final bool automaticallyImplyLeading;
|
||||
|
||||
final Widget title;
|
||||
|
||||
final List<Widget>? actions;
|
||||
|
||||
final bool automaticallyImplyActions;
|
||||
|
||||
final Widget flexibleSpace;
|
||||
|
||||
final PreferredSizeWidget? bottom;
|
||||
|
||||
final double? elevation;
|
||||
|
||||
final double? scrolledUnderElevation;
|
||||
|
||||
final Color? shadowColor;
|
||||
|
||||
final Color? surfaceTintColor;
|
||||
|
||||
final bool forceElevated;
|
||||
|
||||
final Color? backgroundColor;
|
||||
|
||||
final Color? foregroundColor;
|
||||
|
||||
final IconThemeData? iconTheme;
|
||||
|
||||
final IconThemeData? actionsIconTheme;
|
||||
|
||||
final bool primary;
|
||||
|
||||
final bool? centerTitle;
|
||||
|
||||
final bool excludeHeaderSemantics;
|
||||
|
||||
final double? titleSpacing;
|
||||
|
||||
final ShapeBorder? shape;
|
||||
|
||||
final double? leadingWidth;
|
||||
|
||||
final TextStyle? toolbarTextStyle;
|
||||
|
||||
final TextStyle? titleTextStyle;
|
||||
|
||||
final SystemUiOverlayStyle? systemOverlayStyle;
|
||||
|
||||
final bool forceMaterialTransparency;
|
||||
|
||||
final bool useDefaultSemanticsOrder;
|
||||
|
||||
final Clip? clipBehavior;
|
||||
|
||||
final EdgeInsetsGeometry? actionsPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double bottomHeight = bottom?.preferredSize.height ?? 0.0;
|
||||
final double topPadding = primary
|
||||
? MediaQuery.viewPaddingOf(context).top
|
||||
: 0.0;
|
||||
final double effectiveCollapsedHeight =
|
||||
topPadding + kToolbarHeight + bottomHeight + 1;
|
||||
|
||||
return SliverPinnedHeader(
|
||||
onPerformLayout: onPerformLayout,
|
||||
delegate: _SliverAppBarDelegate(
|
||||
leading: leading,
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
title: title,
|
||||
actions: actions,
|
||||
automaticallyImplyActions: automaticallyImplyActions,
|
||||
flexibleSpace: flexibleSpace,
|
||||
bottom: bottom,
|
||||
elevation: elevation,
|
||||
scrolledUnderElevation: scrolledUnderElevation,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
forceElevated: forceElevated,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
iconTheme: iconTheme,
|
||||
actionsIconTheme: actionsIconTheme,
|
||||
primary: primary,
|
||||
centerTitle: centerTitle,
|
||||
excludeHeaderSemantics: excludeHeaderSemantics,
|
||||
titleSpacing: titleSpacing,
|
||||
collapsedHeight: effectiveCollapsedHeight,
|
||||
topPadding: topPadding,
|
||||
shape: shape,
|
||||
toolbarHeight: kToolbarHeight,
|
||||
leadingWidth: leadingWidth,
|
||||
toolbarTextStyle: toolbarTextStyle,
|
||||
titleTextStyle: titleTextStyle,
|
||||
systemOverlayStyle: systemOverlayStyle,
|
||||
forceMaterialTransparency: forceMaterialTransparency,
|
||||
useDefaultSemanticsOrder: useDefaultSemanticsOrder,
|
||||
clipBehavior: clipBehavior,
|
||||
actionsPadding: actionsPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ref [FlexibleSpaceBar]
|
||||
class DynamicFlexibleSpaceBar extends StatelessWidget {
|
||||
const DynamicFlexibleSpaceBar({
|
||||
super.key,
|
||||
required this.background,
|
||||
this.collapseMode = CollapseMode.parallax,
|
||||
});
|
||||
|
||||
final Widget background;
|
||||
|
||||
final CollapseMode collapseMode;
|
||||
|
||||
static double _getCollapsePadding(
|
||||
CollapseMode collapseMode,
|
||||
double t,
|
||||
FlexibleSpaceBarSettings settings,
|
||||
) {
|
||||
switch (collapseMode) {
|
||||
case CollapseMode.pin:
|
||||
return -(settings.maxExtent - settings.currentExtent);
|
||||
case CollapseMode.none:
|
||||
return 0.0;
|
||||
case CollapseMode.parallax:
|
||||
final double deltaExtent = settings.maxExtent - settings.minExtent;
|
||||
return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final FlexibleSpaceBarSettings settings = context
|
||||
.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
|
||||
|
||||
double? height;
|
||||
final double opacity;
|
||||
final double topPadding;
|
||||
if (settings.maxExtent == .infinity) {
|
||||
opacity = 1.0;
|
||||
topPadding = 0.0;
|
||||
} else {
|
||||
height = settings.maxExtent;
|
||||
|
||||
final double deltaExtent = settings.maxExtent - settings.minExtent;
|
||||
|
||||
// 0.0 -> Expanded
|
||||
// 1.0 -> Collapsed to toolbar
|
||||
final double t = clampDouble(
|
||||
1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
|
||||
final double fadeStart = math.max(
|
||||
0.0,
|
||||
1.0 - kToolbarHeight / deltaExtent,
|
||||
);
|
||||
const fadeEnd = 1.0;
|
||||
assert(fadeStart <= fadeEnd);
|
||||
// If the min and max extent are the same, the app bar cannot collapse
|
||||
// and the content should be visible, so opacity = 1.
|
||||
opacity = settings.maxExtent == settings.minExtent
|
||||
? 1.0
|
||||
: 1.0 - Interval(fadeStart, fadeEnd).transform(t);
|
||||
|
||||
topPadding = _getCollapsePadding(collapseMode, t, settings);
|
||||
}
|
||||
|
||||
return ClipRect(
|
||||
child: CustomHeightWidget(
|
||||
height: height,
|
||||
offset: Offset(0.0, topPadding),
|
||||
child: _FlexibleSpaceHeaderOpacity(
|
||||
// IOS is relying on this semantics node to correctly traverse
|
||||
// through the app bar when it is collapsed.
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: opacity,
|
||||
child: background,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [_FlexibleSpaceHeaderOpacity]
|
||||
class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget {
|
||||
const _FlexibleSpaceHeaderOpacity({
|
||||
required this.opacity,
|
||||
required super.child,
|
||||
required this.alwaysIncludeSemantics,
|
||||
});
|
||||
|
||||
final double opacity;
|
||||
final bool alwaysIncludeSemantics;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderFlexibleSpaceHeaderOpacity(
|
||||
opacity: opacity,
|
||||
alwaysIncludeSemantics: alwaysIncludeSemantics,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant _RenderFlexibleSpaceHeaderOpacity renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..alwaysIncludeSemantics = alwaysIncludeSemantics
|
||||
..opacity = opacity;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity {
|
||||
_RenderFlexibleSpaceHeaderOpacity({
|
||||
super.opacity,
|
||||
super.alwaysIncludeSemantics,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => false;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child == null) {
|
||||
return;
|
||||
}
|
||||
if ((opacity * 255).roundToDouble() <= 0) {
|
||||
layer = null;
|
||||
return;
|
||||
}
|
||||
assert(needsCompositing);
|
||||
layer = context.pushOpacity(
|
||||
offset,
|
||||
(opacity * 255).round(),
|
||||
super.paint,
|
||||
oldLayer: layer as OpacityLayer?,
|
||||
);
|
||||
assert(() {
|
||||
layer!.debugCreator = debugCreator;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* This file is part of PiliPlus
|
||||
*
|
||||
* PiliPlus is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* PiliPlus is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
|
||||
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart' hide LayoutCallback;
|
||||
import 'package:flutter/widgets.dart'
|
||||
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
|
||||
|
||||
/// ref [SliverPersistentHeader]
|
||||
|
||||
Rect? _trim(
|
||||
Rect? original, {
|
||||
double top = -double.infinity,
|
||||
double right = double.infinity,
|
||||
double bottom = double.infinity,
|
||||
double left = -double.infinity,
|
||||
}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
|
||||
|
||||
abstract class RenderSliverPersistentHeader extends RenderSliver
|
||||
with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
|
||||
RenderSliverPersistentHeader({RenderBox? child}) {
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
SliverPersistentHeaderElement? element;
|
||||
|
||||
double get minExtent =>
|
||||
(element!.widget as SliverPinnedHeader).delegate.minExtent;
|
||||
|
||||
bool _needsUpdateChild = true;
|
||||
|
||||
double get lastShrinkOffset => _lastShrinkOffset;
|
||||
double _lastShrinkOffset = 0.0;
|
||||
|
||||
bool get lastOverlapsContent => _lastOverlapsContent;
|
||||
bool _lastOverlapsContent = false;
|
||||
|
||||
@protected
|
||||
void updateChild(
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
double? maxExtent,
|
||||
) {
|
||||
assert(element != null);
|
||||
element!.build(shrinkOffset, overlapsContent, maxExtent);
|
||||
}
|
||||
|
||||
@override
|
||||
void markNeedsLayout() {
|
||||
_needsUpdateChild = true;
|
||||
super.markNeedsLayout();
|
||||
}
|
||||
|
||||
@protected
|
||||
void updateChildIfNeeded(
|
||||
double scrollOffset,
|
||||
double? maxExtent, {
|
||||
bool overlapsContent = false,
|
||||
}) {
|
||||
final double shrinkOffset = maxExtent == null
|
||||
? scrollOffset
|
||||
: math.min(scrollOffset, maxExtent);
|
||||
if (_needsUpdateChild ||
|
||||
_lastShrinkOffset != shrinkOffset ||
|
||||
_lastOverlapsContent != overlapsContent) {
|
||||
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
|
||||
assert(constraints == this.constraints);
|
||||
updateChild(shrinkOffset, overlapsContent, maxExtent);
|
||||
});
|
||||
_lastShrinkOffset = shrinkOffset;
|
||||
_lastOverlapsContent = overlapsContent;
|
||||
_needsUpdateChild = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double childMainAxisPosition(covariant RenderObject child) =>
|
||||
super.childMainAxisPosition(child);
|
||||
|
||||
@override
|
||||
bool hitTestChildren(
|
||||
SliverHitTestResult result, {
|
||||
required double mainAxisPosition,
|
||||
required double crossAxisPosition,
|
||||
}) {
|
||||
assert(geometry!.hitTestExtent > 0.0);
|
||||
if (child != null) {
|
||||
return hitTestBoxChild(
|
||||
BoxHitTestResult.wrap(result),
|
||||
child!,
|
||||
mainAxisPosition: mainAxisPosition,
|
||||
crossAxisPosition: crossAxisPosition,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
||||
assert(child == this.child);
|
||||
applyPaintTransformForBoxChild(child as RenderBox, transform);
|
||||
}
|
||||
|
||||
void triggerRebuild() {
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
class SliverPinnedHeader extends RenderObjectWidget {
|
||||
const SliverPinnedHeader({
|
||||
super.key,
|
||||
required this.delegate,
|
||||
this.onPerformLayout,
|
||||
});
|
||||
|
||||
final SliverPersistentHeaderDelegate delegate;
|
||||
final LayoutCallback? onPerformLayout;
|
||||
|
||||
@override
|
||||
SliverPersistentHeaderElement createElement() =>
|
||||
SliverPersistentHeaderElement(this);
|
||||
|
||||
@override
|
||||
RenderSliverPinnedHeader createRenderObject(BuildContext context) {
|
||||
return RenderSliverPinnedHeader(onPerformLayout: onPerformLayout);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderSliverPinnedHeader renderObject,
|
||||
) {
|
||||
renderObject.onPerformLayout = onPerformLayout;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSliverPinnedHeader extends RenderSliverPersistentHeader {
|
||||
RenderSliverPinnedHeader({
|
||||
super.child,
|
||||
this.onPerformLayout,
|
||||
});
|
||||
|
||||
LayoutCallback? onPerformLayout;
|
||||
|
||||
({double crossAxisExtent, double maxExtent})? _maxExtent;
|
||||
double? get maxExtent => _maxExtent?.maxExtent;
|
||||
|
||||
void _rawLayout() {
|
||||
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
|
||||
_maxExtent = (
|
||||
crossAxisExtent: constraints.crossAxisExtent,
|
||||
maxExtent: child!.size.height,
|
||||
);
|
||||
onPerformLayout?.call(child!.size);
|
||||
}
|
||||
|
||||
void _layout() {
|
||||
final double shrinkOffset = math.min(
|
||||
constraints.scrollOffset,
|
||||
_maxExtent!.maxExtent,
|
||||
);
|
||||
child!.layout(
|
||||
constraints.asBoxConstraints(
|
||||
maxExtent: math.max(minExtent, _maxExtent!.maxExtent - shrinkOffset),
|
||||
),
|
||||
parentUsesSize: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final constraints = this.constraints;
|
||||
final bool overlapsContent = constraints.overlap > 0.0;
|
||||
|
||||
if (_maxExtent == null) {
|
||||
updateChildIfNeeded(
|
||||
constraints.scrollOffset,
|
||||
_maxExtent?.maxExtent,
|
||||
overlapsContent: overlapsContent,
|
||||
);
|
||||
_rawLayout();
|
||||
} else {
|
||||
if (_maxExtent!.crossAxisExtent == constraints.crossAxisExtent) {
|
||||
updateChildIfNeeded(
|
||||
constraints.scrollOffset,
|
||||
_maxExtent?.maxExtent,
|
||||
overlapsContent: overlapsContent,
|
||||
);
|
||||
_layout();
|
||||
} else {
|
||||
_needsUpdateChild = true;
|
||||
updateChildIfNeeded(
|
||||
constraints.scrollOffset,
|
||||
null,
|
||||
overlapsContent: overlapsContent,
|
||||
);
|
||||
_rawLayout();
|
||||
if (constraints.scrollOffset > 0.0) {
|
||||
_needsUpdateChild = true;
|
||||
updateChildIfNeeded(
|
||||
constraints.scrollOffset,
|
||||
_maxExtent?.maxExtent,
|
||||
overlapsContent: overlapsContent,
|
||||
);
|
||||
_layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
final childExtent = child!.size.height;
|
||||
final maxExtent = _maxExtent!.maxExtent;
|
||||
final double effectiveRemainingPaintExtent = math.max(
|
||||
0,
|
||||
constraints.remainingPaintExtent - constraints.overlap,
|
||||
);
|
||||
final double layoutExtent = clampDouble(
|
||||
maxExtent - constraints.scrollOffset,
|
||||
0.0,
|
||||
effectiveRemainingPaintExtent,
|
||||
);
|
||||
geometry = SliverGeometry(
|
||||
scrollExtent: maxExtent,
|
||||
paintOrigin: constraints.overlap,
|
||||
paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
|
||||
layoutExtent: layoutExtent,
|
||||
maxPaintExtent: maxExtent,
|
||||
maxScrollObstructionExtent: minExtent,
|
||||
cacheExtent: layoutExtent > 0.0
|
||||
? -constraints.cacheOrigin + layoutExtent
|
||||
: layoutExtent,
|
||||
hasVisualOverflow: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null && geometry!.visible) {
|
||||
context.paintChild(child!, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double childMainAxisPosition(RenderBox child) => 0.0;
|
||||
|
||||
@override
|
||||
void showOnScreen({
|
||||
RenderObject? descendant,
|
||||
Rect? rect,
|
||||
Duration duration = Duration.zero,
|
||||
Curve curve = Curves.ease,
|
||||
}) {
|
||||
final Rect? localBounds = descendant != null
|
||||
? MatrixUtils.transformRect(
|
||||
descendant.getTransformTo(this),
|
||||
rect ?? descendant.paintBounds,
|
||||
)
|
||||
: rect;
|
||||
|
||||
final Rect? newRect = _trim(localBounds, top: 0);
|
||||
|
||||
super.showOnScreen(
|
||||
descendant: this,
|
||||
rect: newRect,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* This file is part of PiliPlus
|
||||
*
|
||||
* PiliPlus is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* PiliPlus is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// ref [SliverPersistentHeader]
|
||||
|
||||
abstract class SliverPersistentHeaderDelegate {
|
||||
const SliverPersistentHeaderDelegate();
|
||||
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
double? maxExtent,
|
||||
);
|
||||
|
||||
double get minExtent;
|
||||
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
|
||||
}
|
||||
|
||||
class SliverPersistentHeaderElement extends RenderObjectElement {
|
||||
SliverPersistentHeaderElement(
|
||||
SliverPinnedHeader super.widget,
|
||||
);
|
||||
|
||||
@override
|
||||
RenderSliverPinnedHeader get renderObject =>
|
||||
super.renderObject as RenderSliverPinnedHeader;
|
||||
|
||||
@override
|
||||
void mount(Element? parent, Object? newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
renderObject.element = this;
|
||||
}
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
renderObject.element = null;
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
@override
|
||||
void update(SliverPinnedHeader newWidget) {
|
||||
final oldWidget = widget as SliverPinnedHeader;
|
||||
super.update(newWidget);
|
||||
final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate;
|
||||
final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate;
|
||||
if (newDelegate != oldDelegate &&
|
||||
(newDelegate.runtimeType != oldDelegate.runtimeType ||
|
||||
newDelegate.shouldRebuild(oldDelegate))) {
|
||||
final RenderSliverPinnedHeader renderObject = this.renderObject;
|
||||
_updateChild(
|
||||
newDelegate,
|
||||
renderObject.lastShrinkOffset,
|
||||
renderObject.lastOverlapsContent,
|
||||
renderObject.maxExtent,
|
||||
);
|
||||
renderObject.triggerRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performRebuild() {
|
||||
super.performRebuild();
|
||||
renderObject.triggerRebuild();
|
||||
}
|
||||
|
||||
Element? child;
|
||||
|
||||
void _updateChild(
|
||||
SliverPersistentHeaderDelegate delegate,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
double? maxExtent,
|
||||
) {
|
||||
final Widget newWidget = delegate.build(
|
||||
this,
|
||||
shrinkOffset,
|
||||
overlapsContent,
|
||||
maxExtent,
|
||||
);
|
||||
child = updateChild(child, newWidget, null);
|
||||
}
|
||||
|
||||
void build(double shrinkOffset, bool overlapsContent, double? maxExtent) {
|
||||
owner!.buildScope(this, () {
|
||||
final sliverPersistentHeaderRenderObjectWidget =
|
||||
widget as SliverPinnedHeader;
|
||||
_updateChild(
|
||||
sliverPersistentHeaderRenderObjectWidget.delegate,
|
||||
shrinkOffset,
|
||||
overlapsContent,
|
||||
maxExtent,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void forgetChild(Element child) {
|
||||
assert(child == this.child);
|
||||
this.child = null;
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
|
||||
assert(renderObject.debugValidateChild(child));
|
||||
renderObject.child = child;
|
||||
}
|
||||
|
||||
@override
|
||||
void moveRenderObjectChild(
|
||||
covariant RenderObject child,
|
||||
Object? oldSlot,
|
||||
Object? newSlot,
|
||||
) {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
|
||||
renderObject.child = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(ElementVisitor visitor) {
|
||||
if (child != null) {
|
||||
visitor(child!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
|
||||
class DynamicSliverAppBarMedium extends StatefulWidget {
|
||||
const DynamicSliverAppBarMedium({
|
||||
this.flexibleSpace,
|
||||
super.key,
|
||||
this.leading,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.bottom,
|
||||
this.elevation,
|
||||
this.scrolledUnderElevation,
|
||||
this.shadowColor,
|
||||
this.surfaceTintColor,
|
||||
this.forceElevated = false,
|
||||
this.backgroundColor,
|
||||
this.backgroundGradient,
|
||||
this.foregroundColor,
|
||||
this.iconTheme,
|
||||
this.actionsIconTheme,
|
||||
this.primary = true,
|
||||
this.centerTitle,
|
||||
this.excludeHeaderSemantics = false,
|
||||
this.titleSpacing,
|
||||
this.collapsedHeight,
|
||||
this.expandedHeight,
|
||||
this.floating = false,
|
||||
this.pinned = false,
|
||||
this.snap = false,
|
||||
this.stretch = false,
|
||||
this.stretchTriggerOffset = 100.0,
|
||||
this.onStretchTrigger,
|
||||
this.shape,
|
||||
this.toolbarHeight = kToolbarHeight,
|
||||
this.leadingWidth,
|
||||
this.toolbarTextStyle,
|
||||
this.titleTextStyle,
|
||||
this.systemOverlayStyle,
|
||||
this.forceMaterialTransparency = false,
|
||||
this.clipBehavior,
|
||||
this.appBarClipper,
|
||||
this.callback,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? callback;
|
||||
final Widget? flexibleSpace;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
final Widget? title;
|
||||
final List<Widget>? actions;
|
||||
final PreferredSizeWidget? bottom;
|
||||
final double? elevation;
|
||||
final double? scrolledUnderElevation;
|
||||
final Color? shadowColor;
|
||||
final Color? surfaceTintColor;
|
||||
final bool forceElevated;
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// If backgroundGradient is non null, backgroundColor will be ignored
|
||||
final LinearGradient? backgroundGradient;
|
||||
final Color? foregroundColor;
|
||||
final IconThemeData? iconTheme;
|
||||
final IconThemeData? actionsIconTheme;
|
||||
final bool primary;
|
||||
final bool? centerTitle;
|
||||
final bool excludeHeaderSemantics;
|
||||
final double? titleSpacing;
|
||||
final double? expandedHeight;
|
||||
final double? collapsedHeight;
|
||||
final bool floating;
|
||||
final bool pinned;
|
||||
final ShapeBorder? shape;
|
||||
final double toolbarHeight;
|
||||
final double? leadingWidth;
|
||||
final TextStyle? toolbarTextStyle;
|
||||
final TextStyle? titleTextStyle;
|
||||
final SystemUiOverlayStyle? systemOverlayStyle;
|
||||
final bool forceMaterialTransparency;
|
||||
final Clip? clipBehavior;
|
||||
final bool snap;
|
||||
final bool stretch;
|
||||
final double stretchTriggerOffset;
|
||||
final AsyncCallback? onStretchTrigger;
|
||||
final CustomClipper<Path>? appBarClipper;
|
||||
|
||||
@override
|
||||
State<DynamicSliverAppBarMedium> createState() =>
|
||||
_DynamicSliverAppBarMediumState();
|
||||
}
|
||||
|
||||
class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
|
||||
final GlobalKey _childKey = GlobalKey();
|
||||
|
||||
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
|
||||
// to calculate dynamically the size for the sliver app bar
|
||||
double _height = 0;
|
||||
|
||||
void _updateHeight() {
|
||||
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
|
||||
// otherwise this will throw an error
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
if (_childKey.currentContext == null) return;
|
||||
setState(() {
|
||||
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
|
||||
.size
|
||||
.height;
|
||||
widget.callback?.call(_height);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
double? _width;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final width = MediaQuery.widthOf(context);
|
||||
if (_width != width) {
|
||||
_width = width;
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
|
||||
if (_height == 0) {
|
||||
return SliverToBoxAdapter(
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
width: _width,
|
||||
child: widget.flexibleSpace,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final padding = MediaQuery.viewPaddingOf(context).top;
|
||||
return SliverAppBar.medium(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
title: widget.title,
|
||||
actions: widget.actions,
|
||||
bottom: widget.bottom,
|
||||
elevation: widget.elevation,
|
||||
scrolledUnderElevation: widget.scrolledUnderElevation,
|
||||
shadowColor: widget.shadowColor,
|
||||
surfaceTintColor: widget.surfaceTintColor,
|
||||
forceElevated: widget.forceElevated,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
foregroundColor: widget.foregroundColor,
|
||||
iconTheme: widget.iconTheme,
|
||||
actionsIconTheme: widget.actionsIconTheme,
|
||||
primary: widget.primary,
|
||||
centerTitle: widget.centerTitle,
|
||||
excludeHeaderSemantics: widget.excludeHeaderSemantics,
|
||||
titleSpacing: widget.titleSpacing,
|
||||
floating: widget.floating,
|
||||
pinned: widget.pinned,
|
||||
snap: widget.snap,
|
||||
stretch: widget.stretch,
|
||||
stretchTriggerOffset: widget.stretchTriggerOffset,
|
||||
onStretchTrigger: widget.onStretchTrigger,
|
||||
shape: widget.shape,
|
||||
toolbarHeight: kToolbarHeight,
|
||||
collapsedHeight: kToolbarHeight + padding + 1,
|
||||
expandedHeight: _height - padding,
|
||||
leadingWidth: widget.leadingWidth,
|
||||
toolbarTextStyle: widget.toolbarTextStyle,
|
||||
titleTextStyle: widget.titleTextStyle,
|
||||
systemOverlayStyle: widget.systemOverlayStyle,
|
||||
forceMaterialTransparency: widget.forceMaterialTransparency,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/common/widgets/extra_hit_test_widget.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ExtraHitTestWidget extends SingleChildRenderObjectWidget {
|
||||
const ExtraHitTestWidget({
|
||||
super.key,
|
||||
required this.width,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
final double width;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderExtraHitTestWidget(width: width);
|
||||
}
|
||||
}
|
||||
|
||||
class RenderExtraHitTestWidget extends RenderProxyBox {
|
||||
RenderExtraHitTestWidget({
|
||||
required this._width,
|
||||
});
|
||||
|
||||
final double _width;
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return super.hitTestChildren(result, position: position) ||
|
||||
position.dx <= _width;
|
||||
}
|
||||
}
|
||||
94
lib/common/widgets/extra_hittest_stack.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart'
|
||||
show RenderStack, BoxHitTestResult, BoxHitTestEntry;
|
||||
|
||||
class ExtraHitTestStack extends Stack {
|
||||
const ExtraHitTestStack({
|
||||
super.key,
|
||||
super.alignment,
|
||||
super.textDirection,
|
||||
super.fit,
|
||||
super.clipBehavior,
|
||||
super.children,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderExtraHitTestStack createRenderObject(BuildContext context) {
|
||||
return RenderExtraHitTestStack(
|
||||
alignment: alignment,
|
||||
textDirection: textDirection ?? Directionality.maybeOf(context),
|
||||
fit: fit,
|
||||
clipBehavior: clipBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderExtraHitTestStack renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..alignment = alignment
|
||||
..textDirection = textDirection ?? Directionality.maybeOf(context)
|
||||
..fit = fit
|
||||
..clipBehavior = clipBehavior;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderExtraHitTestStack extends RenderStack {
|
||||
RenderExtraHitTestStack({
|
||||
super.children,
|
||||
super.alignment,
|
||||
super.textDirection,
|
||||
super.fit,
|
||||
super.clipBehavior,
|
||||
});
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
assert(() {
|
||||
if (!hasSize) {
|
||||
if (debugNeedsLayout) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'Cannot hit test a render box that has never been laid out.',
|
||||
),
|
||||
describeForError(
|
||||
'The hitTest() method was called on this RenderBox',
|
||||
),
|
||||
ErrorDescription(
|
||||
"Unfortunately, this object's geometry is not known at this time, "
|
||||
'probably because it has never been laid out. '
|
||||
'This means it cannot be accurately hit-tested.',
|
||||
),
|
||||
ErrorHint(
|
||||
'If you are trying '
|
||||
'to perform a hit test during the layout phase itself, make sure '
|
||||
"you only hit test nodes that have completed layout (e.g. the node's "
|
||||
'children, after their layout() method has been called).',
|
||||
),
|
||||
]);
|
||||
}
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('Cannot hit test a render box with no size.'),
|
||||
describeForError('The hitTest() method was called on this RenderBox'),
|
||||
ErrorDescription(
|
||||
'Although this node is not marked as needing layout, '
|
||||
'its size is not set.',
|
||||
),
|
||||
ErrorHint(
|
||||
'A RenderBox object must have an '
|
||||
'explicit size before it can be hit-tested. Make sure '
|
||||
'that the RenderBox in question sets its size during layout.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
|
||||
result.add(BoxHitTestEntry(this, position));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
777
lib/common/widgets/floating_navigation_bar.dart
Normal file
@@ -0,0 +1,777 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/utils/extension/theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _kMaxLabelTextScaleFactor = 1.3;
|
||||
|
||||
const _kNavigationHeight = 64.0;
|
||||
const _kIndicatorHeight = _kNavigationHeight - 2 * _kIndicatorPaddingInt;
|
||||
const _kIndicatorWidth = 86.0;
|
||||
const _kIndicatorPaddingInt = 4.0;
|
||||
const _kIndicatorPadding = EdgeInsets.all(_kIndicatorPaddingInt);
|
||||
const _kBorderRadius = BorderRadius.all(.circular(_kNavigationHeight / 2));
|
||||
const _kNavigationShape = RoundedSuperellipseBorder(
|
||||
borderRadius: _kBorderRadius,
|
||||
);
|
||||
|
||||
/// ref [NavigationBar]
|
||||
class FloatingNavigationBar extends StatelessWidget {
|
||||
// ignore: prefer_const_constructors_in_immutables
|
||||
FloatingNavigationBar({
|
||||
super.key,
|
||||
this.animationDuration = const Duration(milliseconds: 500),
|
||||
this.selectedIndex = 0,
|
||||
required this.destinations,
|
||||
this.onDestinationSelected,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shadowColor,
|
||||
this.surfaceTintColor,
|
||||
this.indicatorColor,
|
||||
this.indicatorShape,
|
||||
this.labelBehavior,
|
||||
this.overlayColor,
|
||||
this.labelTextStyle,
|
||||
this.labelPadding,
|
||||
this.bottomPadding = 8.0,
|
||||
}) : assert(destinations.length >= 2),
|
||||
assert(0 <= selectedIndex && selectedIndex < destinations.length);
|
||||
|
||||
final Duration animationDuration;
|
||||
final int selectedIndex;
|
||||
final List<Widget> destinations;
|
||||
final ValueChanged<int>? onDestinationSelected;
|
||||
final Color? backgroundColor;
|
||||
final double? elevation;
|
||||
final Color? shadowColor;
|
||||
final Color? surfaceTintColor;
|
||||
final Color? indicatorColor;
|
||||
final ShapeBorder? indicatorShape;
|
||||
final NavigationDestinationLabelBehavior? labelBehavior;
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
final WidgetStateProperty<TextStyle?>? labelTextStyle;
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
final double bottomPadding;
|
||||
|
||||
VoidCallback _handleTap(int index) {
|
||||
return onDestinationSelected != null
|
||||
? () => onDestinationSelected!(index)
|
||||
: () {};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaults = _NavigationBarDefaultsM3(context);
|
||||
|
||||
final navigationBarTheme = NavigationBarTheme.of(context);
|
||||
final effectiveLabelBehavior =
|
||||
labelBehavior ??
|
||||
navigationBarTheme.labelBehavior ??
|
||||
defaults.labelBehavior!;
|
||||
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
|
||||
return UnconstrainedBox(
|
||||
child: Padding(
|
||||
padding: .fromLTRB(
|
||||
padding.left,
|
||||
0,
|
||||
padding.right,
|
||||
bottomPadding + padding.bottom,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _kNavigationHeight,
|
||||
width: destinations.length * _kIndicatorWidth,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: ElevationOverlay.applySurfaceTint(
|
||||
backgroundColor ??
|
||||
navigationBarTheme.backgroundColor ??
|
||||
defaults.backgroundColor!,
|
||||
surfaceTintColor ??
|
||||
navigationBarTheme.surfaceTintColor ??
|
||||
defaults.surfaceTintColor,
|
||||
elevation ??
|
||||
navigationBarTheme.elevation ??
|
||||
defaults.elevation!,
|
||||
),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: defaults.borderSide,
|
||||
borderRadius: _kBorderRadius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: _kIndicatorPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: .stretch,
|
||||
children: <Widget>[
|
||||
for (int i = 0; i < destinations.length; i++)
|
||||
Expanded(
|
||||
child: _SelectableAnimatedBuilder(
|
||||
duration: animationDuration,
|
||||
isSelected: i == selectedIndex,
|
||||
builder: (context, animation) {
|
||||
return _NavigationDestinationInfo(
|
||||
index: i,
|
||||
selectedIndex: selectedIndex,
|
||||
totalNumberOfDestinations: destinations.length,
|
||||
selectedAnimation: animation,
|
||||
labelBehavior: effectiveLabelBehavior,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorShape: indicatorShape,
|
||||
overlayColor: overlayColor,
|
||||
onTap: _handleTap(i),
|
||||
labelTextStyle: labelTextStyle,
|
||||
labelPadding: labelPadding,
|
||||
child: destinations[i],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingNavigationDestination extends StatelessWidget {
|
||||
const FloatingNavigationDestination({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.selectedIcon,
|
||||
required this.label,
|
||||
this.tooltip,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
|
||||
final Widget? selectedIcon;
|
||||
|
||||
final String label;
|
||||
|
||||
final String? tooltip;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final info = _NavigationDestinationInfo.of(context);
|
||||
const selectedState = <WidgetState>{WidgetState.selected};
|
||||
const unselectedState = <WidgetState>{};
|
||||
const disabledState = <WidgetState>{WidgetState.disabled};
|
||||
|
||||
final navigationBarTheme = NavigationBarTheme.of(context);
|
||||
final defaults = _NavigationBarDefaultsM3(context);
|
||||
final animation = info.selectedAnimation;
|
||||
|
||||
return Stack(
|
||||
alignment: .center,
|
||||
clipBehavior: .none,
|
||||
children: [
|
||||
NavigationIndicator(
|
||||
animation: animation,
|
||||
color:
|
||||
info.indicatorColor ??
|
||||
navigationBarTheme.indicatorColor ??
|
||||
defaults.indicatorColor!,
|
||||
),
|
||||
_NavigationDestinationBuilder(
|
||||
label: label,
|
||||
tooltip: tooltip,
|
||||
enabled: enabled,
|
||||
buildIcon: (context) {
|
||||
final IconThemeData selectedIconTheme =
|
||||
navigationBarTheme.iconTheme?.resolve(selectedState) ??
|
||||
defaults.iconTheme!.resolve(selectedState)!;
|
||||
final IconThemeData unselectedIconTheme =
|
||||
navigationBarTheme.iconTheme?.resolve(unselectedState) ??
|
||||
defaults.iconTheme!.resolve(unselectedState)!;
|
||||
final IconThemeData disabledIconTheme =
|
||||
navigationBarTheme.iconTheme?.resolve(disabledState) ??
|
||||
defaults.iconTheme!.resolve(disabledState)!;
|
||||
|
||||
final Widget selectedIconWidget = IconTheme.merge(
|
||||
data: enabled ? selectedIconTheme : disabledIconTheme,
|
||||
child: selectedIcon ?? icon,
|
||||
);
|
||||
final Widget unselectedIconWidget = IconTheme.merge(
|
||||
data: enabled ? unselectedIconTheme : disabledIconTheme,
|
||||
child: icon,
|
||||
);
|
||||
return _StatusTransitionWidgetBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return animation.isForwardOrCompleted
|
||||
? selectedIconWidget
|
||||
: unselectedIconWidget;
|
||||
},
|
||||
);
|
||||
},
|
||||
buildLabel: (context) {
|
||||
final TextStyle? effectiveSelectedLabelTextStyle =
|
||||
info.labelTextStyle?.resolve(selectedState) ??
|
||||
navigationBarTheme.labelTextStyle?.resolve(selectedState) ??
|
||||
defaults.labelTextStyle!.resolve(selectedState);
|
||||
final TextStyle? effectiveUnselectedLabelTextStyle =
|
||||
info.labelTextStyle?.resolve(unselectedState) ??
|
||||
navigationBarTheme.labelTextStyle?.resolve(unselectedState) ??
|
||||
defaults.labelTextStyle!.resolve(unselectedState);
|
||||
final TextStyle? effectiveDisabledLabelTextStyle =
|
||||
info.labelTextStyle?.resolve(disabledState) ??
|
||||
navigationBarTheme.labelTextStyle?.resolve(disabledState) ??
|
||||
defaults.labelTextStyle!.resolve(disabledState);
|
||||
final EdgeInsetsGeometry labelPadding =
|
||||
info.labelPadding ??
|
||||
navigationBarTheme.labelPadding ??
|
||||
defaults.labelPadding!;
|
||||
|
||||
final textStyle = enabled
|
||||
? animation.isForwardOrCompleted
|
||||
? effectiveSelectedLabelTextStyle
|
||||
: effectiveUnselectedLabelTextStyle
|
||||
: effectiveDisabledLabelTextStyle;
|
||||
|
||||
return Padding(
|
||||
padding: labelPadding,
|
||||
child: MediaQuery.withClampedTextScaling(
|
||||
maxScaleFactor: _kMaxLabelTextScaleFactor,
|
||||
child: Text(label, style: textStyle),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestinationBuilder extends StatefulWidget {
|
||||
const _NavigationDestinationBuilder({
|
||||
required this.buildIcon,
|
||||
required this.buildLabel,
|
||||
required this.label,
|
||||
this.tooltip,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final WidgetBuilder buildIcon;
|
||||
|
||||
final WidgetBuilder buildLabel;
|
||||
|
||||
final String label;
|
||||
|
||||
final String? tooltip;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
State<_NavigationDestinationBuilder> createState() =>
|
||||
_NavigationDestinationBuilderState();
|
||||
}
|
||||
|
||||
class _NavigationDestinationBuilderState
|
||||
extends State<_NavigationDestinationBuilder> {
|
||||
final GlobalKey iconKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final info = _NavigationDestinationInfo.of(context);
|
||||
|
||||
final child = GestureDetector(
|
||||
behavior: .opaque,
|
||||
onTap: widget.enabled ? info.onTap : null,
|
||||
child: _NavigationBarDestinationLayout(
|
||||
icon: widget.buildIcon(context),
|
||||
iconKey: iconKey,
|
||||
label: widget.buildLabel(context),
|
||||
),
|
||||
);
|
||||
if (info.labelBehavior == .alwaysShow) {
|
||||
return child;
|
||||
}
|
||||
return _NavigationBarDestinationTooltip(
|
||||
message: widget.tooltip ?? widget.label,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestinationInfo extends InheritedWidget {
|
||||
const _NavigationDestinationInfo({
|
||||
required this.index,
|
||||
required this.selectedIndex,
|
||||
required this.totalNumberOfDestinations,
|
||||
required this.selectedAnimation,
|
||||
required this.labelBehavior,
|
||||
required this.indicatorColor,
|
||||
required this.indicatorShape,
|
||||
required this.overlayColor,
|
||||
required this.onTap,
|
||||
this.labelTextStyle,
|
||||
this.labelPadding,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final int index;
|
||||
|
||||
final int selectedIndex;
|
||||
|
||||
final int totalNumberOfDestinations;
|
||||
|
||||
final Animation<double> selectedAnimation;
|
||||
|
||||
final NavigationDestinationLabelBehavior labelBehavior;
|
||||
|
||||
final Color? indicatorColor;
|
||||
|
||||
final ShapeBorder? indicatorShape;
|
||||
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
final WidgetStateProperty<TextStyle?>? labelTextStyle;
|
||||
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
|
||||
static _NavigationDestinationInfo of(BuildContext context) {
|
||||
final _NavigationDestinationInfo? result = context
|
||||
.dependOnInheritedWidgetOfExactType<_NavigationDestinationInfo>();
|
||||
assert(
|
||||
result != null,
|
||||
'Navigation destinations need a _NavigationDestinationInfo parent, '
|
||||
'which is usually provided by NavigationBar.',
|
||||
);
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_NavigationDestinationInfo oldWidget) {
|
||||
return index != oldWidget.index ||
|
||||
totalNumberOfDestinations != oldWidget.totalNumberOfDestinations ||
|
||||
selectedAnimation != oldWidget.selectedAnimation ||
|
||||
labelBehavior != oldWidget.labelBehavior ||
|
||||
onTap != oldWidget.onTap;
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationIndicator extends StatelessWidget {
|
||||
const NavigationIndicator({
|
||||
super.key,
|
||||
required this.animation,
|
||||
this.color,
|
||||
this.width = _kIndicatorWidth,
|
||||
this.height = _kIndicatorHeight,
|
||||
});
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
final Color? color;
|
||||
|
||||
final double width;
|
||||
|
||||
final double height;
|
||||
|
||||
static final _anim = Tween<double>(
|
||||
begin: .5,
|
||||
end: 1.0,
|
||||
).chain(CurveTween(curve: Curves.easeInOutCubicEmphasized));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
final double scale = animation.isDismissed
|
||||
? 0.0
|
||||
: _anim.evaluate(animation);
|
||||
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.diagonal3Values(scale, 1.0, 1.0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
||||
child: _StatusTransitionWidgetBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return _SelectableAnimatedBuilder(
|
||||
isSelected: animation.isForwardOrCompleted,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
alwaysDoFullAnimation: true,
|
||||
builder: (context, fadeAnimation) {
|
||||
return FadeTransition(
|
||||
opacity: fadeAnimation,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: _kNavigationShape,
|
||||
color: color ?? Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: _kIndicatorWidth,
|
||||
height: _kIndicatorHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationBarDestinationLayout extends StatelessWidget {
|
||||
const _NavigationBarDestinationLayout({
|
||||
required this.icon,
|
||||
required this.iconKey,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
|
||||
final GlobalKey iconKey;
|
||||
|
||||
final Widget label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _DestinationLayoutAnimationBuilder(
|
||||
builder: (context, animation) {
|
||||
return CustomMultiChildLayout(
|
||||
delegate: _NavigationDestinationLayoutDelegate(animation: animation),
|
||||
children: <Widget>[
|
||||
LayoutId(
|
||||
id: _NavigationDestinationLayoutDelegate.iconId,
|
||||
child: KeyedSubtree(key: iconKey, child: icon),
|
||||
),
|
||||
LayoutId(
|
||||
id: _NavigationDestinationLayoutDelegate.labelId,
|
||||
child: FadeTransition(
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: animation,
|
||||
child: label,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DestinationLayoutAnimationBuilder extends StatelessWidget {
|
||||
const _DestinationLayoutAnimationBuilder({required this.builder});
|
||||
|
||||
final Widget Function(BuildContext, Animation<double>) builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final info = _NavigationDestinationInfo.of(context);
|
||||
switch (info.labelBehavior) {
|
||||
case NavigationDestinationLabelBehavior.alwaysShow:
|
||||
return builder(context, kAlwaysCompleteAnimation);
|
||||
case NavigationDestinationLabelBehavior.alwaysHide:
|
||||
return builder(context, kAlwaysDismissedAnimation);
|
||||
case NavigationDestinationLabelBehavior.onlyShowSelected:
|
||||
return _CurvedAnimationBuilder(
|
||||
animation: info.selectedAnimation,
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
reverseCurve: Curves.easeInOutCubicEmphasized.flipped,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationBarDestinationTooltip extends StatelessWidget {
|
||||
const _NavigationBarDestinationTooltip({
|
||||
required this.message,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String message;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: message,
|
||||
verticalOffset: 34,
|
||||
excludeFromSemantics: true,
|
||||
preferBelow: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate {
|
||||
_NavigationDestinationLayoutDelegate({required this.animation})
|
||||
: super(relayout: animation);
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
static const int iconId = 1;
|
||||
|
||||
static const int labelId = 2;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
double halfWidth(Size size) => size.width / 2;
|
||||
double halfHeight(Size size) => size.height / 2;
|
||||
|
||||
final Size iconSize = layoutChild(iconId, BoxConstraints.loose(size));
|
||||
final Size labelSize = layoutChild(labelId, BoxConstraints.loose(size));
|
||||
|
||||
final double yPositionOffset = Tween<double>(
|
||||
begin: halfHeight(iconSize),
|
||||
|
||||
end: halfHeight(iconSize) + halfHeight(labelSize),
|
||||
).transform(animation.value);
|
||||
final double iconYPosition = halfHeight(size) - yPositionOffset;
|
||||
|
||||
positionChild(
|
||||
iconId,
|
||||
Offset(
|
||||
halfWidth(size) - halfWidth(iconSize),
|
||||
iconYPosition,
|
||||
),
|
||||
);
|
||||
|
||||
positionChild(
|
||||
labelId,
|
||||
Offset(
|
||||
halfWidth(size) - halfWidth(labelSize),
|
||||
|
||||
iconYPosition + iconSize.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_NavigationDestinationLayoutDelegate oldDelegate) {
|
||||
return oldDelegate.animation != animation;
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusTransitionWidgetBuilder extends StatusTransitionWidget {
|
||||
const _StatusTransitionWidgetBuilder({
|
||||
required super.animation,
|
||||
required this.builder,
|
||||
// ignore: unused_element_parameter
|
||||
this.child,
|
||||
});
|
||||
|
||||
final TransitionBuilder builder;
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => builder(context, child);
|
||||
}
|
||||
|
||||
class _SelectableAnimatedBuilder extends StatefulWidget {
|
||||
const _SelectableAnimatedBuilder({
|
||||
required this.isSelected,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
this.alwaysDoFullAnimation = false,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
final bool alwaysDoFullAnimation;
|
||||
|
||||
final Widget Function(BuildContext, Animation<double>) builder;
|
||||
|
||||
@override
|
||||
_SelectableAnimatedBuilderState createState() =>
|
||||
_SelectableAnimatedBuilderState();
|
||||
}
|
||||
|
||||
class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this);
|
||||
_controller.duration = widget.duration;
|
||||
_controller.value = widget.isSelected ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.duration != widget.duration) {
|
||||
_controller.duration = widget.duration;
|
||||
}
|
||||
if (oldWidget.isSelected != widget.isSelected) {
|
||||
if (widget.isSelected) {
|
||||
_controller.forward(from: widget.alwaysDoFullAnimation ? 0 : null);
|
||||
} else {
|
||||
_controller.reverse(from: widget.alwaysDoFullAnimation ? 1 : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context, _controller);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurvedAnimationBuilder extends StatefulWidget {
|
||||
const _CurvedAnimationBuilder({
|
||||
required this.animation,
|
||||
required this.curve,
|
||||
required this.reverseCurve,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Animation<double> animation;
|
||||
final Curve curve;
|
||||
final Curve reverseCurve;
|
||||
final Widget Function(BuildContext, Animation<double>) builder;
|
||||
|
||||
@override
|
||||
_CurvedAnimationBuilderState createState() => _CurvedAnimationBuilderState();
|
||||
}
|
||||
|
||||
class _CurvedAnimationBuilderState extends State<_CurvedAnimationBuilder> {
|
||||
late AnimationStatus _animationDirection;
|
||||
AnimationStatus? _preservedDirection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationDirection = widget.animation.status;
|
||||
_updateStatus(widget.animation.status);
|
||||
widget.animation.addStatusListener(_updateStatus);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.animation.removeStatusListener(_updateStatus);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateStatus(AnimationStatus status) {
|
||||
if (_animationDirection != status) {
|
||||
setState(() {
|
||||
_animationDirection = status;
|
||||
});
|
||||
}
|
||||
switch (status) {
|
||||
case AnimationStatus.forward || AnimationStatus.reverse
|
||||
when _preservedDirection != null:
|
||||
break;
|
||||
case AnimationStatus.forward || AnimationStatus.reverse:
|
||||
setState(() {
|
||||
_preservedDirection = status;
|
||||
});
|
||||
case AnimationStatus.completed || AnimationStatus.dismissed:
|
||||
setState(() {
|
||||
_preservedDirection = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shouldUseForwardCurve =
|
||||
(_preservedDirection ?? _animationDirection) != AnimationStatus.reverse;
|
||||
|
||||
final Animation<double> curvedAnimation = CurveTween(
|
||||
curve: shouldUseForwardCurve ? widget.curve : widget.reverseCurve,
|
||||
).animate(widget.animation);
|
||||
|
||||
return widget.builder(context, curvedAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
const _indicatorDark = Color(0x15FFFFFF);
|
||||
const _indicatorLight = Color(0x10000000);
|
||||
|
||||
class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
|
||||
_NavigationBarDefaultsM3(this.context)
|
||||
: super(
|
||||
height: _kNavigationHeight,
|
||||
elevation: 3.0,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
late final _colors = Theme.of(context).colorScheme;
|
||||
late final _textTheme = Theme.of(context).textTheme;
|
||||
|
||||
BorderSide get borderSide => _colors.isDark
|
||||
? const BorderSide(color: Color(0x08FFFFFF))
|
||||
: const BorderSide(color: Color(0x08000000));
|
||||
|
||||
@override
|
||||
Color? get backgroundColor => _colors.surfaceContainer;
|
||||
|
||||
@override
|
||||
Color? get shadowColor => Colors.transparent;
|
||||
|
||||
@override
|
||||
Color? get surfaceTintColor => Colors.transparent;
|
||||
|
||||
@override
|
||||
WidgetStateProperty<IconThemeData?>? get iconTheme {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
return IconThemeData(
|
||||
size: 24.0,
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? _colors.onSurfaceVariant.withValues(alpha: 0.38)
|
||||
: states.contains(WidgetState.selected)
|
||||
? _colors.onSecondaryContainer
|
||||
: _colors.onSurfaceVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Color? get indicatorColor =>
|
||||
_colors.isDark ? _indicatorDark : _indicatorLight;
|
||||
|
||||
@override
|
||||
ShapeBorder? get indicatorShape => const StadiumBorder();
|
||||
|
||||
@override
|
||||
WidgetStateProperty<TextStyle?>? get labelTextStyle {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
final TextStyle style = _textTheme.labelMedium!;
|
||||
return style.apply(
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? _colors.onSurfaceVariant.withValues(alpha: 0.38)
|
||||
: states.contains(WidgetState.selected)
|
||||
? _colors.onSurface
|
||||
: _colors.onSurfaceVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry? get labelPadding => const EdgeInsets.only(top: 2);
|
||||
}
|
||||
392
lib/common/widgets/flutter/chat_list_view.dart
Normal file
@@ -0,0 +1,392 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: prefer_initializing_formals
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide ListView;
|
||||
import 'package:flutter/rendering.dart' hide RenderSliverList;
|
||||
|
||||
class ChatListView extends BoxScrollView {
|
||||
ChatListView.separated({
|
||||
super.key,
|
||||
super.scrollDirection,
|
||||
super.controller,
|
||||
super.primary,
|
||||
super.physics,
|
||||
super.padding,
|
||||
required NullableIndexedWidgetBuilder itemBuilder,
|
||||
@Deprecated(
|
||||
'Use findItemIndexCallback instead. '
|
||||
'findChildIndexCallback returns child indices (which include separators), '
|
||||
'while findItemIndexCallback returns item indices (which do not). '
|
||||
'If you were multiplying results by 2 to account for separators, '
|
||||
'you can remove that workaround when migrating to findItemIndexCallback. '
|
||||
'This feature was deprecated after v3.37.0-1.0.pre.',
|
||||
)
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
ChildIndexGetter? findItemIndexCallback,
|
||||
required IndexedWidgetBuilder separatorBuilder,
|
||||
required int itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
bool addRepaintBoundaries = true,
|
||||
bool addSemanticIndexes = true,
|
||||
@Deprecated(
|
||||
'Use scrollCacheExtent instead. '
|
||||
'This feature was deprecated after v3.41.0-0.0.pre.',
|
||||
)
|
||||
super.cacheExtent,
|
||||
super.scrollCacheExtent,
|
||||
super.dragStartBehavior,
|
||||
super.keyboardDismissBehavior,
|
||||
super.restorationId,
|
||||
super.clipBehavior,
|
||||
super.hitTestBehavior,
|
||||
}) : assert(itemCount >= 0),
|
||||
assert(
|
||||
findItemIndexCallback == null || findChildIndexCallback == null,
|
||||
'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
|
||||
'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
|
||||
),
|
||||
childrenDelegate = SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final int itemIndex = index ~/ 2;
|
||||
if (index.isEven) {
|
||||
return itemBuilder(context, itemIndex);
|
||||
}
|
||||
return separatorBuilder(context, itemIndex);
|
||||
},
|
||||
findChildIndexCallback: findItemIndexCallback != null
|
||||
? (Key key) {
|
||||
final int? itemIndex = findItemIndexCallback(key);
|
||||
return itemIndex == null ? null : itemIndex * 2;
|
||||
}
|
||||
: findChildIndexCallback,
|
||||
childCount: _computeActualChildCount(itemCount),
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
addSemanticIndexes: addSemanticIndexes,
|
||||
semanticIndexCallback: (Widget widget, int index) {
|
||||
return index.isEven ? index ~/ 2 : null;
|
||||
},
|
||||
),
|
||||
super(semanticChildCount: itemCount, reverse: true);
|
||||
|
||||
final SliverChildDelegate childrenDelegate;
|
||||
|
||||
@override
|
||||
Widget buildChildLayout(BuildContext context) {
|
||||
return SliverChatList(delegate: childrenDelegate);
|
||||
}
|
||||
|
||||
static int _computeActualChildCount(int itemCount) {
|
||||
return math.max(0, itemCount * 2 - 1);
|
||||
}
|
||||
}
|
||||
|
||||
class SliverChatList extends SliverMultiBoxAdaptorWidget {
|
||||
const SliverChatList({super.key, required super.delegate});
|
||||
|
||||
@override
|
||||
SliverMultiBoxAdaptorElement createElement() =>
|
||||
SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true);
|
||||
|
||||
@override
|
||||
RenderSliverChatList createRenderObject(BuildContext context) {
|
||||
final element = context as SliverMultiBoxAdaptorElement;
|
||||
return RenderSliverChatList(childManager: element);
|
||||
}
|
||||
}
|
||||
|
||||
class RenderSliverChatList extends RenderSliverMultiBoxAdaptor
|
||||
with ExtendedRenderObjectMixin {
|
||||
RenderSliverChatList({required super.childManager});
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final SliverConstraints constraints = this.constraints;
|
||||
childManager
|
||||
..didStartLayout()
|
||||
..setDidUnderflow(false);
|
||||
|
||||
final double scrollOffset =
|
||||
constraints.scrollOffset + constraints.cacheOrigin;
|
||||
assert(scrollOffset >= 0.0);
|
||||
final double remainingExtent = constraints.remainingCacheExtent;
|
||||
assert(remainingExtent >= 0.0);
|
||||
final double targetEndScrollOffset = scrollOffset + remainingExtent;
|
||||
final BoxConstraints childConstraints = constraints.asBoxConstraints();
|
||||
var leadingGarbage = 0;
|
||||
var trailingGarbage = 0;
|
||||
var reachedEnd = false;
|
||||
|
||||
if (firstChild == null) {
|
||||
if (!addInitialChild()) {
|
||||
geometry = SliverGeometry.zero;
|
||||
childManager.didFinishLayout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
handleCloseToTrailingBegin();
|
||||
|
||||
RenderBox? leadingChildWithLayout, trailingChildWithLayout;
|
||||
|
||||
RenderBox? earliestUsefulChild = firstChild;
|
||||
|
||||
if (childScrollOffset(firstChild!) == null) {
|
||||
var leadingChildrenWithoutLayoutOffset = 0;
|
||||
while (earliestUsefulChild != null &&
|
||||
childScrollOffset(earliestUsefulChild) == null) {
|
||||
earliestUsefulChild = childAfter(earliestUsefulChild);
|
||||
leadingChildrenWithoutLayoutOffset += 1;
|
||||
}
|
||||
|
||||
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
|
||||
|
||||
if (firstChild == null) {
|
||||
if (!addInitialChild()) {
|
||||
geometry = SliverGeometry.zero;
|
||||
childManager.didFinishLayout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
earliestUsefulChild = firstChild;
|
||||
for (
|
||||
double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
|
||||
earliestScrollOffset > scrollOffset;
|
||||
earliestScrollOffset = childScrollOffset(earliestUsefulChild)!
|
||||
) {
|
||||
earliestUsefulChild = insertAndLayoutLeadingChild(
|
||||
childConstraints,
|
||||
parentUsesSize: true,
|
||||
);
|
||||
if (earliestUsefulChild == null) {
|
||||
final childParentData =
|
||||
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
|
||||
childParentData.layoutOffset = 0.0;
|
||||
|
||||
if (scrollOffset == 0.0) {
|
||||
firstChild!.layout(childConstraints, parentUsesSize: true);
|
||||
earliestUsefulChild = firstChild;
|
||||
leadingChildWithLayout = earliestUsefulChild;
|
||||
trailingChildWithLayout ??= earliestUsefulChild;
|
||||
break;
|
||||
} else {
|
||||
geometry = SliverGeometry(scrollOffsetCorrection: -scrollOffset);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final double firstChildScrollOffset =
|
||||
earliestScrollOffset - paintExtentOf(firstChild!);
|
||||
|
||||
if (firstChildScrollOffset < -precisionErrorTolerance) {
|
||||
geometry = SliverGeometry(
|
||||
scrollOffsetCorrection: -firstChildScrollOffset,
|
||||
);
|
||||
final childParentData =
|
||||
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
|
||||
childParentData.layoutOffset = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
final childParentData =
|
||||
earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
|
||||
childParentData.layoutOffset = firstChildScrollOffset;
|
||||
assert(earliestUsefulChild == firstChild);
|
||||
leadingChildWithLayout = earliestUsefulChild;
|
||||
trailingChildWithLayout ??= earliestUsefulChild;
|
||||
}
|
||||
|
||||
assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
|
||||
|
||||
if (scrollOffset < precisionErrorTolerance) {
|
||||
while (indexOf(firstChild!) > 0) {
|
||||
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
|
||||
|
||||
earliestUsefulChild = insertAndLayoutLeadingChild(
|
||||
childConstraints,
|
||||
parentUsesSize: true,
|
||||
);
|
||||
assert(earliestUsefulChild != null);
|
||||
final double firstChildScrollOffset =
|
||||
earliestScrollOffset - paintExtentOf(firstChild!);
|
||||
final childParentData =
|
||||
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
|
||||
childParentData.layoutOffset = 0.0;
|
||||
|
||||
if (firstChildScrollOffset < -precisionErrorTolerance) {
|
||||
geometry = SliverGeometry(
|
||||
scrollOffsetCorrection: -firstChildScrollOffset,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(earliestUsefulChild == firstChild);
|
||||
assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
|
||||
|
||||
if (leadingChildWithLayout == null) {
|
||||
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
|
||||
leadingChildWithLayout = earliestUsefulChild;
|
||||
trailingChildWithLayout = earliestUsefulChild;
|
||||
}
|
||||
|
||||
var inLayoutRange = true;
|
||||
var child = earliestUsefulChild;
|
||||
int index = indexOf(child!);
|
||||
double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
|
||||
bool advance() {
|
||||
assert(child != null);
|
||||
if (child == trailingChildWithLayout) {
|
||||
inLayoutRange = false;
|
||||
}
|
||||
child = childAfter(child!);
|
||||
if (child == null) {
|
||||
inLayoutRange = false;
|
||||
}
|
||||
index += 1;
|
||||
if (!inLayoutRange) {
|
||||
if (child == null || indexOf(child!) != index) {
|
||||
child = insertAndLayoutChild(
|
||||
childConstraints,
|
||||
after: trailingChildWithLayout,
|
||||
parentUsesSize: true,
|
||||
);
|
||||
if (child == null) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
child!.layout(childConstraints, parentUsesSize: true);
|
||||
}
|
||||
trailingChildWithLayout = child;
|
||||
}
|
||||
assert(child != null);
|
||||
final childParentData =
|
||||
child!.parentData! as SliverMultiBoxAdaptorParentData;
|
||||
childParentData.layoutOffset = endScrollOffset;
|
||||
assert(childParentData.index == index);
|
||||
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
|
||||
return true;
|
||||
}
|
||||
|
||||
while (endScrollOffset < scrollOffset) {
|
||||
leadingGarbage += 1;
|
||||
if (!advance()) {
|
||||
assert(leadingGarbage == childCount);
|
||||
assert(child == null);
|
||||
|
||||
collectGarbage(leadingGarbage - 1, 0);
|
||||
assert(firstChild == lastChild);
|
||||
final double extent =
|
||||
childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
|
||||
geometry = SliverGeometry(scrollExtent: extent, maxPaintExtent: extent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (endScrollOffset < targetEndScrollOffset) {
|
||||
if (!advance()) {
|
||||
reachedEnd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (child != null) {
|
||||
child = childAfter(child!);
|
||||
while (child != null) {
|
||||
trailingGarbage += 1;
|
||||
child = childAfter(child!);
|
||||
}
|
||||
}
|
||||
|
||||
collectGarbage(leadingGarbage, trailingGarbage);
|
||||
|
||||
assert(debugAssertChildListIsNonEmptyAndContiguous());
|
||||
final double estimatedMaxScrollOffset;
|
||||
|
||||
///
|
||||
endScrollOffset = handleCloseToTrailingEnd(endScrollOffset);
|
||||
|
||||
if (reachedEnd) {
|
||||
estimatedMaxScrollOffset = endScrollOffset;
|
||||
} else {
|
||||
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
|
||||
constraints,
|
||||
firstIndex: indexOf(firstChild!),
|
||||
lastIndex: indexOf(lastChild!),
|
||||
leadingScrollOffset: childScrollOffset(firstChild!),
|
||||
trailingScrollOffset: endScrollOffset,
|
||||
);
|
||||
assert(
|
||||
estimatedMaxScrollOffset >=
|
||||
endScrollOffset - childScrollOffset(firstChild!)!,
|
||||
);
|
||||
}
|
||||
final double firstChildScrollOffset = childScrollOffset(firstChild!)!;
|
||||
double paintExtent = calculatePaintOffset(
|
||||
constraints,
|
||||
from: firstChildScrollOffset,
|
||||
to: endScrollOffset,
|
||||
);
|
||||
final double cacheExtent = calculateCacheOffset(
|
||||
constraints,
|
||||
from: firstChildScrollOffset,
|
||||
to: endScrollOffset,
|
||||
);
|
||||
final double targetEndScrollOffsetForPaint =
|
||||
constraints.scrollOffset + constraints.remainingPaintExtent;
|
||||
|
||||
///
|
||||
paintExtent += _closeToTrailingDistance;
|
||||
|
||||
geometry = SliverGeometry(
|
||||
scrollExtent: estimatedMaxScrollOffset,
|
||||
paintExtent: paintExtent,
|
||||
cacheExtent: cacheExtent,
|
||||
maxPaintExtent: estimatedMaxScrollOffset,
|
||||
|
||||
hasVisualOverflow:
|
||||
endScrollOffset > targetEndScrollOffsetForPaint ||
|
||||
constraints.scrollOffset > 0.0,
|
||||
);
|
||||
|
||||
if (estimatedMaxScrollOffset == endScrollOffset) {
|
||||
childManager.setDidUnderflow(true);
|
||||
}
|
||||
childManager.didFinishLayout();
|
||||
}
|
||||
}
|
||||
|
||||
const double kChatListPadding = 14.0;
|
||||
|
||||
/// from https://github.com/fluttercandies/extended_list
|
||||
mixin ExtendedRenderObjectMixin on RenderSliverMultiBoxAdaptor {
|
||||
void handleCloseToTrailingBegin() {
|
||||
_closeToTrailingDistance = 0.0;
|
||||
}
|
||||
|
||||
double handleCloseToTrailingEnd(double endScrollOffset) {
|
||||
final extent = constraints.remainingPaintExtent - kChatListPadding;
|
||||
if (endScrollOffset < extent) {
|
||||
_closeToTrailingDistance = extent - endScrollOffset;
|
||||
return extent;
|
||||
}
|
||||
return endScrollOffset;
|
||||
}
|
||||
|
||||
double _closeToTrailingDistance = 0.0;
|
||||
|
||||
@override
|
||||
double? childScrollOffset(RenderObject child) {
|
||||
return (super.childScrollOffset(child) ?? 0.0) + _closeToTrailingDistance;
|
||||
}
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/widgets.dart';
|
||||
///
|
||||
/// @docImport 'stack.dart';
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class CustomMultiChildLayout extends MultiChildRenderObjectWidget {
|
||||
/// Creates a custom multi-child layout.
|
||||
const CustomMultiChildLayout({
|
||||
super.key,
|
||||
required this.delegate,
|
||||
super.children,
|
||||
});
|
||||
|
||||
/// The delegate that controls the layout of the children.
|
||||
final MultiChildLayoutDelegate delegate;
|
||||
|
||||
@override
|
||||
RenderCustomMultiChildLayoutBox createRenderObject(BuildContext context) {
|
||||
return RenderCustomMultiChildLayoutBox(delegate: delegate);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderCustomMultiChildLayoutBox renderObject,
|
||||
) {
|
||||
renderObject.delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
/// A delegate that controls the layout of multiple children.
|
||||
///
|
||||
/// Used with [CustomMultiChildLayout] (in the widgets library) and
|
||||
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
|
||||
///
|
||||
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
|
||||
/// they must produce the same layout. To change the layout, replace the
|
||||
/// delegate with a different instance whose [shouldRelayout] returns true when
|
||||
/// given the previous instance.
|
||||
///
|
||||
/// Override [getSize] to control the overall size of the layout. The size of
|
||||
/// the layout cannot depend on layout properties of the children. This was
|
||||
/// a design decision to simplify the delegate implementations: This way,
|
||||
/// the delegate implementations do not have to also handle various intrinsic
|
||||
/// sizing functions if the parent's size depended on the children.
|
||||
/// If you want to build a custom layout where you define the size of that widget
|
||||
/// based on its children, then you will have to create a custom render object.
|
||||
/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and
|
||||
/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an
|
||||
/// example implementation.
|
||||
///
|
||||
/// Override [performLayout] to size and position the children. An
|
||||
/// implementation of [performLayout] must call [layoutChild] exactly once for
|
||||
/// each child, but it may call [layoutChild] on children in an arbitrary order.
|
||||
/// Typically a delegate will use the size returned from [layoutChild] on one
|
||||
/// child to determine the constraints for [performLayout] on another child or
|
||||
/// to determine the offset for [positionChild] for that child or another child.
|
||||
///
|
||||
/// Override [shouldRelayout] to determine when the layout of the children needs
|
||||
/// to be recomputed when the delegate changes.
|
||||
///
|
||||
/// The most efficient way to trigger a relayout is to supply a `relayout`
|
||||
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
|
||||
/// layout will listen to this value and relayout whenever the Listenable
|
||||
/// notifies its listeners, such as when an [Animation] ticks. This allows
|
||||
/// the custom layout to avoid the build phase of the pipeline.
|
||||
///
|
||||
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
|
||||
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
|
||||
/// the children that the [CustomMultiChildLayout] manages.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// Below is an example implementation of [performLayout] that causes one widget
|
||||
/// (the follower) to be the same size as another (the leader):
|
||||
///
|
||||
/// ```dart
|
||||
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
|
||||
/// // Typical usage is to define an enum like the one below, and use those
|
||||
/// // values as the ids.
|
||||
/// enum _Slot {
|
||||
/// leader,
|
||||
/// follower,
|
||||
/// }
|
||||
///
|
||||
/// class FollowTheLeader extends MultiChildLayoutDelegate {
|
||||
/// @override
|
||||
/// void performLayout(Size size) {
|
||||
/// Size leaderSize = Size.zero;
|
||||
///
|
||||
/// if (hasChild(_Slot.leader)) {
|
||||
/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
|
||||
/// positionChild(_Slot.leader, Offset.zero);
|
||||
/// }
|
||||
///
|
||||
/// if (hasChild(_Slot.follower)) {
|
||||
/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
|
||||
/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
|
||||
/// size.height - leaderSize.height));
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// The delegate gives the leader widget loose constraints, which means the
|
||||
/// child determines what size to be (subject to fitting within the given size).
|
||||
/// The delegate then remembers the size of that child and places it in the
|
||||
/// upper left corner.
|
||||
///
|
||||
/// The delegate then gives the follower widget tight constraints, forcing it to
|
||||
/// match the size of the leader widget. The delegate then places the follower
|
||||
/// widget in the bottom right corner.
|
||||
///
|
||||
/// The leader and follower widget will paint in the order they appear in the
|
||||
/// child list, regardless of the order in which [layoutChild] is called on
|
||||
/// them.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomMultiChildLayout], the widget that uses this delegate.
|
||||
/// * [RenderCustomMultiChildLayoutBox], render object that uses this
|
||||
/// delegate.
|
||||
abstract class MultiChildLayoutDelegate {
|
||||
/// Creates a layout delegate.
|
||||
///
|
||||
/// The layout will update whenever [relayout] notifies its listeners.
|
||||
MultiChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout;
|
||||
|
||||
final Listenable? _relayout;
|
||||
|
||||
Map<Object, RenderBox>? _idToChild;
|
||||
Set<RenderBox>? _debugChildrenNeedingLayout;
|
||||
|
||||
/// True if a non-null LayoutChild was provided for the specified id.
|
||||
///
|
||||
/// Call this from the [performLayout] method to determine which children
|
||||
/// are available, if the child list might vary.
|
||||
///
|
||||
/// This method cannot be called from [getSize] as the size is not allowed
|
||||
/// to depend on the children.
|
||||
bool hasChild(Object childId) => _idToChild![childId] != null;
|
||||
|
||||
/// Ask the child to update its layout within the limits specified by
|
||||
/// the constraints parameter. The child's size is returned.
|
||||
///
|
||||
/// Call this from your [performLayout] function to lay out each
|
||||
/// child. Every child must be laid out using this function exactly
|
||||
/// once each time the [performLayout] function is called.
|
||||
Size layoutChild(Object childId, BoxConstraints constraints) {
|
||||
final RenderBox? child = _idToChild![childId];
|
||||
assert(() {
|
||||
if (child == null) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
|
||||
'There is no child with the id "$childId".',
|
||||
);
|
||||
}
|
||||
if (!_debugChildrenNeedingLayout!.remove(child)) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
|
||||
'Each child must be laid out exactly once.',
|
||||
);
|
||||
}
|
||||
try {
|
||||
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
|
||||
} on AssertionError catch (exception) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".',
|
||||
),
|
||||
DiagnosticsProperty<AssertionError>(
|
||||
'Exception',
|
||||
exception,
|
||||
showName: false,
|
||||
),
|
||||
ErrorDescription(
|
||||
'The minimum width and height must be greater than or equal to zero.\n'
|
||||
'The maximum width must be greater than or equal to the minimum width.\n'
|
||||
'The maximum height must be greater than or equal to the minimum height.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
child!.layout(constraints, parentUsesSize: true);
|
||||
return child.size;
|
||||
}
|
||||
|
||||
/// Specify the child's origin relative to this origin.
|
||||
///
|
||||
/// Call this from your [performLayout] function to position each
|
||||
/// child. If you do not call this for a child, its position will
|
||||
/// remain unchanged. Children initially have their position set to
|
||||
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
|
||||
void positionChild(Object childId, Offset offset) {
|
||||
final RenderBox? child = _idToChild![childId];
|
||||
assert(() {
|
||||
if (child == null) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
|
||||
'There is no child with the id "$childId".',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child!.parentData! as MultiChildLayoutParentData;
|
||||
childParentData.offset = offset;
|
||||
}
|
||||
|
||||
DiagnosticsNode _debugDescribeChild(RenderBox child) {
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child.parentData! as MultiChildLayoutParentData;
|
||||
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
|
||||
}
|
||||
|
||||
void _callPerformLayout(Size size, RenderBox? firstChild) {
|
||||
// A particular layout delegate could be called reentrantly, e.g. if it used
|
||||
// by both a parent and a child. So, we must restore the _idToChild map when
|
||||
// we return.
|
||||
final Map<Object, RenderBox>? previousIdToChild = _idToChild;
|
||||
|
||||
Set<RenderBox>? debugPreviousChildrenNeedingLayout;
|
||||
assert(() {
|
||||
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
|
||||
_debugChildrenNeedingLayout = <RenderBox>{};
|
||||
return true;
|
||||
}());
|
||||
|
||||
try {
|
||||
_idToChild = <Object, RenderBox>{};
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child.parentData! as MultiChildLayoutParentData;
|
||||
assert(() {
|
||||
if (childParentData.id == null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.',
|
||||
),
|
||||
child!.describeForError('The following child has no ID'),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_idToChild![childParentData.id!] = child;
|
||||
assert(() {
|
||||
_debugChildrenNeedingLayout!.add(child!);
|
||||
return true;
|
||||
}());
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
performLayout(size);
|
||||
assert(() {
|
||||
if (_debugChildrenNeedingLayout!.isNotEmpty) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('Each child must be laid out exactly once.'),
|
||||
DiagnosticsBlock(
|
||||
name:
|
||||
'The $this custom multichild layout delegate forgot '
|
||||
'to lay out the following '
|
||||
'${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}',
|
||||
properties: _debugChildrenNeedingLayout!
|
||||
.map<DiagnosticsNode>(_debugDescribeChild)
|
||||
.toList(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
} finally {
|
||||
_idToChild = previousIdToChild;
|
||||
assert(() {
|
||||
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Override this method to return the size of this object given the
|
||||
/// incoming constraints.
|
||||
///
|
||||
/// The size cannot reflect the sizes of the children. If this layout has a
|
||||
/// fixed width or height the returned size can reflect that; the size will be
|
||||
/// constrained to the given constraints.
|
||||
///
|
||||
/// By default, attempts to size the box to the biggest size
|
||||
/// possible given the constraints.
|
||||
Size getSize(BoxConstraints constraints) => constraints.biggest;
|
||||
|
||||
/// Override this method to lay out and position all children given this
|
||||
/// widget's size.
|
||||
///
|
||||
/// This method must call [layoutChild] for each child. It should also specify
|
||||
/// the final position of each child with [positionChild].
|
||||
void performLayout(Size size);
|
||||
|
||||
/// Override this method to return true when the children need to be
|
||||
/// laid out.
|
||||
///
|
||||
/// This should compare the fields of the current delegate and the given
|
||||
/// `oldDelegate` and return true if the fields are such that the layout would
|
||||
/// be different.
|
||||
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
|
||||
|
||||
/// Override this method to include additional information in the
|
||||
/// debugging data printed by [debugDumpRenderTree] and friends.
|
||||
///
|
||||
/// By default, returns the [runtimeType] of the class.
|
||||
@override
|
||||
String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate');
|
||||
}
|
||||
|
||||
/// Defers the layout of multiple children to a delegate.
|
||||
///
|
||||
/// The delegate can determine the layout constraints for each child and can
|
||||
/// decide where to position each child. The delegate can also determine the
|
||||
/// size of the parent, but the size of the parent cannot depend on the sizes of
|
||||
/// the children.
|
||||
class RenderCustomMultiChildLayoutBox extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
/// Creates a render object that customizes the layout of multiple children.
|
||||
RenderCustomMultiChildLayoutBox({
|
||||
List<RenderBox>? children,
|
||||
required MultiChildLayoutDelegate delegate,
|
||||
}) : _delegate = delegate {
|
||||
addAll(children);
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
/// The delegate that controls the layout of the children.
|
||||
MultiChildLayoutDelegate get delegate => _delegate;
|
||||
MultiChildLayoutDelegate _delegate;
|
||||
set delegate(MultiChildLayoutDelegate newDelegate) {
|
||||
if (_delegate == newDelegate) {
|
||||
return;
|
||||
}
|
||||
final MultiChildLayoutDelegate oldDelegate = _delegate;
|
||||
if (newDelegate.runtimeType != oldDelegate.runtimeType ||
|
||||
newDelegate.shouldRelayout(oldDelegate)) {
|
||||
markNeedsLayout();
|
||||
}
|
||||
_delegate = newDelegate;
|
||||
if (attached) {
|
||||
oldDelegate._relayout?.removeListener(markNeedsLayout);
|
||||
newDelegate._relayout?.addListener(markNeedsLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
super.attach(owner);
|
||||
_delegate._relayout?.addListener(markNeedsLayout);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_delegate._relayout?.removeListener(markNeedsLayout);
|
||||
super.detach();
|
||||
}
|
||||
|
||||
Size _getSize(BoxConstraints constraints) {
|
||||
assert(constraints.debugAssertIsValid());
|
||||
return constraints.constrain(_delegate.getSize(constraints));
|
||||
}
|
||||
|
||||
// TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
|
||||
// figure out the intrinsic dimensions. We really should either not support intrinsics,
|
||||
// or we should expose intrinsic delegate callbacks and throw if they're not implemented.
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
Size computeDryLayout(covariant BoxConstraints constraints) {
|
||||
return _getSize(constraints);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _getSize(constraints);
|
||||
delegate._callPerformLayout(size, firstChild);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
@@ -2,25 +2,17 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import, depend_on_referenced_packages
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
/// @docImport 'package:flutter_test/flutter_test.dart';
|
||||
///
|
||||
/// @docImport 'primary_scroll_controller.dart';
|
||||
/// @docImport 'scroll_configuration.dart';
|
||||
/// @docImport 'scroll_view.dart';
|
||||
/// @docImport 'scrollable.dart';
|
||||
/// @docImport 'single_child_scroll_view.dart';
|
||||
/// @docImport 'viewport.dart';
|
||||
library;
|
||||
// ignore_for_file: prefer_initializing_formals
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
|
||||
|
||||
part 'package:PiliPlus/common/widgets/draggable_sheet/dyn.dart';
|
||||
part 'package:PiliPlus/common/widgets/draggable_sheet/topic.dart';
|
||||
|
||||
/// Controls a [DraggableScrollableSheet].
|
||||
///
|
||||
@@ -112,11 +104,10 @@ class DraggableScrollableController extends ChangeNotifier {
|
||||
_assertAttached();
|
||||
assert(size >= 0 && size <= 1);
|
||||
assert(duration != Duration.zero);
|
||||
final AnimationController animationController =
|
||||
AnimationController.unbounded(
|
||||
vsync: _attachedController!.position.context.vsync,
|
||||
value: _attachedController!.extent.currentSize,
|
||||
);
|
||||
final animationController = AnimationController.unbounded(
|
||||
vsync: _attachedController!.position.context.vsync,
|
||||
value: _attachedController!.extent.currentSize,
|
||||
);
|
||||
_animationControllers.add(animationController);
|
||||
_attachedController!.position.goIdle();
|
||||
// This disables any snapping until the next user interaction with the sheet.
|
||||
@@ -583,7 +574,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
}
|
||||
|
||||
List<double> _impliedSnapSizes() {
|
||||
for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
|
||||
for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
|
||||
final double snapSize = widget.snapSizes![index];
|
||||
assert(
|
||||
snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
|
||||
@@ -684,11 +675,11 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
// have changed when the widget was updated.
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
for (
|
||||
int index = 0;
|
||||
var index = 0;
|
||||
index < _scrollController.positions.length;
|
||||
index++
|
||||
) {
|
||||
final _DraggableScrollableSheetScrollPosition position =
|
||||
final position =
|
||||
_scrollController.positions.elementAt(index)
|
||||
as _DraggableScrollableSheetScrollPosition;
|
||||
position.goBallistic(0);
|
||||
@@ -702,7 +693,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
.asMap()
|
||||
.keys
|
||||
.map((int index) {
|
||||
final String snapSizeString = widget.snapSizes![index].toString();
|
||||
final snapSizeString = widget.snapSizes![index].toString();
|
||||
if (index == invalidIndex) {
|
||||
return '>>> $snapSizeString <<<';
|
||||
}
|
||||
@@ -729,9 +720,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
/// [_DraggableScrollableSheetScrollController] as the primary controller for
|
||||
/// descendants.
|
||||
class _DraggableScrollableSheetScrollController extends ScrollController {
|
||||
_DraggableScrollableSheetScrollController({
|
||||
required this.extent,
|
||||
});
|
||||
_DraggableScrollableSheetScrollController({required this.extent});
|
||||
|
||||
_DraggableSheetExtent extent;
|
||||
VoidCallback? onPositionDetached;
|
||||
@@ -806,6 +795,7 @@ class _DraggableScrollableSheetScrollPosition
|
||||
required super.context,
|
||||
super.oldPosition,
|
||||
required this.getExtent,
|
||||
super.initialPixels,
|
||||
});
|
||||
|
||||
VoidCallback? _dragCancelCallback;
|
||||
@@ -816,8 +806,6 @@ class _DraggableScrollableSheetScrollPosition
|
||||
|
||||
_DraggableSheetExtent get extent => getExtent();
|
||||
|
||||
bool _isAtTop = true;
|
||||
|
||||
@override
|
||||
void absorb(ScrollPosition other) {
|
||||
super.absorb(other);
|
||||
@@ -845,9 +833,7 @@ class _DraggableScrollableSheetScrollPosition
|
||||
|
||||
@override
|
||||
void applyUserOffset(double delta) {
|
||||
if (!_isAtTop) {
|
||||
super.applyUserOffset(delta);
|
||||
} else if (!listShouldScroll &&
|
||||
if (!listShouldScroll &&
|
||||
(!(extent.isAtMin || extent.isAtMax) ||
|
||||
(extent.isAtMin && delta < 0) ||
|
||||
(extent.isAtMax && delta > 0))) {
|
||||
@@ -882,10 +868,6 @@ class _DraggableScrollableSheetScrollPosition
|
||||
|
||||
@override
|
||||
void goBallistic(double velocity) {
|
||||
if (!_isAtTop) {
|
||||
super.goBallistic(velocity);
|
||||
return;
|
||||
}
|
||||
if ((velocity == 0.0 && !_shouldSnap()) ||
|
||||
(velocity < 0.0 && listShouldScroll) ||
|
||||
(velocity > 0.0 && extent.isAtMax)) {
|
||||
@@ -917,14 +899,10 @@ class _DraggableScrollableSheetScrollPosition
|
||||
);
|
||||
}
|
||||
|
||||
final AnimationController ballisticController =
|
||||
AnimationController.unbounded(
|
||||
debugLabel: objectRuntimeType(
|
||||
this,
|
||||
'_DraggableScrollableSheetPosition',
|
||||
),
|
||||
vsync: context.vsync,
|
||||
);
|
||||
final ballisticController = AnimationController.unbounded(
|
||||
debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'),
|
||||
vsync: context.vsync,
|
||||
);
|
||||
_ballisticControllers.add(ballisticController);
|
||||
|
||||
double lastPosition = extent.currentPixels;
|
||||
@@ -967,71 +945,12 @@ class _DraggableScrollableSheetScrollPosition
|
||||
|
||||
@override
|
||||
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
|
||||
_isAtTop = pixels == 0;
|
||||
// Save this so we can call it later if we have to [goBallistic] on our own.
|
||||
_dragCancelCallback = dragCancelCallback;
|
||||
return super.drag(details, dragCancelCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that can notify a descendent [DraggableScrollableSheet] that it
|
||||
/// should reset its position to the initial state.
|
||||
///
|
||||
/// The [Scaffold] uses this widget to notify a persistent bottom sheet that
|
||||
/// the user has tapped back if the sheet has started to cover more of the body
|
||||
/// than when at its initial position. This is important for users of assistive
|
||||
/// technology, where dragging may be difficult to communicate.
|
||||
///
|
||||
/// This is just a wrapper on top of [DraggableScrollableController]. It is
|
||||
/// primarily useful for controlling a sheet in a part of the widget tree that
|
||||
/// the current code does not control (e.g. library code trying to affect a sheet
|
||||
/// in library users' code). Generally, it's easier to control the sheet
|
||||
/// directly by creating a controller and passing the controller to the sheet in
|
||||
/// its constructor (see [DraggableScrollableSheet.controller]).
|
||||
class DraggableScrollableActuator extends StatefulWidget {
|
||||
/// Creates a widget that can notify descendent [DraggableScrollableSheet]s
|
||||
/// to reset to their initial position.
|
||||
///
|
||||
/// The [child] parameter is required.
|
||||
const DraggableScrollableActuator({super.key, required this.child});
|
||||
|
||||
/// This child's [DraggableScrollableSheet] descendant will be reset when the
|
||||
/// [reset] method is applied to a context that includes it.
|
||||
final Widget child;
|
||||
|
||||
/// Notifies any descendant [DraggableScrollableSheet] that it should reset
|
||||
/// to its initial position.
|
||||
///
|
||||
/// Returns `true` if a [DraggableScrollableActuator] is available and
|
||||
/// some [DraggableScrollableSheet] is listening for updates, `false`
|
||||
/// otherwise.
|
||||
static bool reset(BuildContext context) {
|
||||
final _InheritedResetNotifier? notifier = context
|
||||
.dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>();
|
||||
return notifier?._sendReset() ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
State<DraggableScrollableActuator> createState() =>
|
||||
_DraggableScrollableActuatorState();
|
||||
}
|
||||
|
||||
class _DraggableScrollableActuatorState
|
||||
extends State<DraggableScrollableActuator> {
|
||||
final _ResetNotifier _notifier = _ResetNotifier();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _InheritedResetNotifier(notifier: _notifier, child: widget.child);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_notifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [ChangeNotifier] to use with [_InheritedResetNotifier] to notify
|
||||
/// descendants that they should reset to initial state.
|
||||
class _ResetNotifier extends ChangeNotifier {
|
||||
@@ -1067,6 +986,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
|
||||
required _ResetNotifier super.notifier,
|
||||
});
|
||||
|
||||
// ignore: unused_element
|
||||
bool _sendReset() => notifier!.sendReset();
|
||||
|
||||
/// Specifies whether the [DraggableScrollableSheet] should reset to its
|
||||
@@ -1080,8 +1000,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
|
||||
return false;
|
||||
}
|
||||
assert(widget is _InheritedResetNotifier);
|
||||
final _InheritedResetNotifier inheritedNotifier =
|
||||
widget as _InheritedResetNotifier;
|
||||
final inheritedNotifier = widget as _InheritedResetNotifier;
|
||||
final bool wasCalled = inheritedNotifier.notifier!._wasCalled;
|
||||
inheritedNotifier.notifier!._wasCalled = false;
|
||||
return wasCalled;
|
||||
@@ -1158,6 +1077,10 @@ class _SnappingSimulation extends Simulation {
|
||||
return pixelSnapSizes.first;
|
||||
}
|
||||
final double nextSize = pixelSnapSizes[indexOfNextSize];
|
||||
// If already snapped - keep this as target size
|
||||
if (nextSize == position) {
|
||||
return nextSize;
|
||||
}
|
||||
final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
|
||||
if (initialVelocity.abs() <= tolerance.velocity) {
|
||||
// If velocity is zero, snap to the nearest snap size with the minimum velocity.
|
||||
@@ -1,789 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'elevated_button_theme.dart';
|
||||
/// @docImport 'menu_anchor.dart';
|
||||
/// @docImport 'text_button_theme.dart';
|
||||
/// @docImport 'text_theme.dart';
|
||||
/// @docImport 'theme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
|
||||
///
|
||||
/// See also:
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * [TextButton], a button with no outline or fill color.
|
||||
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
|
||||
/// the Material Design button types and how they should be used in designs.
|
||||
abstract class ButtonStyleButton extends StatefulWidget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const ButtonStyleButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.onLongPress,
|
||||
required this.onHover,
|
||||
required this.onFocusChange,
|
||||
required this.style,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.clipBehavior,
|
||||
this.statesController,
|
||||
this.isSemanticButton = true,
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
this.iconAlignment,
|
||||
this.tooltip,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// Called when the button is tapped or otherwise activated.
|
||||
///
|
||||
/// If this callback and [onLongPress] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Called when the button is long-pressed.
|
||||
///
|
||||
/// If this callback and [onPressed] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Called when a pointer enters or exits the button response area.
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered this
|
||||
/// part of the material and false if a pointer has exited this part of the
|
||||
/// material.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding
|
||||
/// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s
|
||||
/// that resolve to non-null values will similarly override the corresponding
|
||||
/// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf].
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
|
||||
/// [ButtonStyle.foregroundBuilder] is specified. In those
|
||||
/// cases the default is [Clip.antiAlias].
|
||||
final Clip? clipBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.material.inkwell.statesController}
|
||||
final WidgetStatesController? statesController;
|
||||
|
||||
/// Determine whether this subtree represents a button.
|
||||
///
|
||||
/// If this is null, the screen reader will not announce "button" when this
|
||||
/// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we
|
||||
/// traverse the menu system.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool? isSemanticButton;
|
||||
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
/// Text that describes the action that will occur when the button is pressed or
|
||||
/// hovered over.
|
||||
///
|
||||
/// This text is displayed when the user long-presses or hovers over the button
|
||||
/// in a tooltip. This string is also used for accessibility.
|
||||
///
|
||||
/// If null, the button will not display a tooltip.
|
||||
final String? tooltip;
|
||||
|
||||
/// Typically the button's label.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
/// Returns a [ButtonStyle] that's based primarily on the [Theme]'s
|
||||
/// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values
|
||||
/// filled out (non-null).
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter and by the
|
||||
/// style returned by [themeStyleOf] that some button-specific themes like
|
||||
/// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the
|
||||
/// default style of the [TextButton] subclass can be overridden with its
|
||||
/// [TextButton.style] constructor parameter, or with a [TextButtonTheme].
|
||||
///
|
||||
/// Concrete button subclasses should return a [ButtonStyle] with as many
|
||||
/// non-null properties as possible, where all of the non-null
|
||||
/// [WidgetStateProperty] properties resolve to non-null values.
|
||||
///
|
||||
/// ## Properties that can be null
|
||||
///
|
||||
/// Some properties, like [ButtonStyle.fixedSize] would override other values
|
||||
/// in the same [ButtonStyle] if set, so they are allowed to be null. Here is
|
||||
/// a summary of properties that are allowed to be null when returned in the
|
||||
/// [ButtonStyle] returned by this function, an why:
|
||||
///
|
||||
/// - [ButtonStyle.fixedSize] because it would override other values in the
|
||||
/// same [ButtonStyle], like [ButtonStyle.maximumSize].
|
||||
/// - [ButtonStyle.side] because null is a valid value for a button that has
|
||||
/// no side. [OutlinedButton] returns a non-null default for this, however.
|
||||
/// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]
|
||||
/// because they would override the [ButtonStyle.foregroundColor] and
|
||||
/// [ButtonStyle.backgroundColor] of the same [ButtonStyle].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [themeStyleOf], returns the ButtonStyle of this button's component
|
||||
/// theme.
|
||||
@protected
|
||||
ButtonStyle defaultStyleOf(BuildContext context);
|
||||
|
||||
/// Returns the ButtonStyle that belongs to the button's component theme.
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter.
|
||||
///
|
||||
/// Concrete button subclasses should return the ButtonStyle for the
|
||||
/// nearest subclass-specific inherited theme, and if no such theme
|
||||
/// exists, then the same value from the overall [Theme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
|
||||
@protected
|
||||
ButtonStyle? themeStyleOf(BuildContext context);
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// Buttons are disabled by default. To enable a button, set its [onPressed]
|
||||
/// or [onLongPress] properties to a non-null value.
|
||||
bool get enabled => onPressed != null || onLongPress != null;
|
||||
|
||||
@override
|
||||
State<ButtonStyleButton> createState() => _ButtonStyleState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
FlagProperty('enabled', value: enabled, ifFalse: 'disabled'),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<FocusNode>(
|
||||
'focusNode',
|
||||
focusNode,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns null if [value] is null, otherwise `WidgetStatePropertyAll<T>(value)`.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<T>? allOrNull<T>(T? value) =>
|
||||
value == null ? null : WidgetStatePropertyAll<T>(value);
|
||||
|
||||
/// Returns null if [enabled] and [disabled] are null.
|
||||
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
|
||||
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<Color?>? defaultColor(
|
||||
Color? enabled,
|
||||
Color? disabled,
|
||||
) {
|
||||
if ((enabled ?? disabled) == null) {
|
||||
return null;
|
||||
}
|
||||
return WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
|
||||
WidgetState.disabled: disabled,
|
||||
WidgetState.any: enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/// A convenience method used by subclasses in the framework, that returns an
|
||||
/// interpolated value based on the [fontSizeMultiplier] parameter:
|
||||
///
|
||||
/// * 0 - 1 [geometry1x]
|
||||
/// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1)
|
||||
/// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2)
|
||||
/// * otherwise [geometry3x]
|
||||
///
|
||||
/// This method is used by the framework for estimating the default paddings to
|
||||
/// use on a button with a text label, when the system text scaling setting
|
||||
/// changes. It's usually supplied with empirical [geometry1x], [geometry2x],
|
||||
/// [geometry3x] values adjusted for different system text scaling values, when
|
||||
/// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge]
|
||||
/// value).
|
||||
///
|
||||
/// The `fontSizeMultiplier` argument, for historical reasons, is the default
|
||||
/// font size specified in the [ButtonStyle], scaled by the ambient font
|
||||
/// scaler, then divided by 14.0 (the default font size used in buttons).
|
||||
static EdgeInsetsGeometry scaledPadding(
|
||||
EdgeInsetsGeometry geometry1x,
|
||||
EdgeInsetsGeometry geometry2x,
|
||||
EdgeInsetsGeometry geometry3x,
|
||||
double fontSizeMultiplier,
|
||||
) {
|
||||
return switch (fontSizeMultiplier) {
|
||||
<= 1 => geometry1x,
|
||||
< 2 => EdgeInsetsGeometry.lerp(
|
||||
geometry1x,
|
||||
geometry2x,
|
||||
fontSizeMultiplier - 1,
|
||||
)!,
|
||||
< 3 => EdgeInsetsGeometry.lerp(
|
||||
geometry2x,
|
||||
geometry3x,
|
||||
fontSizeMultiplier - 2,
|
||||
)!,
|
||||
_ => geometry3x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
|
||||
/// * [OutlinedButton], similar to [TextButton], but with an outline.
|
||||
/// * [TextButton], a simple button without a shadow.
|
||||
class _ButtonStyleState extends State<ButtonStyleButton>
|
||||
with TickerProviderStateMixin {
|
||||
AnimationController? controller;
|
||||
double? elevation;
|
||||
Color? backgroundColor;
|
||||
WidgetStatesController? internalStatesController;
|
||||
|
||||
void handleStatesControllerChange() {
|
||||
// Force a rebuild to resolve WidgetStateProperty properties
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
WidgetStatesController get statesController =>
|
||||
widget.statesController ?? internalStatesController!;
|
||||
|
||||
void initStatesController() {
|
||||
if (widget.statesController == null) {
|
||||
internalStatesController = WidgetStatesController();
|
||||
}
|
||||
statesController
|
||||
..update(WidgetState.disabled, !widget.enabled)
|
||||
..addListener(handleStatesControllerChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initStatesController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ButtonStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.statesController != oldWidget.statesController) {
|
||||
oldWidget.statesController?.removeListener(handleStatesControllerChange);
|
||||
if (widget.statesController != null) {
|
||||
internalStatesController?.dispose();
|
||||
internalStatesController = null;
|
||||
}
|
||||
initStatesController();
|
||||
}
|
||||
if (widget.enabled != oldWidget.enabled) {
|
||||
statesController.update(WidgetState.disabled, !widget.enabled);
|
||||
if (!widget.enabled) {
|
||||
// The button may have been disabled while a press gesture is currently underway.
|
||||
statesController.update(WidgetState.pressed, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
statesController.removeListener(handleStatesControllerChange);
|
||||
internalStatesController?.dispose();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
final ButtonStyle? widgetStyle = widget.style;
|
||||
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
|
||||
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
|
||||
|
||||
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
|
||||
final T? widgetValue = getProperty(widgetStyle);
|
||||
final T? themeValue = getProperty(themeStyle);
|
||||
final T? defaultValue = getProperty(defaultStyle);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
}
|
||||
|
||||
T? resolve<T>(
|
||||
WidgetStateProperty<T>? Function(ButtonStyle? style) getProperty,
|
||||
) {
|
||||
return effectiveValue((ButtonStyle? style) {
|
||||
return getProperty(style)?.resolve(statesController.value);
|
||||
});
|
||||
}
|
||||
|
||||
Color? effectiveIconColor() {
|
||||
return widgetStyle?.iconColor?.resolve(statesController.value) ??
|
||||
themeStyle?.iconColor?.resolve(statesController.value) ??
|
||||
widgetStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
themeStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
defaultStyle.iconColor?.resolve(statesController.value) ??
|
||||
// Fallback to foregroundColor if iconColor is null.
|
||||
defaultStyle.foregroundColor?.resolve(statesController.value);
|
||||
}
|
||||
|
||||
final double? resolvedElevation = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.elevation,
|
||||
);
|
||||
final TextStyle? resolvedTextStyle = resolve<TextStyle?>(
|
||||
(ButtonStyle? style) => style?.textStyle,
|
||||
);
|
||||
Color? resolvedBackgroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.backgroundColor,
|
||||
);
|
||||
final Color? resolvedForegroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.foregroundColor,
|
||||
);
|
||||
final Color? resolvedShadowColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.shadowColor,
|
||||
);
|
||||
final Color? resolvedSurfaceTintColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.surfaceTintColor,
|
||||
);
|
||||
final EdgeInsetsGeometry? resolvedPadding = resolve<EdgeInsetsGeometry?>(
|
||||
(ButtonStyle? style) => style?.padding,
|
||||
);
|
||||
final Size? resolvedMinimumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.minimumSize,
|
||||
);
|
||||
final Size? resolvedFixedSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.fixedSize,
|
||||
);
|
||||
final Size? resolvedMaximumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.maximumSize,
|
||||
);
|
||||
final Color? resolvedIconColor = effectiveIconColor();
|
||||
final double? resolvedIconSize = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.iconSize,
|
||||
);
|
||||
final BorderSide? resolvedSide = resolve<BorderSide?>(
|
||||
(ButtonStyle? style) => style?.side,
|
||||
);
|
||||
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>(
|
||||
(ButtonStyle? style) => style?.shape,
|
||||
);
|
||||
|
||||
final WidgetStateMouseCursor mouseCursor = _MouseCursor(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.mouseCursor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final WidgetStateProperty<Color?> overlayColor =
|
||||
WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.overlayColor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final VisualDensity? resolvedVisualDensity = effectiveValue(
|
||||
(ButtonStyle? style) => style?.visualDensity,
|
||||
);
|
||||
final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue(
|
||||
(ButtonStyle? style) => style?.tapTargetSize,
|
||||
);
|
||||
final Duration? resolvedAnimationDuration = effectiveValue(
|
||||
(ButtonStyle? style) => style?.animationDuration,
|
||||
);
|
||||
final bool resolvedEnableFeedback =
|
||||
effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true;
|
||||
final AlignmentGeometry? resolvedAlignment = effectiveValue(
|
||||
(ButtonStyle? style) => style?.alignment,
|
||||
);
|
||||
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
|
||||
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue(
|
||||
(ButtonStyle? style) => style?.splashFactory,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.backgroundBuilder,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.foregroundBuilder,
|
||||
);
|
||||
|
||||
final Clip effectiveClipBehavior =
|
||||
widget.clipBehavior ??
|
||||
((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null
|
||||
? Clip.antiAlias
|
||||
: Clip.none);
|
||||
|
||||
BoxConstraints effectiveConstraints = resolvedVisualDensity
|
||||
.effectiveConstraints(
|
||||
BoxConstraints(
|
||||
minWidth: resolvedMinimumSize!.width,
|
||||
minHeight: resolvedMinimumSize.height,
|
||||
maxWidth: resolvedMaximumSize!.width,
|
||||
maxHeight: resolvedMaximumSize.height,
|
||||
),
|
||||
);
|
||||
if (resolvedFixedSize != null) {
|
||||
final Size size = effectiveConstraints.constrain(resolvedFixedSize);
|
||||
if (size.width.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minWidth: size.width,
|
||||
maxWidth: size.width,
|
||||
);
|
||||
}
|
||||
if (size.height.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minHeight: size.height,
|
||||
maxHeight: size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Per the Material Design team: don't allow the VisualDensity
|
||||
// adjustment to reduce the width of the left/right padding. If we
|
||||
// did, VisualDensity.compact, the default for desktop/web, would
|
||||
// reduce the horizontal padding to zero.
|
||||
final double dy = densityAdjustment.dy;
|
||||
final double dx = math.max(0, densityAdjustment.dx);
|
||||
final EdgeInsetsGeometry padding = resolvedPadding!
|
||||
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
|
||||
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
|
||||
|
||||
// If an opaque button's background is becoming translucent while its
|
||||
// elevation is changing, change the elevation first. Material implicitly
|
||||
// animates its elevation but not its color. SKIA renders non-zero
|
||||
// elevations as a shadow colored fill behind the Material's background.
|
||||
if (resolvedAnimationDuration! > Duration.zero &&
|
||||
elevation != null &&
|
||||
backgroundColor != null &&
|
||||
elevation != resolvedElevation &&
|
||||
backgroundColor!.value != resolvedBackgroundColor!.value &&
|
||||
backgroundColor!.opacity == 1 &&
|
||||
resolvedBackgroundColor.opacity < 1 &&
|
||||
resolvedElevation == 0) {
|
||||
if (controller?.duration != resolvedAnimationDuration) {
|
||||
controller?.dispose();
|
||||
controller =
|
||||
AnimationController(
|
||||
duration: resolvedAnimationDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
setState(() {}); // Rebuild with the final background color.
|
||||
}
|
||||
});
|
||||
}
|
||||
resolvedBackgroundColor =
|
||||
backgroundColor; // Defer changing the background color.
|
||||
controller!.value = 0;
|
||||
controller!.forward();
|
||||
}
|
||||
elevation = resolvedElevation;
|
||||
backgroundColor = resolvedBackgroundColor;
|
||||
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Align(
|
||||
alignment: resolvedAlignment!,
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: resolvedForegroundBuilder != null
|
||||
? resolvedForegroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
widget.child,
|
||||
)
|
||||
: widget.child,
|
||||
),
|
||||
);
|
||||
if (resolvedBackgroundBuilder != null) {
|
||||
result = resolvedBackgroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
result = AnimatedTheme(
|
||||
duration: resolvedAnimationDuration,
|
||||
data: theme.copyWith(
|
||||
iconTheme: iconTheme.merge(
|
||||
IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
onHover: widget.onHover,
|
||||
mouseCursor: mouseCursor,
|
||||
enableFeedback: resolvedEnableFeedback,
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: widget.onFocusChange,
|
||||
autofocus: widget.autofocus,
|
||||
splashFactory: resolvedSplashFactory,
|
||||
overlayColor: overlayColor,
|
||||
highlightColor: Colors.transparent,
|
||||
customBorder: resolvedShape!.copyWith(side: resolvedSide),
|
||||
statesController: statesController,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
result = Tooltip(message: widget.tooltip, child: result);
|
||||
}
|
||||
|
||||
final Size minSize;
|
||||
switch (resolvedTapTargetSize!) {
|
||||
case MaterialTapTargetSize.padded:
|
||||
minSize = Size(
|
||||
kMinInteractiveDimension + densityAdjustment.dx,
|
||||
kMinInteractiveDimension + densityAdjustment.dy,
|
||||
);
|
||||
assert(minSize.width >= 0.0);
|
||||
assert(minSize.height >= 0.0);
|
||||
case MaterialTapTargetSize.shrinkWrap:
|
||||
minSize = Size.zero;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
button: widget.isSemanticButton,
|
||||
enabled: widget.enabled,
|
||||
child: _InputPadding(
|
||||
minSize: minSize,
|
||||
child: ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Material(
|
||||
elevation: resolvedElevation!,
|
||||
textStyle: resolvedTextStyle?.copyWith(
|
||||
color: resolvedForegroundColor,
|
||||
),
|
||||
shape: resolvedShape.copyWith(side: resolvedSide),
|
||||
color: resolvedBackgroundColor,
|
||||
shadowColor: resolvedShadowColor,
|
||||
surfaceTintColor: resolvedSurfaceTintColor,
|
||||
type: resolvedBackgroundColor == null
|
||||
? MaterialType.transparency
|
||||
: MaterialType.button,
|
||||
animationDuration: resolvedAnimationDuration,
|
||||
clipBehavior: effectiveClipBehavior,
|
||||
borderOnForeground: false,
|
||||
child: result,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MouseCursor extends WidgetStateMouseCursor {
|
||||
const _MouseCursor(this.resolveCallback);
|
||||
|
||||
final WidgetPropertyResolver<MouseCursor?> resolveCallback;
|
||||
|
||||
@override
|
||||
MouseCursor resolve(Set<WidgetState> states) => resolveCallback(states)!;
|
||||
|
||||
@override
|
||||
String get debugDescription => 'ButtonStyleButton_MouseCursor';
|
||||
}
|
||||
|
||||
/// A widget to pad the area around a [ButtonStyleButton]'s inner [Material].
|
||||
///
|
||||
/// Redirect taps that occur in the padded area around the child to the center
|
||||
/// of the child. This increases the size of the button and the button's
|
||||
/// "tap target", but not its material or its ink splashes.
|
||||
class _InputPadding extends SingleChildRenderObjectWidget {
|
||||
const _InputPadding({super.child, required this.minSize});
|
||||
|
||||
final Size minSize;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderInputPadding(minSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant _RenderInputPadding renderObject,
|
||||
) {
|
||||
renderObject.minSize = minSize;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderInputPadding extends RenderShiftedBox {
|
||||
_RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
|
||||
|
||||
Size get minSize => _minSize;
|
||||
Size _minSize;
|
||||
set minSize(Size value) {
|
||||
if (_minSize == value) {
|
||||
return;
|
||||
}
|
||||
_minSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Size _computeSize({
|
||||
required BoxConstraints constraints,
|
||||
required ChildLayouter layoutChild,
|
||||
}) {
|
||||
if (child != null) {
|
||||
final Size childSize = layoutChild(child!, constraints);
|
||||
final double height = math.max(childSize.width, minSize.width);
|
||||
final double width = math.max(childSize.height, minSize.height);
|
||||
return constraints.constrain(Size(height, width));
|
||||
}
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
return _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.dryLayoutChild,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(
|
||||
covariant BoxConstraints constraints,
|
||||
TextBaseline baseline,
|
||||
) {
|
||||
final RenderBox? child = this.child;
|
||||
if (child == null) {
|
||||
return null;
|
||||
}
|
||||
final double? result = child.getDryBaseline(constraints, baseline);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
final Size childSize = child.getDryLayout(constraints);
|
||||
return result +
|
||||
Alignment.center
|
||||
.alongOffset(getDryLayout(constraints) - childSize as Offset)
|
||||
.dy;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.layoutChild,
|
||||
);
|
||||
if (child != null) {
|
||||
final BoxParentData childParentData = child!.parentData! as BoxParentData;
|
||||
childParentData.offset = Alignment.center.alongOffset(
|
||||
size - child!.size as Offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
if (super.hitTest(result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
final Offset center = child!.size.center(Offset.zero);
|
||||
return result.addWithRawTransform(
|
||||
transform: MatrixUtils.forceToPoint(center),
|
||||
position: center,
|
||||
hitTest: (BoxHitTestResult result, Offset position) {
|
||||
assert(position == center);
|
||||
return child!.hitTest(result, position: center);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,676 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'elevated_button.dart';
|
||||
/// @docImport 'filled_button.dart';
|
||||
/// @docImport 'material.dart';
|
||||
/// @docImport 'outlined_button.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
|
||||
|
||||
/// A Material Design "Text Button".
|
||||
///
|
||||
/// Use text buttons on toolbars, in dialogs, or inline with other
|
||||
/// content but offset from that content with padding so that the
|
||||
/// button's presence is obvious. Text buttons do not have visible
|
||||
/// borders and must therefore rely on their position relative to
|
||||
/// other content for context. In dialogs and cards, they should be
|
||||
/// grouped together in one of the bottom corners. Avoid using text
|
||||
/// buttons where they would blend in with other content, for example
|
||||
/// in the middle of lists.
|
||||
///
|
||||
/// A text button is a label [child] displayed on a (zero elevation)
|
||||
/// [Material] widget. The label's [Text] and [Icon] widgets are
|
||||
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
|
||||
/// button reacts to touches by filling with the [style]'s
|
||||
/// [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// The text button's default style is defined by [defaultStyleOf].
|
||||
/// The style of this text button can be overridden with its [style]
|
||||
/// parameter. The style of all text buttons in a subtree can be
|
||||
/// overridden with the [TextButtonTheme] and the style of all of the
|
||||
/// text buttons in an app can be overridden with the [Theme]'s
|
||||
/// [ThemeData.textButtonTheme] property.
|
||||
///
|
||||
/// The static [styleFrom] method is a convenient way to create a
|
||||
/// text button [ButtonStyle] from simple values.
|
||||
///
|
||||
/// If the [onPressed] and [onLongPress] callbacks are null, then this
|
||||
/// button will be disabled, it will not react to touch.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows various ways to configure TextButtons, from the
|
||||
/// simplest default appearance to versions that don't resemble
|
||||
/// Material Design at all.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates using the [statesController] parameter to create a button
|
||||
/// that adds support for [WidgetState.selected].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * <https://material.io/design/components/buttons.html>
|
||||
/// * <https://m3.material.io/components/buttons>
|
||||
class TextButton extends ButtonStyleButton {
|
||||
/// Create a [TextButton].
|
||||
const TextButton({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
super.autofocus = false,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
super.isSemanticButton,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
/// Create a text button from a pair of widgets that serve as the button's
|
||||
/// [icon] and [label].
|
||||
///
|
||||
/// The icon and label are arranged in a row and padded by 8 logical pixels
|
||||
/// at the ends, with an 8 pixel gap in between.
|
||||
///
|
||||
/// If [icon] is null, will create a [TextButton] instead.
|
||||
///
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
///
|
||||
factory TextButton.icon({
|
||||
Key? key,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
ValueChanged<bool>? onHover,
|
||||
ValueChanged<bool>? onFocusChange,
|
||||
ButtonStyle? style,
|
||||
FocusNode? focusNode,
|
||||
bool? autofocus,
|
||||
Clip? clipBehavior,
|
||||
WidgetStatesController? statesController,
|
||||
Widget? icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) {
|
||||
if (icon == null) {
|
||||
return TextButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return _TextButtonWithIcon(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
icon: icon,
|
||||
label: label,
|
||||
iconAlignment: iconAlignment,
|
||||
);
|
||||
}
|
||||
|
||||
/// A static convenience method that constructs a text button
|
||||
/// [ButtonStyle] given simple values.
|
||||
///
|
||||
/// The [foregroundColor] and [disabledForegroundColor] colors are used
|
||||
/// to create a [WidgetStateProperty] [ButtonStyle.foregroundColor], and
|
||||
/// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
|
||||
///
|
||||
/// The [backgroundColor] and [disabledBackgroundColor] colors are
|
||||
/// used to create a [WidgetStateProperty] [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
|
||||
/// parameters are used to construct [ButtonStyle.mouseCursor].
|
||||
///
|
||||
/// The [iconColor], [disabledIconColor] are used to construct
|
||||
/// [ButtonStyle.iconColor] and [iconSize] is used to construct
|
||||
/// [ButtonStyle.iconSize].
|
||||
///
|
||||
/// If [iconColor] is null, the button icon will use [foregroundColor]. If [foregroundColor] is also
|
||||
/// null, the button icon will use the default icon color.
|
||||
///
|
||||
/// If [overlayColor] is specified and its value is [Colors.transparent]
|
||||
/// then the pressed/focused/hovered highlights are effectively defeated.
|
||||
/// Otherwise a [WidgetStateProperty] with the same opacities as the
|
||||
/// default is created.
|
||||
///
|
||||
/// All of the other parameters are either used directly or used to
|
||||
/// create a [WidgetStateProperty] with a single value for all
|
||||
/// states.
|
||||
///
|
||||
/// All parameters default to null. By default this method returns
|
||||
/// a [ButtonStyle] that doesn't override anything.
|
||||
///
|
||||
/// For example, to override the default text and icon colors for a
|
||||
/// [TextButton], as well as its overlay color, with all of the
|
||||
/// standard opacity adjustments for the pressed, focused, and
|
||||
/// hovered states, one could write:
|
||||
///
|
||||
/// ```dart
|
||||
/// TextButton(
|
||||
/// style: TextButton.styleFrom(foregroundColor: Colors.green),
|
||||
/// child: const Text('Give Kate a mix tape'),
|
||||
/// onPressed: () {
|
||||
/// // ...
|
||||
/// },
|
||||
/// ),
|
||||
/// ```
|
||||
static ButtonStyle styleFrom({
|
||||
Color? foregroundColor,
|
||||
Color? backgroundColor,
|
||||
Color? disabledForegroundColor,
|
||||
Color? disabledBackgroundColor,
|
||||
Color? shadowColor,
|
||||
Color? surfaceTintColor,
|
||||
Color? iconColor,
|
||||
double? iconSize,
|
||||
IconAlignment? iconAlignment,
|
||||
Color? disabledIconColor,
|
||||
Color? overlayColor,
|
||||
double? elevation,
|
||||
TextStyle? textStyle,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Size? minimumSize,
|
||||
Size? fixedSize,
|
||||
Size? maximumSize,
|
||||
BorderSide? side,
|
||||
OutlinedBorder? shape,
|
||||
MouseCursor? enabledMouseCursor,
|
||||
MouseCursor? disabledMouseCursor,
|
||||
VisualDensity? visualDensity,
|
||||
MaterialTapTargetSize? tapTargetSize,
|
||||
Duration? animationDuration,
|
||||
bool? enableFeedback,
|
||||
AlignmentGeometry? alignment,
|
||||
InteractiveInkFeatureFactory? splashFactory,
|
||||
ButtonLayerBuilder? backgroundBuilder,
|
||||
ButtonLayerBuilder? foregroundBuilder,
|
||||
}) {
|
||||
final WidgetStateProperty<Color?>? backgroundColorProp = switch ((
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? iconColorProp = switch ((
|
||||
iconColor,
|
||||
disabledIconColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(iconColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? overlayColorProp = switch ((
|
||||
foregroundColor,
|
||||
overlayColor,
|
||||
)) {
|
||||
(null, null) => null,
|
||||
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
|
||||
(_, final Color color) || (final Color color, _) =>
|
||||
WidgetStateProperty<Color?>.fromMap(<WidgetState, Color?>{
|
||||
WidgetState.pressed: color.withValues(alpha: 0.1),
|
||||
WidgetState.hovered: color.withValues(alpha: 0.08),
|
||||
WidgetState.focused: color.withValues(alpha: 0.1),
|
||||
}),
|
||||
};
|
||||
|
||||
return ButtonStyle(
|
||||
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
|
||||
foregroundColor: ButtonStyleButton.defaultColor(
|
||||
foregroundColor,
|
||||
disabledForegroundColor,
|
||||
),
|
||||
backgroundColor: backgroundColorProp,
|
||||
overlayColor: overlayColorProp,
|
||||
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
|
||||
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
|
||||
iconColor: iconColorProp,
|
||||
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
|
||||
iconAlignment: iconAlignment,
|
||||
elevation: ButtonStyleButton.allOrNull<double>(elevation),
|
||||
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
|
||||
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
|
||||
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
|
||||
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
|
||||
side: ButtonStyleButton.allOrNull<BorderSide>(side),
|
||||
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
|
||||
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
|
||||
<WidgetStatesConstraint, MouseCursor?>{
|
||||
WidgetState.disabled: disabledMouseCursor,
|
||||
WidgetState.any: enabledMouseCursor,
|
||||
},
|
||||
),
|
||||
visualDensity: visualDensity,
|
||||
tapTargetSize: tapTargetSize,
|
||||
animationDuration: animationDuration,
|
||||
enableFeedback: enableFeedback,
|
||||
alignment: alignment,
|
||||
splashFactory: splashFactory,
|
||||
backgroundBuilder: backgroundBuilder,
|
||||
foregroundBuilder: foregroundBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
/// Defines the button's default appearance.
|
||||
///
|
||||
/// {@template flutter.material.text_button.default_style_of}
|
||||
/// The button [child]'s [Text] and [Icon] widgets are rendered with
|
||||
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
|
||||
/// the style's overlay color when the button is focused, hovered
|
||||
/// or pressed. The button's background color becomes its [Material]
|
||||
/// color and is transparent by default.
|
||||
///
|
||||
/// All of the [ButtonStyle]'s defaults appear below.
|
||||
///
|
||||
/// In this list "Theme.foo" is shorthand for
|
||||
/// `Theme.of(context).foo`. Color scheme values like
|
||||
/// "onSurface(0.38)" are shorthand for
|
||||
/// `onSurface.withValues(alpha: 0.38)`. [WidgetStateProperty] valued
|
||||
/// properties that are not followed by a sublist have the same
|
||||
/// value for all states, otherwise the values are as specified for
|
||||
/// each state and "others" means all other states.
|
||||
///
|
||||
/// The "default font size" below refers to the font size specified in the
|
||||
/// [defaultStyleOf] method (or 14.0 if unspecified), scaled by the
|
||||
/// `MediaQuery.textScalerOf(context).scale` method. And the names of the
|
||||
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been abbreviated
|
||||
/// for readability.
|
||||
///
|
||||
/// The color of the [ButtonStyle.textStyle] is not used, the
|
||||
/// [ButtonStyle.foregroundColor] color is used instead.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// ## Material 2 defaults
|
||||
///
|
||||
/// * `textStyle` - Theme.textTheme.button
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.12)
|
||||
/// * `shadowColor` - Theme.shadowColor
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - (horizontal(12), vertical(8))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 36)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - InkRipple.splashFactory
|
||||
///
|
||||
/// The default padding values for the [TextButton.icon] factory are slightly different:
|
||||
///
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - all(8)
|
||||
/// * `14 < default font size <= 28 `- lerp(all(8), horizontal(4))
|
||||
/// * `28 < default font size` - horizontal(4)
|
||||
///
|
||||
/// The default value for `side`, which defines the appearance of the button's
|
||||
/// outline, is null. That means that the outline is defined by the button
|
||||
/// shape's [OutlinedBorder.side]. Typically the default value of an
|
||||
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
|
||||
///
|
||||
/// ## Material 3 defaults
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is set to true the following defaults will
|
||||
/// be used:
|
||||
///
|
||||
/// {@template flutter.material.text_button.material3_defaults}
|
||||
/// * `textStyle` - Theme.textTheme.labelLarge
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.1)
|
||||
/// * others - null
|
||||
/// * `shadowColor` - Colors.transparent,
|
||||
/// * `surfaceTintColor` - null
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - lerp(horizontal(12), horizontal(4))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 40)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - StadiumBorder()
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - Theme.splashFactory
|
||||
///
|
||||
/// For the [TextButton.icon] factory, the end (generally the right) value of
|
||||
/// `padding` is increased from 12 to 16.
|
||||
/// {@endtemplate}
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
|
||||
return Theme.of(context).useMaterial3
|
||||
? _TextButtonDefaultsM3(context)
|
||||
: styleFrom(
|
||||
foregroundColor: colorScheme.primary,
|
||||
disabledForegroundColor: colorScheme.onSurface.withValues(
|
||||
alpha: 0.38,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
shadowColor: theme.shadowColor,
|
||||
elevation: 0,
|
||||
textStyle: theme.textTheme.labelLarge,
|
||||
padding: _scaledPadding(context),
|
||||
minimumSize: const Size(64, 36),
|
||||
maximumSize: Size.infinite,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
enabledMouseCursor: SystemMouseCursors.click,
|
||||
disabledMouseCursor: SystemMouseCursors.basic,
|
||||
visualDensity: theme.visualDensity,
|
||||
tapTargetSize: theme.materialTapTargetSize,
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the [TextButtonThemeData.style] of the closest
|
||||
/// [TextButtonTheme] ancestor.
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
return TextButtonTheme.of(context).style;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final double defaultFontSize = theme.textTheme.labelLarge?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
return ButtonStyleButton.scaledPadding(
|
||||
theme.useMaterial3
|
||||
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
}
|
||||
|
||||
class _TextButtonWithIcon extends TextButton {
|
||||
_TextButtonWithIcon({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
bool? autofocus,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
required Widget icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) : super(
|
||||
autofocus: autofocus ?? false,
|
||||
child: _TextButtonWithIconChild(
|
||||
icon: icon,
|
||||
label: label,
|
||||
buttonStyle: style,
|
||||
iconAlignment: iconAlignment,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
||||
final ButtonStyle buttonStyle = super.defaultStyleOf(context);
|
||||
final double defaultFontSize =
|
||||
buttonStyle.textStyle?.resolve(const <WidgetState>{})?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
|
||||
useMaterial3
|
||||
? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
return buttonStyle.copyWith(
|
||||
padding: WidgetStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextButtonWithIconChild extends StatelessWidget {
|
||||
const _TextButtonWithIconChild({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.buttonStyle,
|
||||
required this.iconAlignment,
|
||||
});
|
||||
|
||||
final Widget label;
|
||||
final Widget icon;
|
||||
final ButtonStyle? buttonStyle;
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double defaultFontSize =
|
||||
buttonStyle?.textStyle?.resolve(const <WidgetState>{})?.fontSize ??
|
||||
14.0;
|
||||
final double scale =
|
||||
clampDouble(
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0,
|
||||
1.0,
|
||||
2.0,
|
||||
) -
|
||||
1.0;
|
||||
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
|
||||
final IconAlignment effectiveIconAlignment =
|
||||
iconAlignment ??
|
||||
textButtonTheme.style?.iconAlignment ??
|
||||
buttonStyle?.iconAlignment ??
|
||||
IconAlignment.start;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: lerpDouble(8, 4, scale)!,
|
||||
children: effectiveIconAlignment == IconAlignment.start
|
||||
? <Widget>[icon, Flexible(child: label)]
|
||||
: <Widget>[Flexible(child: label), icon],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - TextButton
|
||||
|
||||
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
||||
// "END GENERATED" comments are generated from data in the Material
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
class _TextButtonDefaultsM3 extends ButtonStyle {
|
||||
_TextButtonDefaultsM3(this.context)
|
||||
: super(
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
|
||||
@override
|
||||
WidgetStateProperty<TextStyle?> get textStyle =>
|
||||
WidgetStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get backgroundColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get foregroundColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get overlayColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary.withValues(alpha: 0.08);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get shadowColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get surfaceTintColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get elevation =>
|
||||
const WidgetStatePropertyAll<double>(0.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<EdgeInsetsGeometry>? get padding =>
|
||||
WidgetStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context));
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get minimumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size(64.0, 40.0));
|
||||
|
||||
// No default fixedSize
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get iconSize =>
|
||||
const WidgetStatePropertyAll<double>(18.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get iconColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get maximumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size.infinite);
|
||||
|
||||
// No default side
|
||||
|
||||
@override
|
||||
WidgetStateProperty<OutlinedBorder>? get shape =>
|
||||
const WidgetStatePropertyAll<OutlinedBorder>(StadiumBorder());
|
||||
|
||||
@override
|
||||
WidgetStateProperty<MouseCursor?>? get mouseCursor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return SystemMouseCursors.basic;
|
||||
}
|
||||
return SystemMouseCursors.click;
|
||||
});
|
||||
|
||||
@override
|
||||
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
|
||||
|
||||
@override
|
||||
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
|
||||
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - TextButton
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'card.dart';
|
||||
/// @docImport 'checkbox.dart';
|
||||
@@ -20,7 +20,7 @@ library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide ListTile;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
// Examples can assume:
|
||||
@@ -335,8 +335,10 @@ class ListTile extends StatelessWidget {
|
||||
this.contentPadding,
|
||||
this.enabled = true,
|
||||
this.onTap,
|
||||
this.onTapUp,
|
||||
this.onLongPress,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onFocusChange,
|
||||
this.mouseCursor,
|
||||
this.selected = false,
|
||||
@@ -562,6 +564,8 @@ class ListTile extends StatelessWidget {
|
||||
/// Inoperative if [enabled] is false.
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
final GestureTapUpCallback? onTapUp;
|
||||
|
||||
/// Called when the user long-presses on this list tile.
|
||||
///
|
||||
/// Inoperative if [enabled] is false.
|
||||
@@ -569,6 +573,8 @@ class ListTile extends StatelessWidget {
|
||||
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
final GestureTapUpCallback? onSecondaryTapUp;
|
||||
|
||||
/// {@macro flutter.material.inkwell.onFocusChange}
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
@@ -736,19 +742,6 @@ class ListTile extends StatelessWidget {
|
||||
return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false;
|
||||
}
|
||||
|
||||
Color _tileBackgroundColor(
|
||||
ThemeData theme,
|
||||
ListTileThemeData tileTheme,
|
||||
ListTileThemeData defaults,
|
||||
) {
|
||||
final Color? color = selected
|
||||
? selectedTileColor ??
|
||||
tileTheme.selectedTileColor ??
|
||||
theme.listTileTheme.selectedTileColor
|
||||
: tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor;
|
||||
return color ?? defaults.tileColor!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
@@ -763,6 +756,25 @@ class ListTile extends StatelessWidget {
|
||||
final ListTileThemeData defaults = theme.useMaterial3
|
||||
? _LisTileDefaultsM3(context)
|
||||
: _LisTileDefaultsM2(context, listTileStyle);
|
||||
|
||||
final Color backgroundColor =
|
||||
tileColor ??
|
||||
tileTheme.tileColor ??
|
||||
theme.listTileTheme.tileColor ??
|
||||
defaults.tileColor!;
|
||||
final Color selectedBackgroundColor =
|
||||
selectedTileColor ??
|
||||
tileTheme.selectedTileColor ??
|
||||
theme.listTileTheme.selectedTileColor ??
|
||||
defaults.tileColor!;
|
||||
final effectiveTileColor = selected
|
||||
? selectedBackgroundColor
|
||||
: backgroundColor;
|
||||
final bool hasOpaqueBackground =
|
||||
backgroundColor.alpha > 0 || selectedBackgroundColor.alpha > 0;
|
||||
if (onTap != null || onLongPress != null || hasOpaqueBackground) {
|
||||
assert(_debugCheckBackgroundIsHidden(context));
|
||||
}
|
||||
final Set<WidgetState> states = <WidgetState>{
|
||||
if (!enabled) WidgetState.disabled,
|
||||
if (selected) WidgetState.selected,
|
||||
@@ -910,7 +922,12 @@ class ListTile extends StatelessWidget {
|
||||
|
||||
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
|
||||
final Set<WidgetState> mouseStates = <WidgetState>{
|
||||
if (!enabled || (onTap == null && onLongPress == null))
|
||||
if (!enabled ||
|
||||
(onTap == null &&
|
||||
onTapUp == null &&
|
||||
onLongPress == null &&
|
||||
onSecondaryTap == null &&
|
||||
onSecondaryTapUp == null))
|
||||
WidgetState.disabled,
|
||||
};
|
||||
final MouseCursor effectiveMouseCursor =
|
||||
@@ -981,8 +998,10 @@ class ListTile extends StatelessWidget {
|
||||
return InkWell(
|
||||
customBorder: shape ?? tileTheme.shape,
|
||||
onTap: enabled ? onTap : null,
|
||||
onTapUp: enabled ? onTapUp : null,
|
||||
onLongPress: enabled ? onLongPress : null,
|
||||
onSecondaryTap: enabled ? onSecondaryTap : null,
|
||||
onSecondaryTapUp: enabled ? onSecondaryTapUp : null,
|
||||
onFocusChange: onFocusChange,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
canRequestFocus: enabled,
|
||||
@@ -1002,7 +1021,7 @@ class ListTile extends StatelessWidget {
|
||||
child: Ink(
|
||||
decoration: ShapeDecoration(
|
||||
shape: shape ?? tileTheme.shape ?? const Border(),
|
||||
color: _tileBackgroundColor(theme, tileTheme, defaults),
|
||||
color: effectiveTileColor,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
@@ -1176,6 +1195,68 @@ class ListTile extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _debugCheckBackgroundIsHidden(BuildContext context) {
|
||||
assert(() {
|
||||
final Widget? intermediateWidget = _findIntermediateWidget(context);
|
||||
if (intermediateWidget != null) {
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'ListTile background color or ink splashes may be invisible.',
|
||||
),
|
||||
ErrorDescription(
|
||||
'The ListTile is wrapped in a ${intermediateWidget.runtimeType} that has a background color. '
|
||||
'Because ListTile paints its background and ink splashes on the nearest Material ancestor, '
|
||||
'this ${intermediateWidget.runtimeType} will hide those effects.',
|
||||
),
|
||||
ErrorHint(
|
||||
'To fix this, wrap the ListTile in its own Material widget, '
|
||||
'or remove the background color from the intermediate ${intermediateWidget.runtimeType}.',
|
||||
),
|
||||
]),
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ListTile>(
|
||||
'ListTile',
|
||||
this,
|
||||
expandableValue: true,
|
||||
),
|
||||
DiagnosticsProperty<Widget>(
|
||||
'${intermediateWidget.runtimeType}',
|
||||
intermediateWidget,
|
||||
expandableValue: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget? _findIntermediateWidget(BuildContext context) {
|
||||
Widget? intermediateWidget;
|
||||
(context as Element).visitAncestorElements((Element ancestor) {
|
||||
if (ancestor.widget is Material) {
|
||||
return false;
|
||||
}
|
||||
final Widget widget = ancestor.widget;
|
||||
final Color? color = switch (widget) {
|
||||
ColoredBox(:final Color color) => color,
|
||||
DecoratedBox(decoration: BoxDecoration(:final Color? color)) => color,
|
||||
DecoratedBox(decoration: ShapeDecoration(:final Color? color)) => color,
|
||||
_ => null,
|
||||
};
|
||||
if (color != null && color.a > 0) {
|
||||
intermediateWidget = widget;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return intermediateWidget;
|
||||
}
|
||||
}
|
||||
|
||||
class _IndividualOverrides extends WidgetStateProperty<Color?> {
|
||||
@@ -1493,11 +1574,16 @@ class _RenderListTile extends RenderBox
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
return math.max(
|
||||
_targetTileHeight,
|
||||
title.getMinIntrinsicHeight(width) +
|
||||
(subtitle?.getMinIntrinsicHeight(width) ?? 0.0),
|
||||
);
|
||||
final double titleMinHeight = title.getMinIntrinsicHeight(width);
|
||||
final double? subtitleMinHeight = subtitle?.getMinIntrinsicHeight(width);
|
||||
|
||||
const topAndBottomPaddingMultiplier = 2;
|
||||
final double contentHeight =
|
||||
titleMinHeight +
|
||||
(subtitleMinHeight ?? 0.0) +
|
||||
topAndBottomPaddingMultiplier * _minVerticalPadding;
|
||||
|
||||
return math.max(_targetTileHeight, contentHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||