Compare commits
508 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3741fe54ff | ||
|
|
ec11af3827 | ||
|
|
890dc58dc3 | ||
|
|
b12bdf2eb8 | ||
|
|
59c7f8a030 | ||
|
|
50cf74ccf7 | ||
|
|
15b5c0a874 | ||
|
|
244ef22f54 | ||
|
|
b4daf5fbd8 | ||
|
|
0519ec0e4b | ||
|
|
ff4f97de1a | ||
|
|
773bdafec3 | ||
|
|
3787f99d35 | ||
|
|
2cb8331528 | ||
|
|
5b6443cfa4 | ||
|
|
6fd8212d8b | ||
|
|
0d273f6909 | ||
|
|
255e39b709 | ||
|
|
ea52dd4484 | ||
|
|
b4a46133be | ||
|
|
7c1644efc4 | ||
|
|
775e1aa97d | ||
|
|
2a55d4390a | ||
|
|
d57a34a4e1 | ||
|
|
2785248615 | ||
|
|
c42468e2c8 | ||
|
|
196ddf3f5f | ||
|
|
27302435be | ||
|
|
2b3ec77e92 | ||
|
|
b7a277a57c | ||
|
|
9c8e5b53e7 | ||
|
|
001b746f65 | ||
|
|
a78214de3c | ||
|
|
d88ffb1127 | ||
|
|
f05b901009 | ||
|
|
430837eef6 | ||
|
|
fa583ebd0f | ||
|
|
d2dcba5a59 | ||
|
|
fb5116d525 | ||
|
|
a48f6b1ca5 | ||
|
|
fc0af3f284 | ||
|
|
2288e11398 | ||
|
|
d95283c4ac | ||
|
|
4b56bd5a87 | ||
|
|
62bb605ee8 | ||
|
|
0f8da1999a | ||
|
|
21a2373a5c | ||
|
|
2ca5310825 | ||
|
|
9ccaa3072b | ||
|
|
ded78e534f | ||
|
|
9b0a43efc9 | ||
|
|
10808c2a84 | ||
|
|
38a7afd63a | ||
|
|
54b26d20fa | ||
|
|
ad2bc78ebd | ||
|
|
c4aca389a8 | ||
|
|
cb8333d4c0 | ||
|
|
2f5eed6998 | ||
|
|
935c53e452 | ||
|
|
dd0ccb327b | ||
|
|
919134759b | ||
|
|
c1d42b498a | ||
|
|
a7e67796f1 | ||
|
|
6692c9e851 | ||
|
|
ace949aaa0 | ||
|
|
fbd9687432 | ||
|
|
460a8262c1 | ||
|
|
c8de503fae | ||
|
|
a60cd51ff4 | ||
|
|
aad980ce23 | ||
|
|
e7cda7b9fa | ||
|
|
1d368b7a8b | ||
|
|
725d7055bf | ||
|
|
1fb798db4e | ||
|
|
8e1d5e0dd5 | ||
|
|
2d9a1310b9 | ||
|
|
588ec7babd | ||
|
|
2be13e7283 | ||
|
|
d5d95671ff | ||
|
|
a0eccda6ff | ||
|
|
ec82c86210 | ||
|
|
de03bef226 | ||
|
|
0f8166620e | ||
|
|
76c2de4394 | ||
|
|
0d38ded981 | ||
|
|
646888c06f | ||
|
|
332f6f1bb4 | ||
|
|
aaab5371b2 | ||
|
|
ad931d7ea2 | ||
|
|
377e430d74 | ||
|
|
a797467606 | ||
|
|
5ee83d902d | ||
|
|
27ae296b28 | ||
|
|
e589f27195 | ||
|
|
c89d6a5a59 | ||
|
|
861365930d | ||
|
|
0d4d92a202 | ||
|
|
4c6ad0e385 | ||
|
|
ad45e995e2 | ||
|
|
50a035a479 | ||
|
|
c0dbd6cbb2 | ||
|
|
686af4a330 | ||
|
|
46aad06e34 | ||
|
|
3921b2304d | ||
|
|
bca5b0419c | ||
|
|
9754b061dd | ||
|
|
407b31c5c1 | ||
|
|
37b1228552 | ||
|
|
0acd9ca767 | ||
|
|
8f3c9f029c | ||
|
|
9310732343 | ||
|
|
e767e506f3 | ||
|
|
ef3a612338 | ||
|
|
d66a42a0aa | ||
|
|
0f06de0047 | ||
|
|
963181fef2 | ||
|
|
ffd4f9ee73 | ||
|
|
976622df89 | ||
|
|
13c220338c | ||
|
|
1291dc77c8 | ||
|
|
08e5477e74 | ||
|
|
c4c6a2243e | ||
|
|
58791e3e91 | ||
|
|
d5bb4bc149 | ||
|
|
3d1199363b | ||
|
|
f225fa33e1 | ||
|
|
e85c8b3dde | ||
|
|
737be8dcac | ||
|
|
77dd939172 | ||
|
|
0a5965a423 | ||
|
|
a53be6814c | ||
|
|
415b8e9da3 | ||
|
|
f034c24d13 | ||
|
|
1ac93d6269 | ||
|
|
906c8f7999 | ||
|
|
c904a5ded8 |
50
.github/workflows/build.yml
vendored
@@ -49,14 +49,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
android:
|
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
|
name: Release Android
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 代码迁出
|
- name: 代码迁出
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -78,9 +78,9 @@ jobs:
|
|||||||
flutter-version-file: pubspec.yaml
|
flutter-version-file: pubspec.yaml
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: apply bottom sheet patch
|
- name: Apply Patch
|
||||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
shell: pwsh
|
||||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
run: lib/scripts/patch.ps1 android
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Write key
|
- name: Write key
|
||||||
@@ -95,13 +95,20 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set and Extract version
|
- name: Set and Extract version
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: lib/scripts/build.ps1 android
|
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
|
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: |
|
run: |
|
||||||
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
||||||
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
|
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
|
||||||
@@ -115,53 +122,52 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ github.event.inputs.tag }}
|
tag_name: ${{ github.event.inputs.tag }}
|
||||||
name: ${{ github.event.inputs.tag }}
|
name: ${{ github.event.inputs.tag }}
|
||||||
files: |
|
files: PiliPlus_android_*.apk
|
||||||
PiliPlus_android_*.apk
|
|
||||||
|
|
||||||
- name: 上传
|
- name: 上传
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Android_arm64-v8a
|
name: Android_arm64-v8a
|
||||||
path: |
|
path: PiliPlus_android_*_arm64-v8a.apk
|
||||||
PiliPlus_android_*_arm64-v8a.apk
|
|
||||||
|
|
||||||
- name: 上传
|
- name: 上传
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Android_armeabi-v7a
|
name: Android_armeabi-v7a
|
||||||
path: |
|
path: PiliPlus_android_*_armeabi-v7a.apk
|
||||||
PiliPlus_android_*_armeabi-v7a.apk
|
|
||||||
|
|
||||||
- name: 上传
|
- name: 上传
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Android_x86_64
|
name: Android_x86_64
|
||||||
path: |
|
path: PiliPlus_android_*_x86_64.apk
|
||||||
PiliPlus_android_*_x86_64.apk
|
|
||||||
|
|
||||||
ios:
|
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
|
uses: ./.github/workflows/ios.yml
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||||
|
|
||||||
mac:
|
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
|
uses: ./.github/workflows/mac.yml
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||||
|
|
||||||
win_x64:
|
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
|
uses: ./.github/workflows/win_x64.yml
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||||
|
|
||||||
linux_x64:
|
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
|
uses: ./.github/workflows/linux_x64.yml
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
with:
|
with:
|
||||||
|
|||||||
16
.github/workflows/ios.yml
vendored
@@ -13,10 +13,10 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-macos-app:
|
build-macos-app:
|
||||||
name: Release IOS
|
name: Release IOS
|
||||||
runs-on: macos-latest
|
runs-on: macos-26
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -30,11 +30,18 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: lib/scripts/build.ps1
|
run: lib/scripts/build.ps1
|
||||||
|
|
||||||
|
- name: Apply Patch
|
||||||
|
shell: pwsh
|
||||||
|
run: lib/scripts/patch.ps1 iOS
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Build iOS
|
- name: Build iOS
|
||||||
run: |
|
run: |
|
||||||
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
||||||
ln -sf ./build/ios/iphoneos Payload
|
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
|
- name: Release
|
||||||
if: ${{ github.event.inputs.tag != '' }}
|
if: ${{ github.event.inputs.tag != '' }}
|
||||||
@@ -46,7 +53,8 @@ jobs:
|
|||||||
PiliPlus_ios_*.ipa
|
PiliPlus_ios_*.ipa
|
||||||
|
|
||||||
- name: Upload ios release
|
- name: Upload ios release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: iOS-release
|
name: iOS-release
|
||||||
path: PiliPlus_ios_*.ipa
|
path: PiliPlus_ios_*.ipa
|
||||||
|
|||||||
45
.github/workflows/linux_x64.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -51,6 +51,11 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: lib/scripts/build.ps1
|
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
|
#TODO: deb and rpm packages need to be build
|
||||||
- name: Build Linux
|
- name: Build Linux
|
||||||
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
|
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
|
||||||
@@ -70,7 +75,7 @@ jobs:
|
|||||||
printf "复制文件...\n"
|
printf "复制文件...\n"
|
||||||
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
||||||
cp -r ../assets/linux/DEBIAN .
|
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
|
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||||
|
|
||||||
printf "修改控制文件...\n"
|
printf "修改控制文件...\n"
|
||||||
@@ -112,7 +117,7 @@ jobs:
|
|||||||
SRC_DIR="$PWD/piliplus-${{ env.version }}"
|
SRC_DIR="$PWD/piliplus-${{ env.version }}"
|
||||||
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
|
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
|
||||||
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
|
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"
|
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 }}"
|
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
|
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
|
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
|
||||||
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||||
@@ -161,7 +166,7 @@ jobs:
|
|||||||
%files
|
%files
|
||||||
/opt/PiliPlus
|
/opt/PiliPlus
|
||||||
/usr/bin/piliplus
|
/usr/bin/piliplus
|
||||||
/usr/share/applications/piliplus.desktop
|
/usr/share/applications/com.example.piliplus.desktop
|
||||||
/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
@@ -179,7 +184,7 @@ jobs:
|
|||||||
|
|
||||||
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
|
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Package AppImage
|
- name: Package AppImage
|
||||||
run: |
|
run: |
|
||||||
printf "下载 appimagetool...\n"
|
printf "下载 appimagetool...\n"
|
||||||
@@ -195,13 +200,13 @@ jobs:
|
|||||||
|
|
||||||
printf "复制应用文件...\n"
|
printf "复制应用文件...\n"
|
||||||
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
|
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
|
||||||
|
|
||||||
printf "复制桌面文件和图标...\n"
|
printf "复制桌面文件和图标...\n"
|
||||||
cp assets/linux/piliplus.desktop "$APPDIR/piliplus.desktop"
|
cp assets/linux/com.example.piliplus.desktop "$APPDIR/com.example.piliplus.desktop"
|
||||||
cp assets/linux/piliplus.desktop "$APPDIR/usr/share/applications/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/piliplus.png"
|
||||||
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
|
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
|
||||||
|
|
||||||
printf "创建 AppRun 启动脚本...\n"
|
printf "创建 AppRun 启动脚本...\n"
|
||||||
cat > "$APPDIR/AppRun" <<'APPRUN_EOF'
|
cat > "$APPDIR/AppRun" <<'APPRUN_EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@@ -214,12 +219,12 @@ jobs:
|
|||||||
chmod +x "$APPDIR/AppRun"
|
chmod +x "$APPDIR/AppRun"
|
||||||
|
|
||||||
printf "修改桌面文件中的 Exec 路径...\n"
|
printf "修改桌面文件中的 Exec 路径...\n"
|
||||||
sed -i 's|Exec=piliplus|Exec=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/piliplus.desktop"
|
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
|
||||||
|
|
||||||
printf "打包 AppImage...\n"
|
printf "打包 AppImage...\n"
|
||||||
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
|
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
|
||||||
|
|
||||||
printf "完成: PiliPlus_linux_%s_amd64.AppImage\n" "${{ env.version }}"
|
printf "完成: PiliPlus_linux_%s_amd64.AppImage\n" "${{ env.version }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
@@ -236,25 +241,29 @@ jobs:
|
|||||||
PiliPlus_linux_*.AppImage
|
PiliPlus_linux_*.AppImage
|
||||||
|
|
||||||
- name: Upload linux targz package
|
- name: Upload linux targz package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Linux_targz_amd64_packege
|
name: Linux_targz_amd64_packege
|
||||||
path: PiliPlus_linux_*.tar.gz
|
path: PiliPlus_linux_*.tar.gz
|
||||||
|
|
||||||
- name: Upload linux deb package
|
- name: Upload linux deb package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Linux_deb_amd64_package
|
name: Linux_deb_amd64_package
|
||||||
path: PiliPlus_linux_*.deb
|
path: PiliPlus_linux_*.deb
|
||||||
|
|
||||||
- name: Upload linux rpm package
|
- name: Upload linux rpm package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Linux_rpm_amd64_package
|
name: Linux_rpm_amd64_package
|
||||||
path: PiliPlus_linux_*.rpm
|
path: PiliPlus_linux_*.rpm
|
||||||
|
|
||||||
- name: Upload linux AppImage package
|
- name: Upload linux AppImage package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Linux_AppImage_amd64_package
|
name: Linux_AppImage_amd64_package
|
||||||
path: PiliPlus_linux_*.AppImage
|
path: PiliPlus_linux_*.AppImage
|
||||||
|
|||||||
14
.github/workflows/mac.yml
vendored
@@ -13,10 +13,10 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-mac-app:
|
build-mac-app:
|
||||||
name: Release Mac
|
name: Release Mac
|
||||||
runs-on: macos-latest
|
runs-on: macos-26
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -30,13 +30,18 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: lib/scripts/build.ps1
|
run: lib/scripts/build.ps1
|
||||||
|
|
||||||
|
- name: Apply Patch
|
||||||
|
shell: pwsh
|
||||||
|
run: lib/scripts/patch.ps1 macOS
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Build Mac
|
- name: Build Mac
|
||||||
run: flutter build macos --release --dart-define-from-file=pili_release.json
|
run: flutter build macos --release --dart-define-from-file=pili_release.json
|
||||||
|
|
||||||
- name: Prepare Upload
|
- name: Prepare Upload
|
||||||
run: |
|
run: |
|
||||||
npm install --global create-dmg
|
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
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Rename DMG
|
- name: Rename DMG
|
||||||
@@ -52,7 +57,8 @@ jobs:
|
|||||||
PiliPlus_macos_*.dmg
|
PiliPlus_macos_*.dmg
|
||||||
|
|
||||||
- name: Upload macos release
|
- name: Upload macos release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: macOS-release
|
name: macOS-release
|
||||||
path: PiliPlus_macos_*.dmg
|
path: PiliPlus_macos_*.dmg
|
||||||
|
|||||||
19
.github/workflows/win_x64.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -26,6 +26,11 @@ jobs:
|
|||||||
channel: stable
|
channel: stable
|
||||||
flutter-version-file: pubspec.yaml
|
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
|
- name: Add fastforge and Inno Setup
|
||||||
run: |
|
run: |
|
||||||
dart pub global activate fastforge
|
dart pub global activate fastforge
|
||||||
@@ -54,7 +59,7 @@ jobs:
|
|||||||
- name: Compress
|
- name: Compress
|
||||||
if: ${{ github.event.inputs.tag != '' }}
|
if: ${{ github.event.inputs.tag != '' }}
|
||||||
run: |
|
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
|
shell: pwsh
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
@@ -68,13 +73,15 @@ jobs:
|
|||||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||||
|
|
||||||
- name: Upload windows file release
|
- name: Upload windows file release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Windows-file-x64-release
|
archive: true
|
||||||
|
name: PiliPlus_windows_${{env.version}}_x64_portable
|
||||||
path: Release
|
path: Release
|
||||||
|
|
||||||
- name: Upload windows setup release
|
- name: Upload windows setup release
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
|
archive: false
|
||||||
name: Windows-setup-x64-release
|
name: Windows-setup-x64-release
|
||||||
path: PiliPlus-Win-Setup
|
path: PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -146,4 +146,4 @@ pili_release.json
|
|||||||
|
|
||||||
dist
|
dist
|
||||||
|
|
||||||
test.dart
|
test*.dart
|
||||||
@@ -43,6 +43,9 @@
|
|||||||
|
|
||||||
## feat
|
## feat
|
||||||
|
|
||||||
|
- [x] 编辑动态
|
||||||
|
- [x] DLNA 投屏
|
||||||
|
- [x] 离线缓存/播放
|
||||||
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||||
- [x] 播放音频
|
- [x] 播放音频
|
||||||
- [x] 跳过番剧片头/片尾
|
- [x] 跳过番剧片头/片尾
|
||||||
@@ -151,7 +154,7 @@
|
|||||||
- [x] 粉丝、关注用户、拉黑用户查看
|
- [x] 粉丝、关注用户、拉黑用户查看
|
||||||
- [x] 用户主页查看
|
- [x] 用户主页查看
|
||||||
- [x] 关注/取关用户
|
- [x] 关注/取关用户
|
||||||
- [ ] 离线缓存
|
- [x] 离线缓存
|
||||||
- [x] 稍后再看
|
- [x] 稍后再看
|
||||||
- [x] 观看记录
|
- [x] 观看记录
|
||||||
- [x] 我的收藏
|
- [x] 我的收藏
|
||||||
@@ -216,8 +219,8 @@
|
|||||||
|
|
||||||
## 声明
|
## 声明
|
||||||
|
|
||||||
此项目(PiliPlus)是个人为了兴趣而开发, 仅用于学习和测试,请于下载后24小时内删除。
|
此项目(PiliPlus)是个人为了兴趣而开发,仅用于学习和测试,请于下载后24小时内删除。
|
||||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
所用API皆从官方网站收集,不提供任何破解内容。
|
||||||
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
|
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
|
||||||
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
|
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
|
||||||
本仓库做了更激进的修改,感谢原作者的开源精神。
|
本仓库做了更激进的修改,感谢原作者的开源精神。
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ include: package:flutter_lints/flutter.yaml
|
|||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
- lib/grpc/bilibili/**
|
- lib/grpc/bilibili/**
|
||||||
- lib/grpc/google/**
|
# - lib/grpc/google/**
|
||||||
|
# - lib/common/widgets/flutter/**
|
||||||
|
|
||||||
formatter:
|
formatter:
|
||||||
trailing_commas: preserve
|
trailing_commas: preserve
|
||||||
@@ -63,5 +64,19 @@ linter:
|
|||||||
- use_null_aware_elements
|
- use_null_aware_elements
|
||||||
- unnecessary_lambdas
|
- unnecessary_lambdas
|
||||||
- use_is_even_rather_than_modulo
|
- use_is_even_rather_than_modulo
|
||||||
|
- unnecessary_async
|
||||||
|
- unnecessary_await_in_return
|
||||||
|
- unnecessary_getters_setters
|
||||||
|
- prefer_const_literals_to_create_immutables
|
||||||
|
- no_literal_bool_comparisons
|
||||||
|
- 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
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
@@ -54,10 +56,18 @@ android {
|
|||||||
signingConfig = config ?: signingConfigs["debug"]
|
signingConfig = config ?: signingConfigs["debug"]
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
proguardFiles(
|
if (project.hasProperty("dev")) {
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
applicationIdSuffix = ".dev"
|
||||||
"proguard-rules.pro"
|
resValue(
|
||||||
)
|
type = "string",
|
||||||
|
name = "app_name",
|
||||||
|
value = "PiliPlus dev",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// proguardFiles(
|
||||||
|
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
// "proguard-rules.pro"
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|||||||
@@ -210,4 +210,5 @@
|
|||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
|
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
|
||||||
|
|
||||||
val intent = Intent().apply {
|
val intent = Intent().apply {
|
||||||
component = ComponentName("icu.freedomIntrovert.biliSendCommAntifraud", "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity")
|
component = ComponentName(
|
||||||
|
"icu.freedomIntrovert.biliSendCommAntifraud",
|
||||||
|
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
|
||||||
|
)
|
||||||
putExtra("action", action)
|
putExtra("action", action)
|
||||||
putExtra("oid", oid.toLong())
|
putExtra("oid", oid.toLong())
|
||||||
putExtra("type", type)
|
putExtra("type", type)
|
||||||
@@ -52,23 +55,27 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
putExtra("parent", parent.toLong())
|
putExtra("parent", parent.toLong())
|
||||||
putExtra("ctime", ctime.toLong())
|
putExtra("ctime", ctime.toLong())
|
||||||
putExtra("comment_text", commentText)
|
putExtra("comment_text", commentText)
|
||||||
if(pictures != null)
|
if (pictures != null)
|
||||||
putExtra("pictures", pictures)
|
putExtra("pictures", pictures)
|
||||||
putExtra("source_id", sourceId)
|
putExtra("source_id", sourceId)
|
||||||
putExtra("uid", uid.toLong())
|
putExtra("uid", uid.toLong())
|
||||||
putStringArrayListExtra("cookies", ArrayList(cookies))
|
putStringArrayListExtra("cookies", ArrayList(cookies))
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"linkVerifySettings" -> {
|
"linkVerifySettings" -> {
|
||||||
val uri = ("package:" + context.packageName).toUri()
|
val uri = ("package:" + context.packageName).toUri()
|
||||||
try {
|
try {
|
||||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
|
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
|
||||||
} else {
|
} else {
|
||||||
Intent("android.intent.action.MAIN", uri).setClassName("com.android.settings",
|
Intent("android.intent.action.MAIN", uri).setClassName(
|
||||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity")
|
"com.android.settings",
|
||||||
|
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
@@ -76,33 +83,47 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"music" -> {
|
"music" -> {
|
||||||
val title = call.argument<String>("title")
|
val title = call.argument<String>("title")
|
||||||
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
|
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
|
||||||
putExtra(SearchManager.QUERY, title)
|
putExtra(SearchManager.QUERY, title)
|
||||||
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
|
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
|
||||||
call.argument<String?>("artist")?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
call.argument<String?>("artist")
|
||||||
call.argument<String?>("album")?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
||||||
|
call.argument<String?>("album")
|
||||||
|
?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
||||||
|
|
||||||
addCategory(Intent.CATEGORY_DEFAULT)
|
addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
if (packageManager.resolveActivity(
|
||||||
|
intent,
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY
|
||||||
|
) != null
|
||||||
|
) {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
return@setMethodCallHandler
|
return@setMethodCallHandler
|
||||||
}
|
}
|
||||||
} catch (_: Throwable) {}
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
|
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
|
||||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
if (packageManager.resolveActivity(
|
||||||
|
intent,
|
||||||
|
PackageManager.MATCH_DEFAULT_ONLY
|
||||||
|
) != null
|
||||||
|
) {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
return@setMethodCallHandler
|
return@setMethodCallHandler
|
||||||
}
|
}
|
||||||
} catch (_: Throwable) {}
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
"setPipAutoEnterEnabled" -> {
|
"setPipAutoEnterEnabled" -> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
val params = PictureInPictureParams.Builder()
|
val params = PictureInPictureParams.Builder()
|
||||||
@@ -111,6 +132,7 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
setPictureInPictureParams(params)
|
setPictureInPictureParams(params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +155,7 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java))
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
android.os.Process.killProcess(android.os.Process.myPid())
|
android.os.Process.killProcess(android.os.Process.myPid())
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
@@ -143,7 +166,10 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
methodChannel.invokeMethod("onUserLeaveHint", null)
|
methodChannel.invokeMethod("onUserLeaveHint", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
override fun onPictureInPictureModeChanged(
|
||||||
|
isInPictureInPictureMode: Boolean,
|
||||||
|
newConfig: Configuration?
|
||||||
|
) {
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
MethodChannel(
|
MethodChannel(
|
||||||
flutterEngine!!.dartExecutor.binaryMessenger,
|
flutterEngine!!.dartExecutor.binaryMessenger,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://downloads.gradle.org/distributions/gradle-8.13-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.12.1" apply false
|
id("com.android.application") version "8.12.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
@@ -3,18 +3,18 @@
|
|||||||
ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus
|
ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus
|
||||||
chmod +x /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..."
|
echo "updating mime database..."
|
||||||
update-mime-database /usr/share/mime || true
|
update-mime-database /usr/share/mime || true
|
||||||
fi
|
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..."
|
echo "updating icon cache..."
|
||||||
gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true
|
gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $1 == "config" ] && [ -x /usr/bin/update-desktop-database ]; then
|
if [ $1 == "configure" ] && [ -x /usr/bin/update-desktop-database ]; then
|
||||||
echo "updating desktop database..."
|
echo "configure desktop database..."
|
||||||
update-desktop-database -q /usr/share/applications || true
|
update-desktop-database -q /usr/share/applications || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端
|
|||||||
Exec=piliplus
|
Exec=piliplus
|
||||||
Icon=piliplus
|
Icon=piliplus
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
StartupWMClass=com.example.piliplus
|
||||||
Categories=Video;AudioVideo;Player;
|
Categories=Video;AudioVideo;Player;
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
## 1.0.0
|
|
||||||
|
|
||||||
### 初始版本
|
|
||||||
+ 直播、推荐、动态功能
|
|
||||||
+ 投稿、番剧播放功能
|
|
||||||
+ 播放器手势支持
|
|
||||||
+ 画质、音质、解码格式支持
|
|
||||||
+ 点赞、投币、收藏功能
|
|
||||||
+ 关注/取关、用户主页功能
|
|
||||||
+ 评论功能
|
|
||||||
+ 历史记录、稍后再看功能
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
## 1.0.1
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 升级播放器依赖
|
|
||||||
+ android平台 AV1格式视频支持
|
|
||||||
+ 视频全屏功能
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
## 1.0.10
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 长按倍速抬起后未恢复默认倍速
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
## 1.0.11
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 适配了原生媒体通知栏 @Daydreamer-riri
|
|
||||||
+ 视频主题图标 @Daydreamer-riri
|
|
||||||
+ 关闭软件后自动画中画播放
|
|
||||||
+ UP主分组管理
|
|
||||||
+ md2样式底栏
|
|
||||||
+
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 历史记录记忆播放
|
|
||||||
+ 部分类型视频连播
|
|
||||||
+ 播放速度选择框不支持返回手势
|
|
||||||
+ 播放速度选择框不支持返回手势
|
|
||||||
+ 视频播放速度总是显示1.0X
|
|
||||||
+ 评论页面计数错误
|
|
||||||
+ 退出视频还有声音
|
|
||||||
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 视频加载速度
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
## 1.0.12
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ iOS端视频播放时没有声音
|
|
||||||
+ 超过6分钟弹幕不显示
|
|
||||||
+ 视频详情页网络异常
|
|
||||||
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
## 1.0.13
|
|
||||||
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 视频详情页稍后再看
|
|
||||||
+ 发送弹幕 感谢@orz12
|
|
||||||
+ 消息展示
|
|
||||||
+ up主页显示获赞数
|
|
||||||
+ up主页显示合集
|
|
||||||
+ 视频详情页「ai总结」增加开关
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 首页推荐问题(需要重新登录)
|
|
||||||
+ 长按倍速逻辑
|
|
||||||
+ 视频详情页网络异常
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 设置面板样式 感谢@GuMengYu @KoolShow
|
|
||||||
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
## 1.0.14
|
|
||||||
|
|
||||||
圣诞节快乐~ 🎉
|
|
||||||
|
|
||||||
大部分内容由@orz12提供,感谢👏
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 全屏弹幕消失
|
|
||||||
+ iOS全屏/退出全屏视频暂停
|
|
||||||
+ 个人主页关注状态
|
|
||||||
+ 视频合集向下滑动UI问题
|
|
||||||
+ 媒体库滑动底栏不隐藏
|
|
||||||
+ 个人主页动态加载问题 * 2
|
|
||||||
+ 未登录状态访问个人主页异常
|
|
||||||
+ 视频搜索标题特殊字符转义
|
|
||||||
+ iOS闪退
|
|
||||||
+ 消息页面夜间模式异常
|
|
||||||
+ 消息页面含有撤回消息时异常
|
|
||||||
+ 弹幕速度
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 全屏播放方案优化
|
|
||||||
+ 弹幕加载逻辑优化
|
|
||||||
+ 点赞、投币逻辑优化
|
|
||||||
+ 进度条及播放时间渲染优化
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
## 1.0.15
|
|
||||||
|
|
||||||
元旦快乐~ 🎉
|
|
||||||
|
|
||||||
### 功能
|
|
||||||
+ 转发动态评论展示
|
|
||||||
+ 推荐、最热、收藏视频增肌日期显示
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 全屏播放相关问题
|
|
||||||
+ 评论区@用户展示问题
|
|
||||||
+ 登录状态闪退问题
|
|
||||||
+ pip意外触发问题
|
|
||||||
+ 动态页tab切换样式问题
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 首页默认使用web端推荐
|
|
||||||
+ 取消iOS路由切换效果
|
|
||||||
+ 视频分享中添加Up主
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
## 1.0.16
|
|
||||||
|
|
||||||
|
|
||||||
### 功能
|
|
||||||
+ toast 背景支持透明度调节
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ web端推荐未展示【已关注】
|
|
||||||
+ up主动态页异常
|
|
||||||
+ 未打开自动播放时,视频详情页异常
|
|
||||||
+ 视频暂停状态取消自动ip
|
|
||||||
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
## 1.0.17
|
|
||||||
|
|
||||||
|
|
||||||
### 功能
|
|
||||||
+ 视频全屏时隐藏进度条
|
|
||||||
+ 动态内容增加投稿跳转
|
|
||||||
+ 未开启自动播放时点击封面播放
|
|
||||||
+ 弹幕发送标识
|
|
||||||
+ 定时关闭
|
|
||||||
+ 推荐视频卡片拉黑up功能
|
|
||||||
+ 首页tabbar编辑排序
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 连续跳转搜索页未刷新
|
|
||||||
+ 搜索结果为空时页面异常
|
|
||||||
+ 评论区链接解析
|
|
||||||
+ 视频全屏状态栏背景色
|
|
||||||
+ 私信对话气泡位置
|
|
||||||
+ 设置up关注分组样式
|
|
||||||
+ 每次推荐请求数据相同
|
|
||||||
+ iOS代理网络异常
|
|
||||||
+ 双击切换播放状态无声
|
|
||||||
+ 设置自定义倍速白屏
|
|
||||||
+ 免登录查看1080p
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 首页web端推荐观看数展示
|
|
||||||
+ 首页web端推荐接口更新
|
|
||||||
+ 首页样式
|
|
||||||
+ 搜索页跳转
|
|
||||||
+ 弹幕资源优化
|
|
||||||
+ 图片渲染占用内存优化(部分)
|
|
||||||
+ 两次返回退出应用
|
|
||||||
+ schame 补充
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
## 1.0.18
|
|
||||||
|
|
||||||
|
|
||||||
### 功能
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
## 1.0.19
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 视频404、评论加载错误
|
|
||||||
+ bvav转换
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 视频详情页内存占用
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
## 1.0.2
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 自动检查更新
|
|
||||||
+ 封面图片保存
|
|
||||||
+ 动态跳转番剧
|
|
||||||
+ 历史记录番剧记忆播放
|
|
||||||
+ 一键清空稍后再看
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 切换分P cid未切换
|
|
||||||
+ cookie存储问题
|
|
||||||
+ 登录/退出登录问题
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 页面空/异常状态样式
|
|
||||||
+ 退出登录提示
|
|
||||||
+ 请求节流
|
|
||||||
+ 全屏播放
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
## 1.0.3
|
|
||||||
|
|
||||||
建议卸载1.0.2版本,重新安装
|
|
||||||
### 新功能
|
|
||||||
+ 底部播放进度条设置
|
|
||||||
+ 复制图片链接
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 用户数据格式修改
|
|
||||||
+ video Fit
|
|
||||||
+ 没有audio 资源的视频异常
|
|
||||||
+ 评论区域图片无法点击
|
|
||||||
+ 视频进度条拖动问题
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 页面空/异常状态样式
|
|
||||||
+ 部分页面样式
|
|
||||||
+ 图片预览页面样式
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
## 1.0.4
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 热搜刷新
|
|
||||||
+ 视频搜索排序、筛选
|
|
||||||
+ app字体大小自定义
|
|
||||||
+ app主题色自定义
|
|
||||||
+ 「课堂」类动态渲染
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 搜索词联想richText渲染异常
|
|
||||||
+ 部分动态点赞异常
|
|
||||||
+ 默认视频解码格式
|
|
||||||
+ 搜索页面返回搜索词未清空
|
|
||||||
+ 动态详情评论加载异常
|
|
||||||
+ 动态页面下拉刷新数据异常
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 一些样式修改
|
|
||||||
+ 取消热搜词缓存
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
## 1.0.5
|
|
||||||
|
|
||||||
主要是bug修复跟一部分小功能,弹幕功能需要下一版。
|
|
||||||
问题反馈请前往QQ频道或提交issues。
|
|
||||||
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 高帧率支持
|
|
||||||
+ 默认评论排序设置
|
|
||||||
+ 默认动态类别设置
|
|
||||||
+ 动态合集查看
|
|
||||||
+ 同时观看人数
|
|
||||||
+ iOS路由切换效果
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 收藏夹翻页
|
|
||||||
+ 首页搜索框频繁点击消失
|
|
||||||
+ 评论排序切换空白
|
|
||||||
+ 快速返回首页
|
|
||||||
+ 重复进入个人中心页面数据未刷新
|
|
||||||
+ 动态goods数据异常
|
|
||||||
+ 大会员切换番剧
|
|
||||||
+ 高画质codes匹配
|
|
||||||
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 倍速选择
|
|
||||||
+ 播放器亮度记忆
|
|
||||||
+ 下载对应版本apk
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
## 1.0.6
|
|
||||||
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 首页单列布局
|
|
||||||
+ 首页推荐展示播放量、弹幕数
|
|
||||||
+ 简单弹幕功能实现(持续开发中...)
|
|
||||||
+ 评论区搜索关键词开关 issues#46
|
|
||||||
+ 热搜榜隐藏功能 issues#35
|
|
||||||
+ 自动全屏 issues#37
|
|
||||||
+ 快速收藏功能
|
|
||||||
+ 双击快进/快退开关
|
|
||||||
+ 评论链接跳转视频
|
|
||||||
+ 支持移除单个稍后再看
|
|
||||||
+ app scheme外链跳转
|
|
||||||
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 杜比、无损音频切换
|
|
||||||
+ 收藏夹展示 issues#42
|
|
||||||
+ 搜索建议次 issues#47
|
|
||||||
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 倍速选择优化
|
|
||||||
+ 导航条沉浸
|
|
||||||
+ 取消Hero动画
|
|
||||||
+ 视频锁定逻辑
|
|
||||||
+ 登录逻辑优化
|
|
||||||
+ 图片预览样式
|
|
||||||
+ +评论区用户点击范围
|
|
||||||
+ 关注、粉丝页面优化
|
|
||||||
+ 关闭自动播放时播放器初始化逻辑
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
## 1.0.7
|
|
||||||
|
|
||||||
默认倍速、直播弹幕、专栏等功能开发中
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 弹幕设置、屏蔽功能
|
|
||||||
+ 不是很完美的后台播放功能
|
|
||||||
+ 不是很完美的画中画(pip)功能(Android端)
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 动态页面加载异常
|
|
||||||
+ 网络异常时页面空白
|
|
||||||
+ 竖屏全屏状态栏问题
|
|
||||||
+ iOS端代理请求异常
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 图片预览
|
|
||||||
+ 全屏播放时自动旋转
|
|
||||||
+ 转发内容增加视频标题
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
## 1.0.8
|
|
||||||
|
|
||||||
直播弹幕、循环播放等功能开发中
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 用户拉黑功能
|
|
||||||
+ gif图片保存
|
|
||||||
+ 删除已看历史记录
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 弹幕数量较少
|
|
||||||
+ 弹幕屏蔽设置自动记忆
|
|
||||||
+ 动态页面渲染
|
|
||||||
+ 用户主页数据错乱
|
|
||||||
+ 大家都在搜空白
|
|
||||||
+ 默认自动全屏,顶部操作栏丢失
|
|
||||||
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 全屏状态栏区域显示优化
|
|
||||||
+ 图片保存至PiliPala文件夹
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
## 1.0.9
|
|
||||||
|
|
||||||
|
|
||||||
### 新功能
|
|
||||||
+ 自定义倍速、默认倍速
|
|
||||||
+ 历史记录搜索
|
|
||||||
+ 收藏夹搜索
|
|
||||||
+ 历史记录多选删除
|
|
||||||
+ 视频循环播放
|
|
||||||
+ 免登录看1080P
|
|
||||||
+ 评论区视频链接跳转
|
|
||||||
+ up主分组
|
|
||||||
+ up主投稿搜索
|
|
||||||
|
|
||||||
### 修复
|
|
||||||
+ 搜索视频标题乱码
|
|
||||||
+ 屏幕帧率
|
|
||||||
+ 动态页面渲染
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 优化
|
|
||||||
+ 快进手势
|
|
||||||
+ 视频简介链接匹配
|
|
||||||
+ 视频全屏时安全区域
|
|
||||||
|
|
||||||
更多更新日志可在Github上查看
|
|
||||||
问题反馈、功能建议请查看「关于」页面。
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
PiliPlus is a third-party Bilibili client developed in Flutter,
|
|
||||||
fork from PiliPalaX (https://github.com/orz12/PiliPalaX).
|
|
||||||
|
|
||||||
Top Features:
|
|
||||||
|
|
||||||
* List of recommended videos
|
|
||||||
* List of hottest videos
|
|
||||||
* Popular live streams
|
|
||||||
* List of bangumis
|
|
||||||
* Block videos from blacklisted users
|
|
||||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +0,0 @@
|
|||||||
A third-party Bilibili client developed in Flutter
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PiliPlus
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
PiliPlus 是使用 Flutter 开发的 BiliBili 第三方客户端,
|
|
||||||
是由PiliPalaX仓库fork并进行了差异化开发的版本
|
|
||||||
|
|
||||||
主要功能:
|
|
||||||
|
|
||||||
* 推荐视频列表
|
|
||||||
* 最热视频列表
|
|
||||||
* 热门直播
|
|
||||||
* 番剧列表
|
|
||||||
* 屏蔽黑名单内用户视频
|
|
||||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +0,0 @@
|
|||||||
使用 Flutter 开发的 BiliBili 第三方客户端
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PiliPlus
|
|
||||||
@@ -20,7 +20,5 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>11.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
234
ios/Podfile.lock
@@ -1,131 +1,197 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- appscheme (1.0.4):
|
- app_links (7.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- audio_service (0.0.1):
|
- audio_service (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- audio_session (0.0.1):
|
- audio_session (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- auto_orientation (0.0.1):
|
- auto_orientation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- battery_plus (1.0.0):
|
||||||
|
- Flutter
|
||||||
|
- chat_bottom_container (0.0.1):
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift
|
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- 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
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_inappwebview_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
|
- flutter_inappwebview_ios/Core (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- OrderedSet (~> 6.0.3)
|
||||||
- flutter_mailer (0.0.1):
|
- flutter_mailer (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_native_splash (2.4.3):
|
||||||
|
- Flutter
|
||||||
- flutter_volume_controller (0.0.1):
|
- flutter_volume_controller (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Toast
|
- gt3_flutter_plugin (0.0.9):
|
||||||
- FMDB (2.7.5):
|
|
||||||
- FMDB/standard (= 2.7.5)
|
|
||||||
- FMDB/standard (2.7.5)
|
|
||||||
- gt3_flutter_plugin (0.0.8):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- GT3Captcha-iOS
|
- GT3Captcha-iOS
|
||||||
- GT3Captcha-iOS (0.15.8.3)
|
- GT3Captcha-iOS (0.15.8.3)
|
||||||
|
- image_cropper (0.0.5):
|
||||||
|
- Flutter
|
||||||
|
- TOCropViewController (~> 3.1.1)
|
||||||
|
- image_picker_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- live_photo_maker (0.0.3):
|
||||||
|
- Flutter
|
||||||
- media_kit_libs_ios_video (1.0.4):
|
- media_kit_libs_ios_video (1.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_native_event_loop (1.0.0):
|
- media_kit_native_event_loop (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_video (0.0.1):
|
- media_kit_video (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- OrderedSet (6.0.3)
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
|
||||||
- permission_handler_apple (9.1.1):
|
|
||||||
- Flutter
|
|
||||||
- ReachabilitySwift (5.0.0)
|
|
||||||
- saver_gallery (0.0.1):
|
- saver_gallery (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- screen_brightness_ios (0.1.0):
|
- screen_brightness_ios (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- SDWebImage (5.21.3):
|
||||||
|
- SDWebImage/Core (= 5.21.3)
|
||||||
|
- SDWebImage/Core (5.21.3)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- sqflite (0.0.3):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FlutterMacOS
|
||||||
- status_bar_control (3.2.1):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- system_proxy (0.0.1):
|
- FlutterMacOS
|
||||||
- Flutter
|
- SwiftyGif (5.4.5)
|
||||||
- Toast (4.1.0)
|
- TOCropViewController (3.1.1)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- webview_cookie_manager (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- webview_flutter_wkwebview (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
|
||||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||||
- auto_orientation (from `.symlinks/plugins/auto_orientation/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`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||||
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
||||||
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
|
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
|
||||||
|
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||||
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- live_photo_maker (from `.symlinks/plugins/live_photo_maker/ios`)
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- 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_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/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`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
|
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
- webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`)
|
|
||||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- FMDB
|
- DKImagePickerController
|
||||||
|
- DKPhotoGallery
|
||||||
- GT3Captcha-iOS
|
- GT3Captcha-iOS
|
||||||
- ReachabilitySwift
|
- OrderedSet
|
||||||
- Toast
|
- SDWebImage
|
||||||
|
- SwiftyGif
|
||||||
|
- TOCropViewController
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
appscheme:
|
app_links:
|
||||||
:path: ".symlinks/plugins/appscheme/ios"
|
:path: ".symlinks/plugins/app_links/ios"
|
||||||
audio_service:
|
audio_service:
|
||||||
:path: ".symlinks/plugins/audio_service/ios"
|
:path: ".symlinks/plugins/audio_service/darwin"
|
||||||
audio_session:
|
audio_session:
|
||||||
:path: ".symlinks/plugins/audio_session/ios"
|
:path: ".symlinks/plugins/audio_session/ios"
|
||||||
auto_orientation:
|
auto_orientation:
|
||||||
:path: ".symlinks/plugins/auto_orientation/ios"
|
: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:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
|
file_picker:
|
||||||
|
:path: ".symlinks/plugins/file_picker/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_inappwebview_ios:
|
||||||
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
flutter_mailer:
|
flutter_mailer:
|
||||||
:path: ".symlinks/plugins/flutter_mailer/ios"
|
:path: ".symlinks/plugins/flutter_mailer/ios"
|
||||||
|
flutter_native_splash:
|
||||||
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_volume_controller:
|
flutter_volume_controller:
|
||||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
gt3_flutter_plugin:
|
gt3_flutter_plugin:
|
||||||
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
|
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
|
||||||
|
image_cropper:
|
||||||
|
:path: ".symlinks/plugins/image_cropper/ios"
|
||||||
|
image_picker_ios:
|
||||||
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
live_photo_maker:
|
||||||
|
:path: ".symlinks/plugins/live_photo_maker/ios"
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||||
media_kit_native_event_loop:
|
media_kit_native_event_loop:
|
||||||
@@ -134,8 +200,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
saver_gallery:
|
saver_gallery:
|
||||||
@@ -144,57 +208,55 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
sqflite:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
status_bar_control:
|
sqflite_darwin:
|
||||||
:path: ".symlinks/plugins/status_bar_control/ios"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
system_proxy:
|
|
||||||
:path: ".symlinks/plugins/system_proxy/ios"
|
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
|
||||||
:path: ".symlinks/plugins/volume_controller/ios"
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
webview_cookie_manager:
|
|
||||||
:path: ".symlinks/plugins/webview_cookie_manager/ios"
|
|
||||||
webview_flutter_wkwebview:
|
|
||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
|
||||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
|
||||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
auto_orientation: a1600c9ed72e6e96982fb4e1214463343342432a
|
||||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977
|
||||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
chat_bottom_container: f1eb8323db77a87db50f361142c679f11e892d1b
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
|
flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
|
||||||
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
|
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||||
|
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||||
|
gt3_flutter_plugin: 37090e5fa66ff2a52939eb9d208fc36fa49d36e5
|
||||||
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
image_cropper: e405d3e44183f8e8edbec2e49b01ff9c819c7ac8
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
live_photo_maker: 29280ca88323bd5a33aafd00d98624d5cf522176
|
||||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf
|
||||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
screen_brightness_ios: 9953fd7da5bd480f1a93990daeec2eb42d4f3b52
|
||||||
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
TOCropViewController: 9002a9b12d8104d7478cdc306d80f0efea7fe2c5
|
||||||
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
|
|
||||||
PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be
|
PODFILE CHECKSUM: f62db4fb414ebdecb264109948f76dfef35fdc3d
|
||||||
|
|
||||||
COCOAPODS: 1.14.3
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import UIKit
|
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
application.applicationSupportsShakeToEdit = false // Disable shake to undo
|
application.applicationSupportsShakeToEdit = false // Disable shake to undo
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,27 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>FlutterSceneDelegate</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>FlutterDeepLinkingEnabled</key>
|
<key>FlutterDeepLinkingEnabled</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
@@ -110,5 +131,13 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false/>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class BuildConfig {
|
abstract final class BuildConfig {
|
||||||
static const int versionCode = int.fromEnvironment(
|
static const int versionCode = int.fromEnvironment(
|
||||||
'pili.code',
|
'pili.code',
|
||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
|
|||||||
56
lib/common/assets.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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 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';
|
abstract final class Constants {
|
||||||
|
|
||||||
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 {
|
|
||||||
static const appName = 'PiliPlus';
|
static const appName = 'PiliPlus';
|
||||||
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
|
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
|
||||||
|
|
||||||
@@ -20,9 +7,9 @@ class Constants {
|
|||||||
static const String appKey = 'dfca71928277209b';
|
static const String appKey = 'dfca71928277209b';
|
||||||
// 59b43e04ad6965f34319062b478f83dd TV端
|
// 59b43e04ad6965f34319062b478f83dd TV端
|
||||||
static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e';
|
static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e';
|
||||||
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
// static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
||||||
static const String thirdApi =
|
// static const String thirdApi =
|
||||||
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
|
// 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
|
||||||
|
|
||||||
static const String traceId =
|
static const String traceId =
|
||||||
'11111111111111111111111111111111:1111111111111111:0:0';
|
'11111111111111111111111111111111:1111111111111111:0:0';
|
||||||
@@ -40,8 +27,6 @@ class Constants {
|
|||||||
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
||||||
|
|
||||||
static const baseHeaders = {
|
static const baseHeaders = {
|
||||||
'connection': 'keep-alive',
|
|
||||||
'accept-encoding': 'br,gzip',
|
|
||||||
// 'referer': HttpString.baseUrl,
|
// 'referer': HttpString.baseUrl,
|
||||||
'env': 'prod',
|
'env': 'prod',
|
||||||
'app-key': 'android64',
|
'app-key': 'android64',
|
||||||
@@ -54,243 +39,6 @@ class Constants {
|
|||||||
|
|
||||||
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
|
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
|
||||||
|
|
||||||
// 超分辨率滤镜
|
// 'itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme,sunflowerStyle,cardsEnhance,eva3CardOpus,eva3CardVideo,eva3CardComment,eva3CardVote,eva3CardUser'
|
||||||
static const List<String> mpvAnime4KShaders = [
|
static const dynFeatures = 'itemOpusStyle,listOnlyfans,onlyfansQaCard';
|
||||||
'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),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
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) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
final color = theme.colorScheme.onInverseSurface;
|
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(
|
return Skeleton(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||||
@@ -86,29 +93,19 @@ class DynamicCardSkeleton extends StatelessWidget {
|
|||||||
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
|
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: const ['转发', '评论', '点赞']
|
||||||
for (var i = 0; i < 3; i++)
|
.map(
|
||||||
TextButton.icon(
|
(e) => TextButton.icon(
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.radio_button_unchecked_outlined,
|
Icons.radio_button_unchecked_outlined,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
|
||||||
foregroundColor: theme.colorScheme.outline.withValues(
|
|
||||||
alpha: 0.2,
|
|
||||||
),
|
),
|
||||||
|
style: buttonStyle,
|
||||||
|
label: Text(e),
|
||||||
),
|
),
|
||||||
label: Text(
|
)
|
||||||
i == 0
|
.toList(),
|
||||||
? '转发'
|
|
||||||
: i == 1
|
|
||||||
? '评论'
|
|
||||||
: '点赞',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
|
||||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:PiliPlus/common/style.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||||
|
import 'package:flutter/material.dart' hide LayoutBuilder;
|
||||||
|
|
||||||
class FavPgcItemSkeleton extends StatelessWidget {
|
class FavPgcItemSkeleton extends StatelessWidget {
|
||||||
const FavPgcItemSkeleton({super.key});
|
const FavPgcItemSkeleton({super.key});
|
||||||
@@ -11,7 +12,7 @@ class FavPgcItemSkeleton extends StatelessWidget {
|
|||||||
return Skeleton(
|
return Skeleton(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: StyleString.safeSpace,
|
horizontal: Style.safeSpace,
|
||||||
vertical: 5,
|
vertical: 5,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
|
||||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||||
|
import 'package:PiliPlus/common/style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MediaPgcSkeleton extends StatefulWidget {
|
class MediaPgcSkeleton extends StatefulWidget {
|
||||||
@@ -15,11 +15,9 @@ class _MediaPgcSkeletonState extends State<MediaPgcSkeleton> {
|
|||||||
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
|
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
|
||||||
return Skeleton(
|
return Skeleton(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const .symmetric(
|
||||||
StyleString.safeSpace,
|
horizontal: Style.safeSpace,
|
||||||
7,
|
vertical: 7,
|
||||||
StyleString.safeSpace,
|
|
||||||
7,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class Skeleton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
|
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
|
||||||
var shimmerGradient = LinearGradient(
|
final shimmerGradient = LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
color,
|
color,
|
||||||
@@ -62,7 +62,6 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_shimmerController = AnimationController.unbounded(vsync: this)
|
_shimmerController = AnimationController.unbounded(vsync: this)
|
||||||
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
|
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide LayoutBuilder;
|
||||||
|
|
||||||
class SpaceOpusSkeleton extends StatelessWidget {
|
class SpaceOpusSkeleton extends StatelessWidget {
|
||||||
const SpaceOpusSkeleton({super.key});
|
const SpaceOpusSkeleton({super.key});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
|
||||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||||
|
import 'package:PiliPlus/common/style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class VideoCardHSkeleton extends StatelessWidget {
|
class VideoCardHSkeleton extends StatelessWidget {
|
||||||
@@ -10,25 +10,25 @@ class VideoCardHSkeleton extends StatelessWidget {
|
|||||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||||
return Skeleton(
|
return Skeleton(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const .symmetric(
|
||||||
horizontal: StyleString.safeSpace,
|
horizontal: Style.safeSpace,
|
||||||
vertical: 5,
|
vertical: 5,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: StyleString.aspectRatio,
|
aspectRatio: Style.aspectRatio,
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color,
|
color: color,
|
||||||
borderRadius: StyleString.mdRadius,
|
borderRadius: Style.mdRadius,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
padding: const .fromLTRB(10, 4, 6, 4),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
|
||||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||||
|
import 'package:PiliPlus/common/style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class VideoCardVSkeleton extends StatelessWidget {
|
class VideoCardVSkeleton extends StatelessWidget {
|
||||||
@@ -13,11 +13,11 @@ class VideoCardVSkeleton extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: StyleString.aspectRatio,
|
aspectRatio: Style.aspectRatio,
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color,
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,19 +7,21 @@ class MultiSelectAppBarWidget extends StatelessWidget
|
|||||||
final MultiSelectBase ctr;
|
final MultiSelectBase ctr;
|
||||||
final bool? visible;
|
final bool? visible;
|
||||||
final AppBar child;
|
final AppBar child;
|
||||||
final List<Widget>? children;
|
final List<Widget>? actions;
|
||||||
|
|
||||||
const MultiSelectAppBarWidget({
|
const MultiSelectAppBarWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.ctr,
|
required this.ctr,
|
||||||
this.visible,
|
this.visible,
|
||||||
this.children,
|
this.actions,
|
||||||
required this.child,
|
required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (visible ?? ctr.enableMultiSelect.value) {
|
if (visible ?? ctr.enableMultiSelect.value) {
|
||||||
|
final style = TextButton.styleFrom(visualDensity: VisualDensity.compact);
|
||||||
|
final colorScheme = ColorScheme.of(context);
|
||||||
return AppBar(
|
return AppBar(
|
||||||
bottom: child.bottom,
|
bottom: child.bottom,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -30,21 +32,22 @@ class MultiSelectAppBarWidget extends StatelessWidget
|
|||||||
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
|
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: style,
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
),
|
|
||||||
onPressed: () => ctr.handleSelect(checked: true),
|
onPressed: () => ctr.handleSelect(checked: true),
|
||||||
child: const Text('全选'),
|
child: const Text('全选'),
|
||||||
),
|
),
|
||||||
...?children,
|
...?actions,
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: style,
|
||||||
visualDensity: VisualDensity.compact,
|
onPressed: () {
|
||||||
),
|
if (ctr.checkedCount == 0) {
|
||||||
onPressed: ctr.onRemove,
|
return;
|
||||||
|
}
|
||||||
|
ctr.onRemove();
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'移除',
|
'移除',
|
||||||
style: TextStyle(color: Get.theme.colorScheme.error),
|
style: TextStyle(color: colorScheme.error),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
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,
|
||||||
|
}) {
|
||||||
|
const gap = 6.0;
|
||||||
|
const size = 22.0;
|
||||||
|
const padding = 0.8;
|
||||||
|
const 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/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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
class PBadge extends StatelessWidget {
|
class PBadge extends StatelessWidget {
|
||||||
final String? text;
|
final String? text;
|
||||||
@@ -59,7 +59,7 @@ class PBadge extends StatelessWidget {
|
|||||||
bgColor = Colors.black45;
|
bgColor = Colors.black45;
|
||||||
color = Colors.white;
|
color = Colors.white;
|
||||||
case PBadgeType.error:
|
case PBadgeType.error:
|
||||||
if (Get.isDarkMode) {
|
if (theme.isDark) {
|
||||||
bgColor = theme.errorContainer;
|
bgColor = theme.errorContainer;
|
||||||
color = theme.onErrorContainer;
|
color = theme.onErrorContainer;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,39 +1,32 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/style.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
|
||||||
|
|
||||||
class ColorPalette extends StatelessWidget {
|
class ColorPalette extends StatelessWidget {
|
||||||
final Color color;
|
final ColorScheme colorScheme;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
|
final bool showBgColor;
|
||||||
|
|
||||||
const ColorPalette({
|
const ColorPalette({
|
||||||
super.key,
|
super.key,
|
||||||
required this.color,
|
required this.colorScheme,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
|
this.showBgColor = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final primary = colorScheme.primary;
|
||||||
final Hct hct = Hct.fromInt(color.toARGB32());
|
final tertiary = colorScheme.tertiary;
|
||||||
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());
|
final primaryContainer = colorScheme.primaryContainer;
|
||||||
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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Widget child = ClipOval(
|
Widget child = ClipOval(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
coloredBox(primary),
|
_coloredBox(primary),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
coloredBox(tertiary),
|
_coloredBox(tertiary),
|
||||||
coloredBox(primaryContainer),
|
_coloredBox(primaryContainer),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -50,7 +43,7 @@ class ColorPalette extends StatelessWidget {
|
|||||||
width: 23,
|
width: 23,
|
||||||
height: 23,
|
height: 23,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(Hct.from(hct.hue, 30.0, 40.0).toInt()),
|
color: colorScheme.surfaceContainer,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -62,15 +55,25 @@ class ColorPalette extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Container(
|
if (showBgColor) {
|
||||||
width: 50,
|
return Container(
|
||||||
height: 50,
|
width: 50,
|
||||||
padding: const EdgeInsets.all(6),
|
height: 50,
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(6),
|
||||||
color: theme.colorScheme.onInverseSurface,
|
decoration: BoxDecoration(
|
||||||
borderRadius: StyleString.mdRadius,
|
color: colorScheme.onInverseSurface,
|
||||||
),
|
borderRadius: Style.mdRadius,
|
||||||
child: child,
|
),
|
||||||
);
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
lib/common/widgets/cropped_image.dart
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* 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 Size preferredSize,
|
||||||
|
required ui.Image image,
|
||||||
|
required Rect srcRect,
|
||||||
|
required Rect dstRect,
|
||||||
|
required RRect rrect,
|
||||||
|
required Paint imgPaint,
|
||||||
|
required Paint borderPaint,
|
||||||
|
}) : _preferredSize = preferredSize,
|
||||||
|
_image = image,
|
||||||
|
_srcRect = srcRect,
|
||||||
|
_dstRect = dstRect,
|
||||||
|
_rrect = rrect,
|
||||||
|
_imgPaint = imgPaint,
|
||||||
|
_borderPaint = 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;
|
||||||
|
}
|
||||||
113
lib/common/widgets/custom_arc.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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 double preferredSize,
|
||||||
|
required Color color,
|
||||||
|
required double progress,
|
||||||
|
required double strokeWidth,
|
||||||
|
}) : _preferredSize = preferredSize,
|
||||||
|
_color = color,
|
||||||
|
_progress = progress,
|
||||||
|
_strokeWidth = 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;
|
||||||
|
}
|
||||||
96
lib/common/widgets/custom_height_widget.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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({
|
||||||
|
double? height,
|
||||||
|
required Offset offset,
|
||||||
|
}) : _height = height,
|
||||||
|
_offset = 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,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;
|
|
||||||
}
|
|
||||||
@@ -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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final colorScheme = ColorScheme.of(context);
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
|
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.primaryContainer.withValues(
|
color: colorScheme.primaryContainer.withValues(alpha: toastOpacity),
|
||||||
alpha: toastOpacity,
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
msg,
|
msg,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: theme.colorScheme.onPrimaryContainer,
|
color: colorScheme.onPrimaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -41,7 +39,7 @@ class LoadingWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||||
@@ -50,6 +48,7 @@ class LoadingWidget extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
spacing: 20,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
//loading animation
|
//loading animation
|
||||||
@@ -57,12 +56,8 @@ class LoadingWidget extends StatelessWidget {
|
|||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
||||||
),
|
),
|
||||||
|
|
||||||
//msg
|
//msg
|
||||||
Container(
|
Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||||
margin: const EdgeInsets.only(top: 20),
|
|
||||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,135 +1,63 @@
|
|||||||
import 'dart:math' as math;
|
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||||
import 'dart:ui' show clampDouble;
|
|
||||||
|
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/rendering.dart'
|
||||||
|
show
|
||||||
enum TooltipType { top, right }
|
ContainerRenderObjectMixin,
|
||||||
|
RenderBoxContainerDefaultsMixin,
|
||||||
|
MultiChildLayoutParentData;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class CustomTooltip extends StatefulWidget {
|
class CustomTooltip extends StatefulWidget {
|
||||||
const CustomTooltip({
|
const CustomTooltip({
|
||||||
super.key,
|
super.key,
|
||||||
this.type = TooltipType.top,
|
|
||||||
required this.overlayWidget,
|
required this.overlayWidget,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.indicator,
|
required this.indicator,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TooltipType type;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Widget Function() overlayWidget;
|
final ValueGetter<Widget> overlayWidget;
|
||||||
final Widget Function()? indicator;
|
final ValueGetter<Widget> indicator;
|
||||||
|
|
||||||
static final List<CustomTooltipState> _openedTooltips =
|
|
||||||
<CustomTooltipState>[];
|
|
||||||
|
|
||||||
static bool dismissAllToolTips() {
|
|
||||||
if (_openedTooltips.isNotEmpty) {
|
|
||||||
final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
|
|
||||||
for (final CustomTooltipState state in openedTooltips) {
|
|
||||||
assert(state.mounted);
|
|
||||||
state._scheduleDismissTooltip();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CustomTooltip> createState() => CustomTooltipState();
|
State<CustomTooltip> createState() => _CustomTooltipState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomTooltipState extends State<CustomTooltip>
|
class _CustomTooltipState extends State<CustomTooltip> {
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
static const Duration _fadeInDuration = Duration(milliseconds: 150);
|
|
||||||
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
|
|
||||||
|
|
||||||
final OverlayPortalController _overlayController = OverlayPortalController();
|
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;
|
LongPressGestureRecognizer? _longPressRecognizer;
|
||||||
|
LongPressGestureRecognizer get longPressRecognizer =>
|
||||||
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
_longPressRecognizer ??= LongPressGestureRecognizer()
|
||||||
void _handleStatusChanged(AnimationStatus status) {
|
..onLongPress = _scheduleShowTooltip;
|
||||||
assert(mounted);
|
|
||||||
switch ((_animationStatus.isDismissed, status.isDismissed)) {
|
|
||||||
case (false, true):
|
|
||||||
CustomTooltip._openedTooltips.remove(this);
|
|
||||||
_overlayController.hide();
|
|
||||||
case (true, false):
|
|
||||||
_overlayController.show();
|
|
||||||
CustomTooltip._openedTooltips.add(this);
|
|
||||||
case (true, true) || (false, false):
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_animationStatus = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scheduleShowTooltip() {
|
void _scheduleShowTooltip() {
|
||||||
_controller.forward();
|
_overlayController.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleDismissTooltip() {
|
void _scheduleDismissTooltip() {
|
||||||
_controller.reverse();
|
_overlayController.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handlePointerDown(PointerDownEvent event) {
|
void _handlePointerDown(PointerDownEvent event) {
|
||||||
assert(mounted);
|
assert(mounted);
|
||||||
const Set<PointerDeviceKind> triggerModeDeviceKinds = <PointerDeviceKind>{
|
longPressRecognizer.addPointer(event);
|
||||||
PointerDeviceKind.invertedStylus,
|
|
||||||
PointerDeviceKind.stylus,
|
|
||||||
PointerDeviceKind.touch,
|
|
||||||
PointerDeviceKind.unknown,
|
|
||||||
PointerDeviceKind.trackpad,
|
|
||||||
};
|
|
||||||
_longPressRecognizer ??= LongPressGestureRecognizer(
|
|
||||||
debugOwner: this,
|
|
||||||
supportedDevices: triggerModeDeviceKinds,
|
|
||||||
);
|
|
||||||
_longPressRecognizer!
|
|
||||||
..onLongPress = _scheduleShowTooltip
|
|
||||||
..addPointer(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCustomTooltipOverlay(BuildContext context) {
|
Widget _buildCustomTooltipOverlay(
|
||||||
final OverlayState overlayState = Overlay.of(
|
BuildContext context,
|
||||||
context,
|
OverlayChildLayoutInfo layoutInfo,
|
||||||
debugRequiredFor: widget,
|
) {
|
||||||
|
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(
|
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
|
||||||
verticalOffset: box.size.height / 2,
|
|
||||||
horizontslOffset: box.size.width / 2,
|
|
||||||
type: widget.type,
|
|
||||||
animation: _overlayAnimation,
|
|
||||||
target: target,
|
target: target,
|
||||||
onDismiss: _scheduleDismissTooltip,
|
onDismiss: _scheduleDismissTooltip,
|
||||||
overlayWidget: widget.overlayWidget,
|
overlayWidget: widget.overlayWidget,
|
||||||
indicator: widget.indicator,
|
indicator: widget.indicator,
|
||||||
);
|
);
|
||||||
|
|
||||||
return SelectionContainer.maybeOf(context) == null
|
return SelectionContainer.maybeOf(context) == null
|
||||||
? overlayChild
|
? overlayChild
|
||||||
: SelectionContainer.disabled(child: overlayChild);
|
: SelectionContainer.disabled(child: overlayChild);
|
||||||
@@ -138,11 +66,10 @@ class CustomTooltipState extends State<CustomTooltip>
|
|||||||
@protected
|
@protected
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
CustomTooltip._openedTooltips.remove(this);
|
_longPressRecognizer
|
||||||
_longPressRecognizer?.onLongPressCancel = null;
|
?..onLongPress = null
|
||||||
_longPressRecognizer?.dispose();
|
..dispose();
|
||||||
_backingController?.dispose();
|
_longPressRecognizer = null;
|
||||||
_backingOverlayAnimation?.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +77,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget result;
|
Widget result;
|
||||||
if (Utils.isMobile) {
|
if (PlatformUtils.isMobile) {
|
||||||
result = Listener(
|
result = Listener(
|
||||||
onPointerDown: _handlePointerDown,
|
onPointerDown: _handlePointerDown,
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
@@ -164,7 +91,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
|||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return OverlayPortal(
|
return OverlayPortal.overlayChildLayoutBuilder(
|
||||||
controller: _overlayController,
|
controller: _overlayController,
|
||||||
overlayChildBuilder: _buildCustomTooltipOverlay,
|
overlayChildBuilder: _buildCustomTooltipOverlay,
|
||||||
child: result,
|
child: result,
|
||||||
@@ -174,232 +101,230 @@ class CustomTooltipState extends State<CustomTooltip>
|
|||||||
|
|
||||||
class _CustomTooltipOverlay extends StatelessWidget {
|
class _CustomTooltipOverlay extends StatelessWidget {
|
||||||
const _CustomTooltipOverlay({
|
const _CustomTooltipOverlay({
|
||||||
required this.verticalOffset,
|
|
||||||
required this.horizontslOffset,
|
|
||||||
required this.type,
|
|
||||||
required this.animation,
|
|
||||||
required this.target,
|
required this.target,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
required this.overlayWidget,
|
required this.overlayWidget,
|
||||||
this.indicator,
|
required this.indicator,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double verticalOffset;
|
|
||||||
final double horizontslOffset;
|
|
||||||
final TooltipType type;
|
|
||||||
final Animation<double> animation;
|
|
||||||
final Offset target;
|
final Offset target;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
final Widget Function() overlayWidget;
|
final ValueGetter<Widget> overlayWidget;
|
||||||
final Widget Function()? indicator;
|
final ValueGetter<Widget> indicator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child = CustomMultiChildLayout(
|
return _ToolTip(
|
||||||
delegate: _CustomMultiTooltipPositionDelegate(
|
target: target,
|
||||||
type: type,
|
preferBelow: false,
|
||||||
target: target,
|
onTap: PlatformUtils.isMobile ? onDismiss : null,
|
||||||
verticalOffset: verticalOffset,
|
|
||||||
horizontslOffset: horizontslOffset,
|
|
||||||
preferBelow: false,
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
LayoutId(
|
indicator(),
|
||||||
id: 'overlay',
|
overlayWidget(),
|
||||||
child: overlayWidget(),
|
|
||||||
),
|
|
||||||
if (indicator != null)
|
|
||||||
LayoutId(
|
|
||||||
id: 'indicator',
|
|
||||||
child: indicator!(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (Utils.isMobile) {
|
|
||||||
return GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: onDismiss,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
|
class _ToolTip extends MultiChildRenderObjectWidget {
|
||||||
_CustomMultiTooltipPositionDelegate({
|
const _ToolTip({
|
||||||
required this.type,
|
super.children,
|
||||||
|
this.onTap,
|
||||||
required this.target,
|
required this.target,
|
||||||
required this.verticalOffset,
|
|
||||||
required this.horizontslOffset,
|
|
||||||
required this.preferBelow,
|
required this.preferBelow,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TooltipType type;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
final Offset target;
|
final Offset target;
|
||||||
|
|
||||||
final double verticalOffset;
|
|
||||||
|
|
||||||
final double horizontslOffset;
|
|
||||||
|
|
||||||
final bool preferBelow;
|
final bool preferBelow;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void performLayout(Size size) {
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
switch (type) {
|
return _RenderToolTip(
|
||||||
case TooltipType.top:
|
onTap: onTap,
|
||||||
Size? indicatorSize;
|
target: target,
|
||||||
if (hasChild('indicator')) {
|
preferBelow: preferBelow,
|
||||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChild('overlay')) {
|
@override
|
||||||
final overlaySize = layoutChild(
|
void updateRenderObject(BuildContext context, _RenderToolTip renderObject) {
|
||||||
'overlay',
|
renderObject
|
||||||
BoxConstraints.loose(size),
|
..onTap = onTap
|
||||||
);
|
..target = target
|
||||||
Offset offset = positionDependentBox(
|
..preferBelow = preferBelow;
|
||||||
type: type,
|
}
|
||||||
size: size,
|
}
|
||||||
childSize: overlaySize,
|
|
||||||
target: target,
|
|
||||||
verticalOffset: verticalOffset,
|
|
||||||
horizontslOffset: horizontslOffset,
|
|
||||||
preferBelow: preferBelow,
|
|
||||||
);
|
|
||||||
if (indicatorSize != null) {
|
|
||||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
|
||||||
positionChild(
|
|
||||||
'indicator',
|
|
||||||
Offset(
|
|
||||||
target.dx - indicatorSize.width / 2,
|
|
||||||
offset.dy + overlaySize.height - 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
positionChild('overlay', offset);
|
|
||||||
}
|
|
||||||
case TooltipType.right:
|
|
||||||
Size? indicatorSize;
|
|
||||||
if (hasChild('indicator')) {
|
|
||||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChild('overlay')) {
|
class _RenderToolTip extends RenderBox
|
||||||
final overlaySize = layoutChild(
|
with
|
||||||
'overlay',
|
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||||
BoxConstraints.loose(size),
|
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||||
);
|
_RenderToolTip({
|
||||||
Offset offset = positionDependentBox(
|
VoidCallback? onTap,
|
||||||
type: type,
|
required Offset target,
|
||||||
size: size,
|
required bool preferBelow,
|
||||||
childSize: overlaySize,
|
}) : _target = target,
|
||||||
target: target,
|
_preferBelow = preferBelow,
|
||||||
verticalOffset: verticalOffset,
|
_hitTestSelf = onTap != null {
|
||||||
horizontslOffset: horizontslOffset,
|
if (onTap != null) {
|
||||||
preferBelow: preferBelow,
|
_tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap;
|
||||||
);
|
}
|
||||||
if (indicatorSize != null) {
|
}
|
||||||
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
|
|
||||||
positionChild(
|
TapGestureRecognizer? _tapGestureRecognizer;
|
||||||
'indicator',
|
|
||||||
Offset(
|
set onTap(VoidCallback? value) {
|
||||||
offset.dx - indicatorSize.width + 1,
|
_tapGestureRecognizer?.onTap = value;
|
||||||
target.dy - indicatorSize.height / 2,
|
}
|
||||||
),
|
|
||||||
);
|
@override
|
||||||
}
|
void dispose() {
|
||||||
positionChild('overlay', offset);
|
_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
|
@override
|
||||||
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) {
|
void performLayout() {
|
||||||
return target != oldDelegate.target ||
|
size = constraints.constrain(constraints.biggest);
|
||||||
verticalOffset != oldDelegate.verticalOffset ||
|
|
||||||
preferBelow != oldDelegate.preferBelow;
|
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 {
|
class Triangle extends LeafRenderObjectWidget {
|
||||||
TrianglePainter(this.color, {this.type = TooltipType.top});
|
const Triangle({
|
||||||
final TooltipType type;
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
|
||||||
final Color color;
|
final Color color;
|
||||||
|
final Size size;
|
||||||
|
|
||||||
@override
|
@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 Color color,
|
||||||
|
required Size preferredSize,
|
||||||
|
}) : _color = color,
|
||||||
|
_preferredSize = 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()
|
final paint = Paint()
|
||||||
..color = color
|
..color = color
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
Path path;
|
final path = Path()
|
||||||
switch (type) {
|
..moveTo(offset.dx, offset.dy)
|
||||||
case TooltipType.top:
|
..lineTo(offset.dx + size.width, offset.dy)
|
||||||
path = Path()
|
..lineTo(offset.dx + size.width / 2, size.height + offset.dy)
|
||||||
..moveTo(0, 0)
|
..close();
|
||||||
..lineTo(size.width, 0)
|
|
||||||
..lineTo(size.width / 2, size.height)
|
|
||||||
..close();
|
|
||||||
case TooltipType.right:
|
|
||||||
path = Path()
|
|
||||||
..moveTo(0, size.height / 2)
|
|
||||||
..lineTo(size.width, 0)
|
|
||||||
..lineTo(size.width, size.height)
|
|
||||||
..close();
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawPath(path, paint);
|
context.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,39 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
Future<void> showConfirmDialog({
|
Future<bool> showConfirmDialog({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String title,
|
required Widget title,
|
||||||
dynamic content,
|
Widget? content,
|
||||||
required VoidCallback onConfirm,
|
// @Deprecated('use `bool result = await showConfirmDialog()` instead')
|
||||||
}) {
|
VoidCallback? onConfirm,
|
||||||
return showDialog(
|
}) async {
|
||||||
context: context,
|
return await showDialog<bool>(
|
||||||
builder: (context) {
|
context: context,
|
||||||
return AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(title),
|
title: title,
|
||||||
content: content is String
|
content: content,
|
||||||
? Text(content)
|
actions: [
|
||||||
: content is Widget
|
TextButton(
|
||||||
? content
|
onPressed: Get.back,
|
||||||
: null,
|
child: Text(
|
||||||
actions: [
|
'取消',
|
||||||
TextButton(
|
style: TextStyle(
|
||||||
onPressed: Get.back,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
child: Text(
|
),
|
||||||
'取消',
|
),
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
|
||||||
),
|
),
|
||||||
),
|
TextButton(
|
||||||
TextButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
Get.back(result: true);
|
||||||
Get.back();
|
onConfirm?.call();
|
||||||
onConfirm();
|
},
|
||||||
},
|
child: const Text('确认'),
|
||||||
child: const Text('确认'),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
) ??
|
||||||
},
|
false;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void showPgcFollowDialog({
|
void showPgcFollowDialog({
|
||||||
|
|||||||
277
lib/common/widgets/dialog/export_import.dart
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import 'dart:async' show FutureOr;
|
||||||
|
import 'dart:convert' show utf8, jsonDecode;
|
||||||
|
import 'dart:io' show File;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/style.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension/context_ext.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());
|
||||||
|
Utils.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?.isNotEmpty != true) {
|
||||||
|
SmartDialog.showToast('剪贴板无数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
final text = data!.text!;
|
||||||
|
late final T json;
|
||||||
|
late 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(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
final isDark = context.isDarkMode;
|
||||||
|
if (isDark != isDarkMode) {
|
||||||
|
isDarkMode = isDark;
|
||||||
|
renderer = TextSpanRenderer(
|
||||||
|
const TextStyle(),
|
||||||
|
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: Theme.of(context).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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> importFromLocalFile<T>({
|
||||||
|
required FutureOr<void> Function(T json) onImport,
|
||||||
|
}) async {
|
||||||
|
final result = await FilePicker.pickFiles();
|
||||||
|
if (result != null) {
|
||||||
|
final path = result.files.first.path;
|
||||||
|
if (path != null) {
|
||||||
|
final data = await File(path).readAsString();
|
||||||
|
late 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) {
|
||||||
|
if (e is FormatException) {}
|
||||||
|
return '解析json失败:$e';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Get.back,
|
||||||
|
child: Text(
|
||||||
|
'取消',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.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,6 +1,7 @@
|
|||||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||||
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -8,92 +9,81 @@ import 'package:get/get.dart';
|
|||||||
Future<void> autoWrapReportDialog(
|
Future<void> autoWrapReportDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Map<String, Map<int, String>> options,
|
Map<String, Map<int, String>> options,
|
||||||
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
|
Future<LoadingState> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||||
onSuccess,
|
onSuccess, {
|
||||||
) {
|
bool ban = true,
|
||||||
|
}) {
|
||||||
int? reasonType;
|
int? reasonType;
|
||||||
String? reasonDesc;
|
String? reasonDesc;
|
||||||
bool banUid = false;
|
bool banUid = false;
|
||||||
late final key = GlobalKey<FormState>();
|
late final key = GlobalKey<FormFieldState<String>>();
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) => AlertDialog(
|
||||||
return AlertDialog(
|
title: const Text('举报'),
|
||||||
title: const Text('举报'),
|
titlePadding: const .only(left: 22, top: 16, right: 22),
|
||||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
contentPadding: const .symmetric(vertical: 5),
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
actionsPadding: const .only(left: 16, right: 16, bottom: 10),
|
||||||
actionsPadding: const EdgeInsets.only(
|
content: Column(
|
||||||
left: 16,
|
mainAxisSize: MainAxisSize.min,
|
||||||
right: 16,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
bottom: 10,
|
children: [
|
||||||
),
|
Flexible(
|
||||||
content: Column(
|
child: SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: AnimatedSize(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
duration: const Duration(milliseconds: 200),
|
||||||
children: [
|
child: Builder(
|
||||||
Flexible(
|
builder: (context) => Column(
|
||||||
child: SingleChildScrollView(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: AnimatedSize(
|
children: [
|
||||||
duration: const Duration(milliseconds: 200),
|
const Padding(
|
||||||
child: Builder(
|
padding: .only(left: 22, right: 22, bottom: 5),
|
||||||
builder: (context) => Column(
|
child: Text('请选择举报的理由:'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
RadioGroup(
|
||||||
const Padding(
|
onChanged: (value) {
|
||||||
padding: EdgeInsets.only(
|
reasonType = value;
|
||||||
left: 22,
|
(context as Element).markNeedsBuild();
|
||||||
right: 22,
|
},
|
||||||
bottom: 5,
|
groupValue: reasonType,
|
||||||
),
|
child: Column(
|
||||||
child: Text('请选择举报的理由:'),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: options.entries.map((entry) {
|
||||||
|
return WrapRadioOptionsGroup<int>(
|
||||||
|
groupTitle: entry.key,
|
||||||
|
options: entry.value,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
RadioGroup(
|
),
|
||||||
onChanged: (value) {
|
if (reasonType == 0)
|
||||||
reasonType = value;
|
Padding(
|
||||||
(context as Element).markNeedsBuild();
|
padding: const .only(left: 22, top: 5, right: 22),
|
||||||
},
|
child: TextFormField(
|
||||||
groupValue: reasonType,
|
key: key,
|
||||||
child: Column(
|
autofocus: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
minLines: 2,
|
||||||
children: options.entries.map((entry) {
|
maxLines: 4,
|
||||||
return WrapRadioOptionsGroup<int>(
|
initialValue: reasonDesc,
|
||||||
groupTitle: entry.key,
|
decoration: const InputDecoration(
|
||||||
options: entry.value,
|
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||||
);
|
border: OutlineInputBorder(),
|
||||||
}).toList(),
|
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(
|
||||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||||
child: CheckBoxText(
|
child: CheckBoxText(
|
||||||
@@ -101,43 +91,42 @@ Future<void> autoWrapReportDialog(
|
|||||||
onChanged: (value) => banUid = value,
|
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) {
|
|
||||||
SmartDialog.dismiss();
|
|
||||||
SmartDialog.showToast('提交失败:$e');
|
|
||||||
if (kDebugMode) rethrow;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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('确定'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +157,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = ColorScheme.of(context);
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -201,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
|
// from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js
|
||||||
static Map<String, Map<int, String>> get commentReport => const {
|
static Map<String, Map<int, String>> get commentReport => const {
|
||||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||||
@@ -263,4 +252,16 @@ class ReportOptions {
|
|||||||
7: '其他', // avoid show form
|
7: '其他', // avoid show form
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static Map<String, Map<int, String>> get imMsgReport => const {
|
||||||
|
'': {
|
||||||
|
1: '色情低俗',
|
||||||
|
2: '政治敏感',
|
||||||
|
3: '违法有害',
|
||||||
|
4: '广告骚扰',
|
||||||
|
5: '人身攻击',
|
||||||
|
6: '诈骗',
|
||||||
|
0: '其他问题',
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
const _reason = ['头像违规', '昵称违规', '签名违规'];
|
||||||
|
|
||||||
|
const _reasonV2 = ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗', '违规引流外链'];
|
||||||
|
|
||||||
Future<void> showMemberReportDialog(
|
Future<void> showMemberReportDialog(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required Object? name,
|
required Object? name,
|
||||||
@@ -17,13 +21,11 @@ Future<void> showMemberReportDialog(
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
horizontal: 20,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
titleTextStyle: theme.textTheme.bodyMedium,
|
titleTextStyle: theme.textTheme.bodyMedium,
|
||||||
title: Column(
|
title: Column(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'举报: $name',
|
'举报: $name',
|
||||||
@@ -34,53 +36,101 @@ Future<void> showMemberReportDialog(
|
|||||||
),
|
),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: .min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: .start,
|
||||||
children: [
|
children: [
|
||||||
const Text('举报内容(必选,可多选)'),
|
const Padding(
|
||||||
|
padding: .only(left: 18),
|
||||||
|
child: Text('举报内容(必选,可多选)'),
|
||||||
|
),
|
||||||
...List.generate(
|
...List.generate(
|
||||||
3,
|
3,
|
||||||
(index) => Builder(
|
(index) => Builder(
|
||||||
builder: (context) => CheckboxListTile(
|
builder: (context) {
|
||||||
dense: true,
|
final checked = reason.contains(index + 1);
|
||||||
value: reason.contains(index + 1),
|
return ListTile(
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
minTileHeight: 40,
|
||||||
onChanged: (value) {
|
onTap: () {
|
||||||
if (value!) {
|
if (!checked) {
|
||||||
reason.add(index + 1);
|
reason.add(index + 1);
|
||||||
} else {
|
} else {
|
||||||
reason.remove(index + 1);
|
reason.remove(index + 1);
|
||||||
}
|
}
|
||||||
(context as Element).markNeedsBuild();
|
(context as Element).markNeedsBuild();
|
||||||
},
|
},
|
||||||
title: Text(const ['头像违规', '昵称违规', '签名违规'][index]),
|
title: Row(
|
||||||
),
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
checked
|
||||||
|
? Icon(
|
||||||
|
size: 22,
|
||||||
|
Icons.check_box,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
size: 22,
|
||||||
|
Icons.check_box_outline_blank,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_reason[index],
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text('举报理由(单选,非必选)'),
|
const Padding(
|
||||||
|
padding: .only(left: 18),
|
||||||
|
child: Text('举报理由(单选,非必选)'),
|
||||||
|
),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) => RadioGroup<int>(
|
builder: (context) => Column(
|
||||||
onChanged: (v) {
|
crossAxisAlignment: .start,
|
||||||
reasonV2 = v;
|
children: List.generate(
|
||||||
(context as Element).markNeedsBuild();
|
_reasonV2.length,
|
||||||
},
|
(index) {
|
||||||
groupValue: reasonV2,
|
final checked = index == reasonV2;
|
||||||
child: Column(
|
return ListTile(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: List.generate(
|
|
||||||
5,
|
|
||||||
(index) => RadioListTile<int>(
|
|
||||||
toggleable: true,
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
contentPadding: const EdgeInsets.only(left: 4),
|
|
||||||
dense: true,
|
dense: true,
|
||||||
value: index,
|
minTileHeight: 40,
|
||||||
title: Text(
|
onTap: () {
|
||||||
const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
|
if (checked) {
|
||||||
|
reasonV2 = null;
|
||||||
|
} else {
|
||||||
|
reasonV2 = index;
|
||||||
|
}
|
||||||
|
(context as Element).markNeedsBuild();
|
||||||
|
},
|
||||||
|
title: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
checked
|
||||||
|
? Icon(
|
||||||
|
size: 22,
|
||||||
|
Icons.radio_button_checked,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
size: 22,
|
||||||
|
Icons.radio_button_off,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_reasonV2[index],
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -96,21 +146,16 @@ Future<void> showMemberReportDialog(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
if (reason.isEmpty) {
|
if (reason.isEmpty) {
|
||||||
SmartDialog.showToast('至少选择一项作为举报内容');
|
SmartDialog.showToast('至少选择一项作为举报内容');
|
||||||
} else {
|
} else {
|
||||||
Get.back();
|
Get.back();
|
||||||
var result = await MemberHttp.reportMember(
|
MemberHttp.reportMember(
|
||||||
mid,
|
mid,
|
||||||
reason: reason.join(','),
|
reason: reason.join(','),
|
||||||
reasonV2: reasonV2 != null ? reasonV2! + 1 : null,
|
reasonV2: reasonV2 != null ? reasonV2! + 1 : null,
|
||||||
);
|
);
|
||||||
if (result['msg'] is String && result['msg'].isNotEmpty) {
|
|
||||||
SmartDialog.showToast(result['msg']);
|
|
||||||
} else {
|
|
||||||
SmartDialog.showToast('举报失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('确定'),
|
child: const Text('确定'),
|
||||||
|
|||||||
@@ -3,62 +3,131 @@ import 'dart:math';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
|
class DisabledIcon extends SingleChildRenderObjectWidget {
|
||||||
final Color? color;
|
|
||||||
final double lineLengthScale;
|
|
||||||
final StrokeCap strokeCap;
|
|
||||||
|
|
||||||
const DisabledIcon({
|
const DisabledIcon({
|
||||||
super.key,
|
super.key,
|
||||||
required T child,
|
required Widget super.child,
|
||||||
|
this.disable = false,
|
||||||
this.color,
|
this.color,
|
||||||
double? lineLengthScale,
|
this.iconSize,
|
||||||
StrokeCap? strokeCap,
|
this.lineLengthScale = 0.9,
|
||||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
this.strokeCap = .butt,
|
||||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
});
|
||||||
super(child: child);
|
|
||||||
|
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
|
@override
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
late final iconTheme = IconTheme.of(context);
|
||||||
|
final icon = _icon;
|
||||||
return RenderMaskedIcon(
|
return RenderMaskedIcon(
|
||||||
color ??
|
disable: disable,
|
||||||
(child is Icon
|
iconSize: iconSize ?? icon?.size ?? iconTheme.size ?? 24.0,
|
||||||
? (child as Icon).color ?? IconTheme.of(context).color!
|
color: color ?? icon?.color ?? iconTheme.color!,
|
||||||
: IconTheme.of(context).color!),
|
strokeCap: strokeCap,
|
||||||
lineLengthScale,
|
lineLengthScale: lineLengthScale,
|
||||||
strokeCap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
class RenderMaskedIcon extends RenderProxyBox {
|
||||||
final Color color;
|
RenderMaskedIcon({
|
||||||
final double lineLengthScale;
|
required bool disable,
|
||||||
final StrokeCap strokeCap;
|
required double iconSize,
|
||||||
|
required Color color,
|
||||||
|
required StrokeCap strokeCap,
|
||||||
|
required double lineLengthScale,
|
||||||
|
}) : _disable = disable,
|
||||||
|
_iconSize = iconSize,
|
||||||
|
_color = color,
|
||||||
|
_strokeCap = strokeCap,
|
||||||
|
_lineLengthScale = 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
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
final strokeWidth = size.width / 12;
|
if (!disable) {
|
||||||
|
return super.paint(context, offset);
|
||||||
|
}
|
||||||
|
|
||||||
final canvas = context.canvas;
|
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 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(
|
final path = Path.combine(
|
||||||
PathOperation.union,
|
PathOperation.union,
|
||||||
Path() // bottom
|
Path() // bottom
|
||||||
@@ -77,7 +146,7 @@ class RenderMaskedIcon extends RenderProxyBox {
|
|||||||
..clipPath(path, doAntiAlias: false);
|
..clipPath(path, doAntiAlias: false);
|
||||||
super.paint(context, offset);
|
super.paint(context, offset);
|
||||||
|
|
||||||
context.canvas.restore();
|
canvas.restore();
|
||||||
|
|
||||||
final linePaint = Paint()
|
final linePaint = Paint()
|
||||||
..color = color
|
..color = color
|
||||||
@@ -95,8 +164,3 @@ class RenderMaskedIcon extends RenderProxyBox {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DisabledIconExt on Icon {
|
|
||||||
DisabledIcon<Icon> disable([double? lineLengthScale]) =>
|
|
||||||
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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/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/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
|
|
||||||
@@ -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 double width,
|
||||||
|
}) : _width = width;
|
||||||
|
|
||||||
|
final double _width;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||||
|
return super.hitTestChildren(result, position: position) ||
|
||||||
|
position.dx <= _width;
|
||||||
|
}
|
||||||
|
}
|
||||||
385
lib/common/widgets/flutter/chat_list_view.dart
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
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,
|
||||||
|
super.cacheExtent,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,10 +17,12 @@ library;
|
|||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart'
|
||||||
|
hide DraggableScrollableSheet, LayoutBuilder;
|
||||||
|
|
||||||
/// Controls a [DraggableScrollableSheet].
|
/// Controls a [DraggableScrollableSheet].
|
||||||
///
|
///
|
||||||
@@ -112,11 +114,10 @@ class DraggableScrollableController extends ChangeNotifier {
|
|||||||
_assertAttached();
|
_assertAttached();
|
||||||
assert(size >= 0 && size <= 1);
|
assert(size >= 0 && size <= 1);
|
||||||
assert(duration != Duration.zero);
|
assert(duration != Duration.zero);
|
||||||
final AnimationController animationController =
|
final animationController = AnimationController.unbounded(
|
||||||
AnimationController.unbounded(
|
vsync: _attachedController!.position.context.vsync,
|
||||||
vsync: _attachedController!.position.context.vsync,
|
value: _attachedController!.extent.currentSize,
|
||||||
value: _attachedController!.extent.currentSize,
|
);
|
||||||
);
|
|
||||||
_animationControllers.add(animationController);
|
_animationControllers.add(animationController);
|
||||||
_attachedController!.position.goIdle();
|
_attachedController!.position.goIdle();
|
||||||
// This disables any snapping until the next user interaction with the sheet.
|
// This disables any snapping until the next user interaction with the sheet.
|
||||||
@@ -583,7 +584,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<double> _impliedSnapSizes() {
|
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];
|
final double snapSize = widget.snapSizes![index];
|
||||||
assert(
|
assert(
|
||||||
snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
|
snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
|
||||||
@@ -684,11 +685,11 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
|||||||
// have changed when the widget was updated.
|
// have changed when the widget was updated.
|
||||||
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||||
for (
|
for (
|
||||||
int index = 0;
|
var index = 0;
|
||||||
index < _scrollController.positions.length;
|
index < _scrollController.positions.length;
|
||||||
index++
|
index++
|
||||||
) {
|
) {
|
||||||
final _DraggableScrollableSheetScrollPosition position =
|
final position =
|
||||||
_scrollController.positions.elementAt(index)
|
_scrollController.positions.elementAt(index)
|
||||||
as _DraggableScrollableSheetScrollPosition;
|
as _DraggableScrollableSheetScrollPosition;
|
||||||
position.goBallistic(0);
|
position.goBallistic(0);
|
||||||
@@ -702,7 +703,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
|||||||
.asMap()
|
.asMap()
|
||||||
.keys
|
.keys
|
||||||
.map((int index) {
|
.map((int index) {
|
||||||
final String snapSizeString = widget.snapSizes![index].toString();
|
final snapSizeString = widget.snapSizes![index].toString();
|
||||||
if (index == invalidIndex) {
|
if (index == invalidIndex) {
|
||||||
return '>>> $snapSizeString <<<';
|
return '>>> $snapSizeString <<<';
|
||||||
}
|
}
|
||||||
@@ -917,14 +918,10 @@ class _DraggableScrollableSheetScrollPosition
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final AnimationController ballisticController =
|
final ballisticController = AnimationController.unbounded(
|
||||||
AnimationController.unbounded(
|
debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'),
|
||||||
debugLabel: objectRuntimeType(
|
vsync: context.vsync,
|
||||||
this,
|
);
|
||||||
'_DraggableScrollableSheetPosition',
|
|
||||||
),
|
|
||||||
vsync: context.vsync,
|
|
||||||
);
|
|
||||||
_ballisticControllers.add(ballisticController);
|
_ballisticControllers.add(ballisticController);
|
||||||
|
|
||||||
double lastPosition = extent.currentPixels;
|
double lastPosition = extent.currentPixels;
|
||||||
@@ -1080,8 +1077,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
assert(widget is _InheritedResetNotifier);
|
assert(widget is _InheritedResetNotifier);
|
||||||
final _InheritedResetNotifier inheritedNotifier =
|
final inheritedNotifier = widget as _InheritedResetNotifier;
|
||||||
widget as _InheritedResetNotifier;
|
|
||||||
final bool wasCalled = inheritedNotifier.notifier!._wasCalled;
|
final bool wasCalled = inheritedNotifier.notifier!._wasCalled;
|
||||||
inheritedNotifier.notifier!._wasCalled = false;
|
inheritedNotifier.notifier!._wasCalled = false;
|
||||||
return wasCalled;
|
return wasCalled;
|
||||||
@@ -1158,6 +1154,10 @@ class _SnappingSimulation extends Simulation {
|
|||||||
return pixelSnapSizes.first;
|
return pixelSnapSizes.first;
|
||||||
}
|
}
|
||||||
final double nextSize = pixelSnapSizes[indexOfNextSize];
|
final double nextSize = pixelSnapSizes[indexOfNextSize];
|
||||||
|
// If already snapped - keep this as target size
|
||||||
|
if (nextSize == position) {
|
||||||
|
return nextSize;
|
||||||
|
}
|
||||||
final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
|
final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
|
||||||
if (initialVelocity.abs() <= tolerance.velocity) {
|
if (initialVelocity.abs() <= tolerance.velocity) {
|
||||||
// If velocity is zero, snap to the nearest snap size with the minimum velocity.
|
// If velocity is zero, snap to the nearest snap size with the minimum velocity.
|
||||||