mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-14 05:03:57 +08:00
Compare commits
485 Commits
1.1.5-pre5
...
2.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6341660788 | ||
|
|
a1dbcae93e | ||
|
|
1526137a64 | ||
|
|
3097b56816 | ||
|
|
db74eccf77 | ||
|
|
14890d342a | ||
|
|
51163dd985 | ||
|
|
f0d9b3a9a7 | ||
|
|
8f3707fbf1 | ||
|
|
f52bbe9804 | ||
|
|
3ec54868d0 | ||
|
|
c0b55f9af3 | ||
|
|
279f21857d | ||
|
|
b897103af0 | ||
|
|
353664fbd4 | ||
|
|
de3505ce07 | ||
|
|
cdc1720358 | ||
|
|
904d210ba2 | ||
|
|
db8dd85b63 | ||
|
|
8ad130567e | ||
|
|
7eb21bc5a2 | ||
|
|
ea4316a847 | ||
|
|
2bbc97a950 | ||
|
|
0178d105ba | ||
|
|
771fa75f48 | ||
|
|
82483b33fc | ||
|
|
886c53c7d8 | ||
|
|
f0050dd6e6 | ||
|
|
e6a2f65b4e | ||
|
|
2fc3f9864f | ||
|
|
64c05a1b06 | ||
|
|
7c4e20f96c | ||
|
|
ace286753c | ||
|
|
f0430eba9f | ||
|
|
bbcceb72a7 | ||
|
|
be4fa6ad2c | ||
|
|
50e1f77e10 | ||
|
|
ba56b45038 | ||
|
|
b4b3764e5f | ||
|
|
2220372e4f | ||
|
|
0957dfc66e | ||
|
|
9578f948b4 | ||
|
|
1724f0d202 | ||
|
|
2bebf200df | ||
|
|
fc7fc18b14 | ||
|
|
8f00ca5680 | ||
|
|
236b524445 | ||
|
|
ae59d257c3 | ||
|
|
662ccfcf0a | ||
|
|
b7ab3655c4 | ||
|
|
eda04b32a4 | ||
|
|
9b1ae39922 | ||
|
|
d1497115da | ||
|
|
7f2682bb7b | ||
|
|
d6579b29ae | ||
|
|
8a8aa6c1e0 | ||
|
|
ed66a4655b | ||
|
|
e04affd0fe | ||
|
|
e293083492 | ||
|
|
7f39f36c75 | ||
|
|
565819febe | ||
|
|
af150118a1 | ||
|
|
470e519a2b | ||
|
|
d73588f1fd | ||
|
|
ffbbd8e702 | ||
|
|
a1815c4cc7 | ||
|
|
b9e543f26b | ||
|
|
0788a4de2d | ||
|
|
b0c6e2f5cd | ||
|
|
9489d8a7ca | ||
|
|
aee4424dbf | ||
|
|
96f9972895 | ||
|
|
6ddf282555 | ||
|
|
e98b2b69bb | ||
|
|
448192b635 | ||
|
|
6cda3a1880 | ||
|
|
99128b2641 | ||
|
|
b8098fe067 | ||
|
|
9fef3284db | ||
|
|
f2b0a3a5ed | ||
|
|
3090cfc6f9 | ||
|
|
98ce99202e | ||
|
|
fddf46a90a | ||
|
|
a5231a55b8 | ||
|
|
b8cae015d7 | ||
|
|
3b09534320 | ||
|
|
702cf988d3 | ||
|
|
5586d12b1f | ||
|
|
4683939364 | ||
|
|
f825f87dc1 | ||
|
|
4ad422c3ea | ||
|
|
c01318c066 | ||
|
|
01a74e191a | ||
|
|
a1f15b5da5 | ||
|
|
1e83a23c5c | ||
|
|
2d69c05f33 | ||
|
|
7a2dbe68c7 | ||
|
|
db08af6ca5 | ||
|
|
fefb5c837b | ||
|
|
a88429d6d7 | ||
|
|
cbe99a32eb | ||
|
|
b65d10ac5f | ||
|
|
868f7f5055 | ||
|
|
e843684109 | ||
|
|
631197e3b9 | ||
|
|
381c385726 | ||
|
|
077255e776 | ||
|
|
0bcc1a7f12 | ||
|
|
9b145b525a | ||
|
|
b61a54bf9b | ||
|
|
cf103a09c1 | ||
|
|
a802bc1cdf | ||
|
|
8d312d8cf1 | ||
|
|
6738142ac0 | ||
|
|
3d99e6c761 | ||
|
|
f9f52e918a | ||
|
|
6108290b4b | ||
|
|
8bae275120 | ||
|
|
0504011ba0 | ||
|
|
dc9d4f9eed | ||
|
|
187c92d691 | ||
|
|
9c7b18710c | ||
|
|
1dbc54f063 | ||
|
|
348bc8b920 | ||
|
|
a375d8525f | ||
|
|
e3e423f9b1 | ||
|
|
62048992be | ||
|
|
ec9498a2ca | ||
|
|
1d35abef63 | ||
|
|
889f6d01c2 | ||
|
|
d9c47be2a9 | ||
|
|
cf44036589 | ||
|
|
7276cde48a | ||
|
|
6782bee11a | ||
|
|
b55e102dc3 | ||
|
|
65ad8a0fdc | ||
|
|
fdb3bf3edc | ||
|
|
95506ad896 | ||
|
|
348b2533dc | ||
|
|
2bdab71138 | ||
|
|
e707764f84 | ||
|
|
4a3d827f7a | ||
|
|
e88cd12dfa | ||
|
|
ee04978e0c | ||
|
|
d15ad4911d | ||
|
|
14b6c115b5 | ||
|
|
ee188da6b0 | ||
|
|
998b70cd87 | ||
|
|
7563a52bed | ||
|
|
7e81fae2bc | ||
|
|
639dfac8af | ||
|
|
d8950adb64 | ||
|
|
9092db86ca | ||
|
|
d7d9655f81 | ||
|
|
a63ca93762 | ||
|
|
243178c112 | ||
|
|
dcb3a02da8 | ||
|
|
b1c0eca328 | ||
|
|
e3a1eb5c87 | ||
|
|
736478b1c5 | ||
|
|
12919804dc | ||
|
|
888b3d8173 | ||
|
|
1e6b0f0b53 | ||
|
|
aa3e5a4737 | ||
|
|
3f3d54fd27 | ||
|
|
a142b15344 | ||
|
|
651e79ce26 | ||
|
|
9b93ce84ab | ||
|
|
dfa258b9e6 | ||
|
|
a5efca4e1f | ||
|
|
1fe84d1d34 | ||
|
|
b978ff5649 | ||
|
|
fa85ae47ac | ||
|
|
3209ecd0ba | ||
|
|
807de41ff0 | ||
|
|
d273e72a44 | ||
|
|
2c0597175d | ||
|
|
85292a3df2 | ||
|
|
9c7c6f9e4e | ||
|
|
511ff71f5f | ||
|
|
e104982246 | ||
|
|
e7e79eb62a | ||
|
|
352e314ee1 | ||
|
|
e9dafbc227 | ||
|
|
96727469ac | ||
|
|
c70c9829c0 | ||
|
|
beb7eb1aea | ||
|
|
8e726f49b2 | ||
|
|
007375371e | ||
|
|
6d79551566 | ||
|
|
483953cf56 | ||
|
|
fbf7116edf | ||
|
|
6c164d81e3 | ||
|
|
d0789734ec | ||
|
|
f3bd305337 | ||
|
|
5ab7000716 | ||
|
|
dc1c33f086 | ||
|
|
920c51100a | ||
|
|
05a385d69e | ||
|
|
9411785d26 | ||
|
|
ed2bd069ee | ||
|
|
0460030a2b | ||
|
|
7e570d11d8 | ||
|
|
32cd3209d0 | ||
|
|
0cb07aef1c | ||
|
|
0c65605ac0 | ||
|
|
8234b7ac92 | ||
|
|
4ac855d393 | ||
|
|
7381939c0f | ||
|
|
a380bcd96a | ||
|
|
d253ef468b | ||
|
|
e8145ef65a | ||
|
|
0c175abc0b | ||
|
|
946a5a1e47 | ||
|
|
29e7e0e556 | ||
|
|
cc1704a021 | ||
|
|
7ab2cf973f | ||
|
|
32386bf146 | ||
|
|
40269da391 | ||
|
|
42e082bbc6 | ||
|
|
1ad710c1cf | ||
|
|
cfa925549e | ||
|
|
ca387787b3 | ||
|
|
29a9b22c29 | ||
|
|
672375b925 | ||
|
|
c099738802 | ||
|
|
50561b8dc1 | ||
|
|
2596859778 | ||
|
|
3d453bafdb | ||
|
|
18e0b93ca7 | ||
|
|
7260a387f9 | ||
|
|
37fa165f59 | ||
|
|
8f08104f37 | ||
|
|
6ee4deab05 | ||
|
|
77fff92939 | ||
|
|
8964197b73 | ||
|
|
dbc7bcd0dd | ||
|
|
207ad2753c | ||
|
|
d6e6e52df2 | ||
|
|
9442b17d63 | ||
|
|
058ff44e39 | ||
|
|
48c7dc0eed | ||
|
|
99634a66ab | ||
|
|
21fad89cde | ||
|
|
5979ddb60c | ||
|
|
bcbfe5c849 | ||
|
|
1640732f5d | ||
|
|
9567910611 | ||
|
|
d1713504a0 | ||
|
|
bce73d9f16 | ||
|
|
6f30d2e331 | ||
|
|
556bda0d68 | ||
|
|
9d5eb55e26 | ||
|
|
110469961d | ||
|
|
fa348db7c5 | ||
|
|
3eac565b5e | ||
|
|
af40e489bc | ||
|
|
361eb4c614 | ||
|
|
7ace981f24 | ||
|
|
bfb2becb2d | ||
|
|
038f03a4e7 | ||
|
|
219228f8b5 | ||
|
|
1f64de5954 | ||
|
|
e9b5cffa91 | ||
|
|
68872f7b14 | ||
|
|
bd158619a4 | ||
|
|
310f497c30 | ||
|
|
30ee413852 | ||
|
|
0ab07a713e | ||
|
|
7eaf05839a | ||
|
|
777c3c2278 | ||
|
|
b9b54ce4f7 | ||
|
|
92e5fae29c | ||
|
|
05e8ded86a | ||
|
|
7a65b777c9 | ||
|
|
0b1f6c4d0e | ||
|
|
923af32c96 | ||
|
|
4eae7e698f | ||
|
|
5a61dbe30c | ||
|
|
036dbcaf21 | ||
|
|
bd97f9a500 | ||
|
|
33278a74b2 | ||
|
|
397f887b91 | ||
|
|
ebe793ccfc | ||
|
|
68464e4e34 | ||
|
|
395893fc7d | ||
|
|
f5657d2d4c | ||
|
|
a3ddc83430 | ||
|
|
d2f8aff421 | ||
|
|
25148509d2 | ||
|
|
2879d0dc00 | ||
|
|
90349189ee | ||
|
|
bdc524e486 | ||
|
|
cb58822009 | ||
|
|
4a2679a589 | ||
|
|
09bd1edeb3 | ||
|
|
00da3c4a0e | ||
|
|
c40d794180 | ||
|
|
34a839d9e2 | ||
|
|
f06d0605ce | ||
|
|
ef975de624 | ||
|
|
d10c737a38 | ||
|
|
28b69a06fa | ||
|
|
069cf555ea | ||
|
|
836ab311d6 | ||
|
|
dbc11c36df | ||
|
|
fffce10b31 | ||
|
|
de85e82bfa | ||
|
|
9855b35b65 | ||
|
|
5a0b045a1f | ||
|
|
c226f8f6df | ||
|
|
fd06fa9cc4 | ||
|
|
2b5f111fb1 | ||
|
|
9f5ce5ae37 | ||
|
|
3d95165d46 | ||
|
|
cfb72f27ac | ||
|
|
bcacc41db3 | ||
|
|
b2da99e334 | ||
|
|
041af37bb0 | ||
|
|
80e007bac6 | ||
|
|
87c7699324 | ||
|
|
11912c5f62 | ||
|
|
236a8b3023 | ||
|
|
63e4bac204 | ||
|
|
2e11247af4 | ||
|
|
13f377f680 | ||
|
|
b9d594bc8b | ||
|
|
2a52157c3f | ||
|
|
a037d8e793 | ||
|
|
49b7ea14c3 | ||
|
|
0a40d11133 | ||
|
|
dff6b6486d | ||
|
|
b51c646415 | ||
|
|
25acf3a9bb | ||
|
|
7ec90e9a22 | ||
|
|
645ce0b7b3 | ||
|
|
864fef5881 | ||
|
|
eea232c6db | ||
|
|
25fca498fc | ||
|
|
c9a02f9c74 | ||
|
|
99602eea95 | ||
|
|
b5fe0faeec | ||
|
|
20a36e8f9a | ||
|
|
161bf2eedb | ||
|
|
fcf4e72d8e | ||
|
|
b46cb69df4 | ||
|
|
43c7620b4c | ||
|
|
1a8f65b075 | ||
|
|
259e7080f8 | ||
|
|
7da6f05a50 | ||
|
|
521ca3ad18 | ||
|
|
31e5692dff | ||
|
|
191bcbc525 | ||
|
|
a0f3b3e442 | ||
|
|
5bcd822251 | ||
|
|
d80324655e | ||
|
|
952d168022 | ||
|
|
af723e161c | ||
|
|
3ff521e103 | ||
|
|
b4a5d985f5 | ||
|
|
1e0e2d2d6e | ||
|
|
d7f7611af4 | ||
|
|
11cdb67050 | ||
|
|
53cf9d54c4 | ||
|
|
2e73688688 | ||
|
|
ce5e85e64b | ||
|
|
02e0d34127 | ||
|
|
830f3b60e0 | ||
|
|
b4fb7d14d4 | ||
|
|
ab1e5cb62a | ||
|
|
348a9e014e | ||
|
|
0baf3fcd36 | ||
|
|
13818533a7 | ||
|
|
0dd3689d65 | ||
|
|
23b6850778 | ||
|
|
d8ca89ac8f | ||
|
|
ae06d5f7f2 | ||
|
|
62506d3eb5 | ||
|
|
f7c61d63a0 | ||
|
|
f46437f891 | ||
|
|
1cd949c365 | ||
|
|
bc5ce11449 | ||
|
|
cef4beaa0c | ||
|
|
02bd68f697 | ||
|
|
2bc3275c1f | ||
|
|
ec107063c3 | ||
|
|
4c2fd38d6c | ||
|
|
1a6653ba93 | ||
|
|
74d5e03a34 | ||
|
|
2b4b1debe6 | ||
|
|
17883eb77e | ||
|
|
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 |
50
.github/workflows/build.yml
vendored
50
.github/workflows/build.yml
vendored
@@ -49,14 +49,14 @@ on:
|
||||
|
||||
jobs:
|
||||
android:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_android == 'true' }}
|
||||
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_android == 'true' }}
|
||||
name: Release Android
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -78,9 +78,9 @@ jobs:
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: apply bottom sheet patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 android
|
||||
continue-on-error: true
|
||||
|
||||
- name: Write key
|
||||
@@ -95,13 +95,20 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set and Extract version
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1 android
|
||||
|
||||
- name: flutter build apk
|
||||
- name: Flutter Build Release Apk
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub
|
||||
|
||||
- name: rename
|
||||
- name: Flutter Build Dev Apk
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
flutter build apk --release --split-per-abi --android-project-arg dev=1 --pub
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
||||
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
|
||||
@@ -115,53 +122,52 @@ jobs:
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_android_*.apk
|
||||
files: PiliPlus_android_*.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Android_arm64-v8a
|
||||
path: |
|
||||
PiliPlus_android_*_arm64-v8a.apk
|
||||
path: PiliPlus_android_*_arm64-v8a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Android_armeabi-v7a
|
||||
path: |
|
||||
PiliPlus_android_*_armeabi-v7a.apk
|
||||
path: PiliPlus_android_*_armeabi-v7a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Android_x86_64
|
||||
path: |
|
||||
PiliPlus_android_*_x86_64.apk
|
||||
path: PiliPlus_android_*_x86_64.apk
|
||||
|
||||
ios:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_ios == 'true' }}
|
||||
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_ios == 'true' }}
|
||||
uses: ./.github/workflows/ios.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
mac:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_mac == 'true' }}
|
||||
if: ${{ github.event.inputs.build_mac == 'true' }}
|
||||
uses: ./.github/workflows/mac.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
win_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
|
||||
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_win_x64 == 'true' }}
|
||||
uses: ./.github/workflows/win_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
linux_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_linux_x64 == 'true' }}
|
||||
if: ${{ github.event.inputs.build_linux_x64 == 'true' }}
|
||||
uses: ./.github/workflows/linux_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
|
||||
16
.github/workflows/ios.yml
vendored
16
.github/workflows/ios.yml
vendored
@@ -13,10 +13,10 @@ on:
|
||||
jobs:
|
||||
build-macos-app:
|
||||
name: Release IOS
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -30,11 +30,18 @@ jobs:
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 iOS
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build iOS
|
||||
run: |
|
||||
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
|
||||
# make AltSign happy...
|
||||
find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \;
|
||||
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/Runner.app
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
@@ -46,7 +53,8 @@ jobs:
|
||||
PiliPlus_ios_*.ipa
|
||||
|
||||
- name: Upload ios release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: iOS-release
|
||||
path: PiliPlus_ios_*.ipa
|
||||
|
||||
45
.github/workflows/linux_x64.yml
vendored
45
.github/workflows/linux_x64.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -51,6 +51,11 @@ jobs:
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 Linux
|
||||
continue-on-error: true
|
||||
|
||||
#TODO: deb and rpm packages need to be build
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
|
||||
@@ -70,7 +75,7 @@ jobs:
|
||||
printf "复制文件...\n"
|
||||
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
||||
cp -r ../assets/linux/DEBIAN .
|
||||
cp ../assets/linux/piliplus.desktop usr/share/applications
|
||||
cp ../assets/linux/com.example.piliplus.desktop usr/share/applications
|
||||
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
printf "修改控制文件...\n"
|
||||
@@ -112,7 +117,7 @@ jobs:
|
||||
SRC_DIR="$PWD/piliplus-${{ env.version }}"
|
||||
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
|
||||
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
|
||||
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
|
||||
cp assets/linux/com.example.piliplus.desktop "$SRC_DIR/assets/com.example.piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
|
||||
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
|
||||
|
||||
@@ -145,7 +150,7 @@ jobs:
|
||||
|
||||
# 桌面集成
|
||||
mkdir -p %{buildroot}/usr/share/applications
|
||||
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
|
||||
install -m 644 assets/com.example.piliplus.desktop %{buildroot}/usr/share/applications/com.example.piliplus.desktop
|
||||
|
||||
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
|
||||
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
@@ -161,7 +166,7 @@ jobs:
|
||||
%files
|
||||
/opt/PiliPlus
|
||||
/usr/bin/piliplus
|
||||
/usr/share/applications/piliplus.desktop
|
||||
/usr/share/applications/com.example.piliplus.desktop
|
||||
/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
%changelog
|
||||
@@ -179,7 +184,7 @@ jobs:
|
||||
|
||||
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
printf "下载 appimagetool...\n"
|
||||
@@ -195,13 +200,13 @@ jobs:
|
||||
|
||||
printf "复制应用文件...\n"
|
||||
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
|
||||
|
||||
|
||||
printf "复制桌面文件和图标...\n"
|
||||
cp assets/linux/piliplus.desktop "$APPDIR/piliplus.desktop"
|
||||
cp assets/linux/piliplus.desktop "$APPDIR/usr/share/applications/piliplus.desktop"
|
||||
cp assets/linux/com.example.piliplus.desktop "$APPDIR/com.example.piliplus.desktop"
|
||||
cp assets/linux/com.example.piliplus.desktop "$APPDIR/usr/share/applications/com.example.piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$APPDIR/piliplus.png"
|
||||
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
|
||||
|
||||
|
||||
printf "创建 AppRun 启动脚本...\n"
|
||||
cat > "$APPDIR/AppRun" <<'APPRUN_EOF'
|
||||
#!/bin/bash
|
||||
@@ -214,12 +219,12 @@ jobs:
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
printf "修改桌面文件中的 Exec 路径...\n"
|
||||
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
|
||||
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
|
||||
|
||||
printf "打包 AppImage...\n"
|
||||
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
|
||||
|
||||
|
||||
printf "完成: PiliPlus_linux_%s_amd64.AppImage\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
@@ -236,25 +241,29 @@ jobs:
|
||||
PiliPlus_linux_*.AppImage
|
||||
|
||||
- name: Upload linux targz package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_targz_amd64_packege
|
||||
path: PiliPlus_linux_*.tar.gz
|
||||
|
||||
- name: Upload linux deb package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_deb_amd64_package
|
||||
path: PiliPlus_linux_*.deb
|
||||
|
||||
- name: Upload linux rpm package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_rpm_amd64_package
|
||||
path: PiliPlus_linux_*.rpm
|
||||
|
||||
|
||||
- name: Upload linux AppImage package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Linux_AppImage_amd64_package
|
||||
path: PiliPlus_linux_*.AppImage
|
||||
|
||||
14
.github/workflows/mac.yml
vendored
14
.github/workflows/mac.yml
vendored
@@ -13,10 +13,10 @@ on:
|
||||
jobs:
|
||||
build-mac-app:
|
||||
name: Release Mac
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -30,13 +30,18 @@ jobs:
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 macOS
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build Mac
|
||||
run: flutter build macos --release --dart-define-from-file=pili_release.json
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
npm install --global create-dmg
|
||||
create-dmg build/macos/Build/Products/Release/PiliPlus.app
|
||||
create-dmg build/macos/Build/Products/Release/PiliPlus.app || true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Rename DMG
|
||||
@@ -52,7 +57,8 @@ jobs:
|
||||
PiliPlus_macos_*.dmg
|
||||
|
||||
- name: Upload macos release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: macOS-release
|
||||
path: PiliPlus_macos_*.dmg
|
||||
|
||||
20
.github/workflows/win_x64.yml
vendored
20
.github/workflows/win_x64.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -26,6 +26,11 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Apply Patch
|
||||
shell: pwsh
|
||||
run: lib/scripts/patch.ps1 windows
|
||||
continue-on-error: true
|
||||
|
||||
- name: Add fastforge and Inno Setup
|
||||
run: |
|
||||
dart pub global activate fastforge
|
||||
@@ -52,9 +57,8 @@ jobs:
|
||||
mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe
|
||||
|
||||
- name: Compress
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
run: |
|
||||
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64.zip"
|
||||
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64_portable.zip"
|
||||
shell: pwsh
|
||||
|
||||
- name: Release
|
||||
@@ -68,13 +72,15 @@ jobs:
|
||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
- name: Upload windows file release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Windows-file-x64-release
|
||||
path: Release
|
||||
path: PiliPlus_windows_*.zip
|
||||
|
||||
- name: Upload windows setup release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
archive: false
|
||||
name: Windows-setup-x64-release
|
||||
path: PiliPlus-Win-Setup
|
||||
path: PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -146,4 +146,4 @@ pili_release.json
|
||||
|
||||
dist
|
||||
|
||||
test.dart
|
||||
test*.dart
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 编辑动态
|
||||
- [x] DLNA 投屏
|
||||
- [x] 离线缓存/播放
|
||||
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
@@ -218,8 +219,8 @@
|
||||
|
||||
## 声明
|
||||
|
||||
此项目(PiliPlus)是个人为了兴趣而开发, 仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
||||
此项目(PiliPlus)是个人为了兴趣而开发,仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集,不提供任何破解内容。
|
||||
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
|
||||
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
|
||||
本仓库做了更激进的修改,感谢原作者的开源精神。
|
||||
|
||||
@@ -12,7 +12,8 @@ include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/grpc/bilibili/**
|
||||
- lib/grpc/google/**
|
||||
# - lib/grpc/google/**
|
||||
# - lib/common/widgets/flutter/**
|
||||
|
||||
formatter:
|
||||
trailing_commas: preserve
|
||||
@@ -63,5 +64,19 @@ linter:
|
||||
- use_null_aware_elements
|
||||
- unnecessary_lambdas
|
||||
- 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
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@@ -18,8 +18,10 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -54,10 +56,18 @@ android {
|
||||
signingConfig = config ?: signingConfigs["debug"]
|
||||
}
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
if (project.hasProperty("dev")) {
|
||||
applicationIdSuffix = ".dev"
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "app_name",
|
||||
value = "PiliPlus dev",
|
||||
)
|
||||
}
|
||||
// proguardFiles(
|
||||
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
// "proguard-rules.pro"
|
||||
// )
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.piliplus">
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -16,8 +17,7 @@
|
||||
</queries>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name=
|
||||
"android.support.customtabs.action.CustomTabsService" />
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
@@ -35,56 +35,62 @@
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
<application xmlns:tools="http://schemas.android.com/tools"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
tools:replace="android:allowBackup">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
>
|
||||
android:supportsPictureInPicture="true"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<meta-data
|
||||
android:name="flutter_deeplinking_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="PiliPlus">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="*.bilibili.com"/>
|
||||
<data android:host="*.bilibili.cn"/>
|
||||
<data android:host="*.bilibili.tv"/>
|
||||
<data android:host="bilibili.com"/>
|
||||
<data android:host="bilibili.cn"/>
|
||||
<data android:host="bilibili.tv"/>
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*.bilibili.com" />
|
||||
<data android:host="*.bilibili.cn" />
|
||||
<data android:host="*.bilibili.tv" />
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.cn" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="b23.tv" />
|
||||
<!--<data android:host="live.bilibili.com"/>-->
|
||||
<!--<data android:host="www.bilibili.com"/>-->
|
||||
@@ -100,36 +106,56 @@
|
||||
<intent-filter android:label="PiliPlus">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.example.piliplus.SHORTCUT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bilibili"/>
|
||||
|
||||
<data android:scheme="bilibili" />
|
||||
<data android:host="download" />
|
||||
<data android:host="forward" />
|
||||
<data android:host="comment"
|
||||
<data
|
||||
android:host="comment"
|
||||
android:pathPattern="/detail/.*/.*/.*" />
|
||||
<data android:host="uper" />
|
||||
<data android:host="article"
|
||||
<data
|
||||
android:host="article"
|
||||
android:pathPattern="/readlist" />
|
||||
<data android:host="opus" />
|
||||
<data android:host="advertise" android:path="/home" />
|
||||
<data
|
||||
android:host="advertise"
|
||||
android:path="/home" />
|
||||
<data android:host="clip" />
|
||||
<data android:host="search" android:pathPattern=".*" />
|
||||
<data
|
||||
android:host="search"
|
||||
android:pathPattern=".*" />
|
||||
<data android:host="stardust-search" />
|
||||
<data android:host="music" />
|
||||
<data android:host="cheese" />
|
||||
<data android:host="bangumi"
|
||||
<data
|
||||
android:host="bangumi"
|
||||
android:pathPattern="/season.*" />
|
||||
<data android:host="bangumi" android:pathPattern="/.*" />
|
||||
<data android:host="pictureshow"
|
||||
<data
|
||||
android:host="bangumi"
|
||||
android:pathPattern="/.*" />
|
||||
<data
|
||||
android:host="pictureshow"
|
||||
android:pathPrefix="/creative_center" />
|
||||
<data android:host="cliparea" />
|
||||
<data android:host="im" />
|
||||
<data android:host="im" android:path="/notifications" />
|
||||
<data
|
||||
android:host="im"
|
||||
android:path="/notifications" />
|
||||
<data android:host="following" />
|
||||
<data android:host="following"
|
||||
<data
|
||||
android:host="following"
|
||||
android:pathPattern="/detail/.*" />
|
||||
<data android:host="following"
|
||||
<data
|
||||
android:host="following"
|
||||
android:path="/publishInfo/" />
|
||||
<data android:host="laser" android:pathPattern="/.*" />
|
||||
<data
|
||||
android:host="laser"
|
||||
android:pathPattern="/.*" />
|
||||
<data android:host="livearea" />
|
||||
<data android:host="live" />
|
||||
<data android:host="catalog" />
|
||||
@@ -147,28 +173,44 @@
|
||||
<data android:host="video" />
|
||||
<data android:host="story" />
|
||||
<data android:host="podcast" />
|
||||
<data android:host="main" android:path="/favorite" />
|
||||
<data android:host="pgc" android:path="/theater/match" />
|
||||
<data android:host="pgc" android:path="/theater/square" />
|
||||
<data android:host="m.bilibili.com"
|
||||
<data
|
||||
android:host="main"
|
||||
android:path="/favorite" />
|
||||
<data
|
||||
android:host="pgc"
|
||||
android:path="/theater/match" />
|
||||
<data
|
||||
android:host="pgc"
|
||||
android:path="/theater/square" />
|
||||
<data
|
||||
android:host="m.bilibili.com"
|
||||
android:path="/topic-detail" />
|
||||
<data android:host="article" />
|
||||
<data android:host="pegasus"
|
||||
<data
|
||||
android:host="pegasus"
|
||||
android:pathPattern="/channel/v2/.*" />
|
||||
<data android:host="feed" android:pathPattern="/channel" />
|
||||
<data
|
||||
android:host="feed"
|
||||
android:pathPattern="/channel" />
|
||||
<data android:host="vip" />
|
||||
<data android:host="user_center" android:path="/vip" />
|
||||
<data
|
||||
android:host="user_center"
|
||||
android:path="/vip" />
|
||||
<data android:host="history" />
|
||||
<data android:host="charge" android:path="/rank" />
|
||||
<data
|
||||
android:host="charge"
|
||||
android:path="/rank" />
|
||||
<data android:host="assistant" />
|
||||
<data android:host="feedback" />
|
||||
<data android:host="auth" android:path="/launch" />
|
||||
<data
|
||||
android:host="auth"
|
||||
android:path="/launch" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
@@ -177,32 +219,37 @@
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:theme="@style/Ucrop.CropTheme"/>
|
||||
android:theme="@style/Ucrop.CropTheme" />
|
||||
|
||||
<receiver
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</receiver>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!--
|
||||
Media access permissions.
|
||||
Android 13 or higher.
|
||||
@@ -210,5 +257,5 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
</manifest>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package com.example.piliplus
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.SearchManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
@@ -16,6 +21,7 @@ import com.ryanheise.audioservice.AudioServiceActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.system.exitProcess
|
||||
import java.io.File
|
||||
|
||||
class MainActivity : AudioServiceActivity() {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
@@ -133,6 +139,38 @@ class MainActivity : AudioServiceActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
"createShortcut" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
val shortcutManager =
|
||||
context.getSystemService(ShortcutManager::class.java)
|
||||
if (shortcutManager.isRequestPinShortcutSupported) {
|
||||
val id = call.argument<String>("id")!!
|
||||
val uri = call.argument<String>("uri")!!
|
||||
val label = call.argument<String>("label")!!
|
||||
val icon = call.argument<String>("icon")!!
|
||||
val bitmap = BitmapFactory.decodeFile(icon)
|
||||
val shortcut =
|
||||
ShortcutInfo.Builder(context, id)
|
||||
.setShortLabel(label)
|
||||
.setIcon(Icon.createWithAdaptiveBitmap(bitmap))
|
||||
.setIntent(Intent(Intent.ACTION_VIEW, uri.toUri()))
|
||||
.build()
|
||||
val pinIntent =
|
||||
shortcutManager.createShortcutResultIntent(shortcut)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, 0, pinIntent, PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
shortcutManager.requestPinShortcut(
|
||||
shortcut,
|
||||
pendingIntent.intentSender
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
10
android/app/src/main/res/drawable/download.xml
Normal file
10
android/app/src/main/res/drawable/download.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#515151"
|
||||
android:pathData="M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71zM5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1z" />
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/search.xml
Normal file
10
android/app/src/main/res/drawable/search.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#515151"
|
||||
android:pathData="M15.5 14h-.79l-.28-.27c1.2-1.4 1.82-3.31 1.48-5.34-.47-2.78-2.79-5-5.59-5.34-4.23-.52-7.79 3.04-7.27 7.27.34 2.8 2.56 5.12 5.34 5.59 2.03.34 3.94-.28 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||
</vector>
|
||||
@@ -1,3 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">PiliPlus</string>
|
||||
<string name="search">搜索</string>
|
||||
<string name="offline_video">离线视频</string>
|
||||
</resources>
|
||||
20
android/app/src/main/res/xml-v25/shortcuts.xml
Normal file
20
android/app/src/main/res/xml-v25/shortcuts.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:icon="@drawable/search"
|
||||
android:shortcutId="search"
|
||||
android:shortcutLongLabel="@string/search"
|
||||
android:shortcutShortLabel="@string/search">
|
||||
<intent
|
||||
android:action="com.example.piliplus.SHORTCUT"
|
||||
android:data="bilibili://search" />
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:icon="@drawable/download"
|
||||
android:shortcutId="offline_video"
|
||||
android:shortcutLongLabel="@string/offline_video"
|
||||
android:shortcutShortLabel="@string/offline_video">
|
||||
<intent
|
||||
android:action="com.example.piliplus.SHORTCUT"
|
||||
android:data="bilibili://download" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -20,7 +20,7 @@ pluginManagement {
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.12.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
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
|
||||
chmod +x /usr/bin/piliplus
|
||||
|
||||
if [ $1 == "config" ] && [ -x /usr/binupdate-mime-database ]; then
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/update-mime-database ]; then
|
||||
echo "updating mime database..."
|
||||
update-mime-database /usr/share/mime || true
|
||||
fi
|
||||
|
||||
if [ $1 == "config" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
echo "updating icon cache..."
|
||||
gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true
|
||||
fi
|
||||
|
||||
if [ $1 == "config" ] && [ -x /usr/bin/update-desktop-database ]; then
|
||||
echo "updating desktop database..."
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/update-desktop-database ]; then
|
||||
echo "configure desktop database..."
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
Exec=piliplus
|
||||
Icon=piliplus
|
||||
Terminal=false
|
||||
StartupWMClass=com.example.piliplus
|
||||
Categories=Video;AudioVideo;Player;
|
||||
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
242
ios/Podfile.lock
242
ios/Podfile.lock
@@ -1,141 +1,205 @@
|
||||
PODS:
|
||||
- appscheme (1.0.4):
|
||||
- app_links (7.0.0):
|
||||
- Flutter
|
||||
- audio_service (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- auto_orientation (0.0.1):
|
||||
- battery_plus (1.0.0):
|
||||
- Flutter
|
||||
- chat_bottom_container (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.9)
|
||||
- DKPhotoGallery (0.0.19):
|
||||
- DKPhotoGallery/Core (= 0.0.19)
|
||||
- DKPhotoGallery/Model (= 0.0.19)
|
||||
- DKPhotoGallery/Preview (= 0.0.19)
|
||||
- DKPhotoGallery/Resource (= 0.0.19)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.19):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- 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
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- Toast
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- gt3_flutter_plugin (0.0.8):
|
||||
- gt3_flutter_plugin (0.0.9):
|
||||
- Flutter
|
||||
- GT3Captcha-iOS
|
||||
- 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):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- native_device_orientation (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.1.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- saver_gallery (0.0.1):
|
||||
- Flutter
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.3):
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- status_bar_control (3.2.1):
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- system_proxy (0.0.1):
|
||||
- Flutter
|
||||
- Toast (4.1.0)
|
||||
- FlutterMacOS
|
||||
- SwiftyGif (5.4.5)
|
||||
- TOCropViewController (3.1.1)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- webview_cookie_manager (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
|
||||
- battery_plus (from `.symlinks/plugins/battery_plus/ios`)
|
||||
- chat_bottom_container (from `.symlinks/plugins/chat_bottom_container/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/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`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/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_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`)
|
||||
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- 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`)
|
||||
- webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- GT3Captcha-iOS
|
||||
- ReachabilitySwift
|
||||
- Toast
|
||||
- OrderedSet
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- TOCropViewController
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
appscheme:
|
||||
:path: ".symlinks/plugins/appscheme/ios"
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
audio_service:
|
||||
:path: ".symlinks/plugins/audio_service/ios"
|
||||
:path: ".symlinks/plugins/audio_service/darwin"
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
auto_orientation:
|
||||
:path: ".symlinks/plugins/auto_orientation/ios"
|
||||
battery_plus:
|
||||
:path: ".symlinks/plugins/battery_plus/ios"
|
||||
chat_bottom_container:
|
||||
:path: ".symlinks/plugins/chat_bottom_container/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_mailer:
|
||||
:path: ".symlinks/plugins/flutter_mailer/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_volume_controller:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
gt3_flutter_plugin:
|
||||
: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:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
native_device_orientation:
|
||||
:path: ".symlinks/plugins/native_device_orientation/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
saver_gallery:
|
||||
@@ -144,57 +208,55 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
status_bar_control:
|
||||
:path: ".symlinks/plugins/status_bar_control/ios"
|
||||
system_proxy:
|
||||
:path: ".symlinks/plugins/system_proxy/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
: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:
|
||||
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
|
||||
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
|
||||
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
|
||||
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||
battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977
|
||||
chat_bottom_container: f1eb8323db77a87db50f361142c679f11e892d1b
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
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
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
|
||||
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
|
||||
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
|
||||
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
image_cropper: e405d3e44183f8e8edbec2e49b01ff9c819c7ac8
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
live_photo_maker: 29280ca88323bd5a33aafd00d98624d5cf522176
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
native_device_orientation: e3580675687d5034770da198f6839ebf2122ef94
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
screen_brightness_ios: 9953fd7da5bd480f1a93990daeec2eb42d4f3b52
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
TOCropViewController: 9002a9b12d8104d7478cdc306d80f0efea7fe2c5
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be
|
||||
PODFILE CHECKSUM: f62db4fb414ebdecb264109948f76dfef35fdc3d
|
||||
|
||||
COCOAPODS: 1.14.3
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -48,6 +48,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -2,13 +2,16 @@ import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
application.applicationSupportsShakeToEdit = false // Disable shake to undo
|
||||
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">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
@@ -110,5 +131,13 @@
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>需要访问本地网络以发现和连接 DLNA 投屏设备</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_ssdp._udp</string>
|
||||
<string>_upnp._tcp</string>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class BuildConfig {
|
||||
abstract final class BuildConfig {
|
||||
static const int versionCode = int.fromEnvironment(
|
||||
'pili.code',
|
||||
defaultValue: 1,
|
||||
|
||||
56
lib/common/assets.dart
Normal file
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';
|
||||
|
||||
class StyleString {
|
||||
static const double cardSpace = 8;
|
||||
static const double safeSpace = 12;
|
||||
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
|
||||
static const Radius imgRadius = Radius.circular(10);
|
||||
static const double aspectRatio = 16 / 10;
|
||||
static const bottomSheetRadius = BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
);
|
||||
}
|
||||
|
||||
class Constants {
|
||||
abstract final class Constants {
|
||||
static const appName = 'PiliPlus';
|
||||
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
|
||||
|
||||
@@ -20,9 +7,9 @@ class Constants {
|
||||
static const String appKey = 'dfca71928277209b';
|
||||
// 59b43e04ad6965f34319062b478f83dd TV端
|
||||
static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e';
|
||||
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
||||
static const String thirdApi =
|
||||
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
|
||||
// static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
|
||||
// static const String thirdApi =
|
||||
// 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
|
||||
|
||||
static const String traceId =
|
||||
'11111111111111111111111111111111:1111111111111111:0:0';
|
||||
@@ -40,8 +27,6 @@ class Constants {
|
||||
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
||||
|
||||
static const baseHeaders = {
|
||||
'connection': 'keep-alive',
|
||||
'accept-encoding': 'br,gzip',
|
||||
// 'referer': HttpString.baseUrl,
|
||||
'env': 'prod',
|
||||
'app-key': 'android64',
|
||||
@@ -54,243 +39,6 @@ class Constants {
|
||||
|
||||
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
|
||||
|
||||
// 超分辨率滤镜
|
||||
static const List<String> mpvAnime4KShaders = [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
'Anime4K_Restore_CNN_VL.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_VL.glsl',
|
||||
'Anime4K_AutoDownscalePre_x2.glsl',
|
||||
'Anime4K_AutoDownscalePre_x4.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl',
|
||||
];
|
||||
|
||||
// 超分辨率滤镜 (轻量)
|
||||
static const mpvAnime4KShadersLite = [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
'Anime4K_Restore_CNN_M.glsl',
|
||||
'Anime4K_Restore_CNN_S.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_M.glsl',
|
||||
'Anime4K_AutoDownscalePre_x2.glsl',
|
||||
'Anime4K_AutoDownscalePre_x4.glsl',
|
||||
'Anime4K_Upscale_CNN_x2_S.glsl',
|
||||
];
|
||||
|
||||
//内容来自 https://passport.bilibili.com/web/generic/country/list
|
||||
static const internationalDialingPrefix = [
|
||||
(id: 1, cname: "中国大陆", countryId: 86),
|
||||
(id: 5, cname: "中国香港特别行政区", countryId: 852),
|
||||
(id: 2, cname: "中国澳门特别行政区", countryId: 853),
|
||||
(id: 3, cname: "中国台湾", countryId: 886),
|
||||
(id: 4, cname: "美国", countryId: 1),
|
||||
(id: 6, cname: "比利时", countryId: 32),
|
||||
(id: 7, cname: "澳大利亚", countryId: 61),
|
||||
(id: 8, cname: "法国", countryId: 33),
|
||||
(id: 9, cname: "加拿大", countryId: 1),
|
||||
(id: 10, cname: "日本", countryId: 81),
|
||||
(id: 11, cname: "新加坡", countryId: 65),
|
||||
(id: 12, cname: "韩国", countryId: 82),
|
||||
(id: 13, cname: "马来西亚", countryId: 60),
|
||||
(id: 14, cname: "英国", countryId: 44),
|
||||
(id: 15, cname: "意大利", countryId: 39),
|
||||
(id: 16, cname: "德国", countryId: 49),
|
||||
(id: 18, cname: "俄罗斯", countryId: 7),
|
||||
(id: 19, cname: "新西兰", countryId: 64),
|
||||
(id: 153, cname: "瓦利斯群岛和富图纳群岛", countryId: 1681),
|
||||
(id: 152, cname: "葡萄牙", countryId: 351),
|
||||
(id: 151, cname: "帕劳", countryId: 680),
|
||||
(id: 150, cname: "诺福克岛", countryId: 672),
|
||||
(id: 149, cname: "挪威", countryId: 47),
|
||||
(id: 148, cname: "纽埃岛", countryId: 683),
|
||||
(id: 147, cname: "尼日利亚", countryId: 234),
|
||||
(id: 146, cname: "尼日尔", countryId: 227),
|
||||
(id: 145, cname: "尼加拉瓜", countryId: 505),
|
||||
(id: 144, cname: "尼泊尔", countryId: 977),
|
||||
(id: 143, cname: "瑙鲁", countryId: 674),
|
||||
(id: 154, cname: "格鲁吉亚", countryId: 995),
|
||||
(id: 155, cname: "瑞典", countryId: 46),
|
||||
(id: 165, cname: "沙特阿拉伯", countryId: 966),
|
||||
(id: 164, cname: "桑给巴尔岛", countryId: 259),
|
||||
(id: 163, cname: "塞舌尔共和国", countryId: 248),
|
||||
(id: 162, cname: "塞浦路斯", countryId: 357),
|
||||
(id: 161, cname: "塞内加尔", countryId: 221),
|
||||
(id: 160, cname: "塞拉利昂", countryId: 232),
|
||||
(id: 159, cname: "萨摩亚,东部", countryId: 684),
|
||||
(id: 158, cname: "萨摩亚,西部", countryId: 685),
|
||||
(id: 157, cname: "萨尔瓦多", countryId: 503),
|
||||
(id: 156, cname: "瑞士", countryId: 41),
|
||||
(id: 166, cname: "圣多美和普林西比", countryId: 239),
|
||||
(id: 142, cname: "塞尔维亚", countryId: 381),
|
||||
(id: 141, cname: "南非", countryId: 27),
|
||||
(id: 128, cname: "毛里塔尼亚", countryId: 222),
|
||||
(id: 127, cname: "毛里求斯", countryId: 230),
|
||||
(id: 126, cname: "马歇尔岛", countryId: 692),
|
||||
(id: 125, cname: "马提尼克岛", countryId: 596),
|
||||
(id: 124, cname: "马其顿", countryId: 389),
|
||||
(id: 123, cname: "马里亚纳岛", countryId: 1670),
|
||||
(id: 122, cname: "马里", countryId: 223),
|
||||
(id: 121, cname: "马拉维", countryId: 265),
|
||||
(id: 120, cname: "马耳他", countryId: 356),
|
||||
(id: 119, cname: "马尔代夫", countryId: 960),
|
||||
(id: 129, cname: "蒙古", countryId: 976),
|
||||
(id: 130, cname: "蒙特塞拉特岛", countryId: 1664),
|
||||
(id: 140, cname: "纳米比亚", countryId: 264),
|
||||
(id: 139, cname: "墨西哥", countryId: 52),
|
||||
(id: 138, cname: "莫桑比克", countryId: 258),
|
||||
(id: 137, cname: "摩纳哥", countryId: 377),
|
||||
(id: 136, cname: "摩洛哥", countryId: 212),
|
||||
(id: 135, cname: "摩尔多瓦", countryId: 373),
|
||||
(id: 134, cname: "缅甸", countryId: 95),
|
||||
(id: 133, cname: "密克罗尼西亚", countryId: 691),
|
||||
(id: 132, cname: "秘鲁", countryId: 51),
|
||||
(id: 131, cname: "孟加拉国", countryId: 880),
|
||||
(id: 118, cname: "马达加斯加", countryId: 261),
|
||||
(id: 167, cname: "圣卢西亚", countryId: 1784),
|
||||
(id: 216, cname: "智利", countryId: 56),
|
||||
(id: 203, cname: "牙买加", countryId: 1876),
|
||||
(id: 202, cname: "叙利亚", countryId: 963),
|
||||
(id: 201, cname: "匈牙利", countryId: 36),
|
||||
(id: 200, cname: "科特迪瓦", countryId: 225),
|
||||
(id: 199, cname: "希腊", countryId: 30),
|
||||
(id: 198, cname: "西班牙", countryId: 34),
|
||||
(id: 197, cname: "乌兹别克斯坦", countryId: 998),
|
||||
(id: 196, cname: "乌拉圭", countryId: 598),
|
||||
(id: 195, cname: "乌克兰", countryId: 380),
|
||||
(id: 194, cname: "乌干达", countryId: 256),
|
||||
(id: 204, cname: "亚美尼亚", countryId: 374),
|
||||
(id: 205, cname: "也门", countryId: 967),
|
||||
(id: 215, cname: "直布罗陀", countryId: 350),
|
||||
(id: 214, cname: "乍得", countryId: 235),
|
||||
(id: 213, cname: "赞比亚", countryId: 260),
|
||||
(id: 212, cname: "越南", countryId: 84),
|
||||
(id: 211, cname: "约旦", countryId: 962),
|
||||
(id: 210, cname: "印尼", countryId: 62),
|
||||
(id: 209, cname: "印度", countryId: 91),
|
||||
(id: 208, cname: "以色列", countryId: 972),
|
||||
(id: 207, cname: "伊朗", countryId: 98),
|
||||
(id: 206, cname: "伊拉克", countryId: 964),
|
||||
(id: 193, cname: "文莱", countryId: 673),
|
||||
(id: 192, cname: "委内瑞拉", countryId: 58),
|
||||
(id: 191, cname: "维珍群岛(英属)", countryId: 1284),
|
||||
(id: 178, cname: "泰国", countryId: 66),
|
||||
(id: 177, cname: "索马里", countryId: 252),
|
||||
(id: 176, cname: "所罗门群岛", countryId: 677),
|
||||
(id: 175, cname: "苏里南", countryId: 597),
|
||||
(id: 174, cname: "苏丹", countryId: 249),
|
||||
(id: 173, cname: "斯威士兰", countryId: 268),
|
||||
(id: 172, cname: "斯洛文尼亚", countryId: 386),
|
||||
(id: 171, cname: "斯洛伐克", countryId: 421),
|
||||
(id: 170, cname: "斯里兰卡", countryId: 94),
|
||||
(id: 169, cname: "圣皮埃尔和密克隆群岛", countryId: 508),
|
||||
(id: 179, cname: "坦桑尼亚", countryId: 255),
|
||||
(id: 180, cname: "汤加", countryId: 676),
|
||||
(id: 190, cname: "维珍群岛(美属)", countryId: 1340),
|
||||
(id: 189, cname: "瓦努阿图", countryId: 678),
|
||||
(id: 188, cname: "托克劳岛", countryId: 690),
|
||||
(id: 187, cname: "土库曼斯坦", countryId: 993),
|
||||
(id: 186, cname: "土耳其", countryId: 90),
|
||||
(id: 185, cname: "图瓦卢", countryId: 688),
|
||||
(id: 184, cname: "突尼斯", countryId: 216),
|
||||
(id: 183, cname: "阿森松岛", countryId: 247),
|
||||
(id: 182, cname: "特立尼达和多巴哥", countryId: 1868),
|
||||
(id: 181, cname: "特克斯和凯科斯", countryId: 1649),
|
||||
(id: 168, cname: "圣马力诺", countryId: 378),
|
||||
(id: 67, cname: "法属圭亚那", countryId: 594),
|
||||
(id: 54, cname: "不丹", countryId: 975),
|
||||
(id: 53, cname: "博茨瓦纳", countryId: 267),
|
||||
(id: 52, cname: "伯利兹", countryId: 501),
|
||||
(id: 51, cname: "玻利维亚", countryId: 591),
|
||||
(id: 50, cname: "波兰", countryId: 48),
|
||||
(id: 49, cname: "波黑", countryId: 387),
|
||||
(id: 48, cname: "波多黎各", countryId: 1787),
|
||||
(id: 47, cname: "冰岛", countryId: 354),
|
||||
(id: 46, cname: "贝宁", countryId: 229),
|
||||
(id: 45, cname: "保加利亚", countryId: 359),
|
||||
(id: 55, cname: "布基纳法索", countryId: 226),
|
||||
(id: 56, cname: "布隆迪", countryId: 257),
|
||||
(id: 66, cname: "法属波利尼西亚", countryId: 689),
|
||||
(id: 65, cname: "法罗岛", countryId: 298),
|
||||
(id: 64, cname: "厄立特里亚", countryId: 291),
|
||||
(id: 63, cname: "厄瓜多尔", countryId: 593),
|
||||
(id: 62, cname: "多米尼加代表", countryId: 1809),
|
||||
(id: 61, cname: "多米尼加", countryId: 1767),
|
||||
(id: 60, cname: "多哥", countryId: 228),
|
||||
(id: 59, cname: "迪戈加西亚岛", countryId: 246),
|
||||
(id: 58, cname: "丹麦", countryId: 45),
|
||||
(id: 57, cname: "赤道几内亚", countryId: 240),
|
||||
(id: 44, cname: "百慕大群岛", countryId: 1441),
|
||||
(id: 43, cname: "白俄罗斯", countryId: 375),
|
||||
(id: 42, cname: "巴西", countryId: 55),
|
||||
(id: 29, cname: "爱尔兰", countryId: 353),
|
||||
(id: 28, cname: "埃塞俄比亚", countryId: 251),
|
||||
(id: 27, cname: "埃及", countryId: 20),
|
||||
(id: 26, cname: "阿塞拜疆", countryId: 994),
|
||||
(id: 25, cname: "阿曼", countryId: 968),
|
||||
(id: 24, cname: "阿联酋", countryId: 971),
|
||||
(id: 23, cname: "阿根廷", countryId: 54),
|
||||
(id: 22, cname: "阿富汗", countryId: 93),
|
||||
(id: 21, cname: "阿尔及利亚", countryId: 213),
|
||||
(id: 20, cname: "阿尔巴尼亚", countryId: 355),
|
||||
(id: 30, cname: "爱沙尼亚", countryId: 372),
|
||||
(id: 31, cname: "安道尔", countryId: 376),
|
||||
(id: 41, cname: "巴拿马", countryId: 507),
|
||||
(id: 40, cname: "巴林", countryId: 973),
|
||||
(id: 39, cname: "巴拉圭", countryId: 595),
|
||||
(id: 38, cname: "巴基斯坦", countryId: 92),
|
||||
(id: 37, cname: "巴哈马群岛", countryId: 1242),
|
||||
(id: 36, cname: "巴布亚新几内亚", countryId: 675),
|
||||
(id: 35, cname: "巴巴多斯", countryId: 1246),
|
||||
(id: 34, cname: "奥地利", countryId: 43),
|
||||
(id: 33, cname: "安提瓜岛和巴布达", countryId: 1268),
|
||||
(id: 32, cname: "安哥拉", countryId: 244),
|
||||
(id: 68, cname: "非洲中部", countryId: 236),
|
||||
(id: 117, cname: "罗马尼亚", countryId: 40),
|
||||
(id: 104, cname: "科威特", countryId: 965),
|
||||
(id: 103, cname: "科摩罗", countryId: 269),
|
||||
(id: 102, cname: "开曼群岛", countryId: 1345),
|
||||
(id: 101, cname: "卡塔尔", countryId: 974),
|
||||
(id: 100, cname: "喀麦隆", countryId: 237),
|
||||
(id: 99, cname: "聚会岛", countryId: 262),
|
||||
(id: 98, cname: "津巴布韦", countryId: 263),
|
||||
(id: 97, cname: "捷克", countryId: 420),
|
||||
(id: 96, cname: "柬埔寨", countryId: 855),
|
||||
(id: 95, cname: "加蓬", countryId: 241),
|
||||
(id: 105, cname: "克罗地亚", countryId: 385),
|
||||
(id: 106, cname: "肯尼亚", countryId: 254),
|
||||
(id: 116, cname: "卢旺达", countryId: 250),
|
||||
(id: 115, cname: "卢森堡", countryId: 352),
|
||||
(id: 114, cname: "利比亚", countryId: 218),
|
||||
(id: 113, cname: "利比里亚", countryId: 231),
|
||||
(id: 112, cname: "立陶宛", countryId: 370),
|
||||
(id: 111, cname: "黎巴嫩", countryId: 961),
|
||||
(id: 110, cname: "老挝", countryId: 856),
|
||||
(id: 109, cname: "莱索托", countryId: 266),
|
||||
(id: 108, cname: "拉脱维亚", countryId: 371),
|
||||
(id: 107, cname: "库克岛", countryId: 682),
|
||||
(id: 94, cname: "加纳", countryId: 233),
|
||||
(id: 93, cname: "几内亚比绍", countryId: 245),
|
||||
(id: 92, cname: "几内亚", countryId: 224),
|
||||
(id: 79, cname: "格林纳达", countryId: 1473),
|
||||
(id: 78, cname: "哥斯达黎加", countryId: 506),
|
||||
(id: 77, cname: "哥伦比亚", countryId: 57),
|
||||
(id: 76, cname: "刚果(金)", countryId: 243),
|
||||
(id: 75, cname: "刚果", countryId: 242),
|
||||
(id: 74, cname: "冈比亚", countryId: 220),
|
||||
(id: 73, cname: "福克兰岛", countryId: 500),
|
||||
(id: 72, cname: "佛得角", countryId: 238),
|
||||
(id: 71, cname: "芬兰", countryId: 358),
|
||||
(id: 70, cname: "斐济", countryId: 679),
|
||||
(id: 80, cname: "格陵兰岛", countryId: 299),
|
||||
(id: 81, cname: "古巴", countryId: 53),
|
||||
(id: 91, cname: "吉尔吉斯斯坦", countryId: 996),
|
||||
(id: 90, cname: "吉布提", countryId: 253),
|
||||
(id: 89, cname: "基里巴斯", countryId: 686),
|
||||
(id: 88, cname: "维克岛", countryId: 1808),
|
||||
(id: 87, cname: "洪都拉斯", countryId: 504),
|
||||
(id: 86, cname: "荷兰", countryId: 31),
|
||||
(id: 85, cname: "朝鲜", countryId: 850),
|
||||
(id: 84, cname: "海地", countryId: 509),
|
||||
(id: 83, cname: "关岛", countryId: 1671),
|
||||
(id: 82, cname: "瓜德罗普岛", countryId: 590),
|
||||
(id: 69, cname: "菲律宾", countryId: 63),
|
||||
];
|
||||
// 'itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme,sunflowerStyle,cardsEnhance,eva3CardOpus,eva3CardVideo,eva3CardComment,eva3CardVote,eva3CardUser'
|
||||
static const dynFeatures = 'itemOpusStyle,listOnlyfans,onlyfansQaCard';
|
||||
}
|
||||
|
||||
220
lib/common/dial_prefix.dart
Normal file
220
lib/common/dial_prefix.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
abstract final class Login {
|
||||
//内容来自 https://passport.bilibili.com/web/generic/country/list
|
||||
static const dialPrefix = [
|
||||
(id: 1, cname: "中国大陆", countryId: 86),
|
||||
(id: 5, cname: "中国香港特别行政区", countryId: 852),
|
||||
(id: 2, cname: "中国澳门特别行政区", countryId: 853),
|
||||
(id: 3, cname: "中国台湾", countryId: 886),
|
||||
(id: 4, cname: "美国", countryId: 1),
|
||||
(id: 6, cname: "比利时", countryId: 32),
|
||||
(id: 7, cname: "澳大利亚", countryId: 61),
|
||||
(id: 8, cname: "法国", countryId: 33),
|
||||
(id: 9, cname: "加拿大", countryId: 1),
|
||||
(id: 10, cname: "日本", countryId: 81),
|
||||
(id: 11, cname: "新加坡", countryId: 65),
|
||||
(id: 12, cname: "韩国", countryId: 82),
|
||||
(id: 13, cname: "马来西亚", countryId: 60),
|
||||
(id: 14, cname: "英国", countryId: 44),
|
||||
(id: 15, cname: "意大利", countryId: 39),
|
||||
(id: 16, cname: "德国", countryId: 49),
|
||||
(id: 18, cname: "俄罗斯", countryId: 7),
|
||||
(id: 19, cname: "新西兰", countryId: 64),
|
||||
(id: 153, cname: "瓦利斯群岛和富图纳群岛", countryId: 1681),
|
||||
(id: 152, cname: "葡萄牙", countryId: 351),
|
||||
(id: 151, cname: "帕劳", countryId: 680),
|
||||
(id: 150, cname: "诺福克岛", countryId: 672),
|
||||
(id: 149, cname: "挪威", countryId: 47),
|
||||
(id: 148, cname: "纽埃岛", countryId: 683),
|
||||
(id: 147, cname: "尼日利亚", countryId: 234),
|
||||
(id: 146, cname: "尼日尔", countryId: 227),
|
||||
(id: 145, cname: "尼加拉瓜", countryId: 505),
|
||||
(id: 144, cname: "尼泊尔", countryId: 977),
|
||||
(id: 143, cname: "瑙鲁", countryId: 674),
|
||||
(id: 154, cname: "格鲁吉亚", countryId: 995),
|
||||
(id: 155, cname: "瑞典", countryId: 46),
|
||||
(id: 165, cname: "沙特阿拉伯", countryId: 966),
|
||||
(id: 164, cname: "桑给巴尔岛", countryId: 259),
|
||||
(id: 163, cname: "塞舌尔共和国", countryId: 248),
|
||||
(id: 162, cname: "塞浦路斯", countryId: 357),
|
||||
(id: 161, cname: "塞内加尔", countryId: 221),
|
||||
(id: 160, cname: "塞拉利昂", countryId: 232),
|
||||
(id: 159, cname: "萨摩亚,东部", countryId: 684),
|
||||
(id: 158, cname: "萨摩亚,西部", countryId: 685),
|
||||
(id: 157, cname: "萨尔瓦多", countryId: 503),
|
||||
(id: 156, cname: "瑞士", countryId: 41),
|
||||
(id: 166, cname: "圣多美和普林西比", countryId: 239),
|
||||
(id: 142, cname: "塞尔维亚", countryId: 381),
|
||||
(id: 141, cname: "南非", countryId: 27),
|
||||
(id: 128, cname: "毛里塔尼亚", countryId: 222),
|
||||
(id: 127, cname: "毛里求斯", countryId: 230),
|
||||
(id: 126, cname: "马歇尔岛", countryId: 692),
|
||||
(id: 125, cname: "马提尼克岛", countryId: 596),
|
||||
(id: 124, cname: "马其顿", countryId: 389),
|
||||
(id: 123, cname: "马里亚纳岛", countryId: 1670),
|
||||
(id: 122, cname: "马里", countryId: 223),
|
||||
(id: 121, cname: "马拉维", countryId: 265),
|
||||
(id: 120, cname: "马耳他", countryId: 356),
|
||||
(id: 119, cname: "马尔代夫", countryId: 960),
|
||||
(id: 129, cname: "蒙古", countryId: 976),
|
||||
(id: 130, cname: "蒙特塞拉特岛", countryId: 1664),
|
||||
(id: 140, cname: "纳米比亚", countryId: 264),
|
||||
(id: 139, cname: "墨西哥", countryId: 52),
|
||||
(id: 138, cname: "莫桑比克", countryId: 258),
|
||||
(id: 137, cname: "摩纳哥", countryId: 377),
|
||||
(id: 136, cname: "摩洛哥", countryId: 212),
|
||||
(id: 135, cname: "摩尔多瓦", countryId: 373),
|
||||
(id: 134, cname: "缅甸", countryId: 95),
|
||||
(id: 133, cname: "密克罗尼西亚", countryId: 691),
|
||||
(id: 132, cname: "秘鲁", countryId: 51),
|
||||
(id: 131, cname: "孟加拉国", countryId: 880),
|
||||
(id: 118, cname: "马达加斯加", countryId: 261),
|
||||
(id: 167, cname: "圣卢西亚", countryId: 1784),
|
||||
(id: 216, cname: "智利", countryId: 56),
|
||||
(id: 203, cname: "牙买加", countryId: 1876),
|
||||
(id: 202, cname: "叙利亚", countryId: 963),
|
||||
(id: 201, cname: "匈牙利", countryId: 36),
|
||||
(id: 200, cname: "科特迪瓦", countryId: 225),
|
||||
(id: 199, cname: "希腊", countryId: 30),
|
||||
(id: 198, cname: "西班牙", countryId: 34),
|
||||
(id: 197, cname: "乌兹别克斯坦", countryId: 998),
|
||||
(id: 196, cname: "乌拉圭", countryId: 598),
|
||||
(id: 195, cname: "乌克兰", countryId: 380),
|
||||
(id: 194, cname: "乌干达", countryId: 256),
|
||||
(id: 204, cname: "亚美尼亚", countryId: 374),
|
||||
(id: 205, cname: "也门", countryId: 967),
|
||||
(id: 215, cname: "直布罗陀", countryId: 350),
|
||||
(id: 214, cname: "乍得", countryId: 235),
|
||||
(id: 213, cname: "赞比亚", countryId: 260),
|
||||
(id: 212, cname: "越南", countryId: 84),
|
||||
(id: 211, cname: "约旦", countryId: 962),
|
||||
(id: 210, cname: "印尼", countryId: 62),
|
||||
(id: 209, cname: "印度", countryId: 91),
|
||||
(id: 208, cname: "以色列", countryId: 972),
|
||||
(id: 207, cname: "伊朗", countryId: 98),
|
||||
(id: 206, cname: "伊拉克", countryId: 964),
|
||||
(id: 193, cname: "文莱", countryId: 673),
|
||||
(id: 192, cname: "委内瑞拉", countryId: 58),
|
||||
(id: 191, cname: "维珍群岛(英属)", countryId: 1284),
|
||||
(id: 178, cname: "泰国", countryId: 66),
|
||||
(id: 177, cname: "索马里", countryId: 252),
|
||||
(id: 176, cname: "所罗门群岛", countryId: 677),
|
||||
(id: 175, cname: "苏里南", countryId: 597),
|
||||
(id: 174, cname: "苏丹", countryId: 249),
|
||||
(id: 173, cname: "斯威士兰", countryId: 268),
|
||||
(id: 172, cname: "斯洛文尼亚", countryId: 386),
|
||||
(id: 171, cname: "斯洛伐克", countryId: 421),
|
||||
(id: 170, cname: "斯里兰卡", countryId: 94),
|
||||
(id: 169, cname: "圣皮埃尔和密克隆群岛", countryId: 508),
|
||||
(id: 179, cname: "坦桑尼亚", countryId: 255),
|
||||
(id: 180, cname: "汤加", countryId: 676),
|
||||
(id: 190, cname: "维珍群岛(美属)", countryId: 1340),
|
||||
(id: 189, cname: "瓦努阿图", countryId: 678),
|
||||
(id: 188, cname: "托克劳岛", countryId: 690),
|
||||
(id: 187, cname: "土库曼斯坦", countryId: 993),
|
||||
(id: 186, cname: "土耳其", countryId: 90),
|
||||
(id: 185, cname: "图瓦卢", countryId: 688),
|
||||
(id: 184, cname: "突尼斯", countryId: 216),
|
||||
(id: 183, cname: "阿森松岛", countryId: 247),
|
||||
(id: 182, cname: "特立尼达和多巴哥", countryId: 1868),
|
||||
(id: 181, cname: "特克斯和凯科斯", countryId: 1649),
|
||||
(id: 168, cname: "圣马力诺", countryId: 378),
|
||||
(id: 67, cname: "法属圭亚那", countryId: 594),
|
||||
(id: 54, cname: "不丹", countryId: 975),
|
||||
(id: 53, cname: "博茨瓦纳", countryId: 267),
|
||||
(id: 52, cname: "伯利兹", countryId: 501),
|
||||
(id: 51, cname: "玻利维亚", countryId: 591),
|
||||
(id: 50, cname: "波兰", countryId: 48),
|
||||
(id: 49, cname: "波黑", countryId: 387),
|
||||
(id: 48, cname: "波多黎各", countryId: 1787),
|
||||
(id: 47, cname: "冰岛", countryId: 354),
|
||||
(id: 46, cname: "贝宁", countryId: 229),
|
||||
(id: 45, cname: "保加利亚", countryId: 359),
|
||||
(id: 55, cname: "布基纳法索", countryId: 226),
|
||||
(id: 56, cname: "布隆迪", countryId: 257),
|
||||
(id: 66, cname: "法属波利尼西亚", countryId: 689),
|
||||
(id: 65, cname: "法罗岛", countryId: 298),
|
||||
(id: 64, cname: "厄立特里亚", countryId: 291),
|
||||
(id: 63, cname: "厄瓜多尔", countryId: 593),
|
||||
(id: 62, cname: "多米尼加代表", countryId: 1809),
|
||||
(id: 61, cname: "多米尼加", countryId: 1767),
|
||||
(id: 60, cname: "多哥", countryId: 228),
|
||||
(id: 59, cname: "迪戈加西亚岛", countryId: 246),
|
||||
(id: 58, cname: "丹麦", countryId: 45),
|
||||
(id: 57, cname: "赤道几内亚", countryId: 240),
|
||||
(id: 44, cname: "百慕大群岛", countryId: 1441),
|
||||
(id: 43, cname: "白俄罗斯", countryId: 375),
|
||||
(id: 42, cname: "巴西", countryId: 55),
|
||||
(id: 29, cname: "爱尔兰", countryId: 353),
|
||||
(id: 28, cname: "埃塞俄比亚", countryId: 251),
|
||||
(id: 27, cname: "埃及", countryId: 20),
|
||||
(id: 26, cname: "阿塞拜疆", countryId: 994),
|
||||
(id: 25, cname: "阿曼", countryId: 968),
|
||||
(id: 24, cname: "阿联酋", countryId: 971),
|
||||
(id: 23, cname: "阿根廷", countryId: 54),
|
||||
(id: 22, cname: "阿富汗", countryId: 93),
|
||||
(id: 21, cname: "阿尔及利亚", countryId: 213),
|
||||
(id: 20, cname: "阿尔巴尼亚", countryId: 355),
|
||||
(id: 30, cname: "爱沙尼亚", countryId: 372),
|
||||
(id: 31, cname: "安道尔", countryId: 376),
|
||||
(id: 41, cname: "巴拿马", countryId: 507),
|
||||
(id: 40, cname: "巴林", countryId: 973),
|
||||
(id: 39, cname: "巴拉圭", countryId: 595),
|
||||
(id: 38, cname: "巴基斯坦", countryId: 92),
|
||||
(id: 37, cname: "巴哈马群岛", countryId: 1242),
|
||||
(id: 36, cname: "巴布亚新几内亚", countryId: 675),
|
||||
(id: 35, cname: "巴巴多斯", countryId: 1246),
|
||||
(id: 34, cname: "奥地利", countryId: 43),
|
||||
(id: 33, cname: "安提瓜岛和巴布达", countryId: 1268),
|
||||
(id: 32, cname: "安哥拉", countryId: 244),
|
||||
(id: 68, cname: "非洲中部", countryId: 236),
|
||||
(id: 117, cname: "罗马尼亚", countryId: 40),
|
||||
(id: 104, cname: "科威特", countryId: 965),
|
||||
(id: 103, cname: "科摩罗", countryId: 269),
|
||||
(id: 102, cname: "开曼群岛", countryId: 1345),
|
||||
(id: 101, cname: "卡塔尔", countryId: 974),
|
||||
(id: 100, cname: "喀麦隆", countryId: 237),
|
||||
(id: 99, cname: "聚会岛", countryId: 262),
|
||||
(id: 98, cname: "津巴布韦", countryId: 263),
|
||||
(id: 97, cname: "捷克", countryId: 420),
|
||||
(id: 96, cname: "柬埔寨", countryId: 855),
|
||||
(id: 95, cname: "加蓬", countryId: 241),
|
||||
(id: 105, cname: "克罗地亚", countryId: 385),
|
||||
(id: 106, cname: "肯尼亚", countryId: 254),
|
||||
(id: 116, cname: "卢旺达", countryId: 250),
|
||||
(id: 115, cname: "卢森堡", countryId: 352),
|
||||
(id: 114, cname: "利比亚", countryId: 218),
|
||||
(id: 113, cname: "利比里亚", countryId: 231),
|
||||
(id: 112, cname: "立陶宛", countryId: 370),
|
||||
(id: 111, cname: "黎巴嫩", countryId: 961),
|
||||
(id: 110, cname: "老挝", countryId: 856),
|
||||
(id: 109, cname: "莱索托", countryId: 266),
|
||||
(id: 108, cname: "拉脱维亚", countryId: 371),
|
||||
(id: 107, cname: "库克岛", countryId: 682),
|
||||
(id: 94, cname: "加纳", countryId: 233),
|
||||
(id: 93, cname: "几内亚比绍", countryId: 245),
|
||||
(id: 92, cname: "几内亚", countryId: 224),
|
||||
(id: 79, cname: "格林纳达", countryId: 1473),
|
||||
(id: 78, cname: "哥斯达黎加", countryId: 506),
|
||||
(id: 77, cname: "哥伦比亚", countryId: 57),
|
||||
(id: 76, cname: "刚果(金)", countryId: 243),
|
||||
(id: 75, cname: "刚果", countryId: 242),
|
||||
(id: 74, cname: "冈比亚", countryId: 220),
|
||||
(id: 73, cname: "福克兰岛", countryId: 500),
|
||||
(id: 72, cname: "佛得角", countryId: 238),
|
||||
(id: 71, cname: "芬兰", countryId: 358),
|
||||
(id: 70, cname: "斐济", countryId: 679),
|
||||
(id: 80, cname: "格陵兰岛", countryId: 299),
|
||||
(id: 81, cname: "古巴", countryId: 53),
|
||||
(id: 91, cname: "吉尔吉斯斯坦", countryId: 996),
|
||||
(id: 90, cname: "吉布提", countryId: 253),
|
||||
(id: 89, cname: "基里巴斯", countryId: 686),
|
||||
(id: 88, cname: "维克岛", countryId: 1808),
|
||||
(id: 87, cname: "洪都拉斯", countryId: 504),
|
||||
(id: 86, cname: "荷兰", countryId: 31),
|
||||
(id: 85, cname: "朝鲜", countryId: 850),
|
||||
(id: 84, cname: "海地", countryId: 509),
|
||||
(id: 83, cname: "关岛", countryId: 1671),
|
||||
(id: 82, cname: "瓜德罗普岛", countryId: 590),
|
||||
(id: 69, cname: "菲律宾", countryId: 63),
|
||||
];
|
||||
}
|
||||
@@ -9,6 +9,13 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final color = theme.colorScheme.onInverseSurface;
|
||||
final buttonStyle = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const .symmetric(horizontal: 15),
|
||||
foregroundColor: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.2,
|
||||
),
|
||||
);
|
||||
return Skeleton(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||
@@ -86,29 +93,19 @@ class DynamicCardSkeleton extends StatelessWidget {
|
||||
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
for (var i = 0; i < 3; i++)
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.radio_button_unchecked_outlined,
|
||||
size: 20,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.2,
|
||||
children: const ['转发', '评论', '点赞']
|
||||
.map(
|
||||
(e) => TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.radio_button_unchecked_outlined,
|
||||
size: 20,
|
||||
),
|
||||
style: buttonStyle,
|
||||
label: Text(e),
|
||||
),
|
||||
label: Text(
|
||||
i == 0
|
||||
? '转发'
|
||||
: i == 1
|
||||
? '评论'
|
||||
: '点赞',
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:PiliPlus/common/constants.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 {
|
||||
const FavPgcItemSkeleton({super.key});
|
||||
@@ -11,7 +12,7 @@ class FavPgcItemSkeleton extends StatelessWidget {
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
horizontal: Style.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MediaPgcSkeleton extends StatefulWidget {
|
||||
@@ -15,11 +15,9 @@ class _MediaPgcSkeletonState extends State<MediaPgcSkeleton> {
|
||||
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace,
|
||||
7,
|
||||
StyleString.safeSpace,
|
||||
7,
|
||||
padding: const .symmetric(
|
||||
horizontal: Style.safeSpace,
|
||||
vertical: 7,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -11,7 +11,7 @@ class Skeleton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
|
||||
var shimmerGradient = LinearGradient(
|
||||
final shimmerGradient = LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
color,
|
||||
@@ -62,7 +62,6 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_shimmerController = AnimationController.unbounded(vsync: this)
|
||||
..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/widgets/flutter/layout_builder.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide LayoutBuilder;
|
||||
|
||||
class SpaceOpusSkeleton extends StatelessWidget {
|
||||
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/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoCardHSkeleton extends StatelessWidget {
|
||||
@@ -10,25 +10,25 @@ class VideoCardHSkeleton extends StatelessWidget {
|
||||
final color = Theme.of(context).colorScheme.onInverseSurface;
|
||||
return Skeleton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: StyleString.safeSpace,
|
||||
padding: const .symmetric(
|
||||
horizontal: Style.safeSpace,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
aspectRatio: Style.aspectRatio,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
borderRadius: Style.mdRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
|
||||
padding: const .fromLTRB(10, 4, 6, 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/skeleton/skeleton.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoCardVSkeleton extends StatelessWidget {
|
||||
@@ -13,11 +13,11 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
aspectRatio: Style.aspectRatio,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
borderRadius: Style.mdRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
24
lib/common/style.dart
Normal file
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 bool? visible;
|
||||
final AppBar child;
|
||||
final List<Widget>? children;
|
||||
final List<Widget>? actions;
|
||||
|
||||
const MultiSelectAppBarWidget({
|
||||
super.key,
|
||||
required this.ctr,
|
||||
this.visible,
|
||||
this.children,
|
||||
this.actions,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (visible ?? ctr.enableMultiSelect.value) {
|
||||
final style = TextButton.styleFrom(visualDensity: VisualDensity.compact);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return AppBar(
|
||||
bottom: child.bottom,
|
||||
leading: IconButton(
|
||||
@@ -30,21 +32,22 @@ class MultiSelectAppBarWidget extends StatelessWidget
|
||||
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
style: style,
|
||||
onPressed: () => ctr.handleSelect(checked: true),
|
||||
child: const Text('全选'),
|
||||
),
|
||||
...?children,
|
||||
...?actions,
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: ctr.onRemove,
|
||||
style: style,
|
||||
onPressed: () {
|
||||
if (ctr.checkedCount == 0) {
|
||||
return;
|
||||
}
|
||||
ctr.onRemove();
|
||||
},
|
||||
child: Text(
|
||||
'移除',
|
||||
style: TextStyle(color: Get.theme.colorScheme.error),
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
|
||||
56
lib/common/widgets/avatars.dart
Normal file
56
lib/common/widgets/avatars.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/model_owner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget avatars({
|
||||
required ColorScheme colorScheme,
|
||||
required Iterable<Owner> users,
|
||||
double gap = 6.0,
|
||||
}) {
|
||||
const size = 22.0;
|
||||
const padding = 0.8;
|
||||
final offset = size - gap;
|
||||
const imgSize = size - 2 * padding;
|
||||
if (users.length == 1) {
|
||||
return NetworkImgLayer(
|
||||
src: users.first.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
);
|
||||
} else {
|
||||
final decoration = BoxDecoration(
|
||||
shape: .circle,
|
||||
border: Border.all(color: colorScheme.surface),
|
||||
);
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: offset * users.length + gap,
|
||||
child: Stack(
|
||||
clipBehavior: .none,
|
||||
children: users.indexed
|
||||
.map(
|
||||
(e) => Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: size,
|
||||
left: e.$1 * offset,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: Padding(
|
||||
padding: const .all(padding),
|
||||
child: NetworkImgLayer(
|
||||
src: e.$2.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/common/widgets/back_detector.dart
Normal file
42
lib/common/widgets/back_detector.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/gestures.dart' show kBackMouseButton;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show KeyDownEvent;
|
||||
|
||||
class BackDetector extends StatelessWidget {
|
||||
const BackDetector({
|
||||
super.key,
|
||||
required this.onBack,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final VoidCallback onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: _onKeyEvent,
|
||||
child: Listener(
|
||||
behavior: .translucent,
|
||||
onPointerDown: _onPointerDown,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event.logicalKey == .escape && event is KeyDownEvent) {
|
||||
onBack();
|
||||
return .handled;
|
||||
}
|
||||
return .ignored;
|
||||
}
|
||||
|
||||
void _onPointerDown(PointerDownEvent event) {
|
||||
if (event.buttons == kBackMouseButton) {
|
||||
onBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension/theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PBadge extends StatelessWidget {
|
||||
final String? text;
|
||||
@@ -59,7 +59,7 @@ class PBadge extends StatelessWidget {
|
||||
bgColor = Colors.black45;
|
||||
color = Colors.white;
|
||||
case PBadgeType.error:
|
||||
if (Get.isDarkMode) {
|
||||
if (theme.isDark) {
|
||||
bgColor = theme.errorContainer;
|
||||
color = theme.onErrorContainer;
|
||||
} else {
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
class ColorPalette extends StatelessWidget {
|
||||
final Color color;
|
||||
final ColorScheme colorScheme;
|
||||
final bool selected;
|
||||
final bool showBgColor;
|
||||
|
||||
const ColorPalette({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.colorScheme,
|
||||
required this.selected,
|
||||
this.showBgColor = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final Hct hct = Hct.fromInt(color.toARGB32());
|
||||
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());
|
||||
final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
|
||||
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());
|
||||
Widget coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
final primary = colorScheme.primary;
|
||||
final tertiary = colorScheme.tertiary;
|
||||
final primaryContainer = colorScheme.primaryContainer;
|
||||
Widget child = ClipOval(
|
||||
child: Column(
|
||||
children: [
|
||||
coloredBox(primary),
|
||||
_coloredBox(primary),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
coloredBox(tertiary),
|
||||
coloredBox(primaryContainer),
|
||||
_coloredBox(tertiary),
|
||||
_coloredBox(primaryContainer),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -50,7 +43,7 @@ class ColorPalette extends StatelessWidget {
|
||||
width: 23,
|
||||
height: 23,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(Hct.from(hct.hue, 30.0, 40.0).toInt()),
|
||||
color: colorScheme.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
@@ -62,15 +55,25 @@ class ColorPalette extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
if (showBgColor) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onInverseSurface,
|
||||
borderRadius: Style.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
static Widget _coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
21
lib/common/widgets/colored_box_transition.dart
Normal file
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
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
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
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
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withValues(
|
||||
alpha: toastOpacity,
|
||||
),
|
||||
color: colorScheme.primaryContainer.withValues(alpha: toastOpacity),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: Text(
|
||||
msg,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -41,7 +39,7 @@ class LoadingWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
@@ -50,6 +48,7 @@ class LoadingWidget extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//loading animation
|
||||
@@ -57,12 +56,8 @@ class LoadingWidget extends StatelessWidget {
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
|
||||
),
|
||||
|
||||
//msg
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,135 +1,63 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show clampDouble;
|
||||
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum TooltipType { top, right }
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
ContainerRenderObjectMixin,
|
||||
RenderBoxContainerDefaultsMixin,
|
||||
MultiChildLayoutParentData;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CustomTooltip extends StatefulWidget {
|
||||
const CustomTooltip({
|
||||
super.key,
|
||||
this.type = TooltipType.top,
|
||||
required this.overlayWidget,
|
||||
required this.child,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
final Widget child;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
|
||||
static final List<CustomTooltipState> _openedTooltips =
|
||||
<CustomTooltipState>[];
|
||||
|
||||
static bool dismissAllToolTips() {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
|
||||
for (final CustomTooltipState state in openedTooltips) {
|
||||
assert(state.mounted);
|
||||
state._scheduleDismissTooltip();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
State<CustomTooltip> createState() => CustomTooltipState();
|
||||
State<CustomTooltip> createState() => _CustomTooltipState();
|
||||
}
|
||||
|
||||
class CustomTooltipState extends State<CustomTooltip>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const Duration _fadeInDuration = Duration(milliseconds: 150);
|
||||
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
|
||||
|
||||
class _CustomTooltipState extends State<CustomTooltip> {
|
||||
final OverlayPortalController _overlayController = OverlayPortalController();
|
||||
|
||||
AnimationController? _backingController;
|
||||
AnimationController get _controller {
|
||||
return _backingController ??= AnimationController(
|
||||
duration: _fadeInDuration,
|
||||
reverseDuration: _fadeOutDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener(_handleStatusChanged);
|
||||
}
|
||||
|
||||
CurvedAnimation? _backingOverlayAnimation;
|
||||
CurvedAnimation get _overlayAnimation {
|
||||
return _backingOverlayAnimation ??= CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
}
|
||||
|
||||
LongPressGestureRecognizer? _longPressRecognizer;
|
||||
|
||||
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
assert(mounted);
|
||||
switch ((_animationStatus.isDismissed, status.isDismissed)) {
|
||||
case (false, true):
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_overlayController.hide();
|
||||
case (true, false):
|
||||
_overlayController.show();
|
||||
CustomTooltip._openedTooltips.add(this);
|
||||
case (true, true) || (false, false):
|
||||
break;
|
||||
}
|
||||
_animationStatus = status;
|
||||
}
|
||||
LongPressGestureRecognizer get longPressRecognizer =>
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer()
|
||||
..onLongPress = _scheduleShowTooltip;
|
||||
|
||||
void _scheduleShowTooltip() {
|
||||
_controller.forward();
|
||||
_overlayController.show();
|
||||
}
|
||||
|
||||
void _scheduleDismissTooltip() {
|
||||
_controller.reverse();
|
||||
_overlayController.hide();
|
||||
}
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
assert(mounted);
|
||||
const Set<PointerDeviceKind> triggerModeDeviceKinds = <PointerDeviceKind>{
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.unknown,
|
||||
PointerDeviceKind.trackpad,
|
||||
};
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer(
|
||||
debugOwner: this,
|
||||
supportedDevices: triggerModeDeviceKinds,
|
||||
);
|
||||
_longPressRecognizer!
|
||||
..onLongPress = _scheduleShowTooltip
|
||||
..addPointer(event);
|
||||
longPressRecognizer.addPointer(event);
|
||||
}
|
||||
|
||||
Widget _buildCustomTooltipOverlay(BuildContext context) {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
Widget _buildCustomTooltipOverlay(
|
||||
BuildContext context,
|
||||
OverlayChildLayoutInfo layoutInfo,
|
||||
) {
|
||||
final target = MatrixUtils.transformPoint(
|
||||
layoutInfo.childPaintTransform,
|
||||
layoutInfo.childSize.topCenter(Offset.zero),
|
||||
);
|
||||
final RenderBox box = this.context.findRenderObject()! as RenderBox;
|
||||
final Offset target = box.localToGlobal(
|
||||
box.size.center(Offset.zero),
|
||||
ancestor: overlayState.context.findRenderObject(),
|
||||
);
|
||||
|
||||
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
|
||||
verticalOffset: box.size.height / 2,
|
||||
horizontslOffset: box.size.width / 2,
|
||||
type: widget.type,
|
||||
animation: _overlayAnimation,
|
||||
target: target,
|
||||
onDismiss: _scheduleDismissTooltip,
|
||||
overlayWidget: widget.overlayWidget,
|
||||
indicator: widget.indicator,
|
||||
);
|
||||
|
||||
return SelectionContainer.maybeOf(context) == null
|
||||
? overlayChild
|
||||
: SelectionContainer.disabled(child: overlayChild);
|
||||
@@ -138,11 +66,10 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_longPressRecognizer?.onLongPressCancel = null;
|
||||
_longPressRecognizer?.dispose();
|
||||
_backingController?.dispose();
|
||||
_backingOverlayAnimation?.dispose();
|
||||
_longPressRecognizer
|
||||
?..onLongPress = null
|
||||
..dispose();
|
||||
_longPressRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -150,7 +77,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget result;
|
||||
if (Utils.isMobile) {
|
||||
if (PlatformUtils.isMobile) {
|
||||
result = Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -164,7 +91,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
return OverlayPortal(
|
||||
return OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildCustomTooltipOverlay,
|
||||
child: result,
|
||||
@@ -174,232 +101,230 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
|
||||
class _CustomTooltipOverlay extends StatelessWidget {
|
||||
const _CustomTooltipOverlay({
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.type,
|
||||
required this.animation,
|
||||
required this.target,
|
||||
required this.onDismiss,
|
||||
required this.overlayWidget,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final double verticalOffset;
|
||||
final double horizontslOffset;
|
||||
final TooltipType type;
|
||||
final Animation<double> animation;
|
||||
final Offset target;
|
||||
final VoidCallback onDismiss;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = CustomMultiChildLayout(
|
||||
delegate: _CustomMultiTooltipPositionDelegate(
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: false,
|
||||
),
|
||||
return _ToolTip(
|
||||
target: target,
|
||||
preferBelow: false,
|
||||
onTap: PlatformUtils.isMobile ? onDismiss : null,
|
||||
children: [
|
||||
LayoutId(
|
||||
id: 'overlay',
|
||||
child: overlayWidget(),
|
||||
),
|
||||
if (indicator != null)
|
||||
LayoutId(
|
||||
id: 'indicator',
|
||||
child: indicator!(),
|
||||
),
|
||||
indicator(),
|
||||
overlayWidget(),
|
||||
],
|
||||
);
|
||||
if (Utils.isMobile) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomMultiTooltipPositionDelegate({
|
||||
required this.type,
|
||||
class _ToolTip extends MultiChildRenderObjectWidget {
|
||||
const _ToolTip({
|
||||
super.children,
|
||||
this.onTap,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.preferBelow,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final Offset target;
|
||||
|
||||
final double verticalOffset;
|
||||
|
||||
final double horizontslOffset;
|
||||
|
||||
final bool preferBelow;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderToolTip(
|
||||
onTap: onTap,
|
||||
target: target,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
case TooltipType.right:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderToolTip renderObject) {
|
||||
renderObject
|
||||
..onTap = onTap
|
||||
..target = target
|
||||
..preferBelow = preferBelow;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
offset.dx - indicatorSize.width + 1,
|
||||
target.dy - indicatorSize.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
class _RenderToolTip extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
_RenderToolTip({
|
||||
VoidCallback? onTap,
|
||||
required Offset target,
|
||||
required bool preferBelow,
|
||||
}) : _target = target,
|
||||
_preferBelow = preferBelow,
|
||||
_hitTestSelf = onTap != null {
|
||||
if (onTap != null) {
|
||||
_tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap;
|
||||
}
|
||||
}
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
set onTap(VoidCallback? value) {
|
||||
_tapGestureRecognizer?.onTap = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapGestureRecognizer
|
||||
?..onTap = null
|
||||
..dispose();
|
||||
_tapGestureRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final bool _hitTestSelf;
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => _hitTestSelf;
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry) {
|
||||
if (event is PointerDownEvent) {
|
||||
_tapGestureRecognizer?.addPointer(event);
|
||||
}
|
||||
}
|
||||
|
||||
Offset _target;
|
||||
Offset get target => _target;
|
||||
set target(Offset value) {
|
||||
if (_target == value) return;
|
||||
_target = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
bool _preferBelow;
|
||||
bool get preferBelow => _preferBelow;
|
||||
set preferBelow(bool value) {
|
||||
if (_preferBelow == value) return;
|
||||
_preferBelow = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) {
|
||||
return target != oldDelegate.target ||
|
||||
verticalOffset != oldDelegate.verticalOffset ||
|
||||
preferBelow != oldDelegate.preferBelow;
|
||||
void performLayout() {
|
||||
size = constraints.constrain(constraints.biggest);
|
||||
|
||||
final c = BoxConstraints.loose(size);
|
||||
RenderBox indicator = firstChild!..layout(c, parentUsesSize: true);
|
||||
RenderBox overlay = lastChild!..layout(c, parentUsesSize: true);
|
||||
|
||||
final indicatorSize = indicator.size;
|
||||
final overlaySize = overlay.size;
|
||||
|
||||
final indicatorParentData =
|
||||
indicator.parentData as MultiChildLayoutParentData;
|
||||
final overlayParentData = overlay.parentData as MultiChildLayoutParentData;
|
||||
|
||||
Offset offset = positionDependentBox(
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
overlayParentData.offset = offset;
|
||||
indicatorParentData.offset = Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
class TrianglePainter extends CustomPainter {
|
||||
TrianglePainter(this.color, {this.type = TooltipType.top});
|
||||
final TooltipType type;
|
||||
class Triangle extends LeafRenderObjectWidget {
|
||||
const Triangle({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderTriangle(
|
||||
color: color,
|
||||
preferredSize: size,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderTriangle renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..color = color
|
||||
..preferredSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderTriangle extends RenderBox {
|
||||
RenderTriangle({
|
||||
required 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()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
Path path;
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
path = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width / 2, size.height)
|
||||
..close();
|
||||
case TooltipType.right:
|
||||
path = Path()
|
||||
..moveTo(0, size.height / 2)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width, size.height)
|
||||
..close();
|
||||
}
|
||||
final path = Path()
|
||||
..moveTo(offset.dx, offset.dy)
|
||||
..lineTo(offset.dx + size.width, offset.dy)
|
||||
..lineTo(offset.dx + size.width / 2, size.height + offset.dy)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color;
|
||||
}
|
||||
|
||||
Offset positionDependentBox({
|
||||
required TooltipType type,
|
||||
required Size size,
|
||||
required Size childSize,
|
||||
required Offset target,
|
||||
required bool preferBelow,
|
||||
double verticalOffset = 0.0,
|
||||
double horizontslOffset = 0.0,
|
||||
double margin = 10.0,
|
||||
}) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
// VERTICAL DIRECTION
|
||||
final bool fitsBelow =
|
||||
target.dy + verticalOffset + childSize.height <= size.height - margin;
|
||||
final bool fitsAbove =
|
||||
target.dy - verticalOffset - childSize.height >= margin;
|
||||
final bool tooltipBelow = fitsAbove == fitsBelow
|
||||
? preferBelow
|
||||
: fitsBelow;
|
||||
final double y;
|
||||
if (tooltipBelow) {
|
||||
y = math.min(target.dy + verticalOffset, size.height - margin);
|
||||
} else {
|
||||
y = math.max(target.dy - verticalOffset - childSize.height, margin);
|
||||
} // HORIZONTAL DIRECTION
|
||||
final double flexibleSpace = size.width - childSize.width;
|
||||
final double x = flexibleSpace <= 2 * margin
|
||||
// If there's not enough horizontal space for margin + child, center the
|
||||
// child.
|
||||
? flexibleSpace / 2.0
|
||||
: clampDouble(
|
||||
target.dx - childSize.width / 2,
|
||||
margin,
|
||||
flexibleSpace - margin,
|
||||
);
|
||||
return Offset(x, y);
|
||||
case TooltipType.right:
|
||||
final double dy = math.max(margin, target.dy - childSize.height / 2);
|
||||
final double dx = math.min(
|
||||
target.dx + horizontslOffset,
|
||||
size.width - childSize.width - margin,
|
||||
);
|
||||
return Offset(dx, dy);
|
||||
context.canvas.drawPath(path, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
Future<void> showConfirmDialog({
|
||||
Future<bool> showConfirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
dynamic content,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
assert(content is String? || content is Widget);
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
required Widget title,
|
||||
Widget? content,
|
||||
// @Deprecated('use `bool result = await showConfirmDialog()` instead')
|
||||
VoidCallback? onConfirm,
|
||||
}) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: title,
|
||||
content: content,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: true);
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
void showPgcFollowDialog({
|
||||
|
||||
277
lib/common/widgets/dialog/export_import.dart
Normal file
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/utils/extension.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -8,92 +9,81 @@ import 'package:get/get.dart';
|
||||
Future<void> autoWrapReportDialog(
|
||||
BuildContext context,
|
||||
Map<String, Map<int, String>> options,
|
||||
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess,
|
||||
) {
|
||||
Future<LoadingState> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess, {
|
||||
bool ban = true,
|
||||
}) {
|
||||
int? reasonType;
|
||||
String? reasonDesc;
|
||||
bool banUid = false;
|
||||
late final key = GlobalKey<FormState>();
|
||||
late final key = GlobalKey<FormFieldState<String>>();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('举报'),
|
||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
||||
actionsPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 22,
|
||||
right: 22,
|
||||
bottom: 5,
|
||||
),
|
||||
child: Text('请选择举报的理由:'),
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('举报'),
|
||||
titlePadding: const .only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const .symmetric(vertical: 5),
|
||||
actionsPadding: const .only(left: 16, right: 16, bottom: 10),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: .only(left: 22, right: 22, bottom: 5),
|
||||
child: Text('请选择举报的理由:'),
|
||||
),
|
||||
RadioGroup(
|
||||
onChanged: (value) {
|
||||
reasonType = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonType,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.entries.map((entry) {
|
||||
return WrapRadioOptionsGroup<int>(
|
||||
groupTitle: entry.key,
|
||||
options: entry.value,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
RadioGroup(
|
||||
onChanged: (value) {
|
||||
reasonType = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonType,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.entries.map((entry) {
|
||||
return WrapRadioOptionsGroup<int>(
|
||||
groupTitle: entry.key,
|
||||
options: entry.value,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const .only(left: 22, top: 5, right: 22),
|
||||
child: TextFormField(
|
||||
key: key,
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
initialValue: reasonDesc,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: .all(10),
|
||||
labelStyle: TextStyle(fontSize: 14),
|
||||
floatingLabelStyle: TextStyle(fontSize: 14),
|
||||
),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
validator: (value) =>
|
||||
value.isNullOrEmpty ? '理由不能为空' : null,
|
||||
),
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 22,
|
||||
top: 5,
|
||||
right: 22,
|
||||
),
|
||||
child: Form(
|
||||
key: key,
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
initialValue: reasonDesc,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
validator: (value) =>
|
||||
value.isNullOrEmpty ? '理由不能为空' : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ban)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
@@ -101,43 +91,42 @@ Future<void> autoWrapReportDialog(
|
||||
onChanged: (value) => banUid = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (reasonType == null ||
|
||||
(reasonType == 0 && key.currentState?.validate() != true)) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final data = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (data['code'] == 0) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
SmartDialog.showToast(data['message'].toString());
|
||||
}
|
||||
} catch (e) {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
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
|
||||
static Map<String, Map<int, String>> get commentReport => const {
|
||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||
@@ -263,4 +252,16 @@ class ReportOptions {
|
||||
7: '其他', // avoid show form
|
||||
},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get imMsgReport => const {
|
||||
'': {
|
||||
1: '色情低俗',
|
||||
2: '政治敏感',
|
||||
3: '违法有害',
|
||||
4: '广告骚扰',
|
||||
5: '人身攻击',
|
||||
6: '诈骗',
|
||||
0: '其他问题',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
const _reason = ['头像违规', '昵称违规', '签名违规'];
|
||||
|
||||
const _reasonV2 = ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗', '违规引流外链'];
|
||||
|
||||
Future<void> showMemberReportDialog(
|
||||
BuildContext context, {
|
||||
required Object? name,
|
||||
@@ -17,13 +21,11 @@ Future<void> showMemberReportDialog(
|
||||
final theme = Theme.of(context);
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
titleTextStyle: theme.textTheme.bodyMedium,
|
||||
title: Column(
|
||||
spacing: 4,
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
Text(
|
||||
'举报: $name',
|
||||
@@ -34,53 +36,101 @@ Future<void> showMemberReportDialog(
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
const Text('举报内容(必选,可多选)'),
|
||||
const Padding(
|
||||
padding: .only(left: 18),
|
||||
child: Text('举报内容(必选,可多选)'),
|
||||
),
|
||||
...List.generate(
|
||||
3,
|
||||
(index) => Builder(
|
||||
builder: (context) => CheckboxListTile(
|
||||
dense: true,
|
||||
value: reason.contains(index + 1),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (value) {
|
||||
if (value!) {
|
||||
reason.add(index + 1);
|
||||
} else {
|
||||
reason.remove(index + 1);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
title: Text(const ['头像违规', '昵称违规', '签名违规'][index]),
|
||||
),
|
||||
builder: (context) {
|
||||
final checked = reason.contains(index + 1);
|
||||
return ListTile(
|
||||
dense: true,
|
||||
minTileHeight: 40,
|
||||
onTap: () {
|
||||
if (!checked) {
|
||||
reason.add(index + 1);
|
||||
} else {
|
||||
reason.remove(index + 1);
|
||||
}
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
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: (context) => RadioGroup<int>(
|
||||
onChanged: (v) {
|
||||
reasonV2 = v;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonV2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(
|
||||
5,
|
||||
(index) => RadioListTile<int>(
|
||||
toggleable: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: const EdgeInsets.only(left: 4),
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: .start,
|
||||
children: List.generate(
|
||||
_reasonV2.length,
|
||||
(index) {
|
||||
final checked = index == reasonV2;
|
||||
return ListTile(
|
||||
dense: true,
|
||||
value: index,
|
||||
title: Text(
|
||||
const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
|
||||
minTileHeight: 40,
|
||||
onTap: () {
|
||||
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(
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
if (reason.isEmpty) {
|
||||
SmartDialog.showToast('至少选择一项作为举报内容');
|
||||
} else {
|
||||
Get.back();
|
||||
var result = await MemberHttp.reportMember(
|
||||
MemberHttp.reportMember(
|
||||
mid,
|
||||
reason: reason.join(','),
|
||||
reasonV2: reasonV2 != null ? reasonV2! + 1 : null,
|
||||
);
|
||||
if (result['msg'] is String && result['msg'].isNotEmpty) {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
} else {
|
||||
SmartDialog.showToast('举报失败');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
|
||||
@@ -3,62 +3,131 @@ import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
|
||||
final Color? color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
class DisabledIcon extends SingleChildRenderObjectWidget {
|
||||
const DisabledIcon({
|
||||
super.key,
|
||||
required T child,
|
||||
required Widget super.child,
|
||||
this.disable = false,
|
||||
this.color,
|
||||
double? lineLengthScale,
|
||||
StrokeCap? strokeCap,
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
this.iconSize,
|
||||
this.lineLengthScale = 0.9,
|
||||
this.strokeCap = .butt,
|
||||
});
|
||||
|
||||
final bool disable;
|
||||
final Color? color;
|
||||
final double? iconSize;
|
||||
final StrokeCap strokeCap;
|
||||
final double lineLengthScale;
|
||||
|
||||
Icon? get _icon => child is Icon ? child as Icon : null;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
late final iconTheme = IconTheme.of(context);
|
||||
final icon = _icon;
|
||||
return RenderMaskedIcon(
|
||||
color ??
|
||||
(child is Icon
|
||||
? (child as Icon).color ?? IconTheme.of(context).color!
|
||||
: IconTheme.of(context).color!),
|
||||
lineLengthScale,
|
||||
strokeCap,
|
||||
disable: disable,
|
||||
iconSize: iconSize ?? icon?.size ?? iconTheme.size ?? 24.0,
|
||||
color: color ?? icon?.color ?? iconTheme.color!,
|
||||
strokeCap: strokeCap,
|
||||
lineLengthScale: lineLengthScale,
|
||||
);
|
||||
}
|
||||
|
||||
T enable() => child as T;
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) {
|
||||
late final iconTheme = IconTheme.of(context);
|
||||
final icon = _icon;
|
||||
renderObject
|
||||
..disable = disable
|
||||
..iconSize = iconSize ?? icon?.size ?? iconTheme.size ?? 24.0
|
||||
..color = color ?? icon?.color ?? iconTheme.color!
|
||||
..strokeCap = strokeCap
|
||||
..lineLengthScale = lineLengthScale;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderMaskedIcon extends RenderProxyBox {
|
||||
final Color color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
RenderMaskedIcon({
|
||||
required bool disable,
|
||||
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
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final strokeWidth = size.width / 12;
|
||||
if (!disable) {
|
||||
return super.paint(context, offset);
|
||||
}
|
||||
|
||||
final canvas = context.canvas;
|
||||
var rect = offset & size;
|
||||
|
||||
var rectOffset = offset;
|
||||
Size size = this.size;
|
||||
final exceedWidth = size.width > _iconSize;
|
||||
final exceedHeight = size.height > _iconSize;
|
||||
if (exceedWidth || exceedHeight) {
|
||||
final dx = exceedWidth ? (size.width - _iconSize) / 2.0 : 0.0;
|
||||
final dy = exceedHeight ? (size.height - _iconSize) / 2.0 : 0.0;
|
||||
size = Size.square(_iconSize);
|
||||
rectOffset += Offset(dx, dy);
|
||||
} else if (size.width < _iconSize && size.height < _iconSize) {
|
||||
size = Size.square(_iconSize);
|
||||
}
|
||||
|
||||
final strokeWidth = size.width / 12;
|
||||
|
||||
var rect = rectOffset & size;
|
||||
|
||||
final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4
|
||||
|
||||
// final path = Path.combine(
|
||||
// PathOperation.difference,
|
||||
// Path()..addRect(rect),
|
||||
// Path()..moveTo(rect.left, rect.top)
|
||||
// ..relativeLineTo(sqrt2Width, 0)
|
||||
// ..lineTo(rect.right, rect.bottom - sqrt2Width)
|
||||
// ..lineTo(rect.right, rect.bottom)
|
||||
// ..close(),
|
||||
// );
|
||||
|
||||
final path = Path.combine(
|
||||
PathOperation.union,
|
||||
Path() // bottom
|
||||
@@ -77,7 +146,7 @@ class RenderMaskedIcon extends RenderProxyBox {
|
||||
..clipPath(path, doAntiAlias: false);
|
||||
super.paint(context, offset);
|
||||
|
||||
context.canvas.restore();
|
||||
canvas.restore();
|
||||
|
||||
final linePaint = Paint()
|
||||
..color = color
|
||||
@@ -95,8 +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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
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;
|
||||
}
|
||||
}
|
||||
777
lib/common/widgets/floating_navigation_bar.dart
Normal file
777
lib/common/widgets/floating_navigation_bar.dart
Normal file
@@ -0,0 +1,777 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/utils/extension/theme_ext.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _kMaxLabelTextScaleFactor = 1.3;
|
||||
|
||||
const _kNavigationHeight = 64.0;
|
||||
const _kIndicatorHeight = _kNavigationHeight - 2 * _kIndicatorPaddingInt;
|
||||
const _kIndicatorWidth = 86.0;
|
||||
const _kIndicatorPaddingInt = 4.0;
|
||||
const _kIndicatorPadding = EdgeInsets.all(_kIndicatorPaddingInt);
|
||||
const _kBorderRadius = BorderRadius.all(.circular(_kNavigationHeight / 2));
|
||||
const _kNavigationShape = RoundedSuperellipseBorder(
|
||||
borderRadius: _kBorderRadius,
|
||||
);
|
||||
|
||||
/// ref [NavigationBar]
|
||||
class FloatingNavigationBar extends StatelessWidget {
|
||||
// ignore: prefer_const_constructors_in_immutables
|
||||
FloatingNavigationBar({
|
||||
super.key,
|
||||
this.animationDuration = const Duration(milliseconds: 500),
|
||||
this.selectedIndex = 0,
|
||||
required this.destinations,
|
||||
this.onDestinationSelected,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shadowColor,
|
||||
this.surfaceTintColor,
|
||||
this.indicatorColor,
|
||||
this.indicatorShape,
|
||||
this.labelBehavior,
|
||||
this.overlayColor,
|
||||
this.labelTextStyle,
|
||||
this.labelPadding,
|
||||
this.bottomPadding = 8.0,
|
||||
}) : assert(destinations.length >= 2),
|
||||
assert(0 <= selectedIndex && selectedIndex < destinations.length);
|
||||
|
||||
final Duration animationDuration;
|
||||
final int selectedIndex;
|
||||
final List<Widget> destinations;
|
||||
final ValueChanged<int>? onDestinationSelected;
|
||||
final Color? backgroundColor;
|
||||
final double? elevation;
|
||||
final Color? shadowColor;
|
||||
final Color? surfaceTintColor;
|
||||
final Color? indicatorColor;
|
||||
final ShapeBorder? indicatorShape;
|
||||
final NavigationDestinationLabelBehavior? labelBehavior;
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
final WidgetStateProperty<TextStyle?>? labelTextStyle;
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
final double bottomPadding;
|
||||
|
||||
VoidCallback _handleTap(int index) {
|
||||
return onDestinationSelected != null
|
||||
? () => onDestinationSelected!(index)
|
||||
: () {};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaults = _NavigationBarDefaultsM3(context);
|
||||
|
||||
final navigationBarTheme = NavigationBarTheme.of(context);
|
||||
final effectiveLabelBehavior =
|
||||
labelBehavior ??
|
||||
navigationBarTheme.labelBehavior ??
|
||||
defaults.labelBehavior!;
|
||||
|
||||
final padding = MediaQuery.viewPaddingOf(context);
|
||||
|
||||
return UnconstrainedBox(
|
||||
child: Padding(
|
||||
padding: .fromLTRB(
|
||||
padding.left,
|
||||
0,
|
||||
padding.right,
|
||||
bottomPadding + padding.bottom,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _kNavigationHeight,
|
||||
width: destinations.length * _kIndicatorWidth,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: ElevationOverlay.applySurfaceTint(
|
||||
backgroundColor ??
|
||||
navigationBarTheme.backgroundColor ??
|
||||
defaults.backgroundColor!,
|
||||
surfaceTintColor ??
|
||||
navigationBarTheme.surfaceTintColor ??
|
||||
defaults.surfaceTintColor,
|
||||
elevation ??
|
||||
navigationBarTheme.elevation ??
|
||||
defaults.elevation!,
|
||||
),
|
||||
shape: RoundedSuperellipseBorder(
|
||||
side: defaults.borderSide,
|
||||
borderRadius: _kBorderRadius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: _kIndicatorPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: .stretch,
|
||||
children: <Widget>[
|
||||
for (int i = 0; i < destinations.length; i++)
|
||||
Expanded(
|
||||
child: _SelectableAnimatedBuilder(
|
||||
duration: animationDuration,
|
||||
isSelected: i == selectedIndex,
|
||||
builder: (context, animation) {
|
||||
return _NavigationDestinationInfo(
|
||||
index: i,
|
||||
selectedIndex: selectedIndex,
|
||||
totalNumberOfDestinations: destinations.length,
|
||||
selectedAnimation: animation,
|
||||
labelBehavior: effectiveLabelBehavior,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorShape: indicatorShape,
|
||||
overlayColor: overlayColor,
|
||||
onTap: _handleTap(i),
|
||||
labelTextStyle: labelTextStyle,
|
||||
labelPadding: labelPadding,
|
||||
child: destinations[i],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingNavigationDestination extends StatelessWidget {
|
||||
const FloatingNavigationDestination({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.selectedIcon,
|
||||
required this.label,
|
||||
this.tooltip,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
|
||||
final Widget? selectedIcon;
|
||||
|
||||
final String label;
|
||||
|
||||
final String? tooltip;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final info = _NavigationDestinationInfo.of(context);
|
||||
const selectedState = <WidgetState>{WidgetState.selected};
|
||||
const unselectedState = <WidgetState>{};
|
||||
const disabledState = <WidgetState>{WidgetState.disabled};
|
||||
|
||||
final navigationBarTheme = NavigationBarTheme.of(context);
|
||||
final defaults = _NavigationBarDefaultsM3(context);
|
||||
final animation = info.selectedAnimation;
|
||||
|
||||
return Stack(
|
||||
alignment: .center,
|
||||
clipBehavior: .none,
|
||||
children: [
|
||||
NavigationIndicator(
|
||||
animation: animation,
|
||||
color:
|
||||
info.indicatorColor ??
|
||||
navigationBarTheme.indicatorColor ??
|
||||
defaults.indicatorColor!,
|
||||
),
|
||||
_NavigationDestinationBuilder(
|
||||
label: label,
|
||||
tooltip: tooltip,
|
||||
enabled: enabled,
|
||||
buildIcon: (context) {
|
||||
final IconThemeData selectedIconTheme =
|
||||
navigationBarTheme.iconTheme?.resolve(selectedState) ??
|
||||
defaults.iconTheme!.resolve(selectedState)!;
|
||||
final IconThemeData unselectedIconTheme =
|
||||
navigationBarTheme.iconTheme?.resolve(unselectedState) ??
|
||||
defaults.iconTheme!.resolve(unselectedState)!;
|
||||
final IconThemeData disabledIconTheme =
|
||||
navigationBarTheme.iconTheme?.resolve(disabledState) ??
|
||||
defaults.iconTheme!.resolve(disabledState)!;
|
||||
|
||||
final Widget selectedIconWidget = IconTheme.merge(
|
||||
data: enabled ? selectedIconTheme : disabledIconTheme,
|
||||
child: selectedIcon ?? icon,
|
||||
);
|
||||
final Widget unselectedIconWidget = IconTheme.merge(
|
||||
data: enabled ? unselectedIconTheme : disabledIconTheme,
|
||||
child: icon,
|
||||
);
|
||||
return _StatusTransitionWidgetBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return animation.isForwardOrCompleted
|
||||
? selectedIconWidget
|
||||
: unselectedIconWidget;
|
||||
},
|
||||
);
|
||||
},
|
||||
buildLabel: (context) {
|
||||
final TextStyle? effectiveSelectedLabelTextStyle =
|
||||
info.labelTextStyle?.resolve(selectedState) ??
|
||||
navigationBarTheme.labelTextStyle?.resolve(selectedState) ??
|
||||
defaults.labelTextStyle!.resolve(selectedState);
|
||||
final TextStyle? effectiveUnselectedLabelTextStyle =
|
||||
info.labelTextStyle?.resolve(unselectedState) ??
|
||||
navigationBarTheme.labelTextStyle?.resolve(unselectedState) ??
|
||||
defaults.labelTextStyle!.resolve(unselectedState);
|
||||
final TextStyle? effectiveDisabledLabelTextStyle =
|
||||
info.labelTextStyle?.resolve(disabledState) ??
|
||||
navigationBarTheme.labelTextStyle?.resolve(disabledState) ??
|
||||
defaults.labelTextStyle!.resolve(disabledState);
|
||||
final EdgeInsetsGeometry labelPadding =
|
||||
info.labelPadding ??
|
||||
navigationBarTheme.labelPadding ??
|
||||
defaults.labelPadding!;
|
||||
|
||||
final textStyle = enabled
|
||||
? animation.isForwardOrCompleted
|
||||
? effectiveSelectedLabelTextStyle
|
||||
: effectiveUnselectedLabelTextStyle
|
||||
: effectiveDisabledLabelTextStyle;
|
||||
|
||||
return Padding(
|
||||
padding: labelPadding,
|
||||
child: MediaQuery.withClampedTextScaling(
|
||||
maxScaleFactor: _kMaxLabelTextScaleFactor,
|
||||
child: Text(label, style: textStyle),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestinationBuilder extends StatefulWidget {
|
||||
const _NavigationDestinationBuilder({
|
||||
required this.buildIcon,
|
||||
required this.buildLabel,
|
||||
required this.label,
|
||||
this.tooltip,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final WidgetBuilder buildIcon;
|
||||
|
||||
final WidgetBuilder buildLabel;
|
||||
|
||||
final String label;
|
||||
|
||||
final String? tooltip;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
State<_NavigationDestinationBuilder> createState() =>
|
||||
_NavigationDestinationBuilderState();
|
||||
}
|
||||
|
||||
class _NavigationDestinationBuilderState
|
||||
extends State<_NavigationDestinationBuilder> {
|
||||
final GlobalKey iconKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final info = _NavigationDestinationInfo.of(context);
|
||||
|
||||
final child = GestureDetector(
|
||||
behavior: .opaque,
|
||||
onTap: widget.enabled ? info.onTap : null,
|
||||
child: _NavigationBarDestinationLayout(
|
||||
icon: widget.buildIcon(context),
|
||||
iconKey: iconKey,
|
||||
label: widget.buildLabel(context),
|
||||
),
|
||||
);
|
||||
if (info.labelBehavior == .alwaysShow) {
|
||||
return child;
|
||||
}
|
||||
return _NavigationBarDestinationTooltip(
|
||||
message: widget.tooltip ?? widget.label,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestinationInfo extends InheritedWidget {
|
||||
const _NavigationDestinationInfo({
|
||||
required this.index,
|
||||
required this.selectedIndex,
|
||||
required this.totalNumberOfDestinations,
|
||||
required this.selectedAnimation,
|
||||
required this.labelBehavior,
|
||||
required this.indicatorColor,
|
||||
required this.indicatorShape,
|
||||
required this.overlayColor,
|
||||
required this.onTap,
|
||||
this.labelTextStyle,
|
||||
this.labelPadding,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final int index;
|
||||
|
||||
final int selectedIndex;
|
||||
|
||||
final int totalNumberOfDestinations;
|
||||
|
||||
final Animation<double> selectedAnimation;
|
||||
|
||||
final NavigationDestinationLabelBehavior labelBehavior;
|
||||
|
||||
final Color? indicatorColor;
|
||||
|
||||
final ShapeBorder? indicatorShape;
|
||||
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
final WidgetStateProperty<TextStyle?>? labelTextStyle;
|
||||
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
|
||||
static _NavigationDestinationInfo of(BuildContext context) {
|
||||
final _NavigationDestinationInfo? result = context
|
||||
.dependOnInheritedWidgetOfExactType<_NavigationDestinationInfo>();
|
||||
assert(
|
||||
result != null,
|
||||
'Navigation destinations need a _NavigationDestinationInfo parent, '
|
||||
'which is usually provided by NavigationBar.',
|
||||
);
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_NavigationDestinationInfo oldWidget) {
|
||||
return index != oldWidget.index ||
|
||||
totalNumberOfDestinations != oldWidget.totalNumberOfDestinations ||
|
||||
selectedAnimation != oldWidget.selectedAnimation ||
|
||||
labelBehavior != oldWidget.labelBehavior ||
|
||||
onTap != oldWidget.onTap;
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationIndicator extends StatelessWidget {
|
||||
const NavigationIndicator({
|
||||
super.key,
|
||||
required this.animation,
|
||||
this.color,
|
||||
this.width = _kIndicatorWidth,
|
||||
this.height = _kIndicatorHeight,
|
||||
});
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
final Color? color;
|
||||
|
||||
final double width;
|
||||
|
||||
final double height;
|
||||
|
||||
static final _anim = Tween<double>(
|
||||
begin: .5,
|
||||
end: 1.0,
|
||||
).chain(CurveTween(curve: Curves.easeInOutCubicEmphasized));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
final double scale = animation.isDismissed
|
||||
? 0.0
|
||||
: _anim.evaluate(animation);
|
||||
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.diagonal3Values(scale, 1.0, 1.0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
||||
child: _StatusTransitionWidgetBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return _SelectableAnimatedBuilder(
|
||||
isSelected: animation.isForwardOrCompleted,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
alwaysDoFullAnimation: true,
|
||||
builder: (context, fadeAnimation) {
|
||||
return FadeTransition(
|
||||
opacity: fadeAnimation,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: _kNavigationShape,
|
||||
color: color ?? Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: _kIndicatorWidth,
|
||||
height: _kIndicatorHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationBarDestinationLayout extends StatelessWidget {
|
||||
const _NavigationBarDestinationLayout({
|
||||
required this.icon,
|
||||
required this.iconKey,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
|
||||
final GlobalKey iconKey;
|
||||
|
||||
final Widget label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _DestinationLayoutAnimationBuilder(
|
||||
builder: (context, animation) {
|
||||
return CustomMultiChildLayout(
|
||||
delegate: _NavigationDestinationLayoutDelegate(animation: animation),
|
||||
children: <Widget>[
|
||||
LayoutId(
|
||||
id: _NavigationDestinationLayoutDelegate.iconId,
|
||||
child: KeyedSubtree(key: iconKey, child: icon),
|
||||
),
|
||||
LayoutId(
|
||||
id: _NavigationDestinationLayoutDelegate.labelId,
|
||||
child: FadeTransition(
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: animation,
|
||||
child: label,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DestinationLayoutAnimationBuilder extends StatelessWidget {
|
||||
const _DestinationLayoutAnimationBuilder({required this.builder});
|
||||
|
||||
final Widget Function(BuildContext, Animation<double>) builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final info = _NavigationDestinationInfo.of(context);
|
||||
switch (info.labelBehavior) {
|
||||
case NavigationDestinationLabelBehavior.alwaysShow:
|
||||
return builder(context, kAlwaysCompleteAnimation);
|
||||
case NavigationDestinationLabelBehavior.alwaysHide:
|
||||
return builder(context, kAlwaysDismissedAnimation);
|
||||
case NavigationDestinationLabelBehavior.onlyShowSelected:
|
||||
return _CurvedAnimationBuilder(
|
||||
animation: info.selectedAnimation,
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
reverseCurve: Curves.easeInOutCubicEmphasized.flipped,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationBarDestinationTooltip extends StatelessWidget {
|
||||
const _NavigationBarDestinationTooltip({
|
||||
required this.message,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String message;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: message,
|
||||
verticalOffset: 34,
|
||||
excludeFromSemantics: true,
|
||||
preferBelow: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate {
|
||||
_NavigationDestinationLayoutDelegate({required this.animation})
|
||||
: super(relayout: animation);
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
static const int iconId = 1;
|
||||
|
||||
static const int labelId = 2;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
double halfWidth(Size size) => size.width / 2;
|
||||
double halfHeight(Size size) => size.height / 2;
|
||||
|
||||
final Size iconSize = layoutChild(iconId, BoxConstraints.loose(size));
|
||||
final Size labelSize = layoutChild(labelId, BoxConstraints.loose(size));
|
||||
|
||||
final double yPositionOffset = Tween<double>(
|
||||
begin: halfHeight(iconSize),
|
||||
|
||||
end: halfHeight(iconSize) + halfHeight(labelSize),
|
||||
).transform(animation.value);
|
||||
final double iconYPosition = halfHeight(size) - yPositionOffset;
|
||||
|
||||
positionChild(
|
||||
iconId,
|
||||
Offset(
|
||||
halfWidth(size) - halfWidth(iconSize),
|
||||
iconYPosition,
|
||||
),
|
||||
);
|
||||
|
||||
positionChild(
|
||||
labelId,
|
||||
Offset(
|
||||
halfWidth(size) - halfWidth(labelSize),
|
||||
|
||||
iconYPosition + iconSize.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_NavigationDestinationLayoutDelegate oldDelegate) {
|
||||
return oldDelegate.animation != animation;
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusTransitionWidgetBuilder extends StatusTransitionWidget {
|
||||
const _StatusTransitionWidgetBuilder({
|
||||
required super.animation,
|
||||
required this.builder,
|
||||
// ignore: unused_element_parameter
|
||||
this.child,
|
||||
});
|
||||
|
||||
final TransitionBuilder builder;
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => builder(context, child);
|
||||
}
|
||||
|
||||
class _SelectableAnimatedBuilder extends StatefulWidget {
|
||||
const _SelectableAnimatedBuilder({
|
||||
required this.isSelected,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
this.alwaysDoFullAnimation = false,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
final bool alwaysDoFullAnimation;
|
||||
|
||||
final Widget Function(BuildContext, Animation<double>) builder;
|
||||
|
||||
@override
|
||||
_SelectableAnimatedBuilderState createState() =>
|
||||
_SelectableAnimatedBuilderState();
|
||||
}
|
||||
|
||||
class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this);
|
||||
_controller.duration = widget.duration;
|
||||
_controller.value = widget.isSelected ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.duration != widget.duration) {
|
||||
_controller.duration = widget.duration;
|
||||
}
|
||||
if (oldWidget.isSelected != widget.isSelected) {
|
||||
if (widget.isSelected) {
|
||||
_controller.forward(from: widget.alwaysDoFullAnimation ? 0 : null);
|
||||
} else {
|
||||
_controller.reverse(from: widget.alwaysDoFullAnimation ? 1 : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context, _controller);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurvedAnimationBuilder extends StatefulWidget {
|
||||
const _CurvedAnimationBuilder({
|
||||
required this.animation,
|
||||
required this.curve,
|
||||
required this.reverseCurve,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final Animation<double> animation;
|
||||
final Curve curve;
|
||||
final Curve reverseCurve;
|
||||
final Widget Function(BuildContext, Animation<double>) builder;
|
||||
|
||||
@override
|
||||
_CurvedAnimationBuilderState createState() => _CurvedAnimationBuilderState();
|
||||
}
|
||||
|
||||
class _CurvedAnimationBuilderState extends State<_CurvedAnimationBuilder> {
|
||||
late AnimationStatus _animationDirection;
|
||||
AnimationStatus? _preservedDirection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationDirection = widget.animation.status;
|
||||
_updateStatus(widget.animation.status);
|
||||
widget.animation.addStatusListener(_updateStatus);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.animation.removeStatusListener(_updateStatus);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateStatus(AnimationStatus status) {
|
||||
if (_animationDirection != status) {
|
||||
setState(() {
|
||||
_animationDirection = status;
|
||||
});
|
||||
}
|
||||
switch (status) {
|
||||
case AnimationStatus.forward || AnimationStatus.reverse
|
||||
when _preservedDirection != null:
|
||||
break;
|
||||
case AnimationStatus.forward || AnimationStatus.reverse:
|
||||
setState(() {
|
||||
_preservedDirection = status;
|
||||
});
|
||||
case AnimationStatus.completed || AnimationStatus.dismissed:
|
||||
setState(() {
|
||||
_preservedDirection = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shouldUseForwardCurve =
|
||||
(_preservedDirection ?? _animationDirection) != AnimationStatus.reverse;
|
||||
|
||||
final Animation<double> curvedAnimation = CurveTween(
|
||||
curve: shouldUseForwardCurve ? widget.curve : widget.reverseCurve,
|
||||
).animate(widget.animation);
|
||||
|
||||
return widget.builder(context, curvedAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
const _indicatorDark = Color(0x15FFFFFF);
|
||||
const _indicatorLight = Color(0x10000000);
|
||||
|
||||
class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
|
||||
_NavigationBarDefaultsM3(this.context)
|
||||
: super(
|
||||
height: _kNavigationHeight,
|
||||
elevation: 3.0,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
late final _colors = Theme.of(context).colorScheme;
|
||||
late final _textTheme = Theme.of(context).textTheme;
|
||||
|
||||
BorderSide get borderSide => _colors.brightness.isDark
|
||||
? const BorderSide(color: Color(0x08FFFFFF))
|
||||
: const BorderSide(color: Color(0x08000000));
|
||||
|
||||
@override
|
||||
Color? get backgroundColor => _colors.surfaceContainer;
|
||||
|
||||
@override
|
||||
Color? get shadowColor => Colors.transparent;
|
||||
|
||||
@override
|
||||
Color? get surfaceTintColor => Colors.transparent;
|
||||
|
||||
@override
|
||||
WidgetStateProperty<IconThemeData?>? get iconTheme {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
return IconThemeData(
|
||||
size: 24.0,
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? _colors.onSurfaceVariant.withValues(alpha: 0.38)
|
||||
: states.contains(WidgetState.selected)
|
||||
? _colors.onSecondaryContainer
|
||||
: _colors.onSurfaceVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Color? get indicatorColor =>
|
||||
_colors.brightness.isDark ? _indicatorDark : _indicatorLight;
|
||||
|
||||
@override
|
||||
ShapeBorder? get indicatorShape => const StadiumBorder();
|
||||
|
||||
@override
|
||||
WidgetStateProperty<TextStyle?>? get labelTextStyle {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
final TextStyle style = _textTheme.labelMedium!;
|
||||
return style.apply(
|
||||
color: states.contains(WidgetState.disabled)
|
||||
? _colors.onSurfaceVariant.withValues(alpha: 0.38)
|
||||
: states.contains(WidgetState.selected)
|
||||
? _colors.onSurface
|
||||
: _colors.onSurfaceVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry? get labelPadding => const EdgeInsets.only(top: 2);
|
||||
}
|
||||
385
lib/common/widgets/flutter/chat_list_view.dart
Normal file
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 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide DraggableScrollableSheet, LayoutBuilder;
|
||||
|
||||
/// Controls a [DraggableScrollableSheet].
|
||||
///
|
||||
@@ -112,11 +114,10 @@ class DraggableScrollableController extends ChangeNotifier {
|
||||
_assertAttached();
|
||||
assert(size >= 0 && size <= 1);
|
||||
assert(duration != Duration.zero);
|
||||
final AnimationController animationController =
|
||||
AnimationController.unbounded(
|
||||
vsync: _attachedController!.position.context.vsync,
|
||||
value: _attachedController!.extent.currentSize,
|
||||
);
|
||||
final animationController = AnimationController.unbounded(
|
||||
vsync: _attachedController!.position.context.vsync,
|
||||
value: _attachedController!.extent.currentSize,
|
||||
);
|
||||
_animationControllers.add(animationController);
|
||||
_attachedController!.position.goIdle();
|
||||
// This disables any snapping until the next user interaction with the sheet.
|
||||
@@ -583,7 +584,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
}
|
||||
|
||||
List<double> _impliedSnapSizes() {
|
||||
for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
|
||||
for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
|
||||
final double snapSize = widget.snapSizes![index];
|
||||
assert(
|
||||
snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
|
||||
@@ -684,11 +685,11 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
// have changed when the widget was updated.
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
for (
|
||||
int index = 0;
|
||||
var index = 0;
|
||||
index < _scrollController.positions.length;
|
||||
index++
|
||||
) {
|
||||
final _DraggableScrollableSheetScrollPosition position =
|
||||
final position =
|
||||
_scrollController.positions.elementAt(index)
|
||||
as _DraggableScrollableSheetScrollPosition;
|
||||
position.goBallistic(0);
|
||||
@@ -702,7 +703,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
.asMap()
|
||||
.keys
|
||||
.map((int index) {
|
||||
final String snapSizeString = widget.snapSizes![index].toString();
|
||||
final snapSizeString = widget.snapSizes![index].toString();
|
||||
if (index == invalidIndex) {
|
||||
return '>>> $snapSizeString <<<';
|
||||
}
|
||||
@@ -917,14 +918,10 @@ class _DraggableScrollableSheetScrollPosition
|
||||
);
|
||||
}
|
||||
|
||||
final AnimationController ballisticController =
|
||||
AnimationController.unbounded(
|
||||
debugLabel: objectRuntimeType(
|
||||
this,
|
||||
'_DraggableScrollableSheetPosition',
|
||||
),
|
||||
vsync: context.vsync,
|
||||
);
|
||||
final ballisticController = AnimationController.unbounded(
|
||||
debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'),
|
||||
vsync: context.vsync,
|
||||
);
|
||||
_ballisticControllers.add(ballisticController);
|
||||
|
||||
double lastPosition = extent.currentPixels;
|
||||
@@ -1080,8 +1077,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
|
||||
return false;
|
||||
}
|
||||
assert(widget is _InheritedResetNotifier);
|
||||
final _InheritedResetNotifier inheritedNotifier =
|
||||
widget as _InheritedResetNotifier;
|
||||
final inheritedNotifier = widget as _InheritedResetNotifier;
|
||||
final bool wasCalled = inheritedNotifier.notifier!._wasCalled;
|
||||
inheritedNotifier.notifier!._wasCalled = false;
|
||||
return wasCalled;
|
||||
@@ -1158,6 +1154,10 @@ class _SnappingSimulation extends Simulation {
|
||||
return pixelSnapSizes.first;
|
||||
}
|
||||
final double nextSize = pixelSnapSizes[indexOfNextSize];
|
||||
// If already snapped - keep this as target size
|
||||
if (nextSize == position) {
|
||||
return nextSize;
|
||||
}
|
||||
final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
|
||||
if (initialVelocity.abs() <= tolerance.velocity) {
|
||||
// If velocity is zero, snap to the nearest snap size with the minimum velocity.
|
||||
@@ -17,10 +17,12 @@ library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide DraggableScrollableSheet, LayoutBuilder;
|
||||
|
||||
/// Controls a [DraggableScrollableSheet].
|
||||
///
|
||||
@@ -112,11 +114,10 @@ class DraggableScrollableController extends ChangeNotifier {
|
||||
_assertAttached();
|
||||
assert(size >= 0 && size <= 1);
|
||||
assert(duration != Duration.zero);
|
||||
final AnimationController animationController =
|
||||
AnimationController.unbounded(
|
||||
vsync: _attachedController!.position.context.vsync,
|
||||
value: _attachedController!.extent.currentSize,
|
||||
);
|
||||
final animationController = AnimationController.unbounded(
|
||||
vsync: _attachedController!.position.context.vsync,
|
||||
value: _attachedController!.extent.currentSize,
|
||||
);
|
||||
_animationControllers.add(animationController);
|
||||
_attachedController!.position.goIdle();
|
||||
// This disables any snapping until the next user interaction with the sheet.
|
||||
@@ -587,7 +588,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
}
|
||||
|
||||
List<double> _impliedSnapSizes() {
|
||||
for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
|
||||
for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
|
||||
final double snapSize = widget.snapSizes![index];
|
||||
assert(
|
||||
snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
|
||||
@@ -688,11 +689,11 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
// have changed when the widget was updated.
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
for (
|
||||
int index = 0;
|
||||
var index = 0;
|
||||
index < _scrollController.positions.length;
|
||||
index++
|
||||
) {
|
||||
final _DraggableScrollableSheetScrollPosition position =
|
||||
final position =
|
||||
_scrollController.positions.elementAt(index)
|
||||
as _DraggableScrollableSheetScrollPosition;
|
||||
position.goBallistic(0);
|
||||
@@ -706,7 +707,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
|
||||
.asMap()
|
||||
.keys
|
||||
.map((int index) {
|
||||
final String snapSizeString = widget.snapSizes![index].toString();
|
||||
final snapSizeString = widget.snapSizes![index].toString();
|
||||
if (index == invalidIndex) {
|
||||
return '>>> $snapSizeString <<<';
|
||||
}
|
||||
@@ -920,14 +921,10 @@ class _DraggableScrollableSheetScrollPosition
|
||||
);
|
||||
}
|
||||
|
||||
final AnimationController ballisticController =
|
||||
AnimationController.unbounded(
|
||||
debugLabel: objectRuntimeType(
|
||||
this,
|
||||
'_DraggableScrollableSheetPosition',
|
||||
),
|
||||
vsync: context.vsync,
|
||||
);
|
||||
final ballisticController = AnimationController.unbounded(
|
||||
debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'),
|
||||
vsync: context.vsync,
|
||||
);
|
||||
_ballisticControllers.add(ballisticController);
|
||||
|
||||
double lastPosition = extent.currentPixels;
|
||||
@@ -1082,8 +1079,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
|
||||
return false;
|
||||
}
|
||||
assert(widget is _InheritedResetNotifier);
|
||||
final _InheritedResetNotifier inheritedNotifier =
|
||||
widget as _InheritedResetNotifier;
|
||||
final inheritedNotifier = widget as _InheritedResetNotifier;
|
||||
final bool wasCalled = inheritedNotifier.notifier!._wasCalled;
|
||||
inheritedNotifier.notifier!._wasCalled = false;
|
||||
return wasCalled;
|
||||
@@ -1160,6 +1156,10 @@ class _SnappingSimulation extends Simulation {
|
||||
return pixelSnapSizes.first;
|
||||
}
|
||||
final double nextSize = pixelSnapSizes[indexOfNextSize];
|
||||
// If already snapped - keep this as target size
|
||||
if (nextSize == position) {
|
||||
return nextSize;
|
||||
}
|
||||
final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
|
||||
if (initialVelocity.abs() <= tolerance.velocity) {
|
||||
// If velocity is zero, snap to the nearest snap size with the minimum velocity.
|
||||
526
lib/common/widgets/flutter/layout_builder.dart
Normal file
526
lib/common/widgets/flutter/layout_builder.dart
Normal file
@@ -0,0 +1,526 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// An abstract superclass for widgets that defer their building until layout.
|
||||
///
|
||||
/// Similar to the [Builder] widget except that the implementation calls the [builder]
|
||||
/// function at layout time and provides the [LayoutInfoType] that is required to
|
||||
/// configure the child widget subtree.
|
||||
///
|
||||
/// This is useful when the child widget tree relies on information that are only
|
||||
/// available during layout, and doesn't depend on the child's intrinsic size.
|
||||
///
|
||||
/// The [LayoutInfoType] should typically be immutable. The equality of the
|
||||
/// [LayoutInfoType] type is used by the implementation to avoid unnecessary
|
||||
/// rebuilds: if the new [LayoutInfoType] computed during layout is the same as
|
||||
/// (defined by `LayoutInfoType.==`) the previous [LayoutInfoType], the
|
||||
/// implementation will try to avoid calling the [builder] again unless
|
||||
/// [updateShouldRebuild] returns true. The corresponding [RenderObject] produced
|
||||
/// by this widget retains the most up-to-date [LayoutInfoType] for this purpose,
|
||||
/// which may keep a [LayoutInfoType] object in memory until the widget is removed
|
||||
/// from the tree.
|
||||
///
|
||||
/// Subclasses must return a [RenderObject] that mixes in [RenderAbstractLayoutBuilderMixin].
|
||||
abstract class AbstractLayoutBuilder<LayoutInfoType>
|
||||
extends RenderObjectWidget {
|
||||
/// Creates a widget that defers its building until layout.
|
||||
const AbstractLayoutBuilder({super.key});
|
||||
|
||||
/// Called at layout time to construct the widget tree.
|
||||
///
|
||||
/// The builder must not return null.
|
||||
Widget Function(BuildContext context, LayoutInfoType layoutInfo) get builder;
|
||||
|
||||
@override
|
||||
RenderObjectElement createElement() =>
|
||||
_LayoutBuilderElement<LayoutInfoType>(this);
|
||||
|
||||
/// Whether [builder] needs to be called again even if the layout constraints
|
||||
/// are the same.
|
||||
///
|
||||
/// When this widget's configuration is updated, the [builder] callback most
|
||||
/// likely needs to be called to build this widget's child. However,
|
||||
/// subclasses may provide ways in which the widget can be updated without
|
||||
/// needing to rebuild the child. Such subclasses can use this method to tell
|
||||
/// the framework when the child widget should be rebuilt.
|
||||
///
|
||||
/// When this method is called by the framework, the newly configured widget
|
||||
/// is asked if it requires a rebuild, and it is passed the old widget as a
|
||||
/// parameter.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [State.setState] and [State.didUpdateWidget], which talk about widget
|
||||
/// configuration changes and how they're triggered.
|
||||
/// * [Element.update], the method that actually updates the widget's
|
||||
/// configuration.
|
||||
@protected
|
||||
bool updateShouldRebuild(
|
||||
covariant AbstractLayoutBuilder<LayoutInfoType> oldWidget,
|
||||
) => true;
|
||||
|
||||
@override
|
||||
RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject>
|
||||
createRenderObject(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
// updateRenderObject is redundant with the logic in the LayoutBuilderElement below.
|
||||
}
|
||||
|
||||
/// A specialized [AbstractLayoutBuilder] whose widget subtree depends on the
|
||||
/// incoming [ConstraintType] that will be imposed on the widget.
|
||||
///
|
||||
/// {@template flutter.widgets.ConstrainedLayoutBuilder}
|
||||
/// The [builder] function is called in the following situations:
|
||||
///
|
||||
/// * The first time the widget is laid out.
|
||||
/// * When the parent widget passes different layout constraints.
|
||||
/// * When the parent widget updates this widget and [updateShouldRebuild] returns `true`.
|
||||
/// * When the dependencies that the [builder] function subscribes to change.
|
||||
///
|
||||
/// The [builder] function is _not_ called during layout if the parent passes
|
||||
/// the same constraints repeatedly.
|
||||
///
|
||||
/// In the event that an ancestor skips the layout of this subtree so the
|
||||
/// constraints become outdated, the `builder` rebuilds with the last known
|
||||
/// constraints.
|
||||
/// {@endtemplate}
|
||||
abstract class ConstrainedLayoutBuilder<ConstraintType extends Constraints>
|
||||
extends AbstractLayoutBuilder<ConstraintType> {
|
||||
/// Creates a widget that defers its building until layout.
|
||||
const ConstrainedLayoutBuilder({super.key, required this.builder});
|
||||
|
||||
@override
|
||||
final Widget Function(BuildContext context, ConstraintType constraints)
|
||||
builder;
|
||||
}
|
||||
|
||||
class _LayoutBuilderElement<LayoutInfoType> extends RenderObjectElement {
|
||||
_LayoutBuilderElement(AbstractLayoutBuilder<LayoutInfoType> super.widget);
|
||||
|
||||
@override
|
||||
RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject>
|
||||
get renderObject =>
|
||||
super.renderObject
|
||||
as RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject>;
|
||||
|
||||
Element? _child;
|
||||
|
||||
// @override
|
||||
// BuildScope get buildScope => _buildScope;
|
||||
|
||||
// late final BuildScope _buildScope = BuildScope(
|
||||
// scheduleRebuild: _scheduleRebuild,
|
||||
// );
|
||||
|
||||
// To schedule a rebuild, markNeedsLayout needs to be called on this Element's
|
||||
// render object (as the rebuilding is done in its performLayout call). However,
|
||||
// the render tree should typically be kept clean during the postFrameCallbacks
|
||||
// and the idle phase, so the layout data can be safely read.
|
||||
// bool _deferredCallbackScheduled = false;
|
||||
// void _scheduleRebuild() {
|
||||
// if (_deferredCallbackScheduled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// final bool deferMarkNeedsLayout =
|
||||
// switch (SchedulerBinding.instance.schedulerPhase) {
|
||||
// SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true,
|
||||
// SchedulerPhase.transientCallbacks ||
|
||||
// SchedulerPhase.midFrameMicrotasks ||
|
||||
// SchedulerPhase.persistentCallbacks => false,
|
||||
// };
|
||||
// if (!deferMarkNeedsLayout) {
|
||||
// renderObject.scheduleLayoutCallback();
|
||||
// return;
|
||||
// }
|
||||
// _deferredCallbackScheduled = true;
|
||||
// SchedulerBinding.instance.scheduleFrameCallback(_frameCallback);
|
||||
// }
|
||||
|
||||
// void _frameCallback(Duration timestamp) {
|
||||
// _deferredCallbackScheduled = false;
|
||||
// // This method is only called when the render tree is stable, if the Element
|
||||
// // is deactivated it will never be reincorporated back to the tree.
|
||||
// if (mounted) {
|
||||
// renderObject.scheduleLayoutCallback();
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
void visitChildren(ElementVisitor visitor) {
|
||||
if (_child != null) {
|
||||
visitor(_child!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void forgetChild(Element child) {
|
||||
assert(child == _child);
|
||||
_child = null;
|
||||
super.forgetChild(child);
|
||||
}
|
||||
|
||||
@override
|
||||
void mount(Element? parent, Object? newSlot) {
|
||||
super.mount(parent, newSlot); // Creates the renderObject.
|
||||
renderObject._updateCallback(_rebuildWithConstraints);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(AbstractLayoutBuilder<LayoutInfoType> newWidget) {
|
||||
assert(widget != newWidget);
|
||||
final oldWidget = widget as AbstractLayoutBuilder<LayoutInfoType>;
|
||||
super.update(newWidget);
|
||||
assert(widget == newWidget);
|
||||
|
||||
renderObject._updateCallback(_rebuildWithConstraints);
|
||||
if (newWidget.updateShouldRebuild(oldWidget)) {
|
||||
_needsBuild = true;
|
||||
renderObject.scheduleLayoutCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void markNeedsBuild() {
|
||||
// Calling super.markNeedsBuild is not needed. This Element does not need
|
||||
// to performRebuild since this call already does what performRebuild does,
|
||||
// So the element is clean as soon as this method returns and does not have
|
||||
// to be added to the dirty list or marked as dirty.
|
||||
renderObject.scheduleLayoutCallback();
|
||||
_needsBuild = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void performRebuild() {
|
||||
// This gets called if markNeedsBuild() is called on us.
|
||||
// That might happen if, e.g., our builder uses Inherited widgets.
|
||||
|
||||
// Force the callback to be called, even if the layout constraints are the
|
||||
// same. This is because that callback may depend on the updated widget
|
||||
// configuration, or an inherited widget.
|
||||
renderObject.scheduleLayoutCallback();
|
||||
_needsBuild = true;
|
||||
super
|
||||
.performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
|
||||
}
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
renderObject._callback = null;
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
// The LayoutInfoType that was used to invoke the layout callback with last time,
|
||||
// during layout. The `_previousLayoutInfo` value is compared to the new one
|
||||
// to determine whether [LayoutBuilderBase.builder] needs to be called.
|
||||
LayoutInfoType? _previousLayoutInfo;
|
||||
bool _needsBuild = true;
|
||||
|
||||
void _rebuildWithConstraints(Constraints _) {
|
||||
final LayoutInfoType layoutInfo = renderObject.layoutInfo;
|
||||
@pragma('vm:notify-debugger-on-exception')
|
||||
void updateChildCallback() {
|
||||
Widget built;
|
||||
try {
|
||||
assert(layoutInfo == renderObject.layoutInfo);
|
||||
built = (widget as AbstractLayoutBuilder<LayoutInfoType>).builder(
|
||||
this,
|
||||
layoutInfo,
|
||||
);
|
||||
debugWidgetBuilderValue(widget, built);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(
|
||||
_reportException(
|
||||
ErrorDescription('building $widget'),
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
try {
|
||||
_child = updateChild(_child, built, null);
|
||||
assert(_child != null);
|
||||
} catch (e, stack) {
|
||||
built = ErrorWidget.builder(
|
||||
_reportException(
|
||||
ErrorDescription('building $widget'),
|
||||
e,
|
||||
stack,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)),
|
||||
],
|
||||
),
|
||||
);
|
||||
_child = updateChild(null, built, slot);
|
||||
} finally {
|
||||
_needsBuild = false;
|
||||
_previousLayoutInfo = layoutInfo;
|
||||
}
|
||||
}
|
||||
|
||||
final VoidCallback? callback =
|
||||
_needsBuild || (layoutInfo != _previousLayoutInfo)
|
||||
? updateChildCallback
|
||||
: null;
|
||||
owner!.buildScope(this, callback);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertRenderObjectChild(RenderObject child, Object? slot) {
|
||||
final RenderObjectWithChildMixin<RenderObject> renderObject =
|
||||
this.renderObject;
|
||||
assert(slot == null);
|
||||
assert(renderObject.debugValidateChild(child));
|
||||
renderObject.child = child;
|
||||
assert(renderObject == this.renderObject);
|
||||
}
|
||||
|
||||
@override
|
||||
void moveRenderObjectChild(
|
||||
RenderObject child,
|
||||
Object? oldSlot,
|
||||
Object? newSlot,
|
||||
) {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeRenderObjectChild(RenderObject child, Object? slot) {
|
||||
final RenderAbstractLayoutBuilderMixin<LayoutInfoType, RenderObject>
|
||||
renderObject = this.renderObject;
|
||||
assert(renderObject.child == child);
|
||||
renderObject.child = null;
|
||||
assert(renderObject == this.renderObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with
|
||||
/// the the same `LayoutInfoType`.
|
||||
///
|
||||
/// Provides a [layoutCallback] implementation which, if needed, invokes
|
||||
/// [AbstractLayoutBuilder]'s builder callback.
|
||||
///
|
||||
/// Implementers can override the [layoutInfo] implementation with a value
|
||||
/// that is safe to access in [layoutCallback], which is called in
|
||||
/// [performLayout]. The default [layoutInfo] returns the incoming
|
||||
/// [Constraints].
|
||||
///
|
||||
/// This mixin replaces [RenderConstrainedLayoutBuilder].
|
||||
mixin RenderAbstractLayoutBuilderMixin<
|
||||
LayoutInfoType,
|
||||
ChildType extends RenderObject
|
||||
>
|
||||
on
|
||||
RenderObjectWithChildMixin<ChildType>,
|
||||
RenderObjectWithLayoutCallbackMixin {
|
||||
LayoutCallback<Constraints>? _callback;
|
||||
|
||||
/// Change the layout callback.
|
||||
void _updateCallback(LayoutCallback<Constraints> value) {
|
||||
if (value == _callback) {
|
||||
return;
|
||||
}
|
||||
_callback = value;
|
||||
scheduleLayoutCallback();
|
||||
}
|
||||
|
||||
/// Invokes the builder callback supplied via [AbstractLayoutBuilder] and
|
||||
/// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed.
|
||||
///
|
||||
/// No further work will be done if [layoutInfo] has not changed since the last
|
||||
/// time this method was called, and [AbstractLayoutBuilder.updateShouldRebuild]
|
||||
/// returned `false` when the widget was rebuilt.
|
||||
///
|
||||
/// This method should typically be called as soon as possible in the class's
|
||||
/// [performLayout] implementation, before any layout work is done.
|
||||
@visibleForOverriding
|
||||
@override
|
||||
void layoutCallback() => _callback!(constraints);
|
||||
|
||||
/// The information to invoke the [AbstractLayoutBuilder.builder] callback with.
|
||||
///
|
||||
/// This is typically the information that are only made available in
|
||||
/// [performLayout], which is inaccessible for regular [Builder] widget,
|
||||
/// such as the incoming [Constraints], which are the default value.
|
||||
@protected
|
||||
LayoutInfoType get layoutInfo => constraints as LayoutInfoType;
|
||||
}
|
||||
|
||||
/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with
|
||||
/// the the same `LayoutInfoType`.
|
||||
///
|
||||
/// Use [RenderAbstractLayoutBuilderMixin] instead, which replaces this mixin.
|
||||
typedef RenderConstrainedLayoutBuilder<
|
||||
LayoutInfoType,
|
||||
ChildType extends RenderObject
|
||||
> = RenderAbstractLayoutBuilderMixin<LayoutInfoType, ChildType>;
|
||||
|
||||
/// Builds a widget tree that can depend on the parent widget's size.
|
||||
///
|
||||
/// Similar to the [Builder] widget except that the framework calls the [builder]
|
||||
/// function at layout time and provides the parent widget's constraints. This
|
||||
/// is useful when the parent constrains the child's size and doesn't depend on
|
||||
/// the child's intrinsic size. The [LayoutBuilder]'s final size will match its
|
||||
/// child's size.
|
||||
///
|
||||
/// {@macro flutter.widgets.ConstrainedLayoutBuilder}
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw}
|
||||
///
|
||||
/// If the child should be smaller than the parent, consider wrapping the child
|
||||
/// in an [Align] widget. If the child might want to be bigger, consider
|
||||
/// wrapping it in a [SingleChildScrollView] or [OverflowBox].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example uses a [LayoutBuilder] to build a different widget depending on the available width. Resize the
|
||||
/// DartPad window to see [LayoutBuilder] in action!
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/layout_builder/layout_builder.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverLayoutBuilder], the sliver counterpart of this widget.
|
||||
/// * [Builder], which calls a `builder` function at build time.
|
||||
/// * [StatefulBuilder], which passes its `builder` function a `setState` callback.
|
||||
/// * [CustomSingleChildLayout], which positions its child during layout.
|
||||
/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
|
||||
class LayoutBuilder extends ConstrainedLayoutBuilder<BoxConstraints> {
|
||||
/// Creates a widget that defers its building until layout.
|
||||
const LayoutBuilder({super.key, required super.builder});
|
||||
|
||||
@override
|
||||
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox>
|
||||
createRenderObject(
|
||||
BuildContext context,
|
||||
) => _RenderLayoutBuilder();
|
||||
}
|
||||
|
||||
class _RenderLayoutBuilder extends RenderBox
|
||||
with
|
||||
RenderObjectWithChildMixin<RenderBox>,
|
||||
RenderObjectWithLayoutCallbackMixin,
|
||||
RenderAbstractLayoutBuilderMixin<BoxConstraints, RenderBox> {
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
assert(_debugThrowIfNotCheckingIntrinsics());
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
assert(_debugThrowIfNotCheckingIntrinsics());
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
assert(_debugThrowIfNotCheckingIntrinsics());
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
assert(_debugThrowIfNotCheckingIntrinsics());
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
assert(
|
||||
debugCannotComputeDryLayout(
|
||||
reason:
|
||||
'Calculating the dry layout would require running the layout callback '
|
||||
'speculatively, which might mutate the live render object tree.',
|
||||
),
|
||||
);
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(
|
||||
BoxConstraints constraints,
|
||||
TextBaseline baseline,
|
||||
) {
|
||||
assert(
|
||||
debugCannotComputeDryLayout(
|
||||
reason:
|
||||
'Calculating the dry baseline would require running the layout callback '
|
||||
'speculatively, which might mutate the live render object tree.',
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
runLayoutCallback();
|
||||
if (child != null) {
|
||||
child!.layout(constraints, parentUsesSize: true);
|
||||
size = constraints.constrain(child!.size);
|
||||
} else {
|
||||
size = constraints.biggest;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||
return child?.getDistanceToActualBaseline(baseline) ??
|
||||
super.computeDistanceToActualBaseline(baseline);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return child?.hitTest(result, position: position) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null) {
|
||||
context.paintChild(child!, offset);
|
||||
}
|
||||
}
|
||||
|
||||
bool _debugThrowIfNotCheckingIntrinsics() {
|
||||
assert(() {
|
||||
if (!RenderObject.debugCheckingIntrinsics) {
|
||||
throw FlutterError(
|
||||
'LayoutBuilder does not support returning intrinsic dimensions.\n'
|
||||
'Calculating the intrinsic dimensions would require running the layout '
|
||||
'callback speculatively, which might mutate the live render object tree.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FlutterErrorDetails _reportException(
|
||||
DiagnosticsNode context,
|
||||
Object exception,
|
||||
StackTrace stack, {
|
||||
InformationCollector? informationCollector,
|
||||
}) {
|
||||
final details = FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'widgets library',
|
||||
context: context,
|
||||
informationCollector: informationCollector,
|
||||
);
|
||||
FlutterError.reportError(details);
|
||||
return details;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide ListTile;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
// Examples can assume:
|
||||
@@ -335,8 +335,10 @@ class ListTile extends StatelessWidget {
|
||||
this.contentPadding,
|
||||
this.enabled = true,
|
||||
this.onTap,
|
||||
this.onTapUp,
|
||||
this.onLongPress,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onFocusChange,
|
||||
this.mouseCursor,
|
||||
this.selected = false,
|
||||
@@ -562,6 +564,8 @@ class ListTile extends StatelessWidget {
|
||||
/// Inoperative if [enabled] is false.
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
final GestureTapUpCallback? onTapUp;
|
||||
|
||||
/// Called when the user long-presses on this list tile.
|
||||
///
|
||||
/// Inoperative if [enabled] is false.
|
||||
@@ -569,6 +573,8 @@ class ListTile extends StatelessWidget {
|
||||
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
final GestureTapUpCallback? onSecondaryTapUp;
|
||||
|
||||
/// {@macro flutter.material.inkwell.onFocusChange}
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
@@ -910,14 +916,16 @@ class ListTile extends StatelessWidget {
|
||||
|
||||
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
|
||||
final Set<WidgetState> mouseStates = <WidgetState>{
|
||||
if (!enabled || (onTap == null && onLongPress == null))
|
||||
if (!enabled ||
|
||||
(onTap == null &&
|
||||
onTapUp == null &&
|
||||
onLongPress == null &&
|
||||
onSecondaryTap == null &&
|
||||
onSecondaryTapUp == null))
|
||||
WidgetState.disabled,
|
||||
};
|
||||
final MouseCursor effectiveMouseCursor =
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(
|
||||
mouseCursor,
|
||||
mouseStates,
|
||||
) ??
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates) ??
|
||||
tileTheme.mouseCursor?.resolve(mouseStates) ??
|
||||
WidgetStateMouseCursor.clickable.resolve(mouseStates);
|
||||
|
||||
@@ -984,8 +992,10 @@ class ListTile extends StatelessWidget {
|
||||
return InkWell(
|
||||
customBorder: shape ?? tileTheme.shape,
|
||||
onTap: enabled ? onTap : null,
|
||||
onTapUp: enabled ? onTapUp : null,
|
||||
onLongPress: enabled ? onLongPress : null,
|
||||
onSecondaryTap: enabled ? onSecondaryTap : null,
|
||||
onSecondaryTapUp: enabled ? onSecondaryTapUp : null,
|
||||
onFocusChange: onFocusChange,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
canRequestFocus: enabled,
|
||||
@@ -1330,12 +1340,7 @@ class _RenderListTile extends RenderBox
|
||||
@override
|
||||
Iterable<RenderBox> get children {
|
||||
final RenderBox? title = childForSlot(_ListTileSlot.title);
|
||||
return <RenderBox>[
|
||||
?leading,
|
||||
?title,
|
||||
?subtitle,
|
||||
?trailing,
|
||||
];
|
||||
return <RenderBox>[?leading, ?title, ?subtitle, ?trailing];
|
||||
}
|
||||
|
||||
bool get isDense => _isDense;
|
||||
@@ -1501,11 +1506,16 @@ class _RenderListTile extends RenderBox
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
return math.max(
|
||||
_targetTileHeight,
|
||||
title.getMinIntrinsicHeight(width) +
|
||||
(subtitle?.getMinIntrinsicHeight(width) ?? 0.0),
|
||||
);
|
||||
final double titleMinHeight = title.getMinIntrinsicHeight(width);
|
||||
final double? subtitleMinHeight = subtitle?.getMinIntrinsicHeight(width);
|
||||
|
||||
const topAndBottomPaddingMultiplier = 2;
|
||||
final double contentHeight =
|
||||
titleMinHeight +
|
||||
(subtitleMinHeight ?? 0.0) +
|
||||
topAndBottomPaddingMultiplier * _minVerticalPadding;
|
||||
|
||||
return math.max(_targetTileHeight, contentHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2,17 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
///
|
||||
/// @docImport 'single_child_scroll_view.dart';
|
||||
/// @docImport 'text.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart'
|
||||
show DragStartBehavior, HorizontalDragGestureRecognizer;
|
||||
import 'package:flutter/material.dart'
|
||||
hide PageView, Scrollable, ScrollableState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class _ForceImplicitScrollPhysics extends ScrollPhysics {
|
||||
@@ -41,18 +35,18 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
|
||||
///
|
||||
/// You can use a [PageController] to control which page is visible in the view.
|
||||
/// In addition to being able to control the pixel offset of the content inside
|
||||
/// the [CustomPageView], a [PageController] also lets you control the offset in terms
|
||||
/// the [PageView], a [PageController] also lets you control the offset in terms
|
||||
/// of pages, which are increments of the viewport size.
|
||||
///
|
||||
/// The [PageController] can also be used to control the
|
||||
/// [PageController.initialPage], which determines which page is shown when the
|
||||
/// [CustomPageView] is first constructed, and the [PageController.viewportFraction],
|
||||
/// [PageView] is first constructed, and the [PageController.viewportFraction],
|
||||
/// which determines the size of the pages as a fraction of the viewport size.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Here is an example of [CustomPageView]. It creates a centered [Text] in each of the three pages
|
||||
/// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
|
||||
/// which scroll horizontally.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
|
||||
@@ -61,7 +55,7 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
|
||||
/// ## Persisting the scroll position during a session
|
||||
///
|
||||
/// Scroll views attempt to persist their scroll position using [PageStorage].
|
||||
/// For a [CustomPageView], this can be disabled by setting [PageController.keepPage]
|
||||
/// For a [PageView], this can be disabled by setting [PageController.keepPage]
|
||||
/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
|
||||
/// the [key] of this widget is recommended to help disambiguate different
|
||||
/// scroll views from each other.
|
||||
@@ -74,7 +68,8 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
|
||||
/// * [GridView], for a scrollable grid of boxes.
|
||||
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
|
||||
/// the scroll position without using a [ScrollController].
|
||||
class CustomPageView extends StatefulWidget {
|
||||
class PageView<T extends HorizontalDragGestureRecognizer>
|
||||
extends StatefulWidget {
|
||||
/// Creates a scrollable list that works page by page from an explicit [List]
|
||||
/// of widgets.
|
||||
///
|
||||
@@ -88,12 +83,12 @@ class CustomPageView extends StatefulWidget {
|
||||
/// See the documentation at [SliverChildListDelegate.children] for more details.
|
||||
///
|
||||
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
|
||||
/// If [allowImplicitScrolling] is true, the [CustomPageView] will participate in
|
||||
/// If [allowImplicitScrolling] is true, the [PageView] will participate in
|
||||
/// accessibility scrolling more like a [ListView], where implicit scroll
|
||||
/// actions will move to the next page rather than into the contents of the
|
||||
/// [CustomPageView].
|
||||
/// [PageView].
|
||||
/// {@endtemplate}
|
||||
CustomPageView({
|
||||
PageView({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
@@ -109,12 +104,10 @@ class CustomPageView extends StatefulWidget {
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
required this.horizontalDragGestureRecognizer,
|
||||
}) : childrenDelegate = SliverChildListDelegate(children);
|
||||
|
||||
final Widget? header;
|
||||
final Color bgColor;
|
||||
final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;
|
||||
|
||||
/// Creates a scrollable list that works page by page using widgets that are
|
||||
/// created on demand.
|
||||
@@ -123,7 +116,7 @@ class CustomPageView extends StatefulWidget {
|
||||
/// number of children because the builder is called only for those children
|
||||
/// that are actually visible.
|
||||
///
|
||||
/// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum
|
||||
/// Providing a non-null [itemCount] lets the [PageView] compute the maximum
|
||||
/// scroll extent.
|
||||
///
|
||||
/// [itemBuilder] will be called only with indices greater than or equal to
|
||||
@@ -141,7 +134,7 @@ class CustomPageView extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
CustomPageView.builder({
|
||||
PageView.builder({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
@@ -159,8 +152,7 @@ class CustomPageView extends StatefulWidget {
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
required this.horizontalDragGestureRecognizer,
|
||||
}) : childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
@@ -171,14 +163,14 @@ class CustomPageView extends StatefulWidget {
|
||||
/// model.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a [CustomPageView] that uses a custom [SliverChildBuilderDelegate] to support child
|
||||
/// This example shows a [PageView] that uses a custom [SliverChildBuilderDelegate] to support child
|
||||
/// reordering.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
const CustomPageView.custom({
|
||||
const PageView.custom({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
@@ -194,8 +186,7 @@ class CustomPageView extends StatefulWidget {
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
required this.horizontalDragGestureRecognizer,
|
||||
});
|
||||
|
||||
/// Controls whether the widget's pages will respond to
|
||||
@@ -265,10 +256,10 @@ class CustomPageView extends StatefulWidget {
|
||||
/// Called whenever the page in the center of the viewport changes.
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
|
||||
/// A delegate that provides the children for the [CustomPageView].
|
||||
/// A delegate that provides the children for the [PageView].
|
||||
///
|
||||
/// The [PageView.custom] constructor lets you specify this delegate
|
||||
/// explicitly. The [CustomPageView] and [PageView.builder] constructors create a
|
||||
/// explicitly. The [PageView] and [PageView.builder] constructors create a
|
||||
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
|
||||
/// respectively.
|
||||
final SliverChildDelegate childrenDelegate;
|
||||
@@ -304,10 +295,11 @@ class CustomPageView extends StatefulWidget {
|
||||
final bool padEnds;
|
||||
|
||||
@override
|
||||
State<CustomPageView> createState() => _CustomPageViewState();
|
||||
State<PageView<T>> createState() => _PageViewState<T>();
|
||||
}
|
||||
|
||||
class _CustomPageViewState extends State<CustomPageView> {
|
||||
class _PageViewState<T extends HorizontalDragGestureRecognizer>
|
||||
extends State<PageView<T>> {
|
||||
int _lastReportedPage = 0;
|
||||
|
||||
late PageController _controller;
|
||||
@@ -332,7 +324,7 @@ class _CustomPageViewState extends State<CustomPageView> {
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomPageView oldWidget) {
|
||||
void didUpdateWidget(PageView<T> oldWidget) {
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
if (oldWidget.controller == null) {
|
||||
_controller.dispose();
|
||||
@@ -379,7 +371,7 @@ class _CustomPageViewState extends State<CustomPageView> {
|
||||
if (notification.depth == 0 &&
|
||||
widget.onPageChanged != null &&
|
||||
notification is ScrollUpdateNotification) {
|
||||
final PageMetrics metrics = notification.metrics as PageMetrics;
|
||||
final metrics = notification.metrics as PageMetrics;
|
||||
final int currentPage = metrics.page!.round();
|
||||
if (currentPage != _lastReportedPage) {
|
||||
_lastReportedPage = currentPage;
|
||||
@@ -388,9 +380,7 @@ class _CustomPageViewState extends State<CustomPageView> {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: CustomScrollable(
|
||||
header: widget.header,
|
||||
bgColor: widget.bgColor,
|
||||
child: Scrollable<T>(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
axisDirection: axisDirection,
|
||||
controller: _controller,
|
||||
@@ -419,6 +409,7 @@ class _CustomPageViewState extends State<CustomPageView> {
|
||||
],
|
||||
);
|
||||
},
|
||||
horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,57 +2,42 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
///
|
||||
/// @docImport 'page_storage.dart';
|
||||
/// @docImport 'page_view.dart';
|
||||
/// @docImport 'scroll_metrics.dart';
|
||||
/// @docImport 'scroll_notification.dart';
|
||||
/// @docImport 'scroll_view.dart';
|
||||
/// @docImport 'single_child_scroll_view.dart';
|
||||
/// @docImport 'two_dimensional_scroll_view.dart';
|
||||
/// @docImport 'two_dimensional_viewport.dart';
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable_helpers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:flutter/material.dart'
|
||||
hide Scrollable, ScrollableState, EdgeDraggingAutoScroller;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
export 'package:flutter/physics.dart' show Tolerance;
|
||||
|
||||
// The return type of _performEnsureVisible.
|
||||
//
|
||||
// The list of futures represents each pending ScrollPosition call to
|
||||
// ensureVisible. The returned ScrollableState's context is used to find the
|
||||
// next potential ancestor Scrollable.
|
||||
typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
|
||||
typedef _EnsureVisibleResults = (List<Future<void>>, ScrollableState);
|
||||
|
||||
/// A widget that manages scrolling in one dimension and informs the [Viewport]
|
||||
/// through which the content is viewed.
|
||||
///
|
||||
/// [CustomScrollable] implements the interaction model for a scrollable widget,
|
||||
/// [Scrollable] implements the interaction model for a scrollable widget,
|
||||
/// including gesture recognition, but does not have an opinion about how the
|
||||
/// viewport, which actually displays the children, is constructed.
|
||||
///
|
||||
/// It's rare to construct a [CustomScrollable] directly. Instead, consider [ListView]
|
||||
/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView]
|
||||
/// or [GridView], which combine scrolling, viewporting, and a layout model. To
|
||||
/// combine layout models (or to use a custom layout mode), consider using
|
||||
/// [CustomScrollView].
|
||||
///
|
||||
/// The static [CustomScrollable.of] and [CustomScrollable.ensureVisible] functions are
|
||||
/// often used to interact with the [CustomScrollable] widget inside a [ListView] or
|
||||
/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are
|
||||
/// often used to interact with the [Scrollable] widget inside a [ListView] or
|
||||
/// a [GridView].
|
||||
///
|
||||
/// To further customize scrolling behavior with a [CustomScrollable]:
|
||||
/// To further customize scrolling behavior with a [Scrollable]:
|
||||
///
|
||||
/// 1. You can provide a [viewportBuilder] to customize the child model. For
|
||||
/// example, [SingleChildScrollView] uses a viewport that displays a single
|
||||
@@ -62,7 +47,7 @@ typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
|
||||
/// 2. You can provide a custom [ScrollController] that creates a custom
|
||||
/// [ScrollPosition] subclass. For example, [PageView] uses a
|
||||
/// [PageController], which creates a page-oriented scroll position subclass
|
||||
/// that keeps the same page visible when the [CustomScrollable] resizes.
|
||||
/// that keeps the same page visible when the [Scrollable] resizes.
|
||||
///
|
||||
/// ## Persisting the scroll position during a session
|
||||
///
|
||||
@@ -70,7 +55,7 @@ typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
|
||||
/// This can be disabled by setting [ScrollController.keepScrollOffset] to false
|
||||
/// on the [controller]. If it is enabled, using a [PageStorageKey] for the
|
||||
/// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is
|
||||
/// recommended to help disambiguate different [CustomScrollable]s from each other.
|
||||
/// recommended to help disambiguate different [Scrollable]s from each other.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@@ -86,9 +71,10 @@ typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
|
||||
/// child.
|
||||
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
|
||||
/// the scroll position without using a [ScrollController].
|
||||
class CustomScrollable extends StatefulWidget {
|
||||
class Scrollable<T extends HorizontalDragGestureRecognizer>
|
||||
extends StatefulWidget {
|
||||
/// Creates a widget that scrolls.
|
||||
const CustomScrollable({
|
||||
const Scrollable({
|
||||
super.key,
|
||||
this.axisDirection = AxisDirection.down,
|
||||
this.controller,
|
||||
@@ -102,19 +88,15 @@ class CustomScrollable extends StatefulWidget {
|
||||
this.scrollBehavior,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.enableSlide,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
required this.horizontalDragGestureRecognizer,
|
||||
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
|
||||
|
||||
final Widget? header;
|
||||
final bool? enableSlide;
|
||||
final Color bgColor;
|
||||
final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;
|
||||
|
||||
/// {@template flutter.widgets.Scrollable.axisDirection}
|
||||
/// The direction in which this widget scrolls.
|
||||
///
|
||||
/// For example, if the [CustomScrollable.axisDirection] is [AxisDirection.down],
|
||||
/// For example, if the [Scrollable.axisDirection] is [AxisDirection.down],
|
||||
/// increasing the scroll position will cause content below the bottom of the
|
||||
/// viewport to become visible through the viewport. Similarly, if the
|
||||
/// axisDirection is [AxisDirection.right], increasing the scroll position
|
||||
@@ -137,12 +119,12 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// scroll position (see [ScrollController.offset]), or change it (see
|
||||
/// [ScrollController.animateTo]).
|
||||
///
|
||||
/// If null, a [ScrollController] will be created internally by [CustomScrollable]
|
||||
/// If null, a [ScrollController] will be created internally by [Scrollable]
|
||||
/// in order to create and manage the [ScrollPosition].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomScrollable.ensureVisible], which animates the scroll position to
|
||||
/// * [Scrollable.ensureVisible], which animates the scroll position to
|
||||
/// reveal a given [BuildContext].
|
||||
/// {@endtemplate}
|
||||
final ScrollController? controller;
|
||||
@@ -157,8 +139,8 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// the ambient [ScrollConfiguration].
|
||||
///
|
||||
/// If an explicit [ScrollBehavior] is provided to
|
||||
/// [CustomScrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior
|
||||
/// will take precedence after [CustomScrollable.physics].
|
||||
/// [Scrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior
|
||||
/// will take precedence after [Scrollable.physics].
|
||||
///
|
||||
/// The physics can be changed dynamically, but new physics will only take
|
||||
/// effect if the _class_ of the provided object changes. Merely constructing
|
||||
@@ -193,7 +175,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// scroll when the scrollable is asked to scroll via the keyboard using a
|
||||
/// [ScrollAction].
|
||||
///
|
||||
/// If not supplied, the [CustomScrollable] will scroll a default amount when a
|
||||
/// If not supplied, the [Scrollable] will scroll a default amount when a
|
||||
/// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow,
|
||||
/// etc.), or otherwise invoked by a [ScrollAction].
|
||||
///
|
||||
@@ -204,7 +186,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
final ScrollIncrementCalculator? incrementCalculator;
|
||||
|
||||
/// {@template flutter.widgets.scrollable.excludeFromSemantics}
|
||||
/// Whether the scroll actions introduced by this [CustomScrollable] are exposed
|
||||
/// Whether the scroll actions introduced by this [Scrollable] are exposed
|
||||
/// in the semantics tree.
|
||||
///
|
||||
/// Text fields with an overflow are usually scrollable to make sure that the
|
||||
@@ -219,10 +201,10 @@ class CustomScrollable extends StatefulWidget {
|
||||
final bool excludeFromSemantics;
|
||||
|
||||
/// {@template flutter.widgets.scrollable.hitTestBehavior}
|
||||
/// Defines the behavior of gesture detector used in this [CustomScrollable].
|
||||
/// Defines the behavior of gesture detector used in this [Scrollable].
|
||||
///
|
||||
/// This defaults to [HitTestBehavior.opaque] which means it prevents targets
|
||||
/// behind this [CustomScrollable] from receiving events.
|
||||
/// behind this [Scrollable] from receiving events.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
@@ -304,7 +286,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
///
|
||||
/// This is passed to decorators in [ScrollableDetails], and does not directly affect
|
||||
/// clipping of the [CustomScrollable]. This reflects the same [Clip] that is provided
|
||||
/// clipping of the [Scrollable]. This reflects the same [Clip] that is provided
|
||||
/// to [ScrollView.clipBehavior] and is supplied to the [Viewport].
|
||||
final Clip clipBehavior;
|
||||
|
||||
@@ -314,7 +296,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
Axis get axis => axisDirectionToAxis(axisDirection);
|
||||
|
||||
@override
|
||||
CustomScrollableState createState() => CustomScrollableState();
|
||||
ScrollableState<T> createState() => ScrollableState<T>();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@@ -334,31 +316,31 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// ScrollableState? scrollable = Scrollable.maybeOf(context);
|
||||
/// ```
|
||||
///
|
||||
/// Calling this method will create a dependency on the [CustomScrollableState]
|
||||
/// Calling this method will create a dependency on the [ScrollableState]
|
||||
/// that is returned, if there is one. This is typically the closest
|
||||
/// [CustomScrollable], but may be a more distant ancestor if [axis] is used to
|
||||
/// target a specific [CustomScrollable].
|
||||
/// [Scrollable], but may be a more distant ancestor if [axis] is used to
|
||||
/// target a specific [Scrollable].
|
||||
///
|
||||
/// Using the optional [Axis] is useful when Scrollables are nested and the
|
||||
/// target [CustomScrollable] is not the closest instance. When [axis] is provided,
|
||||
/// the nearest enclosing [CustomScrollableState] in that [Axis] is returned, or
|
||||
/// target [Scrollable] is not the closest instance. When [axis] is provided,
|
||||
/// the nearest enclosing [ScrollableState] in that [Axis] is returned, or
|
||||
/// null if there is none.
|
||||
///
|
||||
/// This finds the nearest _ancestor_ [CustomScrollable] of the `context`. This
|
||||
/// means that if the `context` is that of a [CustomScrollable], it will _not_ find
|
||||
/// _that_ [CustomScrollable].
|
||||
/// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
|
||||
/// means that if the `context` is that of a [Scrollable], it will _not_ find
|
||||
/// _that_ [Scrollable].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomScrollable.of], which is similar to this method, but asserts
|
||||
/// if no [CustomScrollable] ancestor is found.
|
||||
static CustomScrollableState? maybeOf(BuildContext context, {Axis? axis}) {
|
||||
/// * [Scrollable.of], which is similar to this method, but asserts
|
||||
/// if no [Scrollable] ancestor is found.
|
||||
static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) {
|
||||
// This is the context that will need to establish the dependency.
|
||||
final BuildContext originalContext = context;
|
||||
final originalContext = context;
|
||||
InheritedElement? element = context
|
||||
.getElementForInheritedWidgetOfExactType<_ScrollableScope>();
|
||||
while (element != null) {
|
||||
final CustomScrollableState scrollable =
|
||||
final ScrollableState scrollable =
|
||||
(element.widget as _ScrollableScope).scrollable;
|
||||
if (axis == null ||
|
||||
axisDirectionToAxis(scrollable.axisDirection) == axis) {
|
||||
@@ -382,28 +364,28 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// ScrollableState scrollable = Scrollable.of(context);
|
||||
/// ```
|
||||
///
|
||||
/// Calling this method will create a dependency on the [CustomScrollableState]
|
||||
/// Calling this method will create a dependency on the [ScrollableState]
|
||||
/// that is returned, if there is one. This is typically the closest
|
||||
/// [CustomScrollable], but may be a more distant ancestor if [axis] is used to
|
||||
/// target a specific [CustomScrollable].
|
||||
/// [Scrollable], but may be a more distant ancestor if [axis] is used to
|
||||
/// target a specific [Scrollable].
|
||||
///
|
||||
/// Using the optional [Axis] is useful when Scrollables are nested and the
|
||||
/// target [CustomScrollable] is not the closest instance. When [axis] is provided,
|
||||
/// the nearest enclosing [CustomScrollableState] in that [Axis] is returned.
|
||||
/// target [Scrollable] is not the closest instance. When [axis] is provided,
|
||||
/// the nearest enclosing [ScrollableState] in that [Axis] is returned.
|
||||
///
|
||||
/// This finds the nearest _ancestor_ [CustomScrollable] of the `context`. This
|
||||
/// means that if the `context` is that of a [CustomScrollable], it will _not_ find
|
||||
/// _that_ [CustomScrollable].
|
||||
/// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
|
||||
/// means that if the `context` is that of a [Scrollable], it will _not_ find
|
||||
/// _that_ [Scrollable].
|
||||
///
|
||||
/// If no [CustomScrollable] ancestor is found, then this method will assert in
|
||||
/// If no [Scrollable] ancestor is found, then this method will assert in
|
||||
/// debug mode, and throw an exception in release mode.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomScrollable.maybeOf], which is similar to this method, but returns null
|
||||
/// if no [CustomScrollable] ancestor is found.
|
||||
static CustomScrollableState of(BuildContext context, {Axis? axis}) {
|
||||
final CustomScrollableState? scrollableState = maybeOf(context, axis: axis);
|
||||
/// * [Scrollable.maybeOf], which is similar to this method, but returns null
|
||||
/// if no [Scrollable] ancestor is found.
|
||||
static ScrollableState of(BuildContext context, {Axis? axis}) {
|
||||
final ScrollableState? scrollableState = maybeOf(context, axis: axis);
|
||||
assert(() {
|
||||
if (scrollableState == null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
@@ -439,16 +421,16 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// This also means that the value returned is only good for the point in time
|
||||
/// when it is called, and callers will not get updated if the value changes.
|
||||
///
|
||||
/// The heuristic used is determined by the [physics] of this [CustomScrollable]
|
||||
/// The heuristic used is determined by the [physics] of this [Scrollable]
|
||||
/// via [ScrollPhysics.recommendDeferredLoading]. That method is called with
|
||||
/// the current [ScrollPosition.activity]'s [ScrollActivity.velocity].
|
||||
///
|
||||
/// The optional [Axis] allows targeting of a specific [CustomScrollable] of that
|
||||
/// The optional [Axis] allows targeting of a specific [Scrollable] of that
|
||||
/// axis, useful when Scrollables are nested. When [axis] is provided,
|
||||
/// [ScrollPosition.recommendDeferredLoading] is called for the nearest
|
||||
/// [CustomScrollable] in that [Axis].
|
||||
/// [Scrollable] in that [Axis].
|
||||
///
|
||||
/// If there is no [CustomScrollable] in the widget tree above the [context], this
|
||||
/// If there is no [Scrollable] in the widget tree above the [context], this
|
||||
/// method returns false.
|
||||
static bool recommendDeferredLoadingForContext(
|
||||
BuildContext context, {
|
||||
@@ -470,7 +452,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
/// Scrolls all scrollables that enclose the given context so as to make the
|
||||
/// given context visible.
|
||||
///
|
||||
/// If a [CustomScrollable] enclosing the provided [BuildContext] is a
|
||||
/// If a [Scrollable] enclosing the provided [BuildContext] is a
|
||||
/// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure
|
||||
/// the target is made visible.
|
||||
static Future<void> ensureVisible(
|
||||
@@ -481,7 +463,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
ScrollPositionAlignmentPolicy alignmentPolicy =
|
||||
ScrollPositionAlignmentPolicy.explicit,
|
||||
}) {
|
||||
final List<Future<void>> futures = <Future<void>>[];
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
// The targetRenderObject is used to record the first target renderObject.
|
||||
// If there are multiple scrollable widgets nested, the targetRenderObject
|
||||
@@ -491,7 +473,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
//
|
||||
// Also see https://github.com/flutter/flutter/issues/65100
|
||||
RenderObject? targetRenderObject;
|
||||
CustomScrollableState? scrollable = CustomScrollable.maybeOf(context);
|
||||
ScrollableState? scrollable = Scrollable.maybeOf(context);
|
||||
while (scrollable != null) {
|
||||
final List<Future<void>> newFutures;
|
||||
(newFutures, scrollable) = scrollable._performEnsureVisible(
|
||||
@@ -506,7 +488,7 @@ class CustomScrollable extends StatefulWidget {
|
||||
|
||||
targetRenderObject ??= context.findRenderObject();
|
||||
context = scrollable.context;
|
||||
scrollable = CustomScrollable.maybeOf(context);
|
||||
scrollable = Scrollable.maybeOf(context);
|
||||
}
|
||||
|
||||
if (futures.isEmpty || duration == Duration.zero) {
|
||||
@@ -528,7 +510,7 @@ class _ScrollableScope extends InheritedWidget {
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final CustomScrollableState scrollable;
|
||||
final ScrollableState scrollable;
|
||||
final ScrollPosition position;
|
||||
|
||||
@override
|
||||
@@ -537,30 +519,31 @@ class _ScrollableScope extends InheritedWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// State object for a [CustomScrollable] widget.
|
||||
/// State object for a [Scrollable] widget.
|
||||
///
|
||||
/// To manipulate a [CustomScrollable] widget's scroll position, use the object
|
||||
/// To manipulate a [Scrollable] widget's scroll position, use the object
|
||||
/// obtained from the [position] property.
|
||||
///
|
||||
/// To be informed of when a [CustomScrollable] widget is scrolling, use a
|
||||
/// To be informed of when a [Scrollable] widget is scrolling, use a
|
||||
/// [NotificationListener] to listen for [ScrollNotification] notifications.
|
||||
///
|
||||
/// This class is not intended to be subclassed. To specialize the behavior of a
|
||||
/// [CustomScrollable], provide it with a [ScrollPhysics].
|
||||
class CustomScrollableState extends State<CustomScrollable>
|
||||
/// [Scrollable], provide it with a [ScrollPhysics].
|
||||
class ScrollableState<T extends HorizontalDragGestureRecognizer>
|
||||
extends State<Scrollable<T>>
|
||||
with TickerProviderStateMixin, RestorationMixin
|
||||
implements ScrollContext {
|
||||
// GETTERS
|
||||
|
||||
/// The manager for this [CustomScrollable] widget's viewport position.
|
||||
/// The manager for this [Scrollable] widget's viewport position.
|
||||
///
|
||||
/// To control what kind of [ScrollPosition] is created for a [CustomScrollable],
|
||||
/// To control what kind of [ScrollPosition] is created for a [Scrollable],
|
||||
/// provide it with custom [ScrollController] that creates the appropriate
|
||||
/// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
|
||||
ScrollPosition get position => _position!;
|
||||
ScrollPosition? _position;
|
||||
|
||||
/// The resolved [ScrollPhysics] of the [CustomScrollableState].
|
||||
/// The resolved [ScrollPhysics] of the [ScrollableState].
|
||||
ScrollPhysics? get resolvedPhysics => _physics;
|
||||
ScrollPhysics? _physics;
|
||||
|
||||
@@ -660,10 +643,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
_fallbackScrollController = ScrollController();
|
||||
}
|
||||
super.initState();
|
||||
_animController = AnimationController(
|
||||
vsync: this,
|
||||
reverseDuration: const Duration(milliseconds: 500),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -677,7 +656,7 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
bool _shouldUpdatePosition(CustomScrollable oldWidget) {
|
||||
bool _shouldUpdatePosition(Scrollable oldWidget) {
|
||||
if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) {
|
||||
return true;
|
||||
}
|
||||
@@ -704,7 +683,7 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
|
||||
@protected
|
||||
@override
|
||||
void didUpdateWidget(CustomScrollable oldWidget) {
|
||||
void didUpdateWidget(Scrollable<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
@@ -746,8 +725,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
|
||||
position.dispose();
|
||||
_persistedScrollOffset.dispose();
|
||||
|
||||
_animController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -777,12 +754,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
bool? _lastCanDrag;
|
||||
Axis? _lastAxisDirection;
|
||||
|
||||
late bool _isRTL = false;
|
||||
Offset? _downPos;
|
||||
bool? _isSliding;
|
||||
|
||||
late AnimationController _animController;
|
||||
|
||||
@override
|
||||
@protected
|
||||
void setCanDrag(bool value) {
|
||||
@@ -829,32 +800,27 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
};
|
||||
case Axis.horizontal:
|
||||
_gestureRecognizers = <Type, GestureRecognizerFactory>{
|
||||
HorizontalDragGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<
|
||||
HorizontalDragGestureRecognizer
|
||||
>(
|
||||
() => HorizontalDragGestureRecognizer(
|
||||
supportedDevices: _configuration.dragDevices,
|
||||
),
|
||||
(HorizontalDragGestureRecognizer instance) {
|
||||
instance
|
||||
..onDown = _handleDragDown
|
||||
..onStart = _handleDragStart
|
||||
..onUpdate = _handleDragUpdate
|
||||
..onEnd = _handleDragEnd
|
||||
..onCancel = _handleDragCancel
|
||||
..minFlingDistance = _physics?.minFlingDistance
|
||||
..minFlingVelocity = _physics?.minFlingVelocity
|
||||
..maxFlingVelocity = _physics?.maxFlingVelocity
|
||||
..velocityTrackerBuilder = _configuration
|
||||
.velocityTrackerBuilder(context)
|
||||
..dragStartBehavior = widget.dragStartBehavior
|
||||
..multitouchDragStrategy = _configuration
|
||||
.getMultitouchDragStrategy(context)
|
||||
..gestureSettings = _mediaQueryGestureSettings
|
||||
..supportedDevices = _configuration.dragDevices;
|
||||
},
|
||||
),
|
||||
T: GestureRecognizerFactoryWithHandlers<T>(
|
||||
widget.horizontalDragGestureRecognizer,
|
||||
(T instance) {
|
||||
instance
|
||||
..onDown = _handleDragDown
|
||||
..onStart = _handleDragStart
|
||||
..onUpdate = _handleDragUpdate
|
||||
..onEnd = _handleDragEnd
|
||||
..onCancel = _handleDragCancel
|
||||
..minFlingDistance = _physics?.minFlingDistance
|
||||
..minFlingVelocity = _physics?.minFlingVelocity
|
||||
..maxFlingVelocity = _physics?.maxFlingVelocity
|
||||
..velocityTrackerBuilder = _configuration
|
||||
.velocityTrackerBuilder(context)
|
||||
..dragStartBehavior = widget.dragStartBehavior
|
||||
..multitouchDragStrategy = _configuration
|
||||
.getMultitouchDragStrategy(context)
|
||||
..gestureSettings = _mediaQueryGestureSettings
|
||||
..supportedDevices = _configuration.dragDevices;
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -875,7 +841,7 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
}
|
||||
_shouldIgnorePointer = value;
|
||||
if (_ignorePointerKey.currentContext != null) {
|
||||
final RenderIgnorePointer renderBox =
|
||||
final renderBox =
|
||||
_ignorePointerKey.currentContext!.findRenderObject()!
|
||||
as RenderIgnorePointer;
|
||||
renderBox.ignoring = _shouldIgnorePointer;
|
||||
@@ -888,65 +854,12 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
ScrollHoldController? _hold;
|
||||
|
||||
void _handleDragDown(DragDownDetails details) {
|
||||
final dx = details.localPosition.dx;
|
||||
const offset = 30;
|
||||
final isLTR = dx <= offset;
|
||||
final isRTL = dx >= _maxWidth - offset;
|
||||
if (isLTR || isRTL) {
|
||||
_isRTL = isRTL;
|
||||
_downPos = details.localPosition;
|
||||
return;
|
||||
}
|
||||
assert(_drag == null);
|
||||
assert(_hold == null);
|
||||
_hold = position.hold(_disposeHold);
|
||||
}
|
||||
|
||||
void _onPan(Offset localPosition) {
|
||||
if (_isSliding == false) {
|
||||
return;
|
||||
} else if (_isSliding == null) {
|
||||
if (_downPos != null) {
|
||||
Offset cumulativeDelta = localPosition - _downPos!;
|
||||
if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) {
|
||||
_downPos = localPosition;
|
||||
_isSliding = true;
|
||||
} else {
|
||||
_downPos = null;
|
||||
_isSliding = false;
|
||||
}
|
||||
} else {
|
||||
_downPos = null;
|
||||
_isSliding = false;
|
||||
}
|
||||
} else if (_isSliding == true) {
|
||||
final from = _downPos!.dx;
|
||||
final to = localPosition.dx;
|
||||
_animController.value =
|
||||
math.max(0, _isRTL ? from - to : to - from) / _maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
void _onDismiss() {
|
||||
if (_isSliding == true) {
|
||||
final dx = _downPos!.dx;
|
||||
if (_animController.value * _maxWidth +
|
||||
(_isRTL ? (_maxWidth - dx) : dx) >=
|
||||
100) {
|
||||
Get.back();
|
||||
} else {
|
||||
_animController.reverse();
|
||||
}
|
||||
}
|
||||
_downPos = null;
|
||||
_isSliding = null;
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
if (_downPos != null) {
|
||||
_onPan(details.localPosition);
|
||||
return;
|
||||
}
|
||||
// It's possible for _hold to become null between _handleDragDown and
|
||||
// _handleDragStart, for example if some user code calls jumpTo or otherwise
|
||||
// triggers a new activity to begin.
|
||||
@@ -960,20 +873,12 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
if (_downPos != null) {
|
||||
_onPan(details.localPosition);
|
||||
return;
|
||||
}
|
||||
// _drag might be null if the drag activity ended and called _disposeDrag.
|
||||
assert(_hold == null || _drag == null);
|
||||
_drag?.update(details);
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
if (_downPos != null) {
|
||||
_onDismiss();
|
||||
return;
|
||||
}
|
||||
// _drag might be null if the drag activity ended and called _disposeDrag.
|
||||
assert(_hold == null || _drag == null);
|
||||
_drag?.end(details);
|
||||
@@ -981,10 +886,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
}
|
||||
|
||||
void _handleDragCancel() {
|
||||
if (_downPos != null) {
|
||||
_onDismiss();
|
||||
return;
|
||||
}
|
||||
if (_gestureDetectorKey.currentContext == null) {
|
||||
// The cancel was caused by the GestureDetector getting disposed, which
|
||||
// means we will get disposed momentarily as well and shouldn't do
|
||||
@@ -1098,10 +999,8 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
return false;
|
||||
}
|
||||
|
||||
late double _maxWidth;
|
||||
|
||||
Widget _buildChrome(BuildContext context, Widget child) {
|
||||
final ScrollableDetails details = ScrollableDetails(
|
||||
final details = ScrollableDetails(
|
||||
direction: widget.axisDirection,
|
||||
controller: _effectiveScrollController,
|
||||
decorationClipBehavior: widget.clipBehavior,
|
||||
@@ -1177,32 +1076,7 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
_maxWidth = constraints.maxWidth;
|
||||
return AnimatedBuilder(
|
||||
animation: _animController,
|
||||
builder: (context, child) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
heightFactor: 1 - _animController.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
color: widget.bgColor,
|
||||
child: widget.header != null
|
||||
? Column(
|
||||
children: [
|
||||
widget.header!,
|
||||
Expanded(child: result),
|
||||
],
|
||||
)
|
||||
: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Returns the Future from calling ensureVisible for the ScrollPosition, as
|
||||
@@ -1250,7 +1124,7 @@ class _ScrollableSelectionHandler extends StatefulWidget {
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final CustomScrollableState state;
|
||||
final ScrollableState state;
|
||||
final ScrollPosition position;
|
||||
final Widget child;
|
||||
final SelectionRegistrar registrar;
|
||||
@@ -1323,7 +1197,7 @@ class _ScrollableSelectionContainerDelegate
|
||||
// An eye-balled value for a smooth scrolling speed.
|
||||
static const double _kDefaultSelectToScrollVelocityScalar = 30;
|
||||
|
||||
final CustomScrollableState state;
|
||||
final ScrollableState state;
|
||||
final EdgeDraggingAutoScroller _autoScroller;
|
||||
bool _scheduledLayoutChange = false;
|
||||
Offset? _currentDragStartRelatedToOrigin;
|
||||
@@ -1456,7 +1330,7 @@ class _ScrollableSelectionContainerDelegate
|
||||
}
|
||||
|
||||
Offset _inferPositionRelatedToOrigin(Offset globalPosition) {
|
||||
final RenderBox box = state.context.findRenderObject()! as RenderBox;
|
||||
final box = state.context.findRenderObject()! as RenderBox;
|
||||
final Offset localPosition = box.globalToLocal(globalPosition);
|
||||
if (!_selectionStartsInScrollable) {
|
||||
// If the selection starts outside of the scrollable, selecting across the
|
||||
@@ -1489,7 +1363,7 @@ class _ScrollableSelectionContainerDelegate
|
||||
bool forceUpdateEnd = true,
|
||||
}) {
|
||||
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
|
||||
final RenderBox box = state.context.findRenderObject()! as RenderBox;
|
||||
final box = state.context.findRenderObject()! as RenderBox;
|
||||
final Matrix4 transform = box.getTransformTo(null);
|
||||
if (currentSelectionStartIndex != -1 &&
|
||||
(_currentDragStartRelatedToOrigin == null || forceUpdateStart)) {
|
||||
@@ -1604,14 +1478,13 @@ class _ScrollableSelectionContainerDelegate
|
||||
if (lineHeight == null || edge == null) {
|
||||
return;
|
||||
}
|
||||
final RenderBox scrollableBox =
|
||||
state.context.findRenderObject()! as RenderBox;
|
||||
final scrollableBox = state.context.findRenderObject()! as RenderBox;
|
||||
final Matrix4 transform = selectable.getTransformTo(scrollableBox);
|
||||
final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
edge.localPosition,
|
||||
);
|
||||
final Rect scrollableRect = Rect.fromLTRB(
|
||||
final scrollableRect = Rect.fromLTRB(
|
||||
0,
|
||||
0,
|
||||
scrollableBox.size.width,
|
||||
@@ -1680,9 +1553,9 @@ class _ScrollableSelectionContainerDelegate
|
||||
}
|
||||
|
||||
bool _globalPositionInScrollable(Offset globalPosition) {
|
||||
final RenderBox box = state.context.findRenderObject()! as RenderBox;
|
||||
final box = state.context.findRenderObject()! as RenderBox;
|
||||
final Offset localPosition = box.globalToLocal(globalPosition);
|
||||
final Rect rect = Rect.fromLTWH(0, 0, box.size.width, box.size.height);
|
||||
final rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height);
|
||||
return rect.contains(localPosition);
|
||||
}
|
||||
|
||||
@@ -1775,7 +1648,7 @@ class _ScrollableSelectionContainerDelegate
|
||||
}
|
||||
}
|
||||
|
||||
Offset _getDeltaToScrollOrigin(CustomScrollableState scrollableState) {
|
||||
Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) {
|
||||
return switch (scrollableState.axisDirection) {
|
||||
AxisDirection.up => Offset(0, -scrollableState.position.pixels),
|
||||
AxisDirection.down => Offset(0, scrollableState.position.pixels),
|
||||
@@ -1794,7 +1667,7 @@ Offset _getDeltaToScrollOrigin(CustomScrollableState scrollableState) {
|
||||
/// [RenderObject.describeSemanticsConfiguration].
|
||||
///
|
||||
/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport,
|
||||
/// two semantics nodes will be used to represent the [CustomScrollable]: The outer
|
||||
/// two semantics nodes will be used to represent the [Scrollable]: The outer
|
||||
/// node will contain all children, that are excluded from scrolling. The inner
|
||||
/// node, which is annotated with the scrolling actions, will house the
|
||||
/// scrollable children.
|
||||
@@ -1930,14 +1803,14 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
(_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect;
|
||||
|
||||
int? firstVisibleIndex;
|
||||
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode!];
|
||||
final List<SemanticsNode> included = <SemanticsNode>[];
|
||||
for (final SemanticsNode child in children) {
|
||||
final excluded = <SemanticsNode>[_innerNode!];
|
||||
final included = <SemanticsNode>[];
|
||||
for (final child in children) {
|
||||
assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
|
||||
if (child.isTagged(RenderViewport.excludeFromScrolling)) {
|
||||
excluded.add(child);
|
||||
} else {
|
||||
if (!child.hasFlag(SemanticsFlag.isHidden)) {
|
||||
if (!child.flagsCollection.isHidden) {
|
||||
firstVisibleIndex ??= child.indexInParent;
|
||||
}
|
||||
included.add(child);
|
||||
@@ -1982,222 +1855,3 @@ class _RestorableScrollOffset extends RestorableValue<double?> {
|
||||
@override
|
||||
bool get enabled => value != null;
|
||||
}
|
||||
|
||||
// 2D SCROLLING
|
||||
|
||||
/// Specifies how to configure the [DragGestureRecognizer]s of a
|
||||
/// [TwoDimensionalScrollable].
|
||||
// TODO(Piinks): Add sample code, https://github.com/flutter/flutter/issues/126298
|
||||
enum DiagonalDragBehavior {
|
||||
/// This behavior will not allow for any diagonal scrolling.
|
||||
///
|
||||
/// Drag gestures in one direction or the other will lock the input axis until
|
||||
/// the gesture is released.
|
||||
none,
|
||||
|
||||
/// This behavior will only allow diagonal scrolling on a weighted
|
||||
/// scale per gesture event.
|
||||
///
|
||||
/// This means that after initially evaluating the drag gesture, the weighted
|
||||
/// evaluation (based on [kTouchSlop]) stands until the gesture is released.
|
||||
weightedEvent,
|
||||
|
||||
/// This behavior will only allow diagonal scrolling on a weighted
|
||||
/// scale that is evaluated throughout a gesture event.
|
||||
///
|
||||
/// This means that during each update to the drag gesture, the scrolling
|
||||
/// axis will be allowed to scroll diagonally if it exceeds the
|
||||
/// [kTouchSlop].
|
||||
weightedContinuous,
|
||||
|
||||
/// This behavior allows free movement in any and all directions when
|
||||
/// dragging.
|
||||
free,
|
||||
}
|
||||
|
||||
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
|
||||
/// to its edge.
|
||||
///
|
||||
/// The scroll velocity is controlled by the [velocityScalar]:
|
||||
///
|
||||
/// velocity = (distance of overscroll) * [velocityScalar].
|
||||
class EdgeDraggingAutoScroller {
|
||||
/// Creates a auto scroller that scrolls the [scrollable].
|
||||
EdgeDraggingAutoScroller(
|
||||
this.scrollable, {
|
||||
this.onScrollViewScrolled,
|
||||
required this.velocityScalar,
|
||||
});
|
||||
|
||||
/// The [CustomScrollable] this auto scroller is scrolling.
|
||||
final CustomScrollableState scrollable;
|
||||
|
||||
/// Called when a scroll view is scrolled.
|
||||
///
|
||||
/// The scroll view may be scrolled multiple times in a row until the drag
|
||||
/// target no longer triggers the auto scroll. This callback will be called
|
||||
/// in between each scroll.
|
||||
final VoidCallback? onScrollViewScrolled;
|
||||
|
||||
/// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
|
||||
/// The velocity scalar per pixel over scroll.
|
||||
///
|
||||
/// It represents how the velocity scale with the over scroll distance. The
|
||||
/// auto-scroll velocity = (distance of overscroll) * velocityScalar.
|
||||
/// {@endtemplate}
|
||||
final double velocityScalar;
|
||||
|
||||
late Rect _dragTargetRelatedToScrollOrigin;
|
||||
|
||||
/// Whether the auto scroll is in progress.
|
||||
bool get scrolling => _scrolling;
|
||||
bool _scrolling = false;
|
||||
|
||||
double _offsetExtent(Offset offset, Axis scrollDirection) {
|
||||
return switch (scrollDirection) {
|
||||
Axis.horizontal => offset.dx,
|
||||
Axis.vertical => offset.dy,
|
||||
};
|
||||
}
|
||||
|
||||
double _sizeExtent(Size size, Axis scrollDirection) {
|
||||
return switch (scrollDirection) {
|
||||
Axis.horizontal => size.width,
|
||||
Axis.vertical => size.height,
|
||||
};
|
||||
}
|
||||
|
||||
AxisDirection get _axisDirection => scrollable.axisDirection;
|
||||
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
|
||||
|
||||
/// Starts the auto scroll if the [dragTarget] is close to the edge.
|
||||
///
|
||||
/// The scroll starts to scroll the [scrollable] if the target rect is close
|
||||
/// to the edge of the [scrollable]; otherwise, it remains stationary.
|
||||
///
|
||||
/// If the scrollable is already scrolling, calling this method updates the
|
||||
/// previous dragTarget to the new value and continues scrolling if necessary.
|
||||
void startAutoScrollIfNecessary(Rect dragTarget) {
|
||||
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
|
||||
_dragTargetRelatedToScrollOrigin = dragTarget.translate(
|
||||
deltaToOrigin.dx,
|
||||
deltaToOrigin.dy,
|
||||
);
|
||||
if (_scrolling) {
|
||||
// The change will be picked up in the next scroll.
|
||||
return;
|
||||
}
|
||||
assert(!_scrolling);
|
||||
_scroll();
|
||||
}
|
||||
|
||||
/// Stop any ongoing auto scrolling.
|
||||
void stopAutoScroll() {
|
||||
_scrolling = false;
|
||||
}
|
||||
|
||||
Future<void> _scroll() async {
|
||||
final RenderBox scrollRenderBox =
|
||||
scrollable.context.findRenderObject()! as RenderBox;
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
scrollRenderBox.getTransformTo(null),
|
||||
Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
scrollRenderBox.size.width,
|
||||
scrollRenderBox.size.height,
|
||||
),
|
||||
);
|
||||
assert(
|
||||
globalRect.size.width >= _dragTargetRelatedToScrollOrigin.size.width &&
|
||||
globalRect.size.height >=
|
||||
_dragTargetRelatedToScrollOrigin.size.height,
|
||||
'Drag target size is larger than scrollable size, which may cause bouncing',
|
||||
);
|
||||
_scrolling = true;
|
||||
double? newOffset;
|
||||
const double overDragMax = 20.0;
|
||||
|
||||
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
|
||||
final Offset viewportOrigin = globalRect.topLeft.translate(
|
||||
deltaToOrigin.dx,
|
||||
deltaToOrigin.dy,
|
||||
);
|
||||
final double viewportStart = _offsetExtent(
|
||||
viewportOrigin,
|
||||
_scrollDirection,
|
||||
);
|
||||
final double viewportEnd =
|
||||
viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
|
||||
|
||||
final double proxyStart = _offsetExtent(
|
||||
_dragTargetRelatedToScrollOrigin.topLeft,
|
||||
_scrollDirection,
|
||||
);
|
||||
final double proxyEnd = _offsetExtent(
|
||||
_dragTargetRelatedToScrollOrigin.bottomRight,
|
||||
_scrollDirection,
|
||||
);
|
||||
switch (_axisDirection) {
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.left:
|
||||
if (proxyEnd > viewportEnd &&
|
||||
scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.max(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.pixels - overDrag,
|
||||
);
|
||||
} else if (proxyStart < viewportStart &&
|
||||
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(
|
||||
viewportStart - proxyStart,
|
||||
overDragMax,
|
||||
);
|
||||
newOffset = math.min(
|
||||
scrollable.position.maxScrollExtent,
|
||||
scrollable.position.pixels + overDrag,
|
||||
);
|
||||
}
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.down:
|
||||
if (proxyStart < viewportStart &&
|
||||
scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(
|
||||
viewportStart - proxyStart,
|
||||
overDragMax,
|
||||
);
|
||||
newOffset = math.max(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.pixels - overDrag,
|
||||
);
|
||||
} else if (proxyEnd > viewportEnd &&
|
||||
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.min(
|
||||
scrollable.position.maxScrollExtent,
|
||||
scrollable.position.pixels + overDrag,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newOffset == null ||
|
||||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
|
||||
// Drag should not trigger scroll.
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
final Duration duration = Duration(
|
||||
milliseconds: (1000 / velocityScalar).round(),
|
||||
);
|
||||
await scrollable.position.animateTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
onScrollViewScrolled?.call();
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
208
lib/common/widgets/flutter/page/scrollable_helpers.dart
Normal file
208
lib/common/widgets/flutter/page/scrollable_helpers.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
// 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.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
///
|
||||
/// @docImport 'overscroll_indicator.dart';
|
||||
/// @docImport 'viewport.dart';
|
||||
|
||||
// ignore_for_file: dangling_library_doc_comments
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide EdgeDraggingAutoScroller, Scrollable, ScrollableState;
|
||||
|
||||
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
|
||||
/// to its edge.
|
||||
///
|
||||
/// The scroll velocity is controlled by the [velocityScalar]:
|
||||
///
|
||||
/// velocity = (distance of overscroll) * [velocityScalar].
|
||||
class EdgeDraggingAutoScroller {
|
||||
/// Creates a auto scroller that scrolls the [scrollable].
|
||||
EdgeDraggingAutoScroller(
|
||||
this.scrollable, {
|
||||
this.onScrollViewScrolled,
|
||||
required this.velocityScalar,
|
||||
});
|
||||
|
||||
/// The [Scrollable] this auto scroller is scrolling.
|
||||
final ScrollableState scrollable;
|
||||
|
||||
/// Called when a scroll view is scrolled.
|
||||
///
|
||||
/// The scroll view may be scrolled multiple times in a row until the drag
|
||||
/// target no longer triggers the auto scroll. This callback will be called
|
||||
/// in between each scroll.
|
||||
final VoidCallback? onScrollViewScrolled;
|
||||
|
||||
/// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
|
||||
/// The velocity scalar per pixel over scroll.
|
||||
///
|
||||
/// It represents how the velocity scale with the over scroll distance. The
|
||||
/// auto-scroll velocity = (distance of overscroll) * velocityScalar.
|
||||
/// {@endtemplate}
|
||||
final double velocityScalar;
|
||||
|
||||
late Rect _dragTargetRelatedToScrollOrigin;
|
||||
|
||||
/// Whether the auto scroll is in progress.
|
||||
bool get scrolling => _scrolling;
|
||||
bool _scrolling = false;
|
||||
|
||||
double _offsetExtent(Offset offset, Axis scrollDirection) {
|
||||
return switch (scrollDirection) {
|
||||
Axis.horizontal => offset.dx,
|
||||
Axis.vertical => offset.dy,
|
||||
};
|
||||
}
|
||||
|
||||
double _sizeExtent(Size size, Axis scrollDirection) {
|
||||
return switch (scrollDirection) {
|
||||
Axis.horizontal => size.width,
|
||||
Axis.vertical => size.height,
|
||||
};
|
||||
}
|
||||
|
||||
AxisDirection get _axisDirection => scrollable.axisDirection;
|
||||
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
|
||||
|
||||
/// Starts the auto scroll if the [dragTarget] is close to the edge.
|
||||
///
|
||||
/// The scroll starts to scroll the [scrollable] if the target rect is close
|
||||
/// to the edge of the [scrollable]; otherwise, it remains stationary.
|
||||
///
|
||||
/// If the scrollable is already scrolling, calling this method updates the
|
||||
/// previous dragTarget to the new value and continues scrolling if necessary.
|
||||
void startAutoScrollIfNecessary(Rect dragTarget) {
|
||||
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
|
||||
_dragTargetRelatedToScrollOrigin = dragTarget.translate(
|
||||
deltaToOrigin.dx,
|
||||
deltaToOrigin.dy,
|
||||
);
|
||||
if (_scrolling) {
|
||||
// The change will be picked up in the next scroll.
|
||||
return;
|
||||
}
|
||||
assert(!_scrolling);
|
||||
_scroll();
|
||||
}
|
||||
|
||||
/// Stop any ongoing auto scrolling.
|
||||
void stopAutoScroll() {
|
||||
_scrolling = false;
|
||||
}
|
||||
|
||||
Future<void> _scroll() async {
|
||||
final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox;
|
||||
final Matrix4 transform = scrollRenderBox.getTransformTo(null);
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
transform,
|
||||
Rect.fromLTRB(
|
||||
0,
|
||||
0,
|
||||
scrollRenderBox.size.width,
|
||||
scrollRenderBox.size.height,
|
||||
),
|
||||
);
|
||||
final Rect transformedDragTarget = MatrixUtils.transformRect(
|
||||
transform,
|
||||
_dragTargetRelatedToScrollOrigin,
|
||||
);
|
||||
|
||||
assert(
|
||||
(globalRect.size.width + precisionErrorTolerance) >=
|
||||
transformedDragTarget.size.width &&
|
||||
(globalRect.size.height + precisionErrorTolerance) >=
|
||||
transformedDragTarget.size.height,
|
||||
'Drag target size is larger than scrollable size, which may cause bouncing',
|
||||
);
|
||||
_scrolling = true;
|
||||
double? newOffset;
|
||||
const overDragMax = 20.0;
|
||||
|
||||
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
|
||||
final Offset viewportOrigin = globalRect.topLeft.translate(
|
||||
deltaToOrigin.dx,
|
||||
deltaToOrigin.dy,
|
||||
);
|
||||
final double viewportStart = _offsetExtent(
|
||||
viewportOrigin,
|
||||
_scrollDirection,
|
||||
);
|
||||
final double viewportEnd =
|
||||
viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
|
||||
|
||||
final double proxyStart = _offsetExtent(
|
||||
_dragTargetRelatedToScrollOrigin.topLeft,
|
||||
_scrollDirection,
|
||||
);
|
||||
final double proxyEnd = _offsetExtent(
|
||||
_dragTargetRelatedToScrollOrigin.bottomRight,
|
||||
_scrollDirection,
|
||||
);
|
||||
switch (_axisDirection) {
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.left:
|
||||
if (proxyEnd > viewportEnd &&
|
||||
scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.max(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.pixels - overDrag,
|
||||
);
|
||||
} else if (proxyStart < viewportStart &&
|
||||
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(
|
||||
viewportStart - proxyStart,
|
||||
overDragMax,
|
||||
);
|
||||
newOffset = math.min(
|
||||
scrollable.position.maxScrollExtent,
|
||||
scrollable.position.pixels + overDrag,
|
||||
);
|
||||
}
|
||||
case AxisDirection.right:
|
||||
case AxisDirection.down:
|
||||
if (proxyStart < viewportStart &&
|
||||
scrollable.position.pixels > scrollable.position.minScrollExtent) {
|
||||
final double overDrag = math.min(
|
||||
viewportStart - proxyStart,
|
||||
overDragMax,
|
||||
);
|
||||
newOffset = math.max(
|
||||
scrollable.position.minScrollExtent,
|
||||
scrollable.position.pixels - overDrag,
|
||||
);
|
||||
} else if (proxyEnd > viewportEnd &&
|
||||
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
|
||||
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
|
||||
newOffset = math.min(
|
||||
scrollable.position.maxScrollExtent,
|
||||
scrollable.position.pixels + overDrag,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newOffset == null ||
|
||||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
|
||||
// Drag should not trigger scroll.
|
||||
_scrolling = false;
|
||||
return;
|
||||
}
|
||||
final duration = Duration(milliseconds: (1000 / velocityScalar).round());
|
||||
await scrollable.position.animateTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
onScrollViewScrolled?.call();
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/page_view.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/gestures.dart'
|
||||
show DragStartBehavior, HorizontalDragGestureRecognizer;
|
||||
import 'package:flutter/material.dart' hide TabBarView, PageView;
|
||||
|
||||
/// A page view that displays the widget which corresponds to the currently
|
||||
@@ -23,11 +22,12 @@ import 'package:flutter/material.dart' hide TabBarView, PageView;
|
||||
/// [children] list and the length of the [TabBar.tabs] list.
|
||||
///
|
||||
/// To see a sample implementation, visit the [TabController] documentation.
|
||||
class CustomTabBarView extends StatefulWidget {
|
||||
class TabBarView<T extends HorizontalDragGestureRecognizer>
|
||||
extends StatefulWidget {
|
||||
/// Creates a page view with one child per tab.
|
||||
///
|
||||
/// The length of [children] must be the same as the [controller]'s length.
|
||||
const CustomTabBarView({
|
||||
const TabBarView({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.controller,
|
||||
@@ -35,13 +35,10 @@ class CustomTabBarView extends StatefulWidget {
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.viewportFraction = 1.0,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
required this.horizontalDragGestureRecognizer,
|
||||
});
|
||||
|
||||
final Widget? header;
|
||||
final Color bgColor;
|
||||
final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;
|
||||
|
||||
/// This widget's selection and animation state.
|
||||
///
|
||||
@@ -77,13 +74,12 @@ class CustomTabBarView extends StatefulWidget {
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
final Axis scrollDirection;
|
||||
|
||||
@override
|
||||
State<CustomTabBarView> createState() => _CustomTabBarViewState();
|
||||
State<TabBarView<T>> createState() => _TabBarViewState<T>();
|
||||
}
|
||||
|
||||
class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
class _TabBarViewState<T extends HorizontalDragGestureRecognizer>
|
||||
extends State<TabBarView<T>> {
|
||||
TabController? _controller;
|
||||
PageController? _pageController;
|
||||
late List<Widget> _childrenWithKey;
|
||||
@@ -168,7 +164,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomTabBarView oldWidget) {
|
||||
void didUpdateWidget(TabBarView<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController();
|
||||
@@ -203,7 +199,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
void _updateChildren() {
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
|
||||
widget.children.map<Widget>((Widget child) {
|
||||
return Semantics(role: SemanticsRole.tabPanel, child: child);
|
||||
return Semantics(role: .tabPanel, child: child);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
@@ -224,7 +220,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool adjacentDestination =
|
||||
final adjacentDestination =
|
||||
(_currentIndex! - _controller!.previousIndex).abs() == 1;
|
||||
if (adjacentDestination) {
|
||||
_warpToAdjacentTab(_controller!.animationDuration);
|
||||
@@ -361,16 +357,14 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: CustomPageView(
|
||||
scrollDirection: widget.scrollDirection,
|
||||
child: PageView<T>(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
controller: _pageController,
|
||||
physics: widget.physics == null
|
||||
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
|
||||
: const PageScrollPhysics().applyTo(widget.physics),
|
||||
header: widget.header,
|
||||
bgColor: widget.bgColor,
|
||||
horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
|
||||
children: _childrenWithKey,
|
||||
),
|
||||
);
|
||||
75
lib/common/widgets/flutter/pop_scope.dart
Normal file
75
lib/common/widgets/flutter/pop_scope.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart' hide PopScope;
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
|
||||
abstract class PopScopeState<T extends StatefulWidget> extends State<T>
|
||||
implements PopEntry<Object> {
|
||||
ModalRoute<dynamic>? _route;
|
||||
|
||||
@override
|
||||
void onPopInvoked(bool didPop) {}
|
||||
|
||||
@override
|
||||
late final ValueNotifier<bool> canPopNotifier;
|
||||
|
||||
bool get initCanPop => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
canPopNotifier = ValueNotifier<bool>(initCanPop);
|
||||
_route = (Get.routing.route as ModalRoute)..registerPopEntry(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_route?.unregisterPopEntry(this);
|
||||
_route = null;
|
||||
canPopNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: camel_case_types
|
||||
typedef popScope = PopScope;
|
||||
|
||||
class PopScope extends StatefulWidget {
|
||||
const PopScope({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.canPop = true,
|
||||
required this.onPopInvokedWithResult,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final PopInvokedWithResultCallback<Object> onPopInvokedWithResult;
|
||||
|
||||
final bool canPop;
|
||||
|
||||
@override
|
||||
State<PopScope> createState() => _PopScopeState();
|
||||
}
|
||||
|
||||
class _PopScopeState<T extends PopScope> extends PopScopeState<T> {
|
||||
@override
|
||||
bool get initCanPop => widget.canPop;
|
||||
|
||||
@override
|
||||
void onPopInvokedWithResult(bool didPop, Object? result) {
|
||||
widget.onPopInvokedWithResult(didPop, result);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(T oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
canPopNotifier.value = widget.canPop;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
157
lib/common/widgets/flutter/popup_menu.dart
Normal file
157
lib/common/widgets/flutter/popup_menu.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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.
|
||||
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomPopupMenuItem<T> extends PopupMenuEntry<T> {
|
||||
const CustomPopupMenuItem({
|
||||
super.key,
|
||||
this.value,
|
||||
this.height = kMinInteractiveDimension,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final T? value;
|
||||
|
||||
@override
|
||||
final double height;
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
bool represents(T? value) => value == this.value;
|
||||
|
||||
@override
|
||||
CustomPopupMenuItemState<T, CustomPopupMenuItem<T>> createState() =>
|
||||
CustomPopupMenuItemState<T, CustomPopupMenuItem<T>>();
|
||||
}
|
||||
|
||||
class CustomPopupMenuItemState<T, W extends CustomPopupMenuItem<T>>
|
||||
extends State<W> {
|
||||
@protected
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||
const Set<WidgetState> states = <WidgetState>{};
|
||||
|
||||
final style =
|
||||
popupMenuTheme.labelTextStyle?.resolve(states)! ??
|
||||
_PopupMenuDefaultsM3(context).labelTextStyle!.resolve(states)!;
|
||||
|
||||
return ListTileTheme.merge(
|
||||
contentPadding: .zero,
|
||||
titleTextStyle: style,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: widget.height),
|
||||
child: Padding(
|
||||
padding: _PopupMenuDefaultsM3.menuItemPadding,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPopupMenuDivider extends PopupMenuEntry<Never> {
|
||||
const CustomPopupMenuDivider({
|
||||
super.key,
|
||||
required this.height,
|
||||
this.thickness,
|
||||
this.indent,
|
||||
this.endIndent,
|
||||
this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
final double height;
|
||||
|
||||
final double? thickness;
|
||||
|
||||
final double? indent;
|
||||
|
||||
final double? endIndent;
|
||||
|
||||
final BorderRadiusGeometry? radius;
|
||||
|
||||
@override
|
||||
bool represents(void value) => false;
|
||||
|
||||
@override
|
||||
State<CustomPopupMenuDivider> createState() => _CustomPopupMenuDividerState();
|
||||
}
|
||||
|
||||
class _CustomPopupMenuDividerState extends State<CustomPopupMenuDivider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Divider(
|
||||
height: widget.height,
|
||||
thickness: widget.thickness,
|
||||
indent: widget.indent,
|
||||
color: ColorScheme.of(context).outline.withValues(alpha: 0.2),
|
||||
endIndent: widget.endIndent,
|
||||
radius: widget.radius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu
|
||||
|
||||
// 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 _PopupMenuDefaultsM3 extends PopupMenuThemeData {
|
||||
_PopupMenuDefaultsM3(this.context)
|
||||
: super(elevation: 3.0);
|
||||
|
||||
final BuildContext context;
|
||||
late final ThemeData _theme = Theme.of(context);
|
||||
late final ColorScheme _colors = _theme.colorScheme;
|
||||
late final TextTheme _textTheme = _theme.textTheme;
|
||||
|
||||
@override WidgetStateProperty<TextStyle?>? get labelTextStyle {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
// TODO(quncheng): Update this hard-coded value to use the latest tokens.
|
||||
final TextStyle style = _textTheme.labelLarge!;
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return style.apply(color: _colors.onSurface.withValues(alpha: 0.38));
|
||||
}
|
||||
return style.apply(color: _colors.onSurface);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Color? get color => _colors.surfaceContainer;
|
||||
|
||||
@override
|
||||
Color? get shadowColor => _colors.shadow;
|
||||
|
||||
@override
|
||||
Color? get surfaceTintColor => Colors.transparent;
|
||||
|
||||
@override
|
||||
ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
||||
|
||||
// TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs
|
||||
// Update this when the token is available.
|
||||
@override
|
||||
EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0);
|
||||
|
||||
// TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs
|
||||
// Update this when the token is available.
|
||||
static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0);
|
||||
}// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - PopupMenu
|
||||
@@ -1,30 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/material.dart' hide RefreshIndicator;
|
||||
|
||||
Widget refreshIndicator({
|
||||
required RefreshCallback onRefresh,
|
||||
required Widget child,
|
||||
}) {
|
||||
return RefreshIndicator(
|
||||
displacement: displacement,
|
||||
onRefresh: onRefresh,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
// 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:async' show Completer;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/scroll_behavior.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'
|
||||
show RefreshScrollPhysics;
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/material.dart' hide RefreshIndicator;
|
||||
|
||||
/// The distance from the child's top or bottom [edgeOffset] where
|
||||
/// the refresh indicator will settle. During the drag that exposes the refresh
|
||||
/// indicator, its actual displacement may significantly exceed this value.
|
||||
///
|
||||
/// In most cases, [displacement] distance starts counting from the parent's
|
||||
/// edges. However, if [edgeOffset] is larger than zero then the [displacement]
|
||||
/// value is calculated from that offset instead of the parent's edge.
|
||||
double displacement = Pref.refreshDisplacement;
|
||||
|
||||
// The over-scroll distance that moves the indicator to its maximum
|
||||
// displacement, as a percentage of the scrollable's container extent.
|
||||
|
||||
double displacement = Pref.refreshDisplacement;
|
||||
double kDragContainerExtentPercentage = Pref.refreshDragPercentage;
|
||||
|
||||
// How much the scroll's drag gesture can overshoot the RefreshIndicator's
|
||||
@@ -39,21 +37,13 @@ const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
|
||||
// has completed.
|
||||
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
|
||||
|
||||
/// The signature for a function that's called when the user has dragged a
|
||||
/// [RefreshIndicator] far enough to demonstrate that they want the app to
|
||||
/// refresh. The returned [Future] must complete when the refresh operation is
|
||||
/// finished.
|
||||
///
|
||||
/// Used by [RefreshIndicator.onRefresh].
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
|
||||
/// Indicates current status of Material `RefreshIndicator`.
|
||||
enum RefreshIndicatorStatus {
|
||||
/// Pointer is down.
|
||||
drag,
|
||||
|
||||
/// Dragged far enough that an up event will run the onRefresh callback.
|
||||
armed,
|
||||
// armed,
|
||||
|
||||
/// Animating to the indicator's final "displacement".
|
||||
snap,
|
||||
@@ -68,19 +58,6 @@ enum RefreshIndicatorStatus {
|
||||
canceled,
|
||||
}
|
||||
|
||||
/// Used to configure how [RefreshIndicator] can be triggered.
|
||||
enum RefreshIndicatorTriggerMode {
|
||||
/// The indicator can be triggered regardless of the scroll position
|
||||
/// of the [Scrollable] when the drag starts.
|
||||
anywhere,
|
||||
|
||||
/// The indicator can only be triggered if the [Scrollable] is at the edge
|
||||
/// when the drag starts.
|
||||
onEdge,
|
||||
}
|
||||
|
||||
enum _IndicatorType { material, adaptive, noSpinner }
|
||||
|
||||
/// A widget that supports the Material "swipe to refresh" idiom.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
|
||||
@@ -155,80 +132,16 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// The [semanticsValue] may be used to specify progress on the widget.
|
||||
const RefreshIndicator({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.material,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates an adaptive [RefreshIndicator] based on whether the target
|
||||
/// platform is iOS or macOS, following Material design's
|
||||
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
|
||||
///
|
||||
/// When the descendant overscrolls, a different spinning progress indicator
|
||||
/// is shown depending on platform. On iOS and macOS,
|
||||
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
|
||||
/// [CircularProgressIndicator] appears.
|
||||
///
|
||||
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
|
||||
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
|
||||
///
|
||||
/// The target platform is based on the current [Theme]: [ThemeData.platform].
|
||||
///
|
||||
/// Notably the scrollable widget itself will have slightly different behavior
|
||||
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
|
||||
const RefreshIndicator.adaptive({
|
||||
super.key,
|
||||
this.isClampingScrollPhysics = false,
|
||||
required this.child,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.adaptive,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates a [RefreshIndicator] with no spinner and calls `onRefresh` when
|
||||
/// successfully armed by a drag event.
|
||||
///
|
||||
/// Events can be optionally listened by using the `onStatusChange` callback.
|
||||
const RefreshIndicator.noSpinner({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onRefresh,
|
||||
this.onStatusChange,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.noSpinner,
|
||||
// The following parameters aren't used because [_IndicatorType.noSpinner] is being used,
|
||||
// which involves showing no spinner, hence the following parameters are useless since
|
||||
// their only use is to change the spinner's appearance.
|
||||
displacement = 0.0,
|
||||
edgeOffset = 0.0,
|
||||
color = null,
|
||||
backgroundColor = null,
|
||||
strokeWidth = 0.0,
|
||||
assert(elevation >= 0.0);
|
||||
}) : assert(elevation >= 0.0);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
@@ -238,15 +151,6 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// Typically a [ListView] or [CustomScrollView].
|
||||
final Widget child;
|
||||
|
||||
/// The distance from the child's top or bottom [edgeOffset] where
|
||||
/// the refresh indicator will settle. During the drag that exposes the refresh
|
||||
/// indicator, its actual displacement may significantly exceed this value.
|
||||
///
|
||||
/// In most cases, [displacement] distance starts counting from the parent's
|
||||
/// edges. However, if [edgeOffset] is larger than zero then the [displacement]
|
||||
/// value is calculated from that offset instead of the parent's edge.
|
||||
final double displacement;
|
||||
|
||||
/// The offset where [RefreshProgressIndicator] starts to appear on drag start.
|
||||
///
|
||||
/// Depending whether the indicator is showing on the top or bottom, the value
|
||||
@@ -268,10 +172,6 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// [Future] must complete when the refresh operation is finished.
|
||||
final RefreshCallback onRefresh;
|
||||
|
||||
/// Called to get the current status of the [RefreshIndicator] to update the UI as needed.
|
||||
/// This is an optional parameter, used to fine tune app cases.
|
||||
final ValueChanged<RefreshIndicatorStatus?>? onStatusChange;
|
||||
|
||||
/// The progress indicator's foreground color. The current theme's
|
||||
/// [ColorScheme.primary] by default.
|
||||
final Color? color;
|
||||
@@ -287,42 +187,18 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// else for more complicated layouts.
|
||||
final ScrollNotificationPredicate notificationPredicate;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel}
|
||||
///
|
||||
/// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]
|
||||
/// if it is null.
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue}
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Defines [strokeWidth] for `RefreshIndicator`.
|
||||
///
|
||||
/// By default, the value of [strokeWidth] is 2.0 pixels.
|
||||
final double strokeWidth;
|
||||
|
||||
final _IndicatorType _indicatorType;
|
||||
|
||||
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
|
||||
///
|
||||
/// The [RefreshIndicator] can be pulled out in two cases,
|
||||
/// 1, Keep dragging if the scrollable widget at the edge with zero scroll position
|
||||
/// when the drag starts.
|
||||
/// 2, Keep dragging after overscroll occurs if the scrollable widget has
|
||||
/// a non-zero scroll position when the drag starts.
|
||||
///
|
||||
/// If this is [RefreshIndicatorTriggerMode.anywhere], both of the cases above can be triggered.
|
||||
///
|
||||
/// If this is [RefreshIndicatorTriggerMode.onEdge], only case 1 can be triggered.
|
||||
///
|
||||
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
|
||||
final RefreshIndicatorTriggerMode triggerMode;
|
||||
|
||||
/// Defines the elevation of the underlying [RefreshIndicator].
|
||||
///
|
||||
/// Defaults to 2.0.
|
||||
final double elevation;
|
||||
|
||||
final bool isClampingScrollPhysics;
|
||||
|
||||
@override
|
||||
RefreshIndicatorState createState() => RefreshIndicatorState();
|
||||
}
|
||||
@@ -340,10 +216,9 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
|
||||
RefreshIndicatorStatus? _status;
|
||||
late Future<void> _pendingRefreshFuture;
|
||||
bool? _isIndicatorAtTop;
|
||||
double? _dragOffset;
|
||||
late Color _effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
late Color _effectiveValueColor;
|
||||
// late Color _backgroundColor;
|
||||
|
||||
static final Animatable<double> _threeQuarterTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
@@ -399,19 +274,20 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
|
||||
void _setupColorTween() {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
// _backgroundColor = colorScheme.surfaceContainerHighest;
|
||||
// Reset the current value color.
|
||||
_effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
_effectiveValueColor = widget.color ?? colorScheme.primary;
|
||||
final Color color = _effectiveValueColor;
|
||||
if (color.alpha == 0x00) {
|
||||
if (color.a == 0) {
|
||||
// Set an always stopped animation instead of a driven tween.
|
||||
_valueColor = AlwaysStoppedAnimation<Color>(color);
|
||||
} else {
|
||||
// Respect the alpha of the given color.
|
||||
_valueColor = _positionController.drive(
|
||||
ColorTween(
|
||||
begin: color.withAlpha(0),
|
||||
end: color.withAlpha(color.alpha),
|
||||
begin: color.withValues(alpha: 0),
|
||||
end: color,
|
||||
).chain(
|
||||
CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)),
|
||||
),
|
||||
@@ -423,17 +299,13 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
// If the notification.dragDetails is null, this scroll is not triggered by
|
||||
// user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll.
|
||||
// In this case, we don't want to trigger the refresh indicator.
|
||||
return ((notification is ScrollStartNotification &&
|
||||
return _status == null &&
|
||||
((notification is ScrollStartNotification &&
|
||||
notification.dragDetails != null) ||
|
||||
(notification is ScrollUpdateNotification &&
|
||||
notification.dragDetails != null &&
|
||||
widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) &&
|
||||
((notification.metrics.axisDirection == AxisDirection.up &&
|
||||
notification.metrics.extentAfter == 0.0) ||
|
||||
(notification.metrics.axisDirection == AxisDirection.down &&
|
||||
notification.metrics.extentBefore == 0.0)) &&
|
||||
_status == null &&
|
||||
_start(notification.metrics.axisDirection);
|
||||
notification.dragDetails != null)) &&
|
||||
notification.metrics.extentBefore == 0.0 &&
|
||||
_start();
|
||||
}
|
||||
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
@@ -443,57 +315,35 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
if (_shouldStart(notification)) {
|
||||
setState(() {
|
||||
_status = RefreshIndicatorStatus.drag;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
final bool? indicatorAtTopNow =
|
||||
switch (notification.metrics.axisDirection) {
|
||||
AxisDirection.down || AxisDirection.up => true,
|
||||
AxisDirection.left || AxisDirection.right => null,
|
||||
};
|
||||
if (indicatorAtTopNow != _isIndicatorAtTop) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
}
|
||||
} else if (notification is ScrollUpdateNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.scrollDelta!;
|
||||
}
|
||||
if (notification is ScrollUpdateNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
if (_status == RefreshIndicatorStatus.armed &&
|
||||
notification.dragDetails == null) {
|
||||
// On iOS start the refresh when the Scrollable bounces back from the
|
||||
// overscroll (ScrollNotification indicating this don't have dragDetails
|
||||
// because the scroll activity is not directly triggered by a drag).
|
||||
_show();
|
||||
|
||||
if (notification.dragDetails == null &&
|
||||
_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
// On iOS start the refresh when the Scrollable bounces back from the
|
||||
// overscroll (ScrollNotification indicating this don't have dragDetails
|
||||
// because the scroll activity is not directly triggered by a drag).
|
||||
_show();
|
||||
}
|
||||
}
|
||||
} else if (notification is OverscrollNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.overscroll;
|
||||
}
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
switch (_status) {
|
||||
case RefreshIndicatorStatus.armed:
|
||||
if (_positionController.value < 1.0) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
} else {
|
||||
_show();
|
||||
}
|
||||
case RefreshIndicatorStatus.drag:
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
if (_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
_show();
|
||||
} else {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
}
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
case RefreshIndicatorStatus.done:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
@@ -519,20 +369,9 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _start(AxisDirection direction) {
|
||||
bool _start() {
|
||||
assert(_status == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
assert(_dragOffset == null);
|
||||
switch (direction) {
|
||||
case AxisDirection.down:
|
||||
case AxisDirection.up:
|
||||
_isIndicatorAtTop = true;
|
||||
case AxisDirection.left:
|
||||
case AxisDirection.right:
|
||||
_isIndicatorAtTop = null;
|
||||
// we do not support horizontal scroll views.
|
||||
return false;
|
||||
}
|
||||
_dragOffset = 0.0;
|
||||
_scaleController.value = 0.0;
|
||||
_positionController.value = 0.0;
|
||||
@@ -541,24 +380,15 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
|
||||
void _checkDragOffset(double containerExtent) {
|
||||
assert(
|
||||
_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed,
|
||||
_status == RefreshIndicatorStatus.drag,
|
||||
);
|
||||
double newValue =
|
||||
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
|
||||
if (_status == RefreshIndicatorStatus.armed) {
|
||||
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
|
||||
}
|
||||
_positionController.value = clampDouble(
|
||||
newValue,
|
||||
0.0,
|
||||
1.0,
|
||||
); // This triggers various rebuilds.
|
||||
if (_status == RefreshIndicatorStatus.drag &&
|
||||
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
|
||||
_status = RefreshIndicatorStatus.armed;
|
||||
widget.onStatusChange?.call(_status);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop showing the refresh indicator.
|
||||
@@ -573,7 +403,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
);
|
||||
setState(() {
|
||||
_status = newMode;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
switch (_status!) {
|
||||
case RefreshIndicatorStatus.done:
|
||||
@@ -586,7 +415,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
0.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
case RefreshIndicatorStatus.armed:
|
||||
case RefreshIndicatorStatus.drag:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
@@ -594,7 +422,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
if (mounted && _status == newMode) {
|
||||
_dragOffset = null;
|
||||
_isIndicatorAtTop = null;
|
||||
setState(() {
|
||||
_status = null;
|
||||
});
|
||||
@@ -607,7 +434,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
final Completer<void> completer = Completer<void>();
|
||||
_pendingRefreshFuture = completer.future;
|
||||
_status = RefreshIndicatorStatus.snap;
|
||||
widget.onStatusChange?.call(_status);
|
||||
_positionController
|
||||
.animateTo(
|
||||
1.0 / _kDragSizeFactorLimit,
|
||||
@@ -646,11 +472,11 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
/// When initiated in this manner, the refresh indicator is independent of any
|
||||
/// actual scroll view. It defaults to showing the indicator at the top. To
|
||||
/// show it at the bottom, set `atTop` to false.
|
||||
Future<void> show({bool atTop = true}) {
|
||||
Future<void> show() {
|
||||
if (_status != RefreshIndicatorStatus.refresh &&
|
||||
_status != RefreshIndicatorStatus.snap) {
|
||||
if (_status == null) {
|
||||
_start(atTop ? AxisDirection.down : AxisDirection.up);
|
||||
_start();
|
||||
}
|
||||
_show();
|
||||
}
|
||||
@@ -661,7 +487,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final Widget child = NotificationListener<ScrollNotification>(
|
||||
Widget child = NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: NotificationListener<OverscrollIndicatorNotification>(
|
||||
onNotification: _handleIndicatorNotification,
|
||||
@@ -671,10 +497,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
assert(() {
|
||||
if (_status == null) {
|
||||
assert(_dragOffset == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
} else {
|
||||
assert(_dragOffset != null);
|
||||
assert(_isIndicatorAtTop != null);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
@@ -683,75 +507,33 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
_status == RefreshIndicatorStatus.refresh ||
|
||||
_status == RefreshIndicatorStatus.done;
|
||||
|
||||
return Stack(
|
||||
child = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
child,
|
||||
if (_status != null)
|
||||
Positioned(
|
||||
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
top: widget.edgeOffset,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: SizeTransition(
|
||||
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
|
||||
axisAlignment: 1.0,
|
||||
sizeFactor: _positionFactor, // This is what brings it down.
|
||||
child: Padding(
|
||||
padding: _isIndicatorAtTop!
|
||||
? EdgeInsets.only(top: widget.displacement)
|
||||
: EdgeInsets.only(bottom: widget.displacement),
|
||||
padding: EdgeInsets.only(top: displacement),
|
||||
child: Align(
|
||||
alignment: _isIndicatorAtTop!
|
||||
? Alignment.topCenter
|
||||
: Alignment.bottomCenter,
|
||||
alignment: Alignment.topCenter,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleFactor,
|
||||
child: AnimatedBuilder(
|
||||
animation: _positionController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final Widget materialIndicator =
|
||||
RefreshProgressIndicator(
|
||||
semanticsLabel:
|
||||
widget.semanticsLabel ??
|
||||
MaterialLocalizations.of(
|
||||
context,
|
||||
).refreshIndicatorSemanticLabel,
|
||||
semanticsValue: widget.semanticsValue,
|
||||
value: showIndeterminateIndicator
|
||||
? null
|
||||
: _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
elevation: widget.elevation,
|
||||
);
|
||||
|
||||
final Widget cupertinoIndicator =
|
||||
CupertinoActivityIndicator(
|
||||
color: widget.color,
|
||||
);
|
||||
|
||||
switch (widget._indicatorType) {
|
||||
case _IndicatorType.material:
|
||||
return materialIndicator;
|
||||
|
||||
case _IndicatorType.adaptive:
|
||||
final ThemeData theme = Theme.of(context);
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return materialIndicator;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return cupertinoIndicator;
|
||||
}
|
||||
|
||||
case _IndicatorType.noSpinner:
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
builder: (context, child) => RefreshProgressIndicator(
|
||||
value: showIndeterminateIndicator ? null : _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
elevation: widget.elevation,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -760,5 +542,84 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!widget.isClampingScrollPhysics &&
|
||||
(Platform.isIOS || Platform.isMacOS)) {
|
||||
return child;
|
||||
}
|
||||
return ScrollConfiguration(
|
||||
behavior: RefreshScrollBehavior(
|
||||
desktopDragDevices,
|
||||
scrollPhysics: RefreshScrollPhysics(
|
||||
parent: const RangeMaintainingScrollPhysics(),
|
||||
onDrag: _onDrag,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
bool _onDrag(double offset, double viewportDimension) {
|
||||
if (_positionController.value > 0.0 && _status == .drag) {
|
||||
_dragOffset = _dragOffset! + offset;
|
||||
_checkDragOffset(viewportDimension);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// late final _refreshKey = GlobalKey();
|
||||
// Widget _m3eRefreshProgressIndicator(bool showIndeterminateIndicator) {
|
||||
// const indicatorMargin = EdgeInsets.all(4);
|
||||
// const indicatorPadding = EdgeInsets.all(6);
|
||||
// const indicatorSize = 41.0;
|
||||
|
||||
// final progress = _value.value;
|
||||
// return Padding(
|
||||
// padding: indicatorMargin,
|
||||
// child: SizedBox(
|
||||
// width: indicatorSize,
|
||||
// height: indicatorSize,
|
||||
// child: Material(
|
||||
// type: MaterialType.circle,
|
||||
// color: _backgroundColor,
|
||||
// elevation: widget.elevation,
|
||||
// child: Padding(
|
||||
// padding: indicatorPadding,
|
||||
// child: showIndeterminateIndicator
|
||||
// ? M3ELoadingIndicator(
|
||||
// childKey: _refreshKey,
|
||||
// color: _effectiveValueColor,
|
||||
// morphs: Morphs.refreshMorphs,
|
||||
// size: null,
|
||||
// )
|
||||
// : RawM3ELoadingIndicator(
|
||||
// key: _refreshKey,
|
||||
// morph: Morphs.manualMorph,
|
||||
// progress: progress,
|
||||
// angle: -progress * math.pi,
|
||||
// color: _valueColor.value!,
|
||||
// size: null,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
// ignore: camel_case_types
|
||||
typedef refreshIndicator = RefreshIndicator;
|
||||
|
||||
class RefreshScrollBehavior extends CustomScrollBehavior {
|
||||
const RefreshScrollBehavior(
|
||||
super.dragDevices, {
|
||||
required this.scrollPhysics,
|
||||
});
|
||||
|
||||
final RefreshScrollPhysics scrollPhysics;
|
||||
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
return scrollPhysics;
|
||||
}
|
||||
}
|
||||
2259
lib/common/widgets/flutter/selectable_text/selectable_region.dart
Normal file
2259
lib/common/widgets/flutter/selectable_text/selectable_region.dart
Normal file
File diff suppressed because it is too large
Load Diff
903
lib/common/widgets/flutter/selectable_text/selectable_text.dart
Normal file
903
lib/common/widgets/flutter/selectable_text/selectable_text.dart
Normal file
@@ -0,0 +1,903 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/selectable_text/text_selection.dart';
|
||||
import 'package:flutter/cupertino.dart'
|
||||
hide TextSelectionGestureDetectorBuilder;
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide SelectableText, TextSelectionGestureDetectorBuilder;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class _TextSpanEditingController extends TextEditingController {
|
||||
_TextSpanEditingController({required TextSpan textSpan})
|
||||
: _textSpan = textSpan,
|
||||
super(text: textSpan.toPlainText(includeSemanticsLabels: false));
|
||||
|
||||
final TextSpan _textSpan;
|
||||
|
||||
@override
|
||||
TextSpan buildTextSpan({
|
||||
required BuildContext context,
|
||||
TextStyle? style,
|
||||
required bool withComposing,
|
||||
}) {
|
||||
// This does not care about composing.
|
||||
return TextSpan(style: style, children: <TextSpan>[_textSpan]);
|
||||
}
|
||||
|
||||
@override
|
||||
set text(String? newText) {
|
||||
// This should never be reached.
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectableTextSelectionGestureDetectorBuilder
|
||||
extends CustomTextSelectionGestureDetectorBuilder {
|
||||
_SelectableTextSelectionGestureDetectorBuilder({
|
||||
required _SelectableTextState state,
|
||||
}) : _state = state,
|
||||
super(delegate: state);
|
||||
|
||||
final _SelectableTextState _state;
|
||||
|
||||
@override
|
||||
void onSingleTapUp(TapDragUpDetails details) {
|
||||
if (!delegate.selectionEnabled) {
|
||||
return;
|
||||
}
|
||||
super.onSingleTapUp(details);
|
||||
_state.widget.onTap?.call();
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of selectable text with a single style.
|
||||
///
|
||||
/// Consider using [SelectionArea] or [SelectableRegion] instead, which enable
|
||||
/// selection on a widget subtree, including but not limited to [Text] widgets.
|
||||
///
|
||||
/// The [SelectableText] widget displays a string of text with a single style.
|
||||
/// The string might break across multiple lines or might all be displayed on
|
||||
/// the same line depending on the layout constraints.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
|
||||
///
|
||||
/// The [style] argument is optional. When omitted, the text will use the style
|
||||
/// from the closest enclosing [DefaultTextStyle]. If the given style's
|
||||
/// [TextStyle.inherit] property is true (the default), the given style will
|
||||
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
|
||||
/// behavior is useful, for example, to make the text bold while using the
|
||||
/// default font family and size.
|
||||
///
|
||||
/// {@macro flutter.material.textfield.wantKeepAlive}
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// ```dart
|
||||
/// const SelectableText(
|
||||
/// 'Hello! How are you?',
|
||||
/// textAlign: TextAlign.center,
|
||||
/// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can
|
||||
/// display a paragraph with differently styled [TextSpan]s. The sample
|
||||
/// that follows displays "Hello beautiful world" with different styles
|
||||
/// for each word.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// ```dart
|
||||
/// const SelectableText.rich(
|
||||
/// TextSpan(
|
||||
/// text: 'Hello', // default text style
|
||||
/// children: <TextSpan>[
|
||||
/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
|
||||
/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
/// ],
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Interactivity
|
||||
///
|
||||
/// To make [SelectableText] react to touch events, use callback [onTap] to achieve
|
||||
/// the desired behavior.
|
||||
///
|
||||
/// ## Scrolling Considerations
|
||||
///
|
||||
/// If this [SelectableText] is not a descendant of [Scaffold] and is being used
|
||||
/// within a [Scrollable] or nested [Scrollable]s, consider placing a
|
||||
/// [ScrollNotificationObserver] above the root [Scrollable] that contains this
|
||||
/// [SelectableText] to ensure proper scroll coordination for [SelectableText]
|
||||
/// and its components like [TextSelectionOverlay].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Text], which is the non selectable version of this widget.
|
||||
/// * [TextField], which is the editable version of this widget.
|
||||
/// * [SelectionArea], which enables the selection of multiple [Text] widgets
|
||||
/// and of other widgets.
|
||||
class SelectableText extends StatefulWidget {
|
||||
/// Creates a selectable text widget.
|
||||
///
|
||||
/// If the [style] argument is null, the text will use the style from the
|
||||
/// closest enclosing [DefaultTextStyle].
|
||||
///
|
||||
|
||||
/// If the [showCursor], [autofocus], [dragStartBehavior],
|
||||
/// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are
|
||||
/// specified, the [maxLines] argument must be greater than zero.
|
||||
const SelectableText(
|
||||
String this.data, {
|
||||
super.key,
|
||||
this.focusNode,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign,
|
||||
this.textDirection,
|
||||
@Deprecated(
|
||||
'Use textScaler instead. '
|
||||
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
this.textScaleFactor,
|
||||
this.textScaler,
|
||||
this.showCursor = false,
|
||||
this.autofocus = false,
|
||||
@Deprecated(
|
||||
'Use `contextMenuBuilder` instead. '
|
||||
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||
)
|
||||
this.toolbarOptions,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorHeight,
|
||||
this.cursorRadius,
|
||||
this.cursorColor,
|
||||
this.selectionColor,
|
||||
this.selectionHeightStyle,
|
||||
this.selectionWidthStyle,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.scrollPhysics,
|
||||
this.scrollBehavior,
|
||||
this.semanticsLabel,
|
||||
this.textHeightBehavior,
|
||||
this.textWidthBasis,
|
||||
this.onSelectionChanged,
|
||||
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||
this.magnifierConfiguration,
|
||||
}) : assert(maxLines == null || maxLines > 0),
|
||||
assert(minLines == null || minLines > 0),
|
||||
assert(
|
||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||
"minLines can't be greater than maxLines",
|
||||
),
|
||||
assert(
|
||||
textScaler == null || textScaleFactor == null,
|
||||
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
|
||||
),
|
||||
textSpan = null;
|
||||
|
||||
/// Creates a selectable text widget with a [TextSpan].
|
||||
///
|
||||
/// The [TextSpan.children] attribute of the [textSpan] parameter must only
|
||||
/// contain [TextSpan]s. Other types of [InlineSpan] are not allowed.
|
||||
const SelectableText.rich(
|
||||
TextSpan this.textSpan, {
|
||||
super.key,
|
||||
this.focusNode,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign,
|
||||
this.textDirection,
|
||||
@Deprecated(
|
||||
'Use textScaler instead. '
|
||||
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
this.textScaleFactor,
|
||||
this.textScaler,
|
||||
this.showCursor = false,
|
||||
this.autofocus = false,
|
||||
@Deprecated(
|
||||
'Use `contextMenuBuilder` instead. '
|
||||
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||
)
|
||||
this.toolbarOptions,
|
||||
this.minLines,
|
||||
this.maxLines,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorHeight,
|
||||
this.cursorRadius,
|
||||
this.cursorColor,
|
||||
this.selectionColor,
|
||||
this.selectionHeightStyle,
|
||||
this.selectionWidthStyle,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.scrollPhysics,
|
||||
this.scrollBehavior,
|
||||
this.semanticsLabel,
|
||||
this.textHeightBehavior,
|
||||
this.textWidthBasis,
|
||||
this.onSelectionChanged,
|
||||
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||
this.magnifierConfiguration,
|
||||
}) : assert(maxLines == null || maxLines > 0),
|
||||
assert(minLines == null || minLines > 0),
|
||||
assert(
|
||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||
"minLines can't be greater than maxLines",
|
||||
),
|
||||
assert(
|
||||
textScaler == null || textScaleFactor == null,
|
||||
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
|
||||
),
|
||||
data = null;
|
||||
|
||||
/// The text to display.
|
||||
///
|
||||
/// This will be null if a [textSpan] is provided instead.
|
||||
final String? data;
|
||||
|
||||
/// The text to display as a [TextSpan].
|
||||
///
|
||||
/// This will be null if [data] is provided instead.
|
||||
final TextSpan? textSpan;
|
||||
|
||||
/// Defines the focus for this widget.
|
||||
///
|
||||
/// Text is only selectable when widget is focused.
|
||||
///
|
||||
/// The [focusNode] is a long-lived object that's typically managed by a
|
||||
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
||||
///
|
||||
/// To give the focus to this widget, provide a [focusNode] and then
|
||||
/// use the current [FocusScope] to request the focus:
|
||||
///
|
||||
/// ```dart
|
||||
/// FocusScope.of(context).requestFocus(myFocusNode);
|
||||
/// ```
|
||||
///
|
||||
/// This happens automatically when the widget is tapped.
|
||||
///
|
||||
/// To be notified when the widget gains or loses the focus, add a listener
|
||||
/// to the [focusNode]:
|
||||
///
|
||||
/// ```dart
|
||||
/// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
|
||||
/// ```
|
||||
///
|
||||
/// If null, this widget will create its own [FocusNode] with
|
||||
/// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
|
||||
/// to be skipped over during focus traversal.
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The style to use for the text.
|
||||
///
|
||||
/// If null, defaults [DefaultTextStyle] of context.
|
||||
final TextStyle? style;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.strutStyle}
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textAlign}
|
||||
final TextAlign? textAlign;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textDirection}
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textScaleFactor}
|
||||
@Deprecated(
|
||||
'Use textScaler instead. '
|
||||
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
final double? textScaleFactor;
|
||||
|
||||
/// {@macro flutter.painting.textPainter.textScaler}
|
||||
final TextScaler? textScaler;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.minLines}
|
||||
final int? minLines;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.maxLines}
|
||||
final int? maxLines;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool showCursor;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorWidth}
|
||||
final double cursorWidth;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorHeight}
|
||||
final double? cursorHeight;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorRadius}
|
||||
final Radius? cursorRadius;
|
||||
|
||||
/// The color of the cursor.
|
||||
///
|
||||
/// The cursor indicates the current text insertion point.
|
||||
///
|
||||
/// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also
|
||||
/// null and [ThemeData.platform] is [TargetPlatform.iOS] or
|
||||
/// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used.
|
||||
/// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used.
|
||||
final Color? cursorColor;
|
||||
|
||||
/// The color to use when painting the selection.
|
||||
///
|
||||
/// If this property is null, this widget gets the selection color from the
|
||||
/// inherited [DefaultSelectionStyle] (if any); if none, the selection
|
||||
/// color is derived from the [CupertinoThemeData.primaryColor] on
|
||||
/// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on
|
||||
/// other platforms.
|
||||
final Color? selectionColor;
|
||||
|
||||
/// Controls how tall the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||
final ui.BoxHeightStyle? selectionHeightStyle;
|
||||
|
||||
/// Controls how wide the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||
final ui.BoxWidthStyle? selectionWidthStyle;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||
final TextSelectionControls? selectionControls;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// Paste and cut will be disabled regardless.
|
||||
///
|
||||
/// If not set, select all and copy will be enabled by default.
|
||||
@Deprecated(
|
||||
'Use `contextMenuBuilder` instead. '
|
||||
'This feature was deprecated after v3.3.0-0.5.pre.',
|
||||
)
|
||||
final ToolbarOptions? toolbarOptions;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// Called when the user taps on this selectable text.
|
||||
///
|
||||
/// The selectable text builds a [GestureDetector] to handle input events like tap,
|
||||
/// to trigger focus requests, to move the caret, adjust the selection, etc.
|
||||
/// Handling some of those events by wrapping the selectable text with a competing
|
||||
/// GestureDetector is problematic.
|
||||
///
|
||||
/// To unconditionally handle taps, without interfering with the selectable text's
|
||||
/// internal gesture detector, provide this callback.
|
||||
///
|
||||
/// To be notified when the text field gains or loses the focus, provide a
|
||||
/// [focusNode] and add a listener to that.
|
||||
///
|
||||
/// To listen to arbitrary pointer events without competing with the
|
||||
/// selectable text's internal gesture detector, use a [Listener].
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
||||
final ScrollPhysics? scrollPhysics;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollBehavior}
|
||||
final ScrollBehavior? scrollBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.Text.semanticsLabel}
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// {@macro dart.ui.textHeightBehavior}
|
||||
final TextHeightBehavior? textHeightBehavior;
|
||||
|
||||
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
||||
final TextWidthBasis? textWidthBasis;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onSelectionChanged}
|
||||
final SelectionChangedCallback? onSelectionChanged;
|
||||
|
||||
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
||||
|
||||
static Widget _defaultContextMenuBuilder(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
return AdaptiveTextSelectionToolbar.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
}
|
||||
|
||||
/// The configuration for the magnifier used when the text is selected.
|
||||
///
|
||||
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
|
||||
/// on Android, and builds nothing on all other platforms. To suppress the
|
||||
/// magnifier, consider passing [TextMagnifierConfiguration.disabled].
|
||||
///
|
||||
/// {@macro flutter.widgets.magnifier.intro}
|
||||
final TextMagnifierConfiguration? magnifierConfiguration;
|
||||
|
||||
@override
|
||||
State<SelectableText> createState() => _SelectableTextState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
DiagnosticsProperty<String>('data', data, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<String>(
|
||||
'semanticsLabel',
|
||||
semanticsLabel,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<FocusNode>(
|
||||
'focusNode',
|
||||
focusNode,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<bool>(
|
||||
'showCursor',
|
||||
showCursor,
|
||||
defaultValue: false,
|
||||
),
|
||||
)
|
||||
..add(IntProperty('minLines', minLines, defaultValue: null))
|
||||
..add(IntProperty('maxLines', maxLines, defaultValue: null))
|
||||
..add(
|
||||
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('cursorHeight', cursorHeight, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Radius>(
|
||||
'cursorRadius',
|
||||
cursorRadius,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Color>(
|
||||
'cursorColor',
|
||||
cursorColor,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Color>(
|
||||
'selectionColor',
|
||||
selectionColor,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'selectionEnabled',
|
||||
value: selectionEnabled,
|
||||
defaultValue: true,
|
||||
ifFalse: 'selection disabled',
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextSelectionControls>(
|
||||
'selectionControls',
|
||||
selectionControls,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ScrollPhysics>(
|
||||
'scrollPhysics',
|
||||
scrollPhysics,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ScrollBehavior>(
|
||||
'scrollBehavior',
|
||||
scrollBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectableTextState extends State<SelectableText>
|
||||
implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
EditableTextState? get _editableText => editableTextKey.currentState;
|
||||
|
||||
late _TextSpanEditingController _controller;
|
||||
|
||||
FocusNode? _focusNode;
|
||||
FocusNode get _effectiveFocusNode =>
|
||||
widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
|
||||
|
||||
bool _showSelectionHandles = false;
|
||||
|
||||
late _SelectableTextSelectionGestureDetectorBuilder
|
||||
_selectionGestureDetectorBuilder;
|
||||
|
||||
// API for TextSelectionGestureDetectorBuilderDelegate.
|
||||
@override
|
||||
late bool forcePressEnabled;
|
||||
|
||||
@override
|
||||
final GlobalKey<EditableTextState> editableTextKey =
|
||||
GlobalKey<EditableTextState>();
|
||||
|
||||
@override
|
||||
bool get selectionEnabled => widget.selectionEnabled;
|
||||
// End of API for TextSelectionGestureDetectorBuilderDelegate.
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectionGestureDetectorBuilder =
|
||||
_SelectableTextSelectionGestureDetectorBuilder(state: this);
|
||||
_controller = _TextSpanEditingController(
|
||||
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
|
||||
);
|
||||
_controller.addListener(_onControllerChanged);
|
||||
_effectiveFocusNode.addListener(_handleFocusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SelectableText oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.data != oldWidget.data ||
|
||||
widget.textSpan != oldWidget.textSpan) {
|
||||
_controller
|
||||
..removeListener(_onControllerChanged)
|
||||
..dispose();
|
||||
_controller = _TextSpanEditingController(
|
||||
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
|
||||
);
|
||||
_controller.addListener(_onControllerChanged);
|
||||
}
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
|
||||
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
|
||||
}
|
||||
if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
|
||||
_showSelectionHandles = false;
|
||||
} else {
|
||||
_showSelectionHandles = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_effectiveFocusNode.removeListener(_handleFocusChanged);
|
||||
_focusNode?.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onControllerChanged() {
|
||||
final bool showSelectionHandles =
|
||||
!_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed;
|
||||
if (showSelectionHandles == _showSelectionHandles) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_showSelectionHandles = showSelectionHandles;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleFocusChanged() {
|
||||
if (!_effectiveFocusNode.hasFocus &&
|
||||
SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
|
||||
// We should only clear the selection when this SelectableText loses
|
||||
// focus while the application is currently running. It is possible
|
||||
// that the application is not currently running, for example on desktop
|
||||
// platforms, clicking on a different window switches the focus to
|
||||
// the new window causing the Flutter application to go inactive. In this
|
||||
// case we want to retain the selection so it remains when we return to
|
||||
// the Flutter application.
|
||||
_controller.value = TextEditingValue(text: _controller.value.text);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSelectionChanged(
|
||||
TextSelection selection,
|
||||
SelectionChangedCause? cause,
|
||||
) {
|
||||
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||
setState(() {
|
||||
_showSelectionHandles = willShowSelectionHandles;
|
||||
});
|
||||
}
|
||||
|
||||
widget.onSelectionChanged?.call(selection, cause);
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
if (cause == SelectionChangedCause.longPress) {
|
||||
_editableText?.bringIntoView(selection.base);
|
||||
}
|
||||
return;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the toolbar when a selection handle is tapped.
|
||||
void _handleSelectionHandleTapped() {
|
||||
if (_controller.selection.isCollapsed) {
|
||||
_editableText!.toggleToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
|
||||
// When the text field is activated by something that doesn't trigger the
|
||||
// selection overlay, we shouldn't show the handles either.
|
||||
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_controller.selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cause == SelectionChangedCause.keyboard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cause == SelectionChangedCause.longPress) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_controller.text.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO(garyq): Assert to block WidgetSpans from being used here are removed,
|
||||
// but we still do not yet have nice handling of things like carets, clipboard,
|
||||
// and other features. We should add proper support. Currently, caret handling
|
||||
// is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
|
||||
// should be landed in SkParagraph after the switch is complete.
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
assert(
|
||||
!(widget.style != null &&
|
||||
!widget.style!.inherit &&
|
||||
(widget.style!.fontSize == null ||
|
||||
widget.style!.textBaseline == null)),
|
||||
'inherit false style must supply fontSize and textBaseline',
|
||||
);
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(
|
||||
context,
|
||||
);
|
||||
final FocusNode focusNode = _effectiveFocusNode;
|
||||
|
||||
TextSelectionControls? textSelectionControls = widget.selectionControls;
|
||||
final bool paintCursorAboveText;
|
||||
final bool cursorOpacityAnimates;
|
||||
Offset? cursorOffset;
|
||||
final Color cursorColor;
|
||||
final Color selectionColor;
|
||||
Radius? cursorRadius = widget.cursorRadius;
|
||||
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.iOS:
|
||||
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||
forcePressEnabled = true;
|
||||
textSelectionControls ??= cupertinoTextSelectionHandleControls;
|
||||
paintCursorAboveText = true;
|
||||
cursorOpacityAnimates = true;
|
||||
cursorColor =
|
||||
widget.cursorColor ??
|
||||
selectionStyle.cursorColor ??
|
||||
cupertinoTheme.primaryColor;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
cupertinoTheme.primaryColor.withValues(alpha: 0.40);
|
||||
cursorRadius ??= const Radius.circular(2.0);
|
||||
cursorOffset = Offset(
|
||||
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
|
||||
0,
|
||||
);
|
||||
|
||||
case TargetPlatform.macOS:
|
||||
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||
forcePressEnabled = false;
|
||||
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
|
||||
paintCursorAboveText = true;
|
||||
cursorOpacityAnimates = true;
|
||||
cursorColor =
|
||||
widget.cursorColor ??
|
||||
selectionStyle.cursorColor ??
|
||||
cupertinoTheme.primaryColor;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
cupertinoTheme.primaryColor.withValues(alpha: 0.40);
|
||||
cursorRadius ??= const Radius.circular(2.0);
|
||||
cursorOffset = Offset(
|
||||
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
|
||||
0,
|
||||
);
|
||||
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
forcePressEnabled = false;
|
||||
textSelectionControls ??= materialTextSelectionHandleControls;
|
||||
paintCursorAboveText = false;
|
||||
cursorOpacityAnimates = false;
|
||||
cursorColor =
|
||||
widget.cursorColor ??
|
||||
selectionStyle.cursorColor ??
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
forcePressEnabled = false;
|
||||
textSelectionControls ??= desktopTextSelectionHandleControls;
|
||||
paintCursorAboveText = false;
|
||||
cursorOpacityAnimates = false;
|
||||
cursorColor =
|
||||
widget.cursorColor ??
|
||||
selectionStyle.cursorColor ??
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
}
|
||||
|
||||
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||
TextStyle? effectiveTextStyle = widget.style;
|
||||
if (effectiveTextStyle == null || effectiveTextStyle.inherit) {
|
||||
effectiveTextStyle = defaultTextStyle.style.merge(
|
||||
widget.style ?? _controller._textSpan.style,
|
||||
);
|
||||
}
|
||||
final TextScaler? effectiveScaler =
|
||||
widget.textScaler ??
|
||||
switch (widget.textScaleFactor) {
|
||||
null => null,
|
||||
final double textScaleFactor => TextScaler.linear(textScaleFactor),
|
||||
};
|
||||
final Widget child = RepaintBoundary(
|
||||
child: EditableText(
|
||||
key: editableTextKey,
|
||||
style: effectiveTextStyle,
|
||||
readOnly: true,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
textWidthBasis:
|
||||
widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||
textHeightBehavior:
|
||||
widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
showCursor: widget.showCursor,
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
strutStyle: widget.strutStyle ?? const StrutStyle(),
|
||||
textAlign:
|
||||
widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||
textDirection: widget.textDirection,
|
||||
textScaler: effectiveScaler,
|
||||
autofocus: widget.autofocus,
|
||||
forceLine: false,
|
||||
minLines: widget.minLines,
|
||||
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
||||
selectionColor: widget.selectionColor ?? selectionColor,
|
||||
selectionControls: widget.selectionEnabled
|
||||
? textSelectionControls
|
||||
: null,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||
rendererIgnoresPointer: true,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
cursorOpacityAnimates: cursorOpacityAnimates,
|
||||
cursorOffset: cursorOffset,
|
||||
paintCursorAboveText: paintCursorAboveText,
|
||||
backgroundCursorColor: CupertinoColors.inactiveGray,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
magnifierConfiguration:
|
||||
widget.magnifierConfiguration ??
|
||||
TextMagnifier.adaptiveMagnifierConfiguration,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
scrollBehavior: widget.scrollBehavior,
|
||||
autofillHints: null,
|
||||
contextMenuBuilder: widget.contextMenuBuilder,
|
||||
),
|
||||
);
|
||||
|
||||
return Semantics(
|
||||
label: widget.semanticsLabel,
|
||||
excludeSemantics: widget.semanticsLabel != null,
|
||||
onLongPress: () {
|
||||
_effectiveFocusNode.requestFocus();
|
||||
},
|
||||
child: _selectionGestureDetectorBuilder.buildGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
148
lib/common/widgets/flutter/selectable_text/selection_area.dart
Normal file
148
lib/common/widgets/flutter/selectable_text/selection_area.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_region.dart';
|
||||
import 'package:flutter/cupertino.dart'
|
||||
hide
|
||||
SelectableRegion,
|
||||
SelectableRegionState,
|
||||
SelectableRegionContextMenuBuilder;
|
||||
import 'package:flutter/material.dart'
|
||||
hide
|
||||
SelectionArea,
|
||||
SelectableRegion,
|
||||
SelectableRegionState,
|
||||
SelectableRegionContextMenuBuilder;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// A widget that introduces an area for user selections with adaptive selection
|
||||
/// controls.
|
||||
///
|
||||
/// This widget creates a [SelectableRegion] with platform-adaptive selection
|
||||
/// controls.
|
||||
///
|
||||
/// Flutter widgets are not selectable by default. To enable selection for
|
||||
/// a specific screen, consider wrapping the body of the [Route] with a
|
||||
/// [SelectionArea].
|
||||
///
|
||||
/// The [SelectionArea] widget must have a [Localizations] ancestor that
|
||||
/// contains a [MaterialLocalizations] delegate; using the [MaterialApp] widget
|
||||
/// ensures that such an ancestor is present.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to make a screen selectable.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/selection_area/selection_area.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SelectableRegion], which provides an overview of the selection system.
|
||||
/// * [SelectableText], which enables selection on a single run of text.
|
||||
/// * [SelectionListener], which enables accessing the [SelectionDetails] of
|
||||
/// the selectable subtree it wraps.
|
||||
class SelectionArea extends StatefulWidget {
|
||||
/// Creates a [SelectionArea].
|
||||
///
|
||||
/// If [selectionControls] is null, a platform specific one is used.
|
||||
const SelectionArea({
|
||||
super.key,
|
||||
this.focusNode,
|
||||
this.selectionControls,
|
||||
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
||||
this.magnifierConfiguration,
|
||||
this.onSelectionChanged,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// The configuration for the magnifier in the selection region.
|
||||
///
|
||||
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
|
||||
/// on Android, and builds nothing on all other platforms. To suppress the
|
||||
/// magnifier, consider passing [TextMagnifierConfiguration.disabled].
|
||||
///
|
||||
/// {@macro flutter.widgets.magnifier.intro}
|
||||
final TextMagnifierConfiguration? magnifierConfiguration;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The delegate to build the selection handles and toolbar.
|
||||
///
|
||||
/// If it is null, the platform specific selection control is used.
|
||||
final TextSelectionControls? selectionControls;
|
||||
|
||||
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
|
||||
///
|
||||
/// If not provided, will build a default menu based on the ambient
|
||||
/// [ThemeData.platform].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to build a custom context menu for any selected
|
||||
/// content in a SelectionArea.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar], which is built by default.
|
||||
final SelectableRegionContextMenuBuilder? contextMenuBuilder;
|
||||
|
||||
/// Called when the selected content changes.
|
||||
final ValueChanged<SelectedContent?>? onSelectionChanged;
|
||||
|
||||
/// The child widget this selection area applies to.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
static Widget _defaultContextMenuBuilder(
|
||||
BuildContext context,
|
||||
SelectableRegionState selectableRegionState,
|
||||
) => AdaptiveTextSelectionToolbar.buttonItems(
|
||||
buttonItems: selectableRegionState.contextMenuButtonItems,
|
||||
anchors: selectableRegionState.contextMenuAnchors,
|
||||
);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => SelectionAreaState();
|
||||
}
|
||||
|
||||
/// State for a [SelectionArea].
|
||||
class SelectionAreaState extends State<SelectionArea> {
|
||||
final GlobalKey<SelectableRegionState> _selectableRegionKey =
|
||||
GlobalKey<SelectableRegionState>();
|
||||
|
||||
/// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps.
|
||||
SelectableRegionState get selectableRegion =>
|
||||
_selectableRegionKey.currentState!;
|
||||
|
||||
@protected
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final TextSelectionControls controls =
|
||||
widget.selectionControls ??
|
||||
switch (Theme.of(context).platform) {
|
||||
TargetPlatform.android ||
|
||||
TargetPlatform.fuchsia => materialTextSelectionHandleControls,
|
||||
TargetPlatform.linux ||
|
||||
TargetPlatform.windows => desktopTextSelectionHandleControls,
|
||||
TargetPlatform.iOS => cupertinoTextSelectionHandleControls,
|
||||
TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls,
|
||||
};
|
||||
return SelectableRegion(
|
||||
key: _selectableRegionKey,
|
||||
selectionControls: controls,
|
||||
focusNode: widget.focusNode,
|
||||
contextMenuBuilder: widget.contextMenuBuilder,
|
||||
magnifierConfiguration:
|
||||
widget.magnifierConfiguration ??
|
||||
TextMagnifier.adaptiveMagnifierConfiguration,
|
||||
onSelectionChanged: widget.onSelectionChanged,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
1125
lib/common/widgets/flutter/selectable_text/tap_and_drag.dart
Normal file
1125
lib/common/widgets/flutter/selectable_text/tap_and_drag.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,13 @@
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/selectable_text/selection_area.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart' hide SelectableText, SelectionArea;
|
||||
|
||||
Widget selectableText(
|
||||
String text, {
|
||||
TextStyle? style,
|
||||
}) {
|
||||
if (Utils.isDesktop) {
|
||||
if (PlatformUtils.isDesktop) {
|
||||
return SelectionArea(
|
||||
child: Text(
|
||||
style: style,
|
||||
@@ -16,6 +18,7 @@ Widget selectableText(
|
||||
return SelectableText(
|
||||
style: style,
|
||||
text,
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +26,7 @@ Widget selectableRichText(
|
||||
TextSpan textSpan, {
|
||||
TextStyle? style,
|
||||
}) {
|
||||
if (Utils.isDesktop) {
|
||||
if (PlatformUtils.isDesktop) {
|
||||
return SelectionArea(
|
||||
child: Text.rich(
|
||||
style: style,
|
||||
@@ -34,5 +37,6 @@ Widget selectableRichText(
|
||||
return SelectableText.rich(
|
||||
style: style,
|
||||
textSpan,
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
421
lib/common/widgets/flutter/selectable_text/text_selection.dart
Normal file
421
lib/common/widgets/flutter/selectable_text/text_selection.dart
Normal file
@@ -0,0 +1,421 @@
|
||||
// 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:PiliPlus/common/widgets/flutter/selectable_text/tap_and_drag.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart'
|
||||
hide
|
||||
BaseTapAndDragGestureRecognizer,
|
||||
TapAndHorizontalDragGestureRecognizer,
|
||||
TapAndPanGestureRecognizer;
|
||||
import 'package:flutter/material.dart' hide TextSelectionGestureDetector;
|
||||
|
||||
class CustomTextSelectionGestureDetectorBuilder
|
||||
extends TextSelectionGestureDetectorBuilder {
|
||||
CustomTextSelectionGestureDetectorBuilder({required super.delegate});
|
||||
|
||||
@override
|
||||
Widget buildGestureDetector({
|
||||
Key? key,
|
||||
HitTestBehavior? behavior,
|
||||
required Widget child,
|
||||
}) {
|
||||
return TextSelectionGestureDetector(
|
||||
key: key,
|
||||
onTapTrackStart: onTapTrackStart,
|
||||
onTapTrackReset: onTapTrackReset,
|
||||
onTapDown: onTapDown,
|
||||
onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
|
||||
onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
onSingleTapUp: onSingleTapUp,
|
||||
onSingleTapCancel: onSingleTapCancel,
|
||||
onUserTap: onUserTap,
|
||||
onSingleLongTapStart: onSingleLongTapStart,
|
||||
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
|
||||
onSingleLongTapEnd: onSingleLongTapEnd,
|
||||
onSingleLongTapCancel: onSingleLongTapCancel,
|
||||
onDoubleTapDown: onDoubleTapDown,
|
||||
onTripleTapDown: onTripleTapDown,
|
||||
onDragSelectionStart: onDragSelectionStart,
|
||||
onDragSelectionUpdate: onDragSelectionUpdate,
|
||||
onDragSelectionEnd: onDragSelectionEnd,
|
||||
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
|
||||
behavior: behavior,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A gesture detector to respond to non-exclusive event chains for a text field.
|
||||
///
|
||||
/// An ordinary [GestureDetector] configured to handle events like tap and
|
||||
/// double tap will only recognize one or the other. This widget detects both:
|
||||
/// the first tap and then any subsequent taps that occurs within a time limit
|
||||
/// after the first.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextField], a Material text field which uses this gesture detector.
|
||||
/// * [CupertinoTextField], a Cupertino text field which uses this gesture
|
||||
/// detector.
|
||||
class TextSelectionGestureDetector extends StatefulWidget {
|
||||
/// Create a [TextSelectionGestureDetector].
|
||||
///
|
||||
/// Multiple callbacks can be called for one sequence of input gesture.
|
||||
const TextSelectionGestureDetector({
|
||||
super.key,
|
||||
this.onTapTrackStart,
|
||||
this.onTapTrackReset,
|
||||
this.onTapDown,
|
||||
this.onForcePressStart,
|
||||
this.onForcePressEnd,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapDown,
|
||||
this.onSingleTapUp,
|
||||
this.onSingleTapCancel,
|
||||
this.onUserTap,
|
||||
this.onSingleLongTapStart,
|
||||
this.onSingleLongTapMoveUpdate,
|
||||
this.onSingleLongTapEnd,
|
||||
this.onSingleLongTapCancel,
|
||||
this.onDoubleTapDown,
|
||||
this.onTripleTapDown,
|
||||
this.onDragSelectionStart,
|
||||
this.onDragSelectionUpdate,
|
||||
this.onDragSelectionEnd,
|
||||
this.onUserTapAlwaysCalled = false,
|
||||
this.behavior,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart}
|
||||
/// Callback used to indicate that a tap tracking has started upon
|
||||
/// a [PointerDownEvent].
|
||||
/// {@endtemplate}
|
||||
final VoidCallback? onTapTrackStart;
|
||||
|
||||
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset}
|
||||
/// Callback used to indicate that a tap tracking has been reset which
|
||||
/// happens on the next [PointerDownEvent] after the timer between two taps
|
||||
/// elapses, the recognizer loses the arena, the gesture is cancelled or
|
||||
/// the recognizer is disposed of.
|
||||
/// {@endtemplate}
|
||||
final VoidCallback? onTapTrackReset;
|
||||
|
||||
/// Called for every tap down including every tap down that's part of a
|
||||
/// double click or a long press, except touches that include enough movement
|
||||
/// to not qualify as taps (e.g. pans and flings).
|
||||
final GestureTapDragDownCallback? onTapDown;
|
||||
|
||||
/// Called when a pointer has tapped down and the force of the pointer has
|
||||
/// just become greater than [ForcePressGestureRecognizer.startPressure].
|
||||
final GestureForcePressStartCallback? onForcePressStart;
|
||||
|
||||
/// Called when a pointer that had previously triggered [onForcePressStart] is
|
||||
/// lifted off the screen.
|
||||
final GestureForcePressEndCallback? onForcePressEnd;
|
||||
|
||||
/// Called for a tap event with the secondary mouse button.
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
/// Called for a tap down event with the secondary mouse button.
|
||||
final GestureTapDownCallback? onSecondaryTapDown;
|
||||
|
||||
/// Called for the first tap in a series of taps, consecutive taps do not call
|
||||
/// this method.
|
||||
///
|
||||
/// For example, if the detector was configured with [onTapDown] and
|
||||
/// [onDoubleTapDown], three quick taps would be recognized as a single tap
|
||||
/// down, followed by a tap up, then a double tap down, followed by a single tap down.
|
||||
final GestureTapDragUpCallback? onSingleTapUp;
|
||||
|
||||
/// Called for each touch that becomes recognized as a gesture that is not a
|
||||
/// short tap, such as a long tap or drag. It is called at the moment when
|
||||
/// another gesture from the touch is recognized.
|
||||
final GestureCancelCallback? onSingleTapCancel;
|
||||
|
||||
/// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
|
||||
/// disabled, which is the default behavior.
|
||||
///
|
||||
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
|
||||
/// including consecutive taps.
|
||||
final GestureTapCallback? onUserTap;
|
||||
|
||||
/// Called for a single long tap that's sustained for longer than
|
||||
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
|
||||
/// double-tap-hold, which calls [onDoubleTapDown] instead.
|
||||
final GestureLongPressStartCallback? onSingleLongTapStart;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is dragged.
|
||||
final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is lifted.
|
||||
final GestureLongPressEndCallback? onSingleLongTapEnd;
|
||||
|
||||
/// Called after [onSingleLongTapStart] when the pointer is canceled.
|
||||
final GestureLongPressCancelCallback? onSingleLongTapCancel;
|
||||
|
||||
/// Called after a momentary hold or a short tap that is close in space and
|
||||
/// time (within [kDoubleTapTimeout]) to a previous short tap.
|
||||
final GestureTapDragDownCallback? onDoubleTapDown;
|
||||
|
||||
/// Called after a momentary hold or a short tap that is close in space and
|
||||
/// time (within [kDoubleTapTimeout]) to a previous double-tap.
|
||||
final GestureTapDragDownCallback? onTripleTapDown;
|
||||
|
||||
/// Called when a mouse starts dragging to select text.
|
||||
final GestureTapDragStartCallback? onDragSelectionStart;
|
||||
|
||||
/// Called repeatedly as a mouse moves while dragging.
|
||||
final GestureTapDragUpdateCallback? onDragSelectionUpdate;
|
||||
|
||||
/// Called when a mouse that was previously dragging is released.
|
||||
final GestureTapDragEndCallback? onDragSelectionEnd;
|
||||
|
||||
/// Whether [onUserTap] will be called for all taps including consecutive taps.
|
||||
///
|
||||
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
|
||||
final bool onUserTapAlwaysCalled;
|
||||
|
||||
/// How this gesture detector should behave during hit testing.
|
||||
///
|
||||
/// This defaults to [HitTestBehavior.deferToChild].
|
||||
final HitTestBehavior? behavior;
|
||||
|
||||
/// Child below this widget.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
|
||||
}
|
||||
|
||||
class _TextSelectionGestureDetectorState
|
||||
extends State<TextSelectionGestureDetector> {
|
||||
// Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
|
||||
// which can grow to be infinitely large, to a value between 1 and 3. The value
|
||||
// that the raw count is converted to is based on the default observed behavior
|
||||
// on the native platforms.
|
||||
//
|
||||
// This method should be used in all instances when details.consecutiveTapCount
|
||||
// would be used.
|
||||
static int _getEffectiveConsecutiveTapCount(int rawCount) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
// From observation, these platform's reset their tap count to 0 when
|
||||
// the number of consecutive taps exceeds 3. For example on Debian Linux
|
||||
// with GTK, when going past a triple click, on the fourth click the
|
||||
// selection is moved to the precise click position, on the fifth click
|
||||
// the word at the position is selected, and on the sixth click the
|
||||
// paragraph at the position is selected.
|
||||
return rawCount <= 3
|
||||
? rawCount
|
||||
: (rawCount % 3 == 0 ? 3 : rawCount % 3);
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
// From observation, these platform's either hold their tap count at 3.
|
||||
// For example on macOS, when going past a triple click, the selection
|
||||
// should be retained at the paragraph that was first selected on triple
|
||||
// click.
|
||||
return math.min(rawCount, 3);
|
||||
case TargetPlatform.windows:
|
||||
// From observation, this platform's consecutive tap actions alternate
|
||||
// between double click and triple click actions. For example, after a
|
||||
// triple click has selected a paragraph, on the next click the word at
|
||||
// the clicked position will be selected, and on the next click the
|
||||
// paragraph at the position is selected.
|
||||
return rawCount < 2 ? rawCount : 2 + rawCount % 2;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTapTrackStart() {
|
||||
widget.onTapTrackStart?.call();
|
||||
}
|
||||
|
||||
void _handleTapTrackReset() {
|
||||
widget.onTapTrackReset?.call();
|
||||
}
|
||||
|
||||
// The down handler is force-run on success of a single tap and optimistically
|
||||
// run before a long press success.
|
||||
void _handleTapDown(TapDragDownDetails details) {
|
||||
widget.onTapDown?.call(details);
|
||||
// This isn't detected as a double tap gesture in the gesture recognizer
|
||||
// because it's 2 single taps, each of which may do different things depending
|
||||
// on whether it's a single tap, the first tap of a double tap, the second
|
||||
// tap held down, a clean double tap etc.
|
||||
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
|
||||
return widget.onDoubleTapDown?.call(details);
|
||||
}
|
||||
|
||||
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
|
||||
return widget.onTripleTapDown?.call(details);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTapUp(TapDragUpDetails details) {
|
||||
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
|
||||
widget.onSingleTapUp?.call(details);
|
||||
widget.onUserTap?.call();
|
||||
} else if (widget.onUserTapAlwaysCalled) {
|
||||
widget.onUserTap?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTapCancel() {
|
||||
widget.onSingleTapCancel?.call();
|
||||
}
|
||||
|
||||
void _handleDragStart(TapDragStartDetails details) {
|
||||
widget.onDragSelectionStart?.call(details);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(TapDragUpdateDetails details) {
|
||||
widget.onDragSelectionUpdate?.call(details);
|
||||
}
|
||||
|
||||
void _handleDragEnd(TapDragEndDetails details) {
|
||||
widget.onDragSelectionEnd?.call(details);
|
||||
}
|
||||
|
||||
void _forcePressStarted(ForcePressDetails details) {
|
||||
widget.onForcePressStart?.call(details);
|
||||
}
|
||||
|
||||
void _forcePressEnded(ForcePressDetails details) {
|
||||
widget.onForcePressEnd?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressStart(LongPressStartDetails details) {
|
||||
widget.onSingleLongTapStart?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
widget.onSingleLongTapMoveUpdate?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressEnd(LongPressEndDetails details) {
|
||||
widget.onSingleLongTapEnd?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressCancel() {
|
||||
widget.onSingleLongTapCancel?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final gestures = <Type, GestureRecognizerFactory>{};
|
||||
|
||||
gestures[TapGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(debugOwner: this),
|
||||
(TapGestureRecognizer instance) {
|
||||
instance
|
||||
..onSecondaryTap = widget.onSecondaryTap
|
||||
..onSecondaryTapDown = widget.onSecondaryTapDown;
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.onSingleLongTapStart != null ||
|
||||
widget.onSingleLongTapMoveUpdate != null ||
|
||||
widget.onSingleLongTapEnd != null ||
|
||||
widget.onSingleLongTapCancel != null) {
|
||||
gestures[LongPressGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(
|
||||
debugOwner: this,
|
||||
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.touch},
|
||||
),
|
||||
(LongPressGestureRecognizer instance) {
|
||||
instance
|
||||
..onLongPressStart = _handleLongPressStart
|
||||
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
|
||||
..onLongPressEnd = _handleLongPressEnd
|
||||
..onLongPressCancel = _handleLongPressCancel;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.onDragSelectionStart != null ||
|
||||
widget.onDragSelectionUpdate != null ||
|
||||
widget.onDragSelectionEnd != null) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
gestures[TapAndHorizontalDragGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<
|
||||
TapAndHorizontalDragGestureRecognizer
|
||||
>(
|
||||
() => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
|
||||
(TapAndHorizontalDragGestureRecognizer instance) {
|
||||
instance
|
||||
// Text selection should start from the position of the first pointer
|
||||
// down event.
|
||||
..dragStartBehavior = DragStartBehavior.down
|
||||
..eagerVictoryOnDrag =
|
||||
defaultTargetPlatform != TargetPlatform.iOS
|
||||
..onTapTrackStart = _handleTapTrackStart
|
||||
..onTapTrackReset = _handleTapTrackReset
|
||||
..onTapDown = _handleTapDown
|
||||
..onDragStart = _handleDragStart
|
||||
..onDragUpdate = _handleDragUpdate
|
||||
..onDragEnd = _handleDragEnd
|
||||
..onTapUp = _handleTapUp
|
||||
..onCancel = _handleTapCancel;
|
||||
},
|
||||
);
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
gestures[TapAndPanGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
|
||||
() => TapAndPanGestureRecognizer(debugOwner: this),
|
||||
(TapAndPanGestureRecognizer instance) {
|
||||
instance
|
||||
// Text selection should start from the position of the first pointer
|
||||
// down event.
|
||||
..dragStartBehavior = DragStartBehavior.down
|
||||
..onTapTrackStart = _handleTapTrackStart
|
||||
..onTapTrackReset = _handleTapTrackReset
|
||||
..onTapDown = _handleTapDown
|
||||
..onDragStart = _handleDragStart
|
||||
..onDragUpdate = _handleDragUpdate
|
||||
..onDragEnd = _handleDragEnd
|
||||
..onTapUp = _handleTapUp
|
||||
..onCancel = _handleTapCancel;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
|
||||
gestures[ForcePressGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
|
||||
() => ForcePressGestureRecognizer(debugOwner: this),
|
||||
(ForcePressGestureRecognizer instance) {
|
||||
instance
|
||||
..onStart = widget.onForcePressStart != null
|
||||
? _forcePressStarted
|
||||
: null
|
||||
..onEnd = widget.onForcePressEnd != null
|
||||
? _forcePressEnded
|
||||
: null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return RawGestureDetector(
|
||||
gestures: gestures,
|
||||
excludeFromSemantics: true,
|
||||
behavior: widget.behavior,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
83
lib/common/widgets/flutter/sliver_layout_builder.dart
Normal file
83
lib/common/widgets/flutter/sliver_layout_builder.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart'
|
||||
hide
|
||||
ConstrainedLayoutBuilder,
|
||||
LayoutBuilder,
|
||||
RenderConstrainedLayoutBuilder;
|
||||
|
||||
/// Builds a sliver widget tree that can depend on its own [SliverConstraints].
|
||||
///
|
||||
/// Similar to the [LayoutBuilder] widget except its builder should return a sliver
|
||||
/// widget, and [SliverLayoutBuilder] is itself a sliver. The framework calls the
|
||||
/// [builder] function at layout time and provides the current [SliverConstraints].
|
||||
/// The [SliverLayoutBuilder]'s final [SliverGeometry] will match the [SliverGeometry]
|
||||
/// of its child.
|
||||
///
|
||||
/// {@macro flutter.widgets.ConstrainedLayoutBuilder}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LayoutBuilder], the non-sliver version of this widget.
|
||||
class SliverLayoutBuilder extends ConstrainedLayoutBuilder<SliverConstraints> {
|
||||
/// Creates a sliver widget that defers its building until layout.
|
||||
const SliverLayoutBuilder({super.key, required super.builder});
|
||||
|
||||
@override
|
||||
RenderConstrainedLayoutBuilder<SliverConstraints, RenderSliver>
|
||||
createRenderObject(
|
||||
BuildContext context,
|
||||
) => _RenderSliverLayoutBuilder();
|
||||
}
|
||||
|
||||
class _RenderSliverLayoutBuilder extends RenderSliver
|
||||
with
|
||||
RenderObjectWithChildMixin<RenderSliver>,
|
||||
RenderObjectWithLayoutCallbackMixin,
|
||||
RenderConstrainedLayoutBuilder<SliverConstraints, RenderSliver> {
|
||||
@override
|
||||
double childMainAxisPosition(RenderObject child) {
|
||||
assert(child == this.child);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
runLayoutCallback();
|
||||
child?.layout(constraints, parentUsesSize: true);
|
||||
geometry = child?.geometry ?? SliverGeometry.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
||||
assert(child == this.child);
|
||||
// child's offset is always (0, 0), transform.translate(0, 0) does not mutate the transform.
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
// This renderObject does not introduce additional offset to child's position.
|
||||
if (child?.geometry?.visible ?? false) {
|
||||
context.paintChild(child!, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(
|
||||
SliverHitTestResult result, {
|
||||
required double mainAxisPosition,
|
||||
required double crossAxisPosition,
|
||||
}) {
|
||||
return child != null &&
|
||||
child!.geometry!.hitTestExtent > 0 &&
|
||||
child!.hitTest(
|
||||
result,
|
||||
mainAxisPosition: mainAxisPosition,
|
||||
crossAxisPosition: crossAxisPosition,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
/// @docImport 'editable.dart';
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
@@ -25,7 +24,7 @@ import 'dart:ui'
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// The start and end positions for a text boundary.
|
||||
@@ -46,6 +45,15 @@ typedef _TextBoundaryAtPositionInText =
|
||||
|
||||
const String _kEllipsis = '\u2026';
|
||||
|
||||
class _UnspecifiedTextScaler extends TextScaler {
|
||||
const _UnspecifiedTextScaler();
|
||||
@override
|
||||
Never get textScaleFactor => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Never scale(double fontSize) => throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// A render object that displays a paragraph of text.
|
||||
class RenderParagraph extends RenderBox
|
||||
with
|
||||
@@ -68,7 +76,7 @@ class RenderParagraph extends RenderBox
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
double textScaleFactor = 1.0,
|
||||
TextScaler textScaler = TextScaler.noScaling,
|
||||
TextScaler textScaler = const _UnspecifiedTextScaler(),
|
||||
int? maxLines,
|
||||
Locale? locale,
|
||||
StrutStyle? strutStyle,
|
||||
@@ -80,26 +88,22 @@ class RenderParagraph extends RenderBox
|
||||
required Color primary,
|
||||
VoidCallback? onShowMore,
|
||||
}) : assert(text.debugAssertIsValid()),
|
||||
assert(maxLines == null || maxLines > 0),
|
||||
assert(
|
||||
maxLines == null ||
|
||||
(maxLines > 0 &&
|
||||
overflow != TextOverflow.ellipsis &&
|
||||
overflow != TextOverflow.fade),
|
||||
),
|
||||
assert(
|
||||
identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0,
|
||||
identical(textScaler, const _UnspecifiedTextScaler()) ||
|
||||
textScaleFactor == 1.0,
|
||||
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
|
||||
),
|
||||
_primary = primary,
|
||||
_onShowMore = onShowMore,
|
||||
_softWrap = softWrap,
|
||||
_overflow = overflow,
|
||||
_selectionColor = selectionColor,
|
||||
_onShowMore = onShowMore,
|
||||
_textPainter = TextPainter(
|
||||
text: text,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
textScaler: textScaler == TextScaler.noScaling
|
||||
textScaler: textScaler == const _UnspecifiedTextScaler()
|
||||
? TextScaler.linear(textScaleFactor)
|
||||
: textScaler,
|
||||
maxLines: maxLines,
|
||||
@@ -146,7 +150,22 @@ class RenderParagraph extends RenderBox
|
||||
|
||||
/// The text to display.
|
||||
InlineSpan get text => _textPainter.text!;
|
||||
set text(InlineSpan value) {
|
||||
set text(({InlineSpan text, Color primary}) params) {
|
||||
final value = params.text;
|
||||
_primary = params.primary;
|
||||
if (_morePainter case final textPainter?) {
|
||||
final textSpan = _moreTextSpan(value.style);
|
||||
switch (textPainter.text!.compareTo(textSpan)) {
|
||||
case RenderComparison.paint:
|
||||
textPainter.text = textSpan;
|
||||
case RenderComparison.layout:
|
||||
textPainter
|
||||
..text = textSpan
|
||||
..layout();
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
switch (_textPainter.text!.compareTo(value)) {
|
||||
case RenderComparison.identical:
|
||||
return;
|
||||
@@ -390,6 +409,9 @@ class RenderParagraph extends RenderBox
|
||||
if (_textPainter.textScaler == value) {
|
||||
return;
|
||||
}
|
||||
_morePainter
|
||||
?..textScaler = value
|
||||
..layout();
|
||||
_textPainter.textScaler = value;
|
||||
_overflowShader = null;
|
||||
markNeedsLayout();
|
||||
@@ -563,7 +585,7 @@ class RenderParagraph extends RenderBox
|
||||
if (position.dx < textPainter.width &&
|
||||
position.dy > height &&
|
||||
position.dy < height + textPainter.height) {
|
||||
result.add(HitTestEntry(_moreTextSpan));
|
||||
result.add(HitTestEntry(textPainter.text as TextSpan));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -689,13 +711,6 @@ class RenderParagraph extends RenderBox
|
||||
|
||||
Color _primary;
|
||||
|
||||
set primary(Color primary) {
|
||||
if (_primary != primary) {
|
||||
_primary = primary;
|
||||
_morePainter?.text = _moreTextSpan;
|
||||
}
|
||||
}
|
||||
|
||||
VoidCallback? _onShowMore;
|
||||
set onShowMore(VoidCallback? onShowMore) {
|
||||
if (_onShowMore != onShowMore) {
|
||||
@@ -706,8 +721,8 @@ class RenderParagraph extends RenderBox
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
TextSpan get _moreTextSpan => TextSpan(
|
||||
style: text.style!.copyWith(color: _primary),
|
||||
TextSpan _moreTextSpan([TextStyle? style]) => TextSpan(
|
||||
style: (style ?? text.style!).copyWith(color: _primary),
|
||||
text: '查看更多',
|
||||
recognizer: _tapGestureRecognizer,
|
||||
);
|
||||
@@ -740,7 +755,7 @@ class RenderParagraph extends RenderBox
|
||||
_tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore;
|
||||
}
|
||||
_morePainter ??= TextPainter(
|
||||
text: _moreTextSpan,
|
||||
text: _moreTextSpan(),
|
||||
textDirection: textDirection,
|
||||
textScaler: textScaler,
|
||||
locale: locale,
|
||||
@@ -842,6 +857,11 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
}
|
||||
|
||||
assert(() {
|
||||
_textPainter.debugPaintTextLayoutBoxes = debugPaintTextLayoutBoxes;
|
||||
return true;
|
||||
}());
|
||||
|
||||
_textPainter.paint(context.canvas, offset);
|
||||
|
||||
paintInlineChildren(context, offset);
|
||||
@@ -1013,8 +1033,9 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
|
||||
if (needsAssembleSemanticsNode) {
|
||||
config.explicitChildNodes = true;
|
||||
config.isSemanticBoundary = true;
|
||||
config
|
||||
..explicitChildNodes = true
|
||||
..isSemanticBoundary = true;
|
||||
} else if (needsChildConfigurationsDelegate) {
|
||||
config.childConfigurationsDelegate =
|
||||
_childSemanticsConfigurationsDelegate;
|
||||
@@ -1043,8 +1064,9 @@ class RenderParagraph extends RenderBox
|
||||
AttributedString(buffer.toString(), attributes: attributes),
|
||||
];
|
||||
}
|
||||
config.attributedLabel = _cachedAttributedLabels![0];
|
||||
config.textDirection = textDirection;
|
||||
config
|
||||
..attributedLabel = _cachedAttributedLabels![0]
|
||||
..textDirection = textDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1126,7 +1148,7 @@ class RenderParagraph extends RenderBox
|
||||
// can be re-used when [assembleSemanticsNode] is called again. This ensures
|
||||
// stable ids for the [SemanticsNode]s of [TextSpan]s across
|
||||
// [assembleSemanticsNode] invocations.
|
||||
LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
|
||||
Map<Key, SemanticsNode>? _cachedChildNodes;
|
||||
|
||||
@override
|
||||
void assembleSemanticsNode(
|
||||
@@ -1143,8 +1165,7 @@ class RenderParagraph extends RenderBox
|
||||
int placeholderIndex = 0;
|
||||
int childIndex = 0;
|
||||
RenderBox? child = firstChild;
|
||||
final LinkedHashMap<Key, SemanticsNode> newChildCache =
|
||||
LinkedHashMap<Key, SemanticsNode>();
|
||||
final Map<Key, SemanticsNode> newChildCache = <Key, SemanticsNode>{};
|
||||
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
|
||||
for (final InlineSpanSemanticsInformation info
|
||||
in _cachedCombinedSemanticsInfos!) {
|
||||
@@ -1214,8 +1235,9 @@ class RenderParagraph extends RenderBox
|
||||
onDoubleTap: final VoidCallback? handler,
|
||||
):
|
||||
if (handler != null) {
|
||||
configuration.onTap = handler;
|
||||
configuration.isLink = true;
|
||||
configuration
|
||||
..onTap = handler
|
||||
..isLink = true;
|
||||
}
|
||||
case LongPressGestureRecognizer(
|
||||
onLongPress: final GestureLongPressCallback? onLongPress,
|
||||
@@ -1285,29 +1307,30 @@ class RenderParagraph extends RenderBox
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(EnumProperty<TextOverflow>('overflow', overflow));
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
properties
|
||||
..add(EnumProperty<TextAlign>('textAlign', textAlign))
|
||||
..add(EnumProperty<TextDirection>('textDirection', textDirection))
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(EnumProperty<TextOverflow>('overflow', overflow))
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1768,8 +1791,7 @@ class _SelectableFragment
|
||||
final TextPosition? existingSelectionEnd = _textSelectionEnd;
|
||||
|
||||
_setSelectionPosition(null, isEnd: isEnd);
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
transform.invert();
|
||||
final Matrix4 transform = paragraph.getTransformTo(null)..invert();
|
||||
final Offset localPosition = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
globalPosition,
|
||||
@@ -1842,8 +1864,7 @@ class _SelectableFragment
|
||||
required bool isEnd,
|
||||
}) {
|
||||
_setSelectionPosition(null, isEnd: isEnd);
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
transform.invert();
|
||||
final Matrix4 transform = paragraph.getTransformTo(null)..invert();
|
||||
final Offset localPosition = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
globalPosition,
|
||||
@@ -2348,8 +2369,8 @@ class _SelectableFragment
|
||||
existingSelectionEnd,
|
||||
);
|
||||
}
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null);
|
||||
originTransform.invert();
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null)
|
||||
..invert();
|
||||
final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
|
||||
originTransform,
|
||||
globalPosition,
|
||||
@@ -2653,8 +2674,8 @@ class _SelectableFragment
|
||||
existingSelectionEnd,
|
||||
);
|
||||
}
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null);
|
||||
originTransform.invert();
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null)
|
||||
..invert();
|
||||
final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
|
||||
originTransform,
|
||||
globalPosition,
|
||||
@@ -3116,8 +3137,7 @@ class _SelectableFragment
|
||||
RenderObject? current = paragraph;
|
||||
while (current != null) {
|
||||
if (current is RenderParagraph) {
|
||||
final Matrix4 currentTransform = current.getTransformTo(null);
|
||||
currentTransform.invert();
|
||||
final Matrix4 currentTransform = current.getTransformTo(null)..invert();
|
||||
final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint(
|
||||
currentTransform,
|
||||
globalPosition,
|
||||
@@ -3809,13 +3829,14 @@ class _SelectableFragment
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
DiagnosticsProperty<String>(
|
||||
'textInsideRange',
|
||||
range.textInside(fullText),
|
||||
),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<TextRange>('range', range));
|
||||
properties.add(DiagnosticsProperty<String>('fullText', fullText));
|
||||
properties
|
||||
..add(
|
||||
DiagnosticsProperty<String>(
|
||||
'textInsideRange',
|
||||
range.textInside(fullText),
|
||||
),
|
||||
)
|
||||
..add(DiagnosticsProperty<TextRange>('range', range))
|
||||
..add(DiagnosticsProperty<String>('fullText', fullText));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import 'dart:ui' as ui;
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart' hide RichText;
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
|
||||
/// A paragraph of rich text.
|
||||
@@ -114,8 +118,8 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
this.textHeightBehavior,
|
||||
this.selectionRegistrar,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
this.onShowMore,
|
||||
}) : assert(maxLines == null || maxLines > 0),
|
||||
assert(selectionRegistrar == null || selectionColor != null),
|
||||
assert(
|
||||
@@ -260,7 +264,7 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
|
||||
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||
renderObject
|
||||
..text = text
|
||||
..text = (text: text, primary: primary)
|
||||
..textAlign = textAlign
|
||||
..textDirection = textDirection ?? Directionality.of(context)
|
||||
..softWrap = softWrap
|
||||
@@ -273,75 +277,75 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
..locale = locale ?? Localizations.maybeLocaleOf(context)
|
||||
..registrar = selectionRegistrar
|
||||
..selectionColor = selectionColor
|
||||
..primary = primary
|
||||
..onShowMore = onShowMore;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
properties.add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
);
|
||||
properties.add(StringProperty('text', text.toPlainText()));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties
|
||||
..add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'))
|
||||
..add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
)
|
||||
..add(StringProperty('text', text.toPlainText()))
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ library;
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/text/rich_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide RichText;
|
||||
import 'package:flutter/material.dart' hide Text, RichText;
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
|
||||
/// A run of text with a single style.
|
||||
@@ -47,7 +47,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
/// Container(
|
||||
/// width: 100,
|
||||
/// decoration: BoxDecoration(border: Border.all()),
|
||||
/// child: Text(overflow: TextOverflow.ellipsis, 'Hello $_name, how are you?'))
|
||||
/// child: const Text(
|
||||
/// 'Hello, how are you?',
|
||||
/// overflow: TextOverflow.ellipsis,
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
@@ -60,10 +64,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// Text(
|
||||
/// const Text(
|
||||
/// 'Hello, how are you?',
|
||||
/// overflow: TextOverflow.fade,
|
||||
/// maxLines: 1,
|
||||
/// 'Hello $_name, how are you?')
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Here soft wrapping is enabled and the [Text] widget tries to wrap the words
|
||||
@@ -74,10 +79,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// Text(
|
||||
/// const Text(
|
||||
/// 'Hello, how are you?',
|
||||
/// overflow: TextOverflow.fade,
|
||||
/// softWrap: false,
|
||||
/// 'Hello $_name, how are you?')
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Here soft wrapping is disabled with `softWrap: false` and the [Text] widget
|
||||
@@ -174,8 +180,8 @@ class Text extends StatelessWidget {
|
||||
this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
this.onShowMore,
|
||||
}) : textSpan = null,
|
||||
assert(
|
||||
textScaler == null || textScaleFactor == null,
|
||||
@@ -213,8 +219,8 @@ class Text extends StatelessWidget {
|
||||
this.textWidthBasis,
|
||||
this.textHeightBehavior,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
this.onShowMore,
|
||||
}) : data = null,
|
||||
assert(
|
||||
textScaler == null || textScaleFactor == null,
|
||||
@@ -236,9 +242,19 @@ class Text extends StatelessWidget {
|
||||
/// If the style's "inherit" property is true, the style will be merged with
|
||||
/// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
|
||||
/// replace the closest enclosing [DefaultTextStyle].
|
||||
///
|
||||
/// The user or platform may override this [style]'s [TextStyle.fontWeight],
|
||||
/// [TextStyle.height], [TextStyle.letterSpacing], and [TextStyle.wordSpacing]
|
||||
/// via a [MediaQuery] ancestor's [MediaQueryData.boldText],
|
||||
/// [MediaQueryData.lineHeightScaleFactorOverride],
|
||||
/// [MediaQueryData.letterSpacingOverride], and [MediaQueryData.wordSpacingOverride]
|
||||
/// regardless of its [TextStyle.inherit] value.
|
||||
final TextStyle? style;
|
||||
|
||||
/// {@macro flutter.painting.textPainter.strutStyle}
|
||||
///
|
||||
/// The user or platform may override this [strutStyle]'s [StrutStyle.height]
|
||||
/// via a [MediaQuery] ancestor's [MediaQueryData.lineHeightScaleFactorOverride].
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
@@ -369,6 +385,30 @@ class Text extends StatelessWidget {
|
||||
const TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
}
|
||||
// TODO(Renzo-Olivares): Investigate ways the framework can automatically
|
||||
// apply MediaQueryData.paragraphSpacingOverride to its own text components.
|
||||
// See: https://github.com/flutter/flutter/issues/177953 and https://github.com/flutter/flutter/issues/177408.
|
||||
final double? lineHeightScaleFactor =
|
||||
MediaQuery.maybeLineHeightScaleFactorOverrideOf(context);
|
||||
final double? letterSpacing = MediaQuery.maybeLetterSpacingOverrideOf(
|
||||
context,
|
||||
);
|
||||
final double? wordSpacing = MediaQuery.maybeWordSpacingOverrideOf(context);
|
||||
final TextSpan effectiveTextSpan =
|
||||
_OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides(
|
||||
lineHeightScaleFactor: lineHeightScaleFactor,
|
||||
letterSpacing: letterSpacing,
|
||||
wordSpacing: wordSpacing,
|
||||
textSpan: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
locale: locale,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
);
|
||||
final StrutStyle? effectiveStrutStyle = strutStyle?.merge(
|
||||
StrutStyle(height: lineHeightScaleFactor),
|
||||
);
|
||||
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
|
||||
final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) {
|
||||
(final TextScaler textScaler, _) => textScaler,
|
||||
@@ -397,7 +437,7 @@ class Text extends StatelessWidget {
|
||||
defaultTextStyle.overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||
strutStyle: strutStyle,
|
||||
strutStyle: effectiveStrutStyle,
|
||||
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||
textHeightBehavior:
|
||||
textHeightBehavior ??
|
||||
@@ -407,11 +447,7 @@ class Text extends StatelessWidget {
|
||||
selectionColor ??
|
||||
DefaultSelectionStyle.of(context).selectionColor ??
|
||||
DefaultSelectionStyle.defaultColor,
|
||||
text: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
text: effectiveTextSpan,
|
||||
primary: primary,
|
||||
),
|
||||
);
|
||||
@@ -429,7 +465,7 @@ class Text extends StatelessWidget {
|
||||
defaultTextStyle.overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines ?? defaultTextStyle.maxLines,
|
||||
strutStyle: strutStyle,
|
||||
strutStyle: effectiveStrutStyle,
|
||||
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||
textHeightBehavior:
|
||||
textHeightBehavior ??
|
||||
@@ -439,13 +475,9 @@ class Text extends StatelessWidget {
|
||||
selectionColor ??
|
||||
DefaultSelectionStyle.of(context).selectionColor ??
|
||||
DefaultSelectionStyle.defaultColor,
|
||||
text: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
onShowMore: onShowMore,
|
||||
text: effectiveTextSpan,
|
||||
primary: primary,
|
||||
onShowMore: onShowMore,
|
||||
);
|
||||
}
|
||||
if (semanticsLabel != null || semanticsIdentifier != null) {
|
||||
@@ -475,49 +507,50 @@ class Text extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
style?.debugFillProperties(properties);
|
||||
properties.add(
|
||||
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
|
||||
properties.add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<ui.TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties
|
||||
..add(
|
||||
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, defaultValue: null))
|
||||
..add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ui.TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
if (semanticsLabel != null) {
|
||||
properties.add(StringProperty('semanticsLabel', semanticsLabel));
|
||||
}
|
||||
@@ -685,7 +718,7 @@ class _SelectableTextContainerDelegate
|
||||
|
||||
SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) {
|
||||
if (event.absorb) {
|
||||
for (int index = 0; index < selectables.length; index += 1) {
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
dispatchSelectionEventToChild(selectables[index], event);
|
||||
}
|
||||
currentSelectionStartIndex = 0;
|
||||
@@ -695,7 +728,7 @@ class _SelectableTextContainerDelegate
|
||||
|
||||
// First pass, if the position is on a placeholder then dispatch the selection
|
||||
// event to the [Selectable] at the location and terminate.
|
||||
for (int index = 0; index < selectables.length; index += 1) {
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
final bool selectableIsPlaceholder = !paragraph
|
||||
.selectableBelongsToParagraph(selectables[index]);
|
||||
if (selectableIsPlaceholder &&
|
||||
@@ -714,9 +747,9 @@ class _SelectableTextContainerDelegate
|
||||
}
|
||||
|
||||
SelectionResult? lastSelectionResult;
|
||||
bool foundStart = false;
|
||||
var foundStart = false;
|
||||
int? lastNextIndex;
|
||||
for (int index = 0; index < selectables.length; index += 1) {
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
if (!paragraph.selectableBelongsToParagraph(selectables[index])) {
|
||||
if (foundStart) {
|
||||
final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
|
||||
@@ -761,7 +794,7 @@ class _SelectableTextContainerDelegate
|
||||
.overlaps(
|
||||
selectables[index].value.selectionRects[0],
|
||||
);
|
||||
int startIndex = 0;
|
||||
var startIndex = 0;
|
||||
if (lastNextIndex != null && selectionAtStartOfSelectable) {
|
||||
startIndex = lastNextIndex + 1;
|
||||
} else {
|
||||
@@ -769,7 +802,7 @@ class _SelectableTextContainerDelegate
|
||||
? 0
|
||||
: index;
|
||||
}
|
||||
for (int i = startIndex; i < index; i += 1) {
|
||||
for (var i = startIndex; i < index; i += 1) {
|
||||
final SelectionEvent synthesizedEvent =
|
||||
SelectParagraphSelectionEvent(
|
||||
globalPosition: event.globalPosition,
|
||||
@@ -788,7 +821,7 @@ class _SelectableTextContainerDelegate
|
||||
if (selectables[index].value != existingGeometry) {
|
||||
if (!foundStart && lastNextIndex == null) {
|
||||
currentSelectionStartIndex = 0;
|
||||
for (int i = 0; i < index; i += 1) {
|
||||
for (var i = 0; i < index; i += 1) {
|
||||
final SelectionEvent synthesizedEvent =
|
||||
SelectParagraphSelectionEvent(
|
||||
globalPosition: event.globalPosition,
|
||||
@@ -829,7 +862,7 @@ class _SelectableTextContainerDelegate
|
||||
);
|
||||
SelectionResult? finalResult;
|
||||
// Begin the search for the selection edge at the opposite edge if it exists.
|
||||
final bool hasOppositeEdge = isEnd
|
||||
final hasOppositeEdge = isEnd
|
||||
? currentSelectionStartIndex != -1
|
||||
: currentSelectionEndIndex != -1;
|
||||
int newIndex = switch ((isEnd, hasOppositeEdge)) {
|
||||
@@ -924,10 +957,10 @@ class _SelectableTextContainerDelegate
|
||||
//
|
||||
// This can happen when there is a scrollable child and the edge being adjusted
|
||||
// has been scrolled out of view.
|
||||
final bool isCurrentEdgeWithinViewport = isEnd
|
||||
final isCurrentEdgeWithinViewport = isEnd
|
||||
? value.endSelectionPoint != null
|
||||
: value.startSelectionPoint != null;
|
||||
final bool isOppositeEdgeWithinViewport = isEnd
|
||||
final isOppositeEdgeWithinViewport = isEnd
|
||||
? value.startSelectionPoint != null
|
||||
: value.endSelectionPoint != null;
|
||||
int newIndex = switch ((
|
||||
@@ -1099,13 +1132,13 @@ class _SelectableTextContainerDelegate
|
||||
if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
int startOffset = 0;
|
||||
int endOffset = 0;
|
||||
bool foundStart = false;
|
||||
var startOffset = 0;
|
||||
var endOffset = 0;
|
||||
var foundStart = false;
|
||||
bool forwardSelection =
|
||||
currentSelectionEndIndex >= currentSelectionStartIndex;
|
||||
if (currentSelectionEndIndex == currentSelectionStartIndex) {
|
||||
// Determining selection direction is innacurate if currentSelectionStartIndex == currentSelectionEndIndex.
|
||||
// Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex.
|
||||
// Use the range from the selectable within the selection as the source of truth for selection direction.
|
||||
final SelectedContentRange rangeAtSelectableInSelection =
|
||||
selectables[currentSelectionStartIndex].getSelection()!;
|
||||
@@ -1113,7 +1146,7 @@ class _SelectableTextContainerDelegate
|
||||
rangeAtSelectableInSelection.endOffset >=
|
||||
rangeAtSelectableInSelection.startOffset;
|
||||
}
|
||||
for (int index = 0; index < selections.length; index++) {
|
||||
for (var index = 0; index < selections.length; index++) {
|
||||
final _SelectionInfo selection = selections[index];
|
||||
if (selection.range == null) {
|
||||
if (foundStart) {
|
||||
@@ -1179,7 +1212,7 @@ class _SelectableTextContainerDelegate
|
||||
/// this method will return `null`.
|
||||
@override
|
||||
SelectedContentRange? getSelection() {
|
||||
final List<_SelectionInfo> selections = <_SelectionInfo>[
|
||||
final selections = <_SelectionInfo>[
|
||||
for (final Selectable selectable in selectables)
|
||||
(
|
||||
contentLength: selectable.contentLength,
|
||||
@@ -1224,7 +1257,7 @@ class _SelectableTextContainerDelegate
|
||||
currentSelectionStartIndex,
|
||||
currentSelectionEndIndex,
|
||||
);
|
||||
for (int index = 0; index < selectables.length; index += 1) {
|
||||
for (var index = 0; index < selectables.length; index += 1) {
|
||||
if (index >= skipStart && index <= skipEnd) {
|
||||
continue;
|
||||
}
|
||||
@@ -1258,3 +1291,55 @@ class _SelectableTextContainerDelegate
|
||||
/// The length of the content that can be selected, and the range that is
|
||||
/// selected.
|
||||
typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});
|
||||
|
||||
/// A utility class for overriding the text styles of a [TextSpan] tree.
|
||||
// When changes are made to this class, the equivalent API in editable_text.dart
|
||||
// must also be updated.
|
||||
// TODO(Renzo-Olivares): Remove after investigating a solution for overriding all
|
||||
// styles for children in an [InlineSpan] tree, see: https://github.com/flutter/flutter/issues/177952.
|
||||
class _OverridingTextStyleTextSpanUtils {
|
||||
static TextSpan applyTextSpacingOverrides({
|
||||
double? lineHeightScaleFactor,
|
||||
double? letterSpacing,
|
||||
double? wordSpacing,
|
||||
required TextSpan textSpan,
|
||||
}) {
|
||||
if (lineHeightScaleFactor == null &&
|
||||
letterSpacing == null &&
|
||||
wordSpacing == null) {
|
||||
return textSpan;
|
||||
}
|
||||
return _applyTextStyleOverrides(
|
||||
TextStyle(
|
||||
height: lineHeightScaleFactor,
|
||||
letterSpacing: letterSpacing,
|
||||
wordSpacing: wordSpacing,
|
||||
),
|
||||
textSpan,
|
||||
);
|
||||
}
|
||||
|
||||
static TextSpan _applyTextStyleOverrides(
|
||||
TextStyle overrideTextStyle,
|
||||
TextSpan textSpan,
|
||||
) {
|
||||
return TextSpan(
|
||||
text: textSpan.text,
|
||||
children: textSpan.children?.map((InlineSpan child) {
|
||||
if (child is TextSpan && child.runtimeType == TextSpan) {
|
||||
return _applyTextStyleOverrides(overrideTextStyle, child);
|
||||
}
|
||||
return child;
|
||||
}).toList(),
|
||||
style: textSpan.style?.merge(overrideTextStyle) ?? overrideTextStyle,
|
||||
recognizer: textSpan.recognizer,
|
||||
mouseCursor: textSpan.mouseCursor,
|
||||
onEnter: textSpan.onEnter,
|
||||
onExit: textSpan.onExit,
|
||||
semanticsLabel: textSpan.semanticsLabel,
|
||||
semanticsIdentifier: textSpan.semanticsIdentifier,
|
||||
locale: textSpan.locale,
|
||||
spellOut: textSpan.spellOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
/// @docImport 'text_field.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -273,8 +273,8 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
});
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.android:
|
||||
final List<Widget> buttons = <Widget>[];
|
||||
for (int i = 0; i < buttonItems.length; i++) {
|
||||
final buttons = <Widget>[];
|
||||
for (var i = 0; i < buttonItems.length; i++) {
|
||||
final ContextMenuButtonItem buttonItem = buttonItems[i];
|
||||
buttons.add(
|
||||
TextSelectionToolbarTextButton(
|
||||
@@ -311,8 +311,7 @@ class AdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children != null && children!.isEmpty) ||
|
||||
(buttonItems != null && buttonItems!.isEmpty)) {
|
||||
if ((children ?? buttonItems)?.isEmpty ?? true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ mixin RichTextTypeMixin {
|
||||
|
||||
extension TextEditingDeltaExt on TextEditingDelta {
|
||||
({RichTextType type, String? rawText, Emote? emote, String? id}) get config {
|
||||
if (this case RichTextTypeMixin e) {
|
||||
if (this case final RichTextTypeMixin e) {
|
||||
return (type: e.type, rawText: e.rawText, emote: e.emote, id: e.id);
|
||||
}
|
||||
return (
|
||||
@@ -62,7 +62,7 @@ extension TextEditingDeltaExt on TextEditingDelta {
|
||||
}
|
||||
|
||||
bool get isText {
|
||||
if (this case RichTextTypeMixin e) {
|
||||
if (this case final RichTextTypeMixin e) {
|
||||
return e.type == RichTextType.text;
|
||||
}
|
||||
return !composing.isValid;
|
||||
@@ -85,11 +85,9 @@ class RichTextEditingDeltaInsertion extends TextEditingDeltaInsertion
|
||||
this.emote,
|
||||
this.id,
|
||||
this.rawText,
|
||||
}) {
|
||||
this.type =
|
||||
type ??
|
||||
(composing.isValid ? RichTextType.composing : RichTextType.text);
|
||||
}
|
||||
}) : type =
|
||||
type ??
|
||||
(composing.isValid ? RichTextType.composing : RichTextType.text);
|
||||
|
||||
@override
|
||||
late final RichTextType type;
|
||||
@@ -116,11 +114,9 @@ class RichTextEditingDeltaReplacement extends TextEditingDeltaReplacement
|
||||
this.emote,
|
||||
this.id,
|
||||
this.rawText,
|
||||
}) {
|
||||
this.type =
|
||||
type ??
|
||||
(composing.isValid ? RichTextType.composing : RichTextType.text);
|
||||
}
|
||||
}) : type =
|
||||
type ??
|
||||
(composing.isValid ? RichTextType.composing : RichTextType.text);
|
||||
|
||||
@override
|
||||
late final RichTextType type;
|
||||
@@ -158,9 +154,7 @@ class RichTextItem {
|
||||
required this.range,
|
||||
this.emote,
|
||||
this.id,
|
||||
}) {
|
||||
_rawText = rawText;
|
||||
}
|
||||
}) : _rawText = rawText;
|
||||
|
||||
RichTextItem.fromStart(
|
||||
this.text, {
|
||||
@@ -168,10 +162,8 @@ class RichTextItem {
|
||||
this.type = RichTextType.text,
|
||||
this.emote,
|
||||
this.id,
|
||||
}) {
|
||||
range = TextRange(start: 0, end: text.length);
|
||||
_rawText = rawText;
|
||||
}
|
||||
}) : range = TextRange(start: 0, end: text.length),
|
||||
_rawText = rawText;
|
||||
|
||||
List<RichTextItem>? onInsert(
|
||||
TextEditingDeltaInsertion delta,
|
||||
@@ -588,7 +580,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
return '';
|
||||
}
|
||||
final buffer = StringBuffer();
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
buffer.write(e.text);
|
||||
}
|
||||
return buffer.toString();
|
||||
@@ -599,7 +591,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
return '';
|
||||
}
|
||||
final buffer = StringBuffer();
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
if (e.type == RichTextType.at) {
|
||||
buffer.write(e.text);
|
||||
} else {
|
||||
@@ -704,11 +696,11 @@ class RichTextEditingController extends TextEditingController {
|
||||
}
|
||||
}
|
||||
|
||||
if (addIndex != null && toAdd?.isNotEmpty == true) {
|
||||
items.insertAll(addIndex, toAdd!);
|
||||
if (addIndex != null && toAdd != null && toAdd.isNotEmpty) {
|
||||
items.insertAll(addIndex, toAdd);
|
||||
}
|
||||
if (toDel?.isNotEmpty == true) {
|
||||
for (var item in toDel!) {
|
||||
if (toDel != null && toDel.isNotEmpty) {
|
||||
for (final item in toDel) {
|
||||
items.remove(item);
|
||||
}
|
||||
}
|
||||
@@ -736,7 +728,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
|
||||
// bool isValid = true;
|
||||
// int cursor = 0;
|
||||
// for (var e in items) {
|
||||
// for (final e in items) {
|
||||
// final range = e.range;
|
||||
// if (range.start == cursor) {
|
||||
// cursor = range.end;
|
||||
@@ -787,7 +779,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
width: 22, // emote.width,
|
||||
height: 22, // emote.height,
|
||||
type: ImageType.emote,
|
||||
boxFit: BoxFit.contain,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -846,7 +838,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
|
||||
TextPosition dragOffset(TextPosition position) {
|
||||
final offset = position.offset;
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (offset >= range.end) {
|
||||
continue;
|
||||
@@ -866,7 +858,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
}
|
||||
|
||||
int tapOffsetSimple(int offset) {
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (offset >= range.end) {
|
||||
continue;
|
||||
@@ -891,7 +883,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
required Offset localPos,
|
||||
required Offset lastTapDownPosition,
|
||||
}) {
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (offset >= range.end) {
|
||||
continue;
|
||||
@@ -902,10 +894,10 @@ class RichTextEditingController extends TextEditingController {
|
||||
// emoji tap
|
||||
if (offset == range.start) {
|
||||
if (e.emote != null) {
|
||||
final cloestOffset = textPainter.getClosestGlyphForOffset(localPos);
|
||||
if (cloestOffset != null) {
|
||||
final offsetRect = cloestOffset.graphemeClusterLayoutBounds;
|
||||
final offsetRange = cloestOffset.graphemeClusterCodeUnitRange;
|
||||
final closestOffset = textPainter.getClosestGlyphForOffset(localPos);
|
||||
if (closestOffset != null) {
|
||||
final offsetRect = closestOffset.graphemeClusterLayoutBounds;
|
||||
final offsetRange = closestOffset.graphemeClusterCodeUnitRange;
|
||||
if (lastTapDownPosition.dx > offsetRect.right) {
|
||||
return offsetRange.end;
|
||||
} else {
|
||||
@@ -930,7 +922,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
int startOffset,
|
||||
int endOffset,
|
||||
) {
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (startOffset >= range.end) {
|
||||
continue;
|
||||
@@ -963,7 +955,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
|
||||
TextSelection keyboardOffset(TextSelection newSelection) {
|
||||
final offset = newSelection.baseOffset;
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (offset >= range.end) {
|
||||
continue;
|
||||
@@ -994,7 +986,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
final startOffset = newSelection.start;
|
||||
final endOffset = newSelection.end;
|
||||
final isNormalized = newSelection.baseOffset < newSelection.extentOffset;
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (startOffset >= range.end) {
|
||||
continue;
|
||||
@@ -1046,7 +1038,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
String text = '';
|
||||
final start = selection.start;
|
||||
final end = selection.end;
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (start >= range.end) {
|
||||
continue;
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -209,7 +209,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children?.isEmpty ?? false) || (buttonItems?.isEmpty ?? false)) {
|
||||
if ((children ?? buttonItems)?.isEmpty ?? true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
@@ -90,7 +90,7 @@ class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
|
||||
final buttonItems = <ContextMenuButtonItem>[];
|
||||
|
||||
// Build suggestion buttons.
|
||||
for (final String suggestion in spanAtCursorIndex.suggestions.take(
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
/// @docImport 'package:flutter/cupertino.dart';
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
@@ -16,10 +15,10 @@ import 'dart:ui'
|
||||
SemanticsInputType,
|
||||
TextBox;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -146,14 +145,13 @@ class VerticalCaretMovementRun implements Iterator<TextPosition> {
|
||||
}
|
||||
assert(lineNumber != _currentLine);
|
||||
|
||||
final Offset newOffset = Offset(
|
||||
final newOffset = Offset(
|
||||
_currentOffset.dx,
|
||||
_lineMetrics[lineNumber].baseline,
|
||||
);
|
||||
final TextPosition closestPosition = _editable._textPainter
|
||||
.getPositionForOffset(newOffset);
|
||||
final MapEntry<Offset, TextPosition> position =
|
||||
MapEntry<Offset, TextPosition>(newOffset, closestPosition);
|
||||
final position = MapEntry<Offset, TextPosition>(newOffset, closestPosition);
|
||||
_positionCache[lineNumber] = position;
|
||||
return position;
|
||||
}
|
||||
@@ -294,8 +292,8 @@ class RenderEditable extends RenderBox
|
||||
bool paintCursorAboveText = false,
|
||||
Offset cursorOffset = Offset.zero,
|
||||
double devicePixelRatio = 1.0,
|
||||
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.max,
|
||||
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.max,
|
||||
bool? enableInteractiveSelection,
|
||||
this.floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5),
|
||||
TextRange? promptRectRange,
|
||||
@@ -417,8 +415,9 @@ class RenderEditable extends RenderBox
|
||||
);
|
||||
|
||||
if (_foregroundRenderObject == null) {
|
||||
final _RenderEditableCustomPaint foregroundRenderObject =
|
||||
_RenderEditableCustomPaint(painter: effectivePainter);
|
||||
final foregroundRenderObject = _RenderEditableCustomPaint(
|
||||
painter: effectivePainter,
|
||||
);
|
||||
adoptChild(foregroundRenderObject);
|
||||
_foregroundRenderObject = foregroundRenderObject;
|
||||
} else {
|
||||
@@ -450,8 +449,9 @@ class RenderEditable extends RenderBox
|
||||
);
|
||||
|
||||
if (_backgroundRenderObject == null) {
|
||||
final _RenderEditableCustomPaint backgroundRenderObject =
|
||||
_RenderEditableCustomPaint(painter: effectivePainter);
|
||||
final backgroundRenderObject = _RenderEditableCustomPaint(
|
||||
painter: effectivePainter,
|
||||
);
|
||||
adoptChild(backgroundRenderObject);
|
||||
_backgroundRenderObject = backgroundRenderObject;
|
||||
} else {
|
||||
@@ -712,7 +712,7 @@ class RenderEditable extends RenderBox
|
||||
// happens in paragraph.cc's layout and TextPainter's
|
||||
// _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
|
||||
// this can be changed to be a strict check instead of an approximation.
|
||||
const double visibleRegionSlop = 0.5;
|
||||
const visibleRegionSlop = 0.5;
|
||||
_selectionStartInViewport.value = visibleRegion
|
||||
.inflate(visibleRegionSlop)
|
||||
.contains(startOffset + effectiveOffset);
|
||||
@@ -1301,7 +1301,7 @@ class RenderEditable extends RenderBox
|
||||
// can be re-used when [assembleSemanticsNode] is called again. This ensures
|
||||
// stable ids for the [SemanticsNode]s of [TextSpan]s across
|
||||
// [assembleSemanticsNode] invocations.
|
||||
LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
|
||||
Map<Key, SemanticsNode>? _cachedChildNodes;
|
||||
|
||||
/// Returns a list of rects that bound the given selection, and the text
|
||||
/// direction. The text direction is used by the engine to calculate
|
||||
@@ -1311,7 +1311,11 @@ class RenderEditable extends RenderBox
|
||||
List<TextBox> getBoxesForSelection(TextSelection selection) {
|
||||
_computeTextMetricsIfNeeded();
|
||||
return _textPainter
|
||||
.getBoxesForSelection(selection)
|
||||
.getBoxesForSelection(
|
||||
selection,
|
||||
boxHeightStyle: selectionHeightStyle,
|
||||
boxWidthStyle: selectionWidthStyle,
|
||||
)
|
||||
.map(
|
||||
(TextBox textBox) => TextBox.fromLTRBD(
|
||||
textBox.left + _paintOffset.dx,
|
||||
@@ -1350,9 +1354,9 @@ class RenderEditable extends RenderBox
|
||||
obscuringCharacter * plainText.length,
|
||||
);
|
||||
} else {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
int offset = 0;
|
||||
final List<StringAttribute> attributes = <StringAttribute>[];
|
||||
final buffer = StringBuffer();
|
||||
var offset = 0;
|
||||
final attributes = <StringAttribute>[];
|
||||
for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
|
||||
final String label = info.semanticsLabel ?? info.text;
|
||||
for (final StringAttribute infoAttribute in info.stringAttributes) {
|
||||
@@ -1381,6 +1385,7 @@ class RenderEditable extends RenderBox
|
||||
..isMultiline = _isMultiline
|
||||
..textDirection = textDirection
|
||||
..isFocused = hasFocus
|
||||
..isFocusable = true
|
||||
..isTextField = true
|
||||
..isReadOnly = readOnly
|
||||
// This is the default for customer that uses RenderEditable directly.
|
||||
@@ -1429,20 +1434,19 @@ class RenderEditable extends RenderBox
|
||||
Iterable<SemanticsNode> children,
|
||||
) {
|
||||
assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
|
||||
final List<SemanticsNode> newChildren = <SemanticsNode>[];
|
||||
final newChildren = <SemanticsNode>[];
|
||||
TextDirection currentDirection = textDirection;
|
||||
Rect currentRect;
|
||||
double ordinal = 0.0;
|
||||
int start = 0;
|
||||
int placeholderIndex = 0;
|
||||
int childIndex = 0;
|
||||
var ordinal = 0.0;
|
||||
var start = 0;
|
||||
var placeholderIndex = 0;
|
||||
var childIndex = 0;
|
||||
RenderBox? child = firstChild;
|
||||
final LinkedHashMap<Key, SemanticsNode> newChildCache =
|
||||
LinkedHashMap<Key, SemanticsNode>();
|
||||
final newChildCache = <Key, SemanticsNode>{};
|
||||
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
|
||||
for (final InlineSpanSemanticsInformation info
|
||||
in _cachedCombinedSemanticsInfos!) {
|
||||
final TextSelection selection = TextSelection(
|
||||
final selection = TextSelection(
|
||||
baseOffset: start,
|
||||
extentOffset: start + info.text.length,
|
||||
);
|
||||
@@ -1456,8 +1460,7 @@ class RenderEditable extends RenderBox
|
||||
.elementAt(childIndex)
|
||||
.isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
|
||||
final SemanticsNode childNode = children.elementAt(childIndex);
|
||||
final TextParentData parentData =
|
||||
child!.parentData! as TextParentData;
|
||||
final parentData = child!.parentData! as TextParentData;
|
||||
assert(parentData.offset != null);
|
||||
newChildren.add(childNode);
|
||||
childIndex += 1;
|
||||
@@ -1465,7 +1468,7 @@ class RenderEditable extends RenderBox
|
||||
child = childAfter(child!);
|
||||
placeholderIndex += 1;
|
||||
} else {
|
||||
final TextDirection initialDirection = currentDirection;
|
||||
final initialDirection = currentDirection;
|
||||
final List<ui.TextBox> rects = _textPainter.getBoxesForSelection(
|
||||
selection,
|
||||
);
|
||||
@@ -1494,7 +1497,7 @@ class RenderEditable extends RenderBox
|
||||
rect.right.ceilToDouble() + 4.0,
|
||||
rect.bottom.ceilToDouble() + 4.0,
|
||||
);
|
||||
final SemanticsConfiguration configuration = SemanticsConfiguration()
|
||||
final configuration = SemanticsConfiguration()
|
||||
..sortKey = OrdinalSortKey(ordinal++)
|
||||
..textDirection = initialDirection
|
||||
..attributedLabel = AttributedString(
|
||||
@@ -1507,8 +1510,9 @@ class RenderEditable extends RenderBox
|
||||
onDoubleTap: final VoidCallback? handler,
|
||||
):
|
||||
if (handler != null) {
|
||||
configuration.onTap = handler;
|
||||
configuration.isLink = true;
|
||||
configuration
|
||||
..onTap = handler
|
||||
..isLink = true;
|
||||
}
|
||||
case LongPressGestureRecognizer(
|
||||
onLongPress: final GestureLongPressCallback? onLongPress,
|
||||
@@ -1531,7 +1535,7 @@ class RenderEditable extends RenderBox
|
||||
if (_cachedChildNodes?.isNotEmpty ?? false) {
|
||||
newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!;
|
||||
} else {
|
||||
final UniqueKey key = UniqueKey();
|
||||
final key = UniqueKey();
|
||||
newChild = SemanticsNode(
|
||||
key: key,
|
||||
showOnScreen: _createShowOnScreenFor(key),
|
||||
@@ -1973,8 +1977,8 @@ class RenderEditable extends RenderBox
|
||||
if (cachedValue != null) {
|
||||
return cachedValue;
|
||||
}
|
||||
int count = 0;
|
||||
for (int index = 0; index < text.length; index += 1) {
|
||||
var count = 0;
|
||||
for (var index = 0; index < text.length; index += 1) {
|
||||
switch (text.codeUnitAt(index)) {
|
||||
case 0x000A: // LF
|
||||
case 0x0085: // NEL
|
||||
@@ -2203,17 +2207,14 @@ class RenderEditable extends RenderBox
|
||||
Offset? to,
|
||||
required SelectionChangedCause cause,
|
||||
}) {
|
||||
final localFrom = globalToLocal(from);
|
||||
_computeTextMetricsIfNeeded();
|
||||
final localFrom = globalToLocal(from);
|
||||
final TextPosition fromPosition = _textPainter.getPositionForOffset(
|
||||
localFrom - _paintOffset,
|
||||
);
|
||||
|
||||
final TextPosition? toPosition = to == null
|
||||
? null
|
||||
: _textPainter.getPositionForOffset(
|
||||
globalToLocal(to) - _paintOffset,
|
||||
);
|
||||
: _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset);
|
||||
|
||||
int baseOffset = fromPosition.offset;
|
||||
int extentOffset = toPosition?.offset ?? fromPosition.offset;
|
||||
@@ -2238,7 +2239,7 @@ class RenderEditable extends RenderBox
|
||||
extentOffset = isNormalized ? newOffset.endOffset : newOffset.startOffset;
|
||||
}
|
||||
|
||||
final TextSelection newSelection = TextSelection(
|
||||
final newSelection = TextSelection(
|
||||
baseOffset: baseOffset,
|
||||
extentOffset: extentOffset,
|
||||
affinity: fromPosition.affinity,
|
||||
@@ -2265,7 +2266,6 @@ class RenderEditable extends RenderBox
|
||||
/// beginning and end of a word respectively.
|
||||
///
|
||||
/// {@macro flutter.rendering.RenderEditable.selectPosition}
|
||||
|
||||
void selectWordsInRange({
|
||||
required Offset from,
|
||||
Offset? to,
|
||||
@@ -2278,9 +2278,7 @@ class RenderEditable extends RenderBox
|
||||
final TextSelection fromWord = getWordAtOffset(fromPosition);
|
||||
final TextPosition toPosition = to == null
|
||||
? fromPosition
|
||||
: _textPainter.getPositionForOffset(
|
||||
globalToLocal(to) - _paintOffset,
|
||||
);
|
||||
: _textPainter.getPositionForOffset(globalToLocal(to) - _paintOffset);
|
||||
final TextSelection toWord = toPosition == fromPosition
|
||||
? fromWord
|
||||
: getWordAtOffset(toPosition);
|
||||
@@ -2472,7 +2470,7 @@ class RenderEditable extends RenderBox
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
_caretPrototype = Rect.fromLTWH(
|
||||
_caretPrototype = Rect.fromLTRB(
|
||||
0.0,
|
||||
0.0,
|
||||
cursorWidth,
|
||||
@@ -2526,9 +2524,7 @@ class RenderEditable extends RenderBox
|
||||
..layout(minWidth: minWidth, maxWidth: maxWidth);
|
||||
final double width = forceLine
|
||||
? constraints.maxWidth
|
||||
: constraints.constrainWidth(
|
||||
_textIntrinsics.size.width + _caretMargin,
|
||||
);
|
||||
: constraints.constrainWidth(_textIntrinsics.size.width + _caretMargin);
|
||||
return Size(
|
||||
width,
|
||||
constraints.constrainHeight(_preferredHeight(constraints.maxWidth)),
|
||||
@@ -2592,19 +2588,20 @@ class RenderEditable extends RenderBox
|
||||
};
|
||||
|
||||
size = Size(width, constraints.constrainHeight(preferredHeight));
|
||||
final Size contentSize = Size(
|
||||
final contentSize = Size(
|
||||
_textPainter.width + _caretMargin,
|
||||
_textPainter.height,
|
||||
);
|
||||
|
||||
final BoxConstraints painterConstraints = BoxConstraints.tight(contentSize);
|
||||
final painterConstraints = BoxConstraints.tight(contentSize);
|
||||
|
||||
_foregroundRenderObject?.layout(painterConstraints);
|
||||
_backgroundRenderObject?.layout(painterConstraints);
|
||||
|
||||
_maxScrollExtent = _getMaxScrollExtent(contentSize);
|
||||
offset.applyViewportDimension(_viewportExtent);
|
||||
offset.applyContentDimensions(0.0, _maxScrollExtent);
|
||||
offset
|
||||
..applyViewportDimension(_viewportExtent)
|
||||
..applyContentDimensions(0.0, _maxScrollExtent);
|
||||
}
|
||||
|
||||
// The relative origin in relation to the distance the user has theoretically
|
||||
@@ -2656,7 +2653,7 @@ class RenderEditable extends RenderBox
|
||||
final double rightBound =
|
||||
math.min(size.width, _textPainter.width) +
|
||||
floatingCursorAddedMargin.right;
|
||||
final Rect boundingRects = Rect.fromLTRB(
|
||||
final boundingRects = Rect.fromLTRB(
|
||||
leftBound,
|
||||
topBound,
|
||||
rightBound,
|
||||
@@ -2780,7 +2777,7 @@ class RenderEditable extends RenderBox
|
||||
startPosition,
|
||||
Rect.zero,
|
||||
);
|
||||
for (final ui.LineMetrics lineMetrics in metrics) {
|
||||
for (final lineMetrics in metrics) {
|
||||
if (lineMetrics.baseline > offset.dy) {
|
||||
return MapEntry<int, Offset>(
|
||||
lineMetrics.lineNumber,
|
||||
@@ -2942,28 +2939,29 @@ class RenderEditable extends RenderBox
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('cursorColor', cursorColor));
|
||||
properties.add(
|
||||
DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines));
|
||||
properties.add(IntProperty('minLines', minLines));
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false),
|
||||
);
|
||||
properties.add(ColorProperty('selectionColor', selectionColor));
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<TextSelection>('selection', selection));
|
||||
properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
|
||||
properties
|
||||
..add(ColorProperty('cursorColor', cursorColor))
|
||||
..add(
|
||||
DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines))
|
||||
..add(IntProperty('minLines', minLines))
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false),
|
||||
)
|
||||
..add(ColorProperty('selectionColor', selectionColor))
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(DiagnosticsProperty<TextSelection>('selection', selection))
|
||||
..add(DiagnosticsProperty<ViewportOffset>('offset', offset));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3154,19 +3152,21 @@ class _TextHighlightPainter extends RenderEditablePainter {
|
||||
|
||||
highlightPaint.color = color;
|
||||
final TextPainter textPainter = renderEditable._textPainter;
|
||||
final List<TextBox> boxes = textPainter.getBoxesForSelection(
|
||||
TextSelection(baseOffset: range.start, extentOffset: range.end),
|
||||
boxHeightStyle: selectionHeightStyle,
|
||||
boxWidthStyle: selectionWidthStyle,
|
||||
);
|
||||
final Set<TextBox> boxes = textPainter
|
||||
.getBoxesForSelection(
|
||||
TextSelection(baseOffset: range.start, extentOffset: range.end),
|
||||
boxHeightStyle: selectionHeightStyle,
|
||||
boxWidthStyle: selectionWidthStyle,
|
||||
)
|
||||
.toSet();
|
||||
|
||||
for (final TextBox box in boxes) {
|
||||
for (final box in boxes) {
|
||||
canvas.drawRect(
|
||||
box
|
||||
.toRect()
|
||||
.shift(renderEditable._paintOffset)
|
||||
.intersect(
|
||||
Rect.fromLTWH(0, 0, textPainter.width, textPainter.height),
|
||||
Rect.fromLTRB(0, 0, textPainter.width, textPainter.height),
|
||||
),
|
||||
highlightPaint,
|
||||
);
|
||||
@@ -3215,7 +3215,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
Color? get caretColor => _caretColor;
|
||||
Color? _caretColor;
|
||||
set caretColor(Color? value) {
|
||||
if (caretColor?.value == value?.value) {
|
||||
if (caretColor?.toARGB32() == value?.toARGB32()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3246,7 +3246,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
Color? get backgroundCursorColor => _backgroundCursorColor;
|
||||
Color? _backgroundCursorColor;
|
||||
set backgroundCursorColor(Color? value) {
|
||||
if (backgroundCursorColor?.value == value?.value) {
|
||||
if (backgroundCursorColor?.toARGB32() == value?.toARGB32()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3287,7 +3287,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
if (radius == null) {
|
||||
canvas.drawRect(integralRect, caretPaint);
|
||||
} else {
|
||||
final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius);
|
||||
final caretRRect = RRect.fromRectAndRadius(integralRect, radius);
|
||||
canvas.drawRRect(caretRRect, caretPaint);
|
||||
}
|
||||
}
|
||||
@@ -3318,7 +3318,7 @@ class _CaretPainter extends RenderEditablePainter {
|
||||
paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition);
|
||||
}
|
||||
|
||||
final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75);
|
||||
final Color? floatingCursorColor = this.caretColor?.withValues(alpha: 0.75);
|
||||
// Floating Cursor.
|
||||
if (floatingCursorRect == null ||
|
||||
floatingCursorColor == null ||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user