mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 03:06:59 +08:00
Compare commits
428 Commits
1.1.5-pre6
...
662ccfcf0a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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:
|
||||
|
||||
14
.github/workflows/ios.yml
vendored
14
.github/workflows/ios.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
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
|
||||
|
||||
12
.github/workflows/mac.yml
vendored
12
.github/workflows/mac.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
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
|
||||
|
||||
19
.github/workflows/win_x64.yml
vendored
19
.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
|
||||
@@ -54,7 +59,7 @@ jobs:
|
||||
- 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 +73,15 @@ jobs:
|
||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
- name: Upload windows file release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Windows-file-x64-release
|
||||
archive: true
|
||||
name: PiliPlus_windows_${{env.version}}_x64_portable
|
||||
path: Release
|
||||
|
||||
- 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,8 +12,8 @@ include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/grpc/bilibili/**
|
||||
- lib/grpc/google/**
|
||||
- lib/common/widgets/flutter/**
|
||||
# - lib/grpc/google/**
|
||||
# - lib/common/widgets/flutter/**
|
||||
|
||||
formatter:
|
||||
trailing_commas: preserve
|
||||
@@ -64,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
|
||||
|
||||
@@ -54,6 +54,14 @@ android {
|
||||
signingConfig = config ?: signingConfigs["debug"]
|
||||
}
|
||||
release {
|
||||
if (project.hasProperty("dev")) {
|
||||
applicationIdSuffix = ".dev"
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "app_name",
|
||||
value = "PiliPlus dev",
|
||||
)
|
||||
}
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
211
ios/Podfile.lock
211
ios/Podfile.lock
@@ -1,83 +1,135 @@
|
||||
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):
|
||||
- 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.4):
|
||||
- Flutter
|
||||
- TOCropViewController (~> 2.8.0)
|
||||
- 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
|
||||
- OrderedSet (6.0.3)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.1.1):
|
||||
- permission_handler_apple (9.3.0):
|
||||
- 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 (2.8.0)
|
||||
- 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`)
|
||||
- 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`)
|
||||
@@ -87,45 +139,58 @@ DEPENDENCIES:
|
||||
- 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"
|
||||
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:
|
||||
@@ -144,57 +209,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
|
||||
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
|
||||
audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910
|
||||
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
|
||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
chat_bottom_container: d8b077152c91b0ab90001e900748ea50353a5520
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
|
||||
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
||||
image_cropper: b8ef14d3fcff4040b0f9da2ca28d98219a5cba0e
|
||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
||||
live_photo_maker: 7d57bfc70a120b4673c10871f354f4b1b6fde5fd
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
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
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
|
||||
screen_brightness_ios: 6a6f7794b67f07c4f1e24f6374b2d8ad367ffb39
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
|
||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class BuildConfig {
|
||||
abstract final class BuildConfig {
|
||||
static const int versionCode = int.fromEnvironment(
|
||||
'pili.code',
|
||||
defaultValue: 1,
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StyleString {
|
||||
abstract final 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 double aspectRatio16x9 = 16 / 9;
|
||||
static const double 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,
|
||||
);
|
||||
}
|
||||
|
||||
class Constants {
|
||||
abstract final class Constants {
|
||||
static const appName = 'PiliPlus';
|
||||
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
|
||||
|
||||
@@ -20,9 +34,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 +54,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,6 +66,9 @@ class Constants {
|
||||
|
||||
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
|
||||
|
||||
// 'itemOpusStyle,opusBigCover,onlyfansVote,endFooterHidden,decorationCard,onlyfansAssetsV2,ugcDelete,onlyfansQaCard,editable,opusPrivateVisible,avatarAutoTheme,sunflowerStyle,cardsEnhance,eva3CardOpus,eva3CardVideo,eva3CardComment,eva3CardVote,eva3CardUser'
|
||||
static const dynFeatures = 'itemOpusStyle,listOnlyfans,onlyfansQaCard';
|
||||
|
||||
// 超分辨率滤镜
|
||||
static const List<String> mpvAnime4KShaders = [
|
||||
'Anime4K_Clamp_Highlights.glsl',
|
||||
|
||||
@@ -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/widgets/flutter/layout_builder.dart';
|
||||
import 'package:flutter/material.dart' hide LayoutBuilder;
|
||||
|
||||
class FavPgcItemSkeleton extends StatelessWidget {
|
||||
const FavPgcItemSkeleton({super.key});
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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,
|
||||
}) {
|
||||
const gap = 6.0;
|
||||
const size = 22.0;
|
||||
const padding = 0.8;
|
||||
const offset = size - gap;
|
||||
const imgSize = size - 2 * padding;
|
||||
if (users.length == 1) {
|
||||
return NetworkImgLayer(
|
||||
src: users.first.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
);
|
||||
} else {
|
||||
final decoration = BoxDecoration(
|
||||
shape: .circle,
|
||||
border: Border.all(color: colorScheme.surface),
|
||||
);
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: offset * users.length + gap,
|
||||
child: Stack(
|
||||
clipBehavior: .none,
|
||||
children: users.indexed
|
||||
.map(
|
||||
(e) => Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: size,
|
||||
left: e.$1 * offset,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: Padding(
|
||||
padding: const .all(padding),
|
||||
child: NetworkImgLayer(
|
||||
src: e.$2.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/common/widgets/back_detector.dart
Normal file
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: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: StyleString.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,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);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class LoadingWidget extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//loading animation
|
||||
@@ -59,10 +60,7 @@ class LoadingWidget extends StatelessWidget {
|
||||
),
|
||||
|
||||
//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,44 @@
|
||||
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,
|
||||
}) {
|
||||
Object? content,
|
||||
// @Deprecated('use `bool result = await showConfirmDialog()` instead')
|
||||
VoidCallback? onConfirm,
|
||||
}) async {
|
||||
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),
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: 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/constants.dart' show StyleString;
|
||||
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: StyleString.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,5 +1,6 @@
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@@ -8,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, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: ColorScheme.of(context).outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (reasonType == null ||
|
||||
(reasonType == 0 && key.currentState?.validate() != true)) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final res = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (res.isSuccess) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
} catch (e, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -1,789 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'elevated_button_theme.dart';
|
||||
/// @docImport 'menu_anchor.dart';
|
||||
/// @docImport 'text_button_theme.dart';
|
||||
/// @docImport 'text_theme.dart';
|
||||
/// @docImport 'theme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
|
||||
///
|
||||
/// See also:
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * [TextButton], a button with no outline or fill color.
|
||||
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
|
||||
/// the Material Design button types and how they should be used in designs.
|
||||
abstract class ButtonStyleButton extends StatefulWidget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const ButtonStyleButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.onLongPress,
|
||||
required this.onHover,
|
||||
required this.onFocusChange,
|
||||
required this.style,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.clipBehavior,
|
||||
this.statesController,
|
||||
this.isSemanticButton = true,
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
this.iconAlignment,
|
||||
this.tooltip,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// Called when the button is tapped or otherwise activated.
|
||||
///
|
||||
/// If this callback and [onLongPress] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Called when the button is long-pressed.
|
||||
///
|
||||
/// If this callback and [onPressed] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Called when a pointer enters or exits the button response area.
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered this
|
||||
/// part of the material and false if a pointer has exited this part of the
|
||||
/// material.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding
|
||||
/// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s
|
||||
/// that resolve to non-null values will similarly override the corresponding
|
||||
/// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf].
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
|
||||
/// [ButtonStyle.foregroundBuilder] is specified. In those
|
||||
/// cases the default is [Clip.antiAlias].
|
||||
final Clip? clipBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.material.inkwell.statesController}
|
||||
final WidgetStatesController? statesController;
|
||||
|
||||
/// Determine whether this subtree represents a button.
|
||||
///
|
||||
/// If this is null, the screen reader will not announce "button" when this
|
||||
/// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we
|
||||
/// traverse the menu system.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool? isSemanticButton;
|
||||
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
/// Text that describes the action that will occur when the button is pressed or
|
||||
/// hovered over.
|
||||
///
|
||||
/// This text is displayed when the user long-presses or hovers over the button
|
||||
/// in a tooltip. This string is also used for accessibility.
|
||||
///
|
||||
/// If null, the button will not display a tooltip.
|
||||
final String? tooltip;
|
||||
|
||||
/// Typically the button's label.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
/// Returns a [ButtonStyle] that's based primarily on the [Theme]'s
|
||||
/// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values
|
||||
/// filled out (non-null).
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter and by the
|
||||
/// style returned by [themeStyleOf] that some button-specific themes like
|
||||
/// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the
|
||||
/// default style of the [TextButton] subclass can be overridden with its
|
||||
/// [TextButton.style] constructor parameter, or with a [TextButtonTheme].
|
||||
///
|
||||
/// Concrete button subclasses should return a [ButtonStyle] with as many
|
||||
/// non-null properties as possible, where all of the non-null
|
||||
/// [WidgetStateProperty] properties resolve to non-null values.
|
||||
///
|
||||
/// ## Properties that can be null
|
||||
///
|
||||
/// Some properties, like [ButtonStyle.fixedSize] would override other values
|
||||
/// in the same [ButtonStyle] if set, so they are allowed to be null. Here is
|
||||
/// a summary of properties that are allowed to be null when returned in the
|
||||
/// [ButtonStyle] returned by this function, an why:
|
||||
///
|
||||
/// - [ButtonStyle.fixedSize] because it would override other values in the
|
||||
/// same [ButtonStyle], like [ButtonStyle.maximumSize].
|
||||
/// - [ButtonStyle.side] because null is a valid value for a button that has
|
||||
/// no side. [OutlinedButton] returns a non-null default for this, however.
|
||||
/// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]
|
||||
/// because they would override the [ButtonStyle.foregroundColor] and
|
||||
/// [ButtonStyle.backgroundColor] of the same [ButtonStyle].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [themeStyleOf], returns the ButtonStyle of this button's component
|
||||
/// theme.
|
||||
@protected
|
||||
ButtonStyle defaultStyleOf(BuildContext context);
|
||||
|
||||
/// Returns the ButtonStyle that belongs to the button's component theme.
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter.
|
||||
///
|
||||
/// Concrete button subclasses should return the ButtonStyle for the
|
||||
/// nearest subclass-specific inherited theme, and if no such theme
|
||||
/// exists, then the same value from the overall [Theme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
|
||||
@protected
|
||||
ButtonStyle? themeStyleOf(BuildContext context);
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// Buttons are disabled by default. To enable a button, set its [onPressed]
|
||||
/// or [onLongPress] properties to a non-null value.
|
||||
bool get enabled => onPressed != null || onLongPress != null;
|
||||
|
||||
@override
|
||||
State<ButtonStyleButton> createState() => _ButtonStyleState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
FlagProperty('enabled', value: enabled, ifFalse: 'disabled'),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<FocusNode>(
|
||||
'focusNode',
|
||||
focusNode,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns null if [value] is null, otherwise `WidgetStatePropertyAll<T>(value)`.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<T>? allOrNull<T>(T? value) =>
|
||||
value == null ? null : WidgetStatePropertyAll<T>(value);
|
||||
|
||||
/// Returns null if [enabled] and [disabled] are null.
|
||||
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
|
||||
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<Color?>? defaultColor(
|
||||
Color? enabled,
|
||||
Color? disabled,
|
||||
) {
|
||||
if ((enabled ?? disabled) == null) {
|
||||
return null;
|
||||
}
|
||||
return WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
|
||||
WidgetState.disabled: disabled,
|
||||
WidgetState.any: enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/// A convenience method used by subclasses in the framework, that returns an
|
||||
/// interpolated value based on the [fontSizeMultiplier] parameter:
|
||||
///
|
||||
/// * 0 - 1 [geometry1x]
|
||||
/// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1)
|
||||
/// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2)
|
||||
/// * otherwise [geometry3x]
|
||||
///
|
||||
/// This method is used by the framework for estimating the default paddings to
|
||||
/// use on a button with a text label, when the system text scaling setting
|
||||
/// changes. It's usually supplied with empirical [geometry1x], [geometry2x],
|
||||
/// [geometry3x] values adjusted for different system text scaling values, when
|
||||
/// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge]
|
||||
/// value).
|
||||
///
|
||||
/// The `fontSizeMultiplier` argument, for historical reasons, is the default
|
||||
/// font size specified in the [ButtonStyle], scaled by the ambient font
|
||||
/// scaler, then divided by 14.0 (the default font size used in buttons).
|
||||
static EdgeInsetsGeometry scaledPadding(
|
||||
EdgeInsetsGeometry geometry1x,
|
||||
EdgeInsetsGeometry geometry2x,
|
||||
EdgeInsetsGeometry geometry3x,
|
||||
double fontSizeMultiplier,
|
||||
) {
|
||||
return switch (fontSizeMultiplier) {
|
||||
<= 1 => geometry1x,
|
||||
< 2 => EdgeInsetsGeometry.lerp(
|
||||
geometry1x,
|
||||
geometry2x,
|
||||
fontSizeMultiplier - 1,
|
||||
)!,
|
||||
< 3 => EdgeInsetsGeometry.lerp(
|
||||
geometry2x,
|
||||
geometry3x,
|
||||
fontSizeMultiplier - 2,
|
||||
)!,
|
||||
_ => geometry3x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
|
||||
/// * [OutlinedButton], similar to [TextButton], but with an outline.
|
||||
/// * [TextButton], a simple button without a shadow.
|
||||
class _ButtonStyleState extends State<ButtonStyleButton>
|
||||
with TickerProviderStateMixin {
|
||||
AnimationController? controller;
|
||||
double? elevation;
|
||||
Color? backgroundColor;
|
||||
WidgetStatesController? internalStatesController;
|
||||
|
||||
void handleStatesControllerChange() {
|
||||
// Force a rebuild to resolve WidgetStateProperty properties
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
WidgetStatesController get statesController =>
|
||||
widget.statesController ?? internalStatesController!;
|
||||
|
||||
void initStatesController() {
|
||||
if (widget.statesController == null) {
|
||||
internalStatesController = WidgetStatesController();
|
||||
}
|
||||
statesController
|
||||
..update(WidgetState.disabled, !widget.enabled)
|
||||
..addListener(handleStatesControllerChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initStatesController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ButtonStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.statesController != oldWidget.statesController) {
|
||||
oldWidget.statesController?.removeListener(handleStatesControllerChange);
|
||||
if (widget.statesController != null) {
|
||||
internalStatesController?.dispose();
|
||||
internalStatesController = null;
|
||||
}
|
||||
initStatesController();
|
||||
}
|
||||
if (widget.enabled != oldWidget.enabled) {
|
||||
statesController.update(WidgetState.disabled, !widget.enabled);
|
||||
if (!widget.enabled) {
|
||||
// The button may have been disabled while a press gesture is currently underway.
|
||||
statesController.update(WidgetState.pressed, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
statesController.removeListener(handleStatesControllerChange);
|
||||
internalStatesController?.dispose();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
final ButtonStyle? widgetStyle = widget.style;
|
||||
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
|
||||
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
|
||||
|
||||
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
|
||||
final T? widgetValue = getProperty(widgetStyle);
|
||||
final T? themeValue = getProperty(themeStyle);
|
||||
final T? defaultValue = getProperty(defaultStyle);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
}
|
||||
|
||||
T? resolve<T>(
|
||||
WidgetStateProperty<T>? Function(ButtonStyle? style) getProperty,
|
||||
) {
|
||||
return effectiveValue((ButtonStyle? style) {
|
||||
return getProperty(style)?.resolve(statesController.value);
|
||||
});
|
||||
}
|
||||
|
||||
Color? effectiveIconColor() {
|
||||
return widgetStyle?.iconColor?.resolve(statesController.value) ??
|
||||
themeStyle?.iconColor?.resolve(statesController.value) ??
|
||||
widgetStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
themeStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
defaultStyle.iconColor?.resolve(statesController.value) ??
|
||||
// Fallback to foregroundColor if iconColor is null.
|
||||
defaultStyle.foregroundColor?.resolve(statesController.value);
|
||||
}
|
||||
|
||||
final double? resolvedElevation = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.elevation,
|
||||
);
|
||||
final TextStyle? resolvedTextStyle = resolve<TextStyle?>(
|
||||
(ButtonStyle? style) => style?.textStyle,
|
||||
);
|
||||
Color? resolvedBackgroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.backgroundColor,
|
||||
);
|
||||
final Color? resolvedForegroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.foregroundColor,
|
||||
);
|
||||
final Color? resolvedShadowColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.shadowColor,
|
||||
);
|
||||
final Color? resolvedSurfaceTintColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.surfaceTintColor,
|
||||
);
|
||||
final EdgeInsetsGeometry? resolvedPadding = resolve<EdgeInsetsGeometry?>(
|
||||
(ButtonStyle? style) => style?.padding,
|
||||
);
|
||||
final Size? resolvedMinimumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.minimumSize,
|
||||
);
|
||||
final Size? resolvedFixedSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.fixedSize,
|
||||
);
|
||||
final Size? resolvedMaximumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.maximumSize,
|
||||
);
|
||||
final Color? resolvedIconColor = effectiveIconColor();
|
||||
final double? resolvedIconSize = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.iconSize,
|
||||
);
|
||||
final BorderSide? resolvedSide = resolve<BorderSide?>(
|
||||
(ButtonStyle? style) => style?.side,
|
||||
);
|
||||
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>(
|
||||
(ButtonStyle? style) => style?.shape,
|
||||
);
|
||||
|
||||
final WidgetStateMouseCursor mouseCursor = _MouseCursor(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.mouseCursor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final WidgetStateProperty<Color?> overlayColor =
|
||||
WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.overlayColor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final VisualDensity? resolvedVisualDensity = effectiveValue(
|
||||
(ButtonStyle? style) => style?.visualDensity,
|
||||
);
|
||||
final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue(
|
||||
(ButtonStyle? style) => style?.tapTargetSize,
|
||||
);
|
||||
final Duration? resolvedAnimationDuration = effectiveValue(
|
||||
(ButtonStyle? style) => style?.animationDuration,
|
||||
);
|
||||
final bool resolvedEnableFeedback =
|
||||
effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true;
|
||||
final AlignmentGeometry? resolvedAlignment = effectiveValue(
|
||||
(ButtonStyle? style) => style?.alignment,
|
||||
);
|
||||
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
|
||||
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue(
|
||||
(ButtonStyle? style) => style?.splashFactory,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.backgroundBuilder,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.foregroundBuilder,
|
||||
);
|
||||
|
||||
final Clip effectiveClipBehavior =
|
||||
widget.clipBehavior ??
|
||||
((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null
|
||||
? Clip.antiAlias
|
||||
: Clip.none);
|
||||
|
||||
BoxConstraints effectiveConstraints = resolvedVisualDensity
|
||||
.effectiveConstraints(
|
||||
BoxConstraints(
|
||||
minWidth: resolvedMinimumSize!.width,
|
||||
minHeight: resolvedMinimumSize.height,
|
||||
maxWidth: resolvedMaximumSize!.width,
|
||||
maxHeight: resolvedMaximumSize.height,
|
||||
),
|
||||
);
|
||||
if (resolvedFixedSize != null) {
|
||||
final Size size = effectiveConstraints.constrain(resolvedFixedSize);
|
||||
if (size.width.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minWidth: size.width,
|
||||
maxWidth: size.width,
|
||||
);
|
||||
}
|
||||
if (size.height.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minHeight: size.height,
|
||||
maxHeight: size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Per the Material Design team: don't allow the VisualDensity
|
||||
// adjustment to reduce the width of the left/right padding. If we
|
||||
// did, VisualDensity.compact, the default for desktop/web, would
|
||||
// reduce the horizontal padding to zero.
|
||||
final double dy = densityAdjustment.dy;
|
||||
final double dx = math.max(0, densityAdjustment.dx);
|
||||
final EdgeInsetsGeometry padding = resolvedPadding!
|
||||
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
|
||||
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
|
||||
|
||||
// If an opaque button's background is becoming translucent while its
|
||||
// elevation is changing, change the elevation first. Material implicitly
|
||||
// animates its elevation but not its color. SKIA renders non-zero
|
||||
// elevations as a shadow colored fill behind the Material's background.
|
||||
if (resolvedAnimationDuration! > Duration.zero &&
|
||||
elevation != null &&
|
||||
backgroundColor != null &&
|
||||
elevation != resolvedElevation &&
|
||||
backgroundColor!.value != resolvedBackgroundColor!.value &&
|
||||
backgroundColor!.opacity == 1 &&
|
||||
resolvedBackgroundColor.opacity < 1 &&
|
||||
resolvedElevation == 0) {
|
||||
if (controller?.duration != resolvedAnimationDuration) {
|
||||
controller?.dispose();
|
||||
controller =
|
||||
AnimationController(
|
||||
duration: resolvedAnimationDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
setState(() {}); // Rebuild with the final background color.
|
||||
}
|
||||
});
|
||||
}
|
||||
resolvedBackgroundColor =
|
||||
backgroundColor; // Defer changing the background color.
|
||||
controller!.value = 0;
|
||||
controller!.forward();
|
||||
}
|
||||
elevation = resolvedElevation;
|
||||
backgroundColor = resolvedBackgroundColor;
|
||||
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Align(
|
||||
alignment: resolvedAlignment!,
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: resolvedForegroundBuilder != null
|
||||
? resolvedForegroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
widget.child,
|
||||
)
|
||||
: widget.child,
|
||||
),
|
||||
);
|
||||
if (resolvedBackgroundBuilder != null) {
|
||||
result = resolvedBackgroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
result = AnimatedTheme(
|
||||
duration: resolvedAnimationDuration,
|
||||
data: theme.copyWith(
|
||||
iconTheme: iconTheme.merge(
|
||||
IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
onHover: widget.onHover,
|
||||
mouseCursor: mouseCursor,
|
||||
enableFeedback: resolvedEnableFeedback,
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: widget.onFocusChange,
|
||||
autofocus: widget.autofocus,
|
||||
splashFactory: resolvedSplashFactory,
|
||||
overlayColor: overlayColor,
|
||||
highlightColor: Colors.transparent,
|
||||
customBorder: resolvedShape!.copyWith(side: resolvedSide),
|
||||
statesController: statesController,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
result = Tooltip(message: widget.tooltip, child: result);
|
||||
}
|
||||
|
||||
final Size minSize;
|
||||
switch (resolvedTapTargetSize!) {
|
||||
case MaterialTapTargetSize.padded:
|
||||
minSize = Size(
|
||||
kMinInteractiveDimension + densityAdjustment.dx,
|
||||
kMinInteractiveDimension + densityAdjustment.dy,
|
||||
);
|
||||
assert(minSize.width >= 0.0);
|
||||
assert(minSize.height >= 0.0);
|
||||
case MaterialTapTargetSize.shrinkWrap:
|
||||
minSize = Size.zero;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
button: widget.isSemanticButton,
|
||||
enabled: widget.enabled,
|
||||
child: _InputPadding(
|
||||
minSize: minSize,
|
||||
child: ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Material(
|
||||
elevation: resolvedElevation!,
|
||||
textStyle: resolvedTextStyle?.copyWith(
|
||||
color: resolvedForegroundColor,
|
||||
),
|
||||
shape: resolvedShape.copyWith(side: resolvedSide),
|
||||
color: resolvedBackgroundColor,
|
||||
shadowColor: resolvedShadowColor,
|
||||
surfaceTintColor: resolvedSurfaceTintColor,
|
||||
type: resolvedBackgroundColor == null
|
||||
? MaterialType.transparency
|
||||
: MaterialType.button,
|
||||
animationDuration: resolvedAnimationDuration,
|
||||
clipBehavior: effectiveClipBehavior,
|
||||
borderOnForeground: false,
|
||||
child: result,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MouseCursor extends WidgetStateMouseCursor {
|
||||
const _MouseCursor(this.resolveCallback);
|
||||
|
||||
final WidgetPropertyResolver<MouseCursor?> resolveCallback;
|
||||
|
||||
@override
|
||||
MouseCursor resolve(Set<WidgetState> states) => resolveCallback(states)!;
|
||||
|
||||
@override
|
||||
String get debugDescription => 'ButtonStyleButton_MouseCursor';
|
||||
}
|
||||
|
||||
/// A widget to pad the area around a [ButtonStyleButton]'s inner [Material].
|
||||
///
|
||||
/// Redirect taps that occur in the padded area around the child to the center
|
||||
/// of the child. This increases the size of the button and the button's
|
||||
/// "tap target", but not its material or its ink splashes.
|
||||
class _InputPadding extends SingleChildRenderObjectWidget {
|
||||
const _InputPadding({super.child, required this.minSize});
|
||||
|
||||
final Size minSize;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderInputPadding(minSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant _RenderInputPadding renderObject,
|
||||
) {
|
||||
renderObject.minSize = minSize;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderInputPadding extends RenderShiftedBox {
|
||||
_RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
|
||||
|
||||
Size get minSize => _minSize;
|
||||
Size _minSize;
|
||||
set minSize(Size value) {
|
||||
if (_minSize == value) {
|
||||
return;
|
||||
}
|
||||
_minSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Size _computeSize({
|
||||
required BoxConstraints constraints,
|
||||
required ChildLayouter layoutChild,
|
||||
}) {
|
||||
if (child != null) {
|
||||
final Size childSize = layoutChild(child!, constraints);
|
||||
final double height = math.max(childSize.width, minSize.width);
|
||||
final double width = math.max(childSize.height, minSize.height);
|
||||
return constraints.constrain(Size(height, width));
|
||||
}
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
return _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.dryLayoutChild,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(
|
||||
covariant BoxConstraints constraints,
|
||||
TextBaseline baseline,
|
||||
) {
|
||||
final RenderBox? child = this.child;
|
||||
if (child == null) {
|
||||
return null;
|
||||
}
|
||||
final double? result = child.getDryBaseline(constraints, baseline);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
final Size childSize = child.getDryLayout(constraints);
|
||||
return result +
|
||||
Alignment.center
|
||||
.alongOffset(getDryLayout(constraints) - childSize as Offset)
|
||||
.dy;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.layoutChild,
|
||||
);
|
||||
if (child != null) {
|
||||
final BoxParentData childParentData = child!.parentData! as BoxParentData;
|
||||
childParentData.offset = Alignment.center.alongOffset(
|
||||
size - child!.size as Offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
if (super.hitTest(result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
final Offset center = child!.size.center(Offset.zero);
|
||||
return result.addWithRawTransform(
|
||||
transform: MatrixUtils.forceToPoint(center),
|
||||
position: center,
|
||||
hitTest: (BoxHitTestResult result, Offset position) {
|
||||
assert(position == center);
|
||||
return child!.hitTest(result, position: center);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
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/flutter/dyn/button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
|
||||
|
||||
/// A Material Design "Text Button".
|
||||
///
|
||||
/// Use text buttons on toolbars, in dialogs, or inline with other
|
||||
/// content but offset from that content with padding so that the
|
||||
/// button's presence is obvious. Text buttons do not have visible
|
||||
/// borders and must therefore rely on their position relative to
|
||||
/// other content for context. In dialogs and cards, they should be
|
||||
/// grouped together in one of the bottom corners. Avoid using text
|
||||
/// buttons where they would blend in with other content, for example
|
||||
/// in the middle of lists.
|
||||
///
|
||||
/// A text button is a label [child] displayed on a (zero elevation)
|
||||
/// [Material] widget. The label's [Text] and [Icon] widgets are
|
||||
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
|
||||
/// button reacts to touches by filling with the [style]'s
|
||||
/// [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// The text button's default style is defined by [defaultStyleOf].
|
||||
/// The style of this text button can be overridden with its [style]
|
||||
/// parameter. The style of all text buttons in a subtree can be
|
||||
/// overridden with the [TextButtonTheme] and the style of all of the
|
||||
/// text buttons in an app can be overridden with the [Theme]'s
|
||||
/// [ThemeData.textButtonTheme] property.
|
||||
///
|
||||
/// The static [styleFrom] method is a convenient way to create a
|
||||
/// text button [ButtonStyle] from simple values.
|
||||
///
|
||||
/// If the [onPressed] and [onLongPress] callbacks are null, then this
|
||||
/// button will be disabled, it will not react to touch.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows various ways to configure TextButtons, from the
|
||||
/// simplest default appearance to versions that don't resemble
|
||||
/// Material Design at all.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates using the [statesController] parameter to create a button
|
||||
/// that adds support for [WidgetState.selected].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * <https://material.io/design/components/buttons.html>
|
||||
/// * <https://m3.material.io/components/buttons>
|
||||
class TextButton extends ButtonStyleButton {
|
||||
/// Create a [TextButton].
|
||||
const TextButton({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
super.autofocus = false,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
super.isSemanticButton,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
/// Create a text button from a pair of widgets that serve as the button's
|
||||
/// [icon] and [label].
|
||||
///
|
||||
/// The icon and label are arranged in a row and padded by 8 logical pixels
|
||||
/// at the ends, with an 8 pixel gap in between.
|
||||
///
|
||||
/// If [icon] is null, will create a [TextButton] instead.
|
||||
///
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
///
|
||||
factory TextButton.icon({
|
||||
Key? key,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
ValueChanged<bool>? onHover,
|
||||
ValueChanged<bool>? onFocusChange,
|
||||
ButtonStyle? style,
|
||||
FocusNode? focusNode,
|
||||
bool? autofocus,
|
||||
Clip? clipBehavior,
|
||||
WidgetStatesController? statesController,
|
||||
Widget? icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) {
|
||||
if (icon == null) {
|
||||
return TextButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return _TextButtonWithIcon(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
icon: icon,
|
||||
label: label,
|
||||
iconAlignment: iconAlignment,
|
||||
);
|
||||
}
|
||||
|
||||
/// A static convenience method that constructs a text button
|
||||
/// [ButtonStyle] given simple values.
|
||||
///
|
||||
/// The [foregroundColor] and [disabledForegroundColor] colors are used
|
||||
/// to create a [WidgetStateProperty] [ButtonStyle.foregroundColor], and
|
||||
/// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
|
||||
///
|
||||
/// The [backgroundColor] and [disabledBackgroundColor] colors are
|
||||
/// used to create a [WidgetStateProperty] [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
|
||||
/// parameters are used to construct [ButtonStyle.mouseCursor].
|
||||
///
|
||||
/// The [iconColor], [disabledIconColor] are used to construct
|
||||
/// [ButtonStyle.iconColor] and [iconSize] is used to construct
|
||||
/// [ButtonStyle.iconSize].
|
||||
///
|
||||
/// If [iconColor] is null, the button icon will use [foregroundColor]. If [foregroundColor] is also
|
||||
/// null, the button icon will use the default icon color.
|
||||
///
|
||||
/// If [overlayColor] is specified and its value is [Colors.transparent]
|
||||
/// then the pressed/focused/hovered highlights are effectively defeated.
|
||||
/// Otherwise a [WidgetStateProperty] with the same opacities as the
|
||||
/// default is created.
|
||||
///
|
||||
/// All of the other parameters are either used directly or used to
|
||||
/// create a [WidgetStateProperty] with a single value for all
|
||||
/// states.
|
||||
///
|
||||
/// All parameters default to null. By default this method returns
|
||||
/// a [ButtonStyle] that doesn't override anything.
|
||||
///
|
||||
/// For example, to override the default text and icon colors for a
|
||||
/// [TextButton], as well as its overlay color, with all of the
|
||||
/// standard opacity adjustments for the pressed, focused, and
|
||||
/// hovered states, one could write:
|
||||
///
|
||||
/// ```dart
|
||||
/// TextButton(
|
||||
/// style: TextButton.styleFrom(foregroundColor: Colors.green),
|
||||
/// child: const Text('Give Kate a mix tape'),
|
||||
/// onPressed: () {
|
||||
/// // ...
|
||||
/// },
|
||||
/// ),
|
||||
/// ```
|
||||
static ButtonStyle styleFrom({
|
||||
Color? foregroundColor,
|
||||
Color? backgroundColor,
|
||||
Color? disabledForegroundColor,
|
||||
Color? disabledBackgroundColor,
|
||||
Color? shadowColor,
|
||||
Color? surfaceTintColor,
|
||||
Color? iconColor,
|
||||
double? iconSize,
|
||||
IconAlignment? iconAlignment,
|
||||
Color? disabledIconColor,
|
||||
Color? overlayColor,
|
||||
double? elevation,
|
||||
TextStyle? textStyle,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Size? minimumSize,
|
||||
Size? fixedSize,
|
||||
Size? maximumSize,
|
||||
BorderSide? side,
|
||||
OutlinedBorder? shape,
|
||||
MouseCursor? enabledMouseCursor,
|
||||
MouseCursor? disabledMouseCursor,
|
||||
VisualDensity? visualDensity,
|
||||
MaterialTapTargetSize? tapTargetSize,
|
||||
Duration? animationDuration,
|
||||
bool? enableFeedback,
|
||||
AlignmentGeometry? alignment,
|
||||
InteractiveInkFeatureFactory? splashFactory,
|
||||
ButtonLayerBuilder? backgroundBuilder,
|
||||
ButtonLayerBuilder? foregroundBuilder,
|
||||
}) {
|
||||
final WidgetStateProperty<Color?>? backgroundColorProp = switch ((
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? iconColorProp = switch ((
|
||||
iconColor,
|
||||
disabledIconColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(iconColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? overlayColorProp = switch ((
|
||||
foregroundColor,
|
||||
overlayColor,
|
||||
)) {
|
||||
(null, null) => null,
|
||||
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
|
||||
(_, final Color color) || (final Color color, _) =>
|
||||
WidgetStateProperty<Color?>.fromMap(<WidgetState, Color?>{
|
||||
WidgetState.pressed: color.withValues(alpha: 0.1),
|
||||
WidgetState.hovered: color.withValues(alpha: 0.08),
|
||||
WidgetState.focused: color.withValues(alpha: 0.1),
|
||||
}),
|
||||
};
|
||||
|
||||
return ButtonStyle(
|
||||
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
|
||||
foregroundColor: ButtonStyleButton.defaultColor(
|
||||
foregroundColor,
|
||||
disabledForegroundColor,
|
||||
),
|
||||
backgroundColor: backgroundColorProp,
|
||||
overlayColor: overlayColorProp,
|
||||
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
|
||||
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
|
||||
iconColor: iconColorProp,
|
||||
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
|
||||
iconAlignment: iconAlignment,
|
||||
elevation: ButtonStyleButton.allOrNull<double>(elevation),
|
||||
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
|
||||
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
|
||||
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
|
||||
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
|
||||
side: ButtonStyleButton.allOrNull<BorderSide>(side),
|
||||
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
|
||||
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
|
||||
<WidgetStatesConstraint, MouseCursor?>{
|
||||
WidgetState.disabled: disabledMouseCursor,
|
||||
WidgetState.any: enabledMouseCursor,
|
||||
},
|
||||
),
|
||||
visualDensity: visualDensity,
|
||||
tapTargetSize: tapTargetSize,
|
||||
animationDuration: animationDuration,
|
||||
enableFeedback: enableFeedback,
|
||||
alignment: alignment,
|
||||
splashFactory: splashFactory,
|
||||
backgroundBuilder: backgroundBuilder,
|
||||
foregroundBuilder: foregroundBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
/// Defines the button's default appearance.
|
||||
///
|
||||
/// {@template flutter.material.text_button.default_style_of}
|
||||
/// The button [child]'s [Text] and [Icon] widgets are rendered with
|
||||
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
|
||||
/// the style's overlay color when the button is focused, hovered
|
||||
/// or pressed. The button's background color becomes its [Material]
|
||||
/// color and is transparent by default.
|
||||
///
|
||||
/// All of the [ButtonStyle]'s defaults appear below.
|
||||
///
|
||||
/// In this list "Theme.foo" is shorthand for
|
||||
/// `Theme.of(context).foo`. Color scheme values like
|
||||
/// "onSurface(0.38)" are shorthand for
|
||||
/// `onSurface.withValues(alpha: 0.38)`. [WidgetStateProperty] valued
|
||||
/// properties that are not followed by a sublist have the same
|
||||
/// value for all states, otherwise the values are as specified for
|
||||
/// each state and "others" means all other states.
|
||||
///
|
||||
/// The "default font size" below refers to the font size specified in the
|
||||
/// [defaultStyleOf] method (or 14.0 if unspecified), scaled by the
|
||||
/// `MediaQuery.textScalerOf(context).scale` method. And the names of the
|
||||
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been abbreviated
|
||||
/// for readability.
|
||||
///
|
||||
/// The color of the [ButtonStyle.textStyle] is not used, the
|
||||
/// [ButtonStyle.foregroundColor] color is used instead.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// ## Material 2 defaults
|
||||
///
|
||||
/// * `textStyle` - Theme.textTheme.button
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.12)
|
||||
/// * `shadowColor` - Theme.shadowColor
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - (horizontal(12), vertical(8))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 36)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - InkRipple.splashFactory
|
||||
///
|
||||
/// The default padding values for the [TextButton.icon] factory are slightly different:
|
||||
///
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - all(8)
|
||||
/// * `14 < default font size <= 28 `- lerp(all(8), horizontal(4))
|
||||
/// * `28 < default font size` - horizontal(4)
|
||||
///
|
||||
/// The default value for `side`, which defines the appearance of the button's
|
||||
/// outline, is null. That means that the outline is defined by the button
|
||||
/// shape's [OutlinedBorder.side]. Typically the default value of an
|
||||
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
|
||||
///
|
||||
/// ## Material 3 defaults
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is set to true the following defaults will
|
||||
/// be used:
|
||||
///
|
||||
/// {@template flutter.material.text_button.material3_defaults}
|
||||
/// * `textStyle` - Theme.textTheme.labelLarge
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.1)
|
||||
/// * others - null
|
||||
/// * `shadowColor` - Colors.transparent,
|
||||
/// * `surfaceTintColor` - null
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - lerp(horizontal(12), horizontal(4))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 40)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - StadiumBorder()
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - Theme.splashFactory
|
||||
///
|
||||
/// For the [TextButton.icon] factory, the end (generally the right) value of
|
||||
/// `padding` is increased from 12 to 16.
|
||||
/// {@endtemplate}
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
|
||||
return Theme.of(context).useMaterial3
|
||||
? _TextButtonDefaultsM3(context)
|
||||
: styleFrom(
|
||||
foregroundColor: colorScheme.primary,
|
||||
disabledForegroundColor: colorScheme.onSurface.withValues(
|
||||
alpha: 0.38,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
shadowColor: theme.shadowColor,
|
||||
elevation: 0,
|
||||
textStyle: theme.textTheme.labelLarge,
|
||||
padding: _scaledPadding(context),
|
||||
minimumSize: const Size(64, 36),
|
||||
maximumSize: Size.infinite,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
enabledMouseCursor: SystemMouseCursors.click,
|
||||
disabledMouseCursor: SystemMouseCursors.basic,
|
||||
visualDensity: theme.visualDensity,
|
||||
tapTargetSize: theme.materialTapTargetSize,
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the [TextButtonThemeData.style] of the closest
|
||||
/// [TextButtonTheme] ancestor.
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
return TextButtonTheme.of(context).style;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final double defaultFontSize = theme.textTheme.labelLarge?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
return ButtonStyleButton.scaledPadding(
|
||||
theme.useMaterial3
|
||||
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
}
|
||||
|
||||
class _TextButtonWithIcon extends TextButton {
|
||||
_TextButtonWithIcon({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
bool? autofocus,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
required Widget icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) : super(
|
||||
autofocus: autofocus ?? false,
|
||||
child: _TextButtonWithIconChild(
|
||||
icon: icon,
|
||||
label: label,
|
||||
buttonStyle: style,
|
||||
iconAlignment: iconAlignment,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
||||
final ButtonStyle buttonStyle = super.defaultStyleOf(context);
|
||||
final double defaultFontSize =
|
||||
buttonStyle.textStyle?.resolve(const <WidgetState>{})?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
|
||||
useMaterial3
|
||||
? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
return buttonStyle.copyWith(
|
||||
padding: WidgetStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextButtonWithIconChild extends StatelessWidget {
|
||||
const _TextButtonWithIconChild({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.buttonStyle,
|
||||
required this.iconAlignment,
|
||||
});
|
||||
|
||||
final Widget label;
|
||||
final Widget icon;
|
||||
final ButtonStyle? buttonStyle;
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double defaultFontSize =
|
||||
buttonStyle?.textStyle?.resolve(const <WidgetState>{})?.fontSize ??
|
||||
14.0;
|
||||
final double scale =
|
||||
clampDouble(
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0,
|
||||
1.0,
|
||||
2.0,
|
||||
) -
|
||||
1.0;
|
||||
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
|
||||
final IconAlignment effectiveIconAlignment =
|
||||
iconAlignment ??
|
||||
textButtonTheme.style?.iconAlignment ??
|
||||
buttonStyle?.iconAlignment ??
|
||||
IconAlignment.start;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: lerpDouble(8, 4, scale)!,
|
||||
children: effectiveIconAlignment == IconAlignment.start
|
||||
? <Widget>[icon, Flexible(child: label)]
|
||||
: <Widget>[Flexible(child: label), icon],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - TextButton
|
||||
|
||||
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
||||
// "END GENERATED" comments are generated from data in the Material
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
class _TextButtonDefaultsM3 extends ButtonStyle {
|
||||
_TextButtonDefaultsM3(this.context)
|
||||
: super(
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
|
||||
@override
|
||||
WidgetStateProperty<TextStyle?> get textStyle =>
|
||||
WidgetStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get backgroundColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get foregroundColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get overlayColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary.withValues(alpha: 0.08);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get shadowColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get surfaceTintColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get elevation =>
|
||||
const WidgetStatePropertyAll<double>(0.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<EdgeInsetsGeometry>? get padding =>
|
||||
WidgetStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context));
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get minimumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size(64.0, 40.0));
|
||||
|
||||
// No default fixedSize
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get iconSize =>
|
||||
const WidgetStatePropertyAll<double>(18.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get iconColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get maximumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size.infinite);
|
||||
|
||||
// No default side
|
||||
|
||||
@override
|
||||
WidgetStateProperty<OutlinedBorder>? get shape =>
|
||||
const WidgetStatePropertyAll<OutlinedBorder>(StadiumBorder());
|
||||
|
||||
@override
|
||||
WidgetStateProperty<MouseCursor?>? get mouseCursor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return SystemMouseCursors.basic;
|
||||
}
|
||||
return SystemMouseCursors.click;
|
||||
});
|
||||
|
||||
@override
|
||||
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
|
||||
|
||||
@override
|
||||
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
|
||||
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - TextButton
|
||||
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/flutter/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
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/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,7 +216,6 @@ 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;
|
||||
@@ -423,17 +298,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 +314,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 +368,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 +379,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!.a == _effectiveValueColor.a) {
|
||||
_status = RefreshIndicatorStatus.armed;
|
||||
widget.onStatusChange?.call(_status);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop showing the refresh indicator.
|
||||
@@ -573,7 +402,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
);
|
||||
setState(() {
|
||||
_status = newMode;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
switch (_status!) {
|
||||
case RefreshIndicatorStatus.done:
|
||||
@@ -586,7 +414,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
0.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
case RefreshIndicatorStatus.armed:
|
||||
case RefreshIndicatorStatus.drag:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
@@ -594,7 +421,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
if (mounted && _status == newMode) {
|
||||
_dragOffset = null;
|
||||
_isIndicatorAtTop = null;
|
||||
setState(() {
|
||||
_status = null;
|
||||
});
|
||||
@@ -607,7 +433,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 +471,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 +486,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 +496,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 +506,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 +541,46 @@ 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 == RefreshIndicatorStatus.drag) {
|
||||
_dragOffset = _dragOffset! + offset;
|
||||
_checkDragOffset(viewportDimension);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import 'dart:ui' as ui show TextHeightBehavior;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -17,9 +16,9 @@ import 'dart:ui'
|
||||
TextBox;
|
||||
|
||||
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
@@ -5,15 +5,11 @@
|
||||
/// @docImport 'editable_text.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'
|
||||
show EditableTextContextMenuBuilder;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart' show SpellCheckService;
|
||||
|
||||
/// Controls how spell check is performed for text input.
|
||||
///
|
||||
@@ -121,351 +117,3 @@ class SpellCheckConfiguration {
|
||||
_spellCheckEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
// Methods for displaying spell check results:
|
||||
|
||||
/// Adjusts spell check results to correspond to [newText] if the only results
|
||||
/// that the handler has access to are the [results] corresponding to
|
||||
/// [resultsText].
|
||||
///
|
||||
/// Used in the case where the request for the spell check results of the
|
||||
/// [newText] is lagging in order to avoid display of incorrect results.
|
||||
List<SuggestionSpan> _correctSpellCheckResults(
|
||||
String newText,
|
||||
String resultsText,
|
||||
List<SuggestionSpan> results,
|
||||
) {
|
||||
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
|
||||
int spanPointer = 0;
|
||||
int offset = 0;
|
||||
|
||||
// Assumes that the order of spans has not been jumbled for optimization
|
||||
// purposes, and will only search since the previously found span.
|
||||
int searchStart = 0;
|
||||
|
||||
while (spanPointer < results.length) {
|
||||
final SuggestionSpan currentSpan = results[spanPointer];
|
||||
final String currentSpanText = resultsText.substring(
|
||||
currentSpan.range.start,
|
||||
currentSpan.range.end,
|
||||
);
|
||||
final int spanLength = currentSpan.range.end - currentSpan.range.start;
|
||||
|
||||
// Try finding SuggestionSpan from resultsText in new text.
|
||||
final String escapedText = RegExp.escape(currentSpanText);
|
||||
final RegExp currentSpanTextRegexp = RegExp('\\b$escapedText\\b');
|
||||
final int foundIndex = newText
|
||||
.substring(searchStart)
|
||||
.indexOf(currentSpanTextRegexp);
|
||||
|
||||
// Check whether word was found exactly where expected or elsewhere in the newText.
|
||||
final bool currentSpanFoundExactly =
|
||||
currentSpan.range.start == foundIndex + searchStart;
|
||||
final bool currentSpanFoundExactlyWithOffset =
|
||||
currentSpan.range.start + offset == foundIndex + searchStart;
|
||||
final bool currentSpanFoundElsewhere = foundIndex >= 0;
|
||||
|
||||
if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) {
|
||||
// currentSpan was found at the same index in newText and resultsText
|
||||
// or at the same index with the previously calculated adjustment by
|
||||
// the offset value, so apply it to new text by adding it to the list of
|
||||
// corrected results.
|
||||
final SuggestionSpan adjustedSpan = SuggestionSpan(
|
||||
TextRange(
|
||||
start: currentSpan.range.start + offset,
|
||||
end: currentSpan.range.end + offset,
|
||||
),
|
||||
currentSpan.suggestions,
|
||||
);
|
||||
|
||||
// Start search for the next misspelled word at the end of currentSpan.
|
||||
searchStart = math.min(
|
||||
currentSpan.range.end + 1 + offset,
|
||||
newText.length,
|
||||
);
|
||||
correctedSpellCheckResults.add(adjustedSpan);
|
||||
} else if (currentSpanFoundElsewhere) {
|
||||
// Word was pushed forward but not modified.
|
||||
final int adjustedSpanStart = searchStart + foundIndex;
|
||||
final int adjustedSpanEnd = adjustedSpanStart + spanLength;
|
||||
final SuggestionSpan adjustedSpan = SuggestionSpan(
|
||||
TextRange(start: adjustedSpanStart, end: adjustedSpanEnd),
|
||||
currentSpan.suggestions,
|
||||
);
|
||||
|
||||
// Start search for the next misspelled word at the end of the
|
||||
// adjusted currentSpan.
|
||||
searchStart = math.min(adjustedSpanEnd + 1, newText.length);
|
||||
// Adjust offset to reflect the difference between where currentSpan
|
||||
// was positioned in resultsText versus in newText.
|
||||
offset = adjustedSpanStart - currentSpan.range.start;
|
||||
correctedSpellCheckResults.add(adjustedSpan);
|
||||
}
|
||||
spanPointer++;
|
||||
}
|
||||
return correctedSpellCheckResults;
|
||||
}
|
||||
|
||||
/// Builds the [TextSpan] tree given the current state of the text input and
|
||||
/// spell check results.
|
||||
///
|
||||
/// The [value] is the current [TextEditingValue] requested to be rendered
|
||||
/// by a text input widget. The [composingWithinCurrentTextRange] value
|
||||
/// represents whether or not there is a valid composing region in the
|
||||
/// [value]. The [style] is the [TextStyle] to render the [value]'s text with,
|
||||
/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled
|
||||
/// words within the [value]'s text with. The [spellCheckResults] are the
|
||||
/// results of spell checking the [value]'s text.
|
||||
TextSpan buildTextSpanWithSpellCheckSuggestions(
|
||||
TextEditingValue value,
|
||||
bool composingWithinCurrentTextRange,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledTextStyle,
|
||||
SpellCheckResults spellCheckResults,
|
||||
) {
|
||||
List<SuggestionSpan> spellCheckResultsSpans =
|
||||
spellCheckResults.suggestionSpans;
|
||||
final String spellCheckResultsText = spellCheckResults.spellCheckedText;
|
||||
|
||||
if (spellCheckResultsText != value.text) {
|
||||
spellCheckResultsSpans = _correctSpellCheckResults(
|
||||
value.text,
|
||||
spellCheckResultsText,
|
||||
spellCheckResultsSpans,
|
||||
);
|
||||
}
|
||||
|
||||
// We will draw the TextSpan tree based on the composing region, if it is
|
||||
// available.
|
||||
// TODO(camsim99): The two separate strategies for building TextSpan trees
|
||||
// based on the availability of a composing region should be merged:
|
||||
// https://github.com/flutter/flutter/issues/124142.
|
||||
final bool shouldConsiderComposingRegion =
|
||||
defaultTargetPlatform == TargetPlatform.android;
|
||||
if (shouldConsiderComposingRegion) {
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: _buildSubtreesWithComposingRegion(
|
||||
spellCheckResultsSpans,
|
||||
value,
|
||||
style,
|
||||
misspelledTextStyle,
|
||||
composingWithinCurrentTextRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: _buildSubtreesWithoutComposingRegion(
|
||||
spellCheckResultsSpans,
|
||||
value,
|
||||
style,
|
||||
misspelledTextStyle,
|
||||
value.selection.baseOffset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the [TextSpan] tree for spell check without considering the composing
|
||||
/// region. Instead, uses the cursor to identify the word that's actively being
|
||||
/// edited and shouldn't be spell checked. This is useful for platforms and IMEs
|
||||
/// that don't use the composing region for the active word.
|
||||
List<TextSpan> _buildSubtreesWithoutComposingRegion(
|
||||
List<SuggestionSpan>? spellCheckSuggestions,
|
||||
TextEditingValue value,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledStyle,
|
||||
int cursorIndex,
|
||||
) {
|
||||
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
|
||||
|
||||
int textPointer = 0;
|
||||
int currentSpanPointer = 0;
|
||||
int endIndex;
|
||||
final String text = value.text;
|
||||
final TextStyle misspelledJointStyle =
|
||||
style?.merge(misspelledStyle) ?? misspelledStyle;
|
||||
bool cursorInCurrentSpan = false;
|
||||
|
||||
// Add text interwoven with any misspelled words to the tree.
|
||||
if (spellCheckSuggestions != null) {
|
||||
while (textPointer < text.length &&
|
||||
currentSpanPointer < spellCheckSuggestions.length) {
|
||||
final SuggestionSpan currentSpan =
|
||||
spellCheckSuggestions[currentSpanPointer];
|
||||
|
||||
if (currentSpan.range.start > textPointer) {
|
||||
endIndex = currentSpan.range.start < text.length
|
||||
? currentSpan.range.start
|
||||
: text.length;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
|
||||
);
|
||||
textPointer = endIndex;
|
||||
} else {
|
||||
endIndex = currentSpan.range.end < text.length
|
||||
? currentSpan.range.end
|
||||
: text.length;
|
||||
cursorInCurrentSpan =
|
||||
currentSpan.range.start <= cursorIndex &&
|
||||
currentSpan.range.end >= cursorIndex;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: cursorInCurrentSpan ? style : misspelledJointStyle,
|
||||
text: text.substring(currentSpan.range.start, endIndex),
|
||||
),
|
||||
);
|
||||
|
||||
textPointer = endIndex;
|
||||
currentSpanPointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text to the tree if applicable.
|
||||
if (textPointer < text.length) {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, text.length)),
|
||||
);
|
||||
}
|
||||
|
||||
return textSpanTreeChildren;
|
||||
}
|
||||
|
||||
/// Builds [TextSpan] subtree for text with misspelled words with logic based on
|
||||
/// a valid composing region.
|
||||
List<TextSpan> _buildSubtreesWithComposingRegion(
|
||||
List<SuggestionSpan>? spellCheckSuggestions,
|
||||
TextEditingValue value,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledStyle,
|
||||
bool composingWithinCurrentTextRange,
|
||||
) {
|
||||
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
|
||||
|
||||
int textPointer = 0;
|
||||
int currentSpanPointer = 0;
|
||||
int endIndex;
|
||||
SuggestionSpan currentSpan;
|
||||
final String text = value.text;
|
||||
final TextRange composingRegion = value.composing;
|
||||
final TextStyle composingTextStyle =
|
||||
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
|
||||
const TextStyle(decoration: TextDecoration.underline);
|
||||
final TextStyle misspelledJointStyle =
|
||||
style?.merge(misspelledStyle) ?? misspelledStyle;
|
||||
bool textPointerWithinComposingRegion = false;
|
||||
bool currentSpanIsComposingRegion = false;
|
||||
|
||||
// Add text interwoven with any misspelled words to the tree.
|
||||
if (spellCheckSuggestions != null) {
|
||||
while (textPointer < text.length &&
|
||||
currentSpanPointer < spellCheckSuggestions.length) {
|
||||
currentSpan = spellCheckSuggestions[currentSpanPointer];
|
||||
|
||||
if (currentSpan.range.start > textPointer) {
|
||||
endIndex = currentSpan.range.start < text.length
|
||||
? currentSpan.range.start
|
||||
: text.length;
|
||||
textPointerWithinComposingRegion =
|
||||
composingRegion.start >= textPointer &&
|
||||
composingRegion.end <= endIndex &&
|
||||
!composingWithinCurrentTextRange;
|
||||
|
||||
if (textPointerWithinComposingRegion) {
|
||||
_addComposingRegionTextSpans(
|
||||
textSpanTreeChildren,
|
||||
text,
|
||||
textPointer,
|
||||
composingRegion,
|
||||
style,
|
||||
composingTextStyle,
|
||||
);
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: style,
|
||||
text: text.substring(composingRegion.end, endIndex),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
|
||||
);
|
||||
}
|
||||
|
||||
textPointer = endIndex;
|
||||
} else {
|
||||
endIndex = currentSpan.range.end < text.length
|
||||
? currentSpan.range.end
|
||||
: text.length;
|
||||
currentSpanIsComposingRegion =
|
||||
textPointer >= composingRegion.start &&
|
||||
endIndex <= composingRegion.end &&
|
||||
!composingWithinCurrentTextRange;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: currentSpanIsComposingRegion
|
||||
? composingTextStyle
|
||||
: misspelledJointStyle,
|
||||
text: text.substring(currentSpan.range.start, endIndex),
|
||||
),
|
||||
);
|
||||
|
||||
textPointer = endIndex;
|
||||
currentSpanPointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text to the tree if applicable.
|
||||
if (textPointer < text.length) {
|
||||
if (textPointer < composingRegion.start &&
|
||||
!composingWithinCurrentTextRange) {
|
||||
_addComposingRegionTextSpans(
|
||||
textSpanTreeChildren,
|
||||
text,
|
||||
textPointer,
|
||||
composingRegion,
|
||||
style,
|
||||
composingTextStyle,
|
||||
);
|
||||
|
||||
if (composingRegion.end != text.length) {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: style,
|
||||
text: text.substring(composingRegion.end, text.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, text.length)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return textSpanTreeChildren;
|
||||
}
|
||||
|
||||
/// Helper method to create [TextSpan] tree children for specified range of
|
||||
/// text up to and including the composing region.
|
||||
void _addComposingRegionTextSpans(
|
||||
List<TextSpan> treeChildren,
|
||||
String text,
|
||||
int start,
|
||||
TextRange composingRegion,
|
||||
TextStyle? style,
|
||||
TextStyle composingTextStyle,
|
||||
) {
|
||||
treeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(start, composingRegion.start)),
|
||||
);
|
||||
treeChildren.add(
|
||||
TextSpan(
|
||||
style: composingTextStyle,
|
||||
text: text.substring(composingRegion.start, composingRegion.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// 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/text_field/adaptive_text_selection_toolbar.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/material.dart'
|
||||
hide EditableText, EditableTextState, AdaptiveTextSelectionToolbar;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SelectionChangedCause, SuggestionSpan;
|
||||
@@ -86,7 +88,7 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
|
||||
final buttonItems = <ContextMenuButtonItem>[];
|
||||
|
||||
// Build suggestion buttons.
|
||||
for (final String suggestion in spanAtCursorIndex.suggestions.take(
|
||||
@@ -110,7 +112,7 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Build delete button.
|
||||
final ContextMenuButtonItem deleteButton = ContextMenuButtonItem(
|
||||
final deleteButton = ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
@@ -172,18 +174,17 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
/// Builds the toolbar buttons based on the [buttonItems].
|
||||
List<Widget> _buildToolbarButtons(BuildContext context) {
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
final TextSelectionToolbarTextButton button =
|
||||
TextSelectionToolbarTextButton(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
|
||||
onPressed: buttonItem.onPressed,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem),
|
||||
style: buttonItem.type == ContextMenuButtonType.delete
|
||||
? const TextStyle(color: Colors.blue)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
final button = TextSelectionToolbarTextButton(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
|
||||
onPressed: buttonItem.onPressed,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem),
|
||||
style: buttonItem.type == ContextMenuButtonType.delete
|
||||
? const TextStyle(color: Colors.blue)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
if (buttonItem.type != ContextMenuButtonType.delete) {
|
||||
return button;
|
||||
@@ -214,7 +215,7 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
mediaQueryData.padding.top +
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding;
|
||||
// Makes up for the Padding.
|
||||
final Offset localAdjustment = Offset(
|
||||
final localAdjustment = Offset(
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
paddingAbove,
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -80,7 +81,7 @@ class SystemContextMenu extends StatefulWidget {
|
||||
),
|
||||
),
|
||||
items: items ?? getDefaultItems(editableTextState),
|
||||
onSystemHide: editableTextState.hideToolbar,
|
||||
onSystemHide: () => editableTextState.hideToolbar(false),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,6 +97,13 @@ class SystemContextMenu extends StatefulWidget {
|
||||
/// of the input.
|
||||
///
|
||||
/// Defaults to the result of [getDefaultItems].
|
||||
///
|
||||
/// To add custom menu items, pass [IOSSystemContextMenuItemCustom] instances
|
||||
/// in the [items] list. Each custom item requires a title and an onPressed callback.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [IOSSystemContextMenuItemCustom], which creates custom menu items.
|
||||
final List<IOSSystemContextMenuItem> items;
|
||||
|
||||
/// Called when the system hides this context menu.
|
||||
@@ -110,8 +118,30 @@ class SystemContextMenu extends StatefulWidget {
|
||||
/// Whether the current device supports showing the system context menu.
|
||||
///
|
||||
/// Currently, this is only supported on newer versions of iOS.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [isSupportedByField], which uses this method and determines whether an
|
||||
/// individual [EditableTextState] supports the system context menu.
|
||||
static bool isSupported(BuildContext context) {
|
||||
return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
|
||||
return defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
(MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false);
|
||||
}
|
||||
|
||||
/// Whether the given field supports showing the system context menu.
|
||||
///
|
||||
/// Currently [SystemContextMenu] is only supported with an active
|
||||
/// [TextInputConnection]. In cases where this isn't possible, such as in a
|
||||
/// read-only field, fall back to using a Flutter-rendered context menu like
|
||||
/// [AdaptiveTextSelectionToolbar].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [isSupported], which is used by this method and determines whether the
|
||||
/// platform in general supports showing the system context menu.
|
||||
static bool isSupportedByField(EditableTextState editableTextState) {
|
||||
return !editableTextState.widget.readOnly &&
|
||||
isSupported(editableTextState.context);
|
||||
}
|
||||
|
||||
/// The default [items] for the given [EditableTextState].
|
||||
@@ -126,17 +156,37 @@ class SystemContextMenu extends StatefulWidget {
|
||||
static List<IOSSystemContextMenuItem> getDefaultItems(
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
return <IOSSystemContextMenuItem>[
|
||||
if (editableTextState.copyEnabled) const IOSSystemContextMenuItemCopy(),
|
||||
if (editableTextState.cutEnabled) const IOSSystemContextMenuItemCut(),
|
||||
if (editableTextState.pasteEnabled) const IOSSystemContextMenuItemPaste(),
|
||||
if (editableTextState.selectAllEnabled)
|
||||
const IOSSystemContextMenuItemSelectAll(),
|
||||
if (editableTextState.lookUpEnabled)
|
||||
const IOSSystemContextMenuItemLookUp(),
|
||||
if (editableTextState.searchWebEnabled)
|
||||
const IOSSystemContextMenuItemSearchWeb(),
|
||||
];
|
||||
final items = <IOSSystemContextMenuItem>[];
|
||||
|
||||
// Use the generic Flutter-rendered context menu model as the single source of truth.
|
||||
for (final ContextMenuButtonItem button
|
||||
in editableTextState.contextMenuButtonItems) {
|
||||
switch (button.type) {
|
||||
case ContextMenuButtonType.copy:
|
||||
items.add(const IOSSystemContextMenuItemCopy());
|
||||
case ContextMenuButtonType.cut:
|
||||
items.add(const IOSSystemContextMenuItemCut());
|
||||
case ContextMenuButtonType.paste:
|
||||
items.add(const IOSSystemContextMenuItemPaste());
|
||||
case ContextMenuButtonType.selectAll:
|
||||
items.add(const IOSSystemContextMenuItemSelectAll());
|
||||
case ContextMenuButtonType.lookUp:
|
||||
items.add(const IOSSystemContextMenuItemLookUp());
|
||||
case ContextMenuButtonType.searchWeb:
|
||||
items.add(const IOSSystemContextMenuItemSearchWeb());
|
||||
case ContextMenuButtonType.share:
|
||||
items.add(const IOSSystemContextMenuItemShare());
|
||||
case ContextMenuButtonType.liveTextInput:
|
||||
items.add(const IOSSystemContextMenuItemLiveText());
|
||||
case ContextMenuButtonType.delete:
|
||||
// No native iOS system menu button for Delete — intentionally ignored.
|
||||
case ContextMenuButtonType.custom:
|
||||
// Custom items are provided explicitly via SystemContextMenu.items,
|
||||
// not via defaults. Intentionally ignore in default mapping.
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -177,259 +227,3 @@ class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a context menu button that will be rendered in the iOS system
|
||||
/// context menu and not by Flutter itself.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemData], which performs a similar role but at the
|
||||
/// method channel level and mirrors the requirements of the method channel
|
||||
/// API.
|
||||
/// * [ContextMenuButtonItem], which performs a similar role for Flutter-drawn
|
||||
/// context menus.
|
||||
@immutable
|
||||
sealed class IOSSystemContextMenuItem {
|
||||
const IOSSystemContextMenuItem();
|
||||
|
||||
/// The text to display to the user.
|
||||
///
|
||||
/// Not exposed for some built-in menu items whose title is always set by the
|
||||
/// platform.
|
||||
String? get title => null;
|
||||
|
||||
/// Returns the representation of this class used by method channels.
|
||||
IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations);
|
||||
|
||||
@override
|
||||
int get hashCode => title.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is IOSSystemContextMenuItem && other.title == title;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// copy button.
|
||||
///
|
||||
/// Should only appear when there is a selection that can be copied.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataCopy], which specifies the data to be sent to
|
||||
/// the platform for this same button.
|
||||
final class IOSSystemContextMenuItemCopy extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemCopy].
|
||||
const IOSSystemContextMenuItemCopy();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataCopy getData(WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataCopy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// cut button.
|
||||
///
|
||||
/// Should only appear when there is a selection that can be cut.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataCut], which specifies the data to be sent to
|
||||
/// the platform for this same button.
|
||||
final class IOSSystemContextMenuItemCut extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemCut].
|
||||
const IOSSystemContextMenuItemCut();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataCut getData(WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataCut();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// paste button.
|
||||
///
|
||||
/// Should only appear when the field can receive pasted content.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataPaste], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemPaste extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemPaste].
|
||||
const IOSSystemContextMenuItemPaste();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataPaste getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return const IOSSystemContextMenuItemDataPaste();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// select all button.
|
||||
///
|
||||
/// Should only appear when the field can have its selection changed.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataSelectAll], which specifies the data to be
|
||||
/// sent to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemSelectAll extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemSelectAll].
|
||||
const IOSSystemContextMenuItemSelectAll();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataSelectAll getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return const IOSSystemContextMenuItemDataSelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in look up button.
|
||||
///
|
||||
/// Should only appear when content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.lookUpButtonLabel].
|
||||
///
|
||||
/// The action is handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataLookUp], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemLookUp extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemLookUp].
|
||||
const IOSSystemContextMenuItemLookUp({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataLookUp getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return IOSSystemContextMenuItemDataLookUp(
|
||||
title: title ?? localizations.lookUpButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemLookUp(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in search web button.
|
||||
///
|
||||
/// Should only appear when content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.searchWebButtonLabel].
|
||||
///
|
||||
/// The action is handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataSearchWeb], which specifies the data to be
|
||||
/// sent to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemSearchWeb extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemSearchWeb].
|
||||
const IOSSystemContextMenuItemSearchWeb({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataSearchWeb getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return IOSSystemContextMenuItemDataSearchWeb(
|
||||
title: title ?? localizations.searchWebButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemSearchWeb(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in share button.
|
||||
///
|
||||
/// Opens the system share dialog.
|
||||
///
|
||||
/// Should only appear when shareable content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.shareButtonLabel].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataShare], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemShare extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemShare].
|
||||
const IOSSystemContextMenuItemShare({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataShare getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return IOSSystemContextMenuItemDataShare(
|
||||
title: title ?? localizations.shareButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemShare(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(justinmc): Support the "custom" type.
|
||||
// https://github.com/flutter/flutter/issues/103163
|
||||
|
||||
@@ -15,8 +15,8 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/cupertino_text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/cupertino/text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart';
|
||||
@@ -26,62 +26,31 @@ import 'package:flutter/cupertino.dart'
|
||||
hide
|
||||
EditableText,
|
||||
EditableTextState,
|
||||
CupertinoSpellCheckSuggestionsToolbar,
|
||||
SystemContextMenu,
|
||||
SpellCheckConfiguration,
|
||||
EditableTextContextMenuBuilder,
|
||||
buildTextSpanWithSpellCheckSuggestions,
|
||||
SystemContextMenu,
|
||||
CupertinoSpellCheckSuggestionsToolbar,
|
||||
SpellCheckConfiguration,
|
||||
CupertinoTextField,
|
||||
TextSelectionGestureDetectorBuilderDelegate,
|
||||
TextSelectionGestureDetectorBuilder,
|
||||
TextSelectionOverlay;
|
||||
TextSelectionOverlay,
|
||||
TextSelectionGestureDetectorBuilderDelegate;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide
|
||||
EditableText,
|
||||
EditableTextState,
|
||||
SpellCheckSuggestionsToolbar,
|
||||
EditableTextContextMenuBuilder,
|
||||
AdaptiveTextSelectionToolbar,
|
||||
SystemContextMenu,
|
||||
SpellCheckSuggestionsToolbar,
|
||||
SpellCheckConfiguration,
|
||||
EditableTextContextMenuBuilder,
|
||||
buildTextSpanWithSpellCheckSuggestions,
|
||||
TextSelectionGestureDetectorBuilderDelegate,
|
||||
TextSelectionGestureDetectorBuilder,
|
||||
TextSelectionOverlay;
|
||||
TextSelectionOverlay,
|
||||
TextSelectionGestureDetectorBuilderDelegate;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
export 'package:flutter/services.dart'
|
||||
show
|
||||
SmartDashesType,
|
||||
SmartQuotesType,
|
||||
TextCapitalization,
|
||||
TextInputAction,
|
||||
TextInputType;
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
// late FocusNode myFocusNode;
|
||||
|
||||
/// Signature for the [RichTextField.buildCounter] callback.
|
||||
typedef InputCounterWidgetBuilder =
|
||||
Widget? Function(
|
||||
/// The build context for the TextField.
|
||||
BuildContext context, {
|
||||
|
||||
/// The length of the string currently in the input.
|
||||
required int currentLength,
|
||||
|
||||
/// The maximum string length that can be entered into the TextField.
|
||||
required int? maxLength,
|
||||
|
||||
/// Whether or not the TextField is currently focused. Mainly provided for
|
||||
/// the [liveRegion] parameter in the [Semantics] widget for accessibility.
|
||||
required bool isFocused,
|
||||
});
|
||||
|
||||
class _TextFieldSelectionGestureDetectorBuilder
|
||||
extends TextSelectionGestureDetectorBuilder {
|
||||
_TextFieldSelectionGestureDetectorBuilder({
|
||||
@@ -209,6 +178,14 @@ class _TextFieldSelectionGestureDetectorBuilder
|
||||
/// [RichTextField] to ensure proper scroll coordination for [RichTextField] and its
|
||||
/// components like [TextSelectionOverlay].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates how to use the [Shortcuts] and [Actions] widgets
|
||||
/// to create a custom `Shift+Enter` keyboard shortcut for inserting a new line
|
||||
/// in a [RichTextField].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_field/text_field.3.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextFormField], which integrates with the [Form] widget.
|
||||
@@ -262,7 +239,8 @@ class RichTextField extends StatefulWidget {
|
||||
///
|
||||
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
|
||||
/// changing the shape of the selection highlighting. These properties default
|
||||
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively.
|
||||
/// to [EditableText.defaultSelectionHeightStyle] and
|
||||
/// [EditableText.defaultSelectionHeightStyle], respectively.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@@ -293,7 +271,7 @@ class RichTextField extends StatefulWidget {
|
||||
this.statesController,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
this.autocorrect,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
@@ -315,12 +293,13 @@ class RichTextField extends StatefulWidget {
|
||||
this.cursorOpacityAnimates,
|
||||
this.cursorColor,
|
||||
this.cursorErrorColor,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
this.selectionHeightStyle,
|
||||
this.selectionWidthStyle,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
bool? enableInteractiveSelection,
|
||||
this.selectAllOnFocus,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.onTapAlwaysCalled = false,
|
||||
@@ -346,6 +325,7 @@ class RichTextField extends StatefulWidget {
|
||||
this.canRequestFocus = true,
|
||||
this.spellCheckConfiguration,
|
||||
this.magnifierConfiguration,
|
||||
this.hintLocales,
|
||||
}) : assert(obscuringCharacter.length == 1),
|
||||
smartDashesType =
|
||||
smartDashesType ??
|
||||
@@ -526,7 +506,7 @@ class RichTextField extends StatefulWidget {
|
||||
final bool obscureText;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autocorrect}
|
||||
final bool autocorrect;
|
||||
final bool? autocorrect;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
|
||||
final SmartDashesType smartDashesType;
|
||||
@@ -578,6 +558,8 @@ class RichTextField extends StatefulWidget {
|
||||
/// field showing how many characters have been entered. If set to a number
|
||||
/// greater than 0, it will also display the maximum number allowed. If set
|
||||
/// to [RichTextField.noMaxLength] then only the current character count is displayed.
|
||||
/// To remove the counter, set [InputDecoration.counterText] to an empty string or
|
||||
/// return null from [RichTextField.buildCounter] callback.
|
||||
///
|
||||
/// After [maxLength] characters have been input, additional input
|
||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||
@@ -642,6 +624,27 @@ class RichTextField extends StatefulWidget {
|
||||
///
|
||||
/// If non-null this property overrides the [decoration]'s
|
||||
/// [InputDecoration.enabled] property.
|
||||
///
|
||||
/// When a text field is disabled, all of its children widgets are also
|
||||
/// disabled, including the [InputDecoration.suffixIcon]. If you need to keep
|
||||
/// the suffix icon interactive while disabling the text field, consider using
|
||||
/// [readOnly] and [enableInteractiveSelection] instead:
|
||||
///
|
||||
/// ```dart
|
||||
/// TextField(
|
||||
/// enabled: true,
|
||||
/// readOnly: true,
|
||||
/// enableInteractiveSelection: false,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// onPressed: () {
|
||||
/// // This will work because the TextField is enabled
|
||||
/// },
|
||||
/// icon: const Icon(Icons.edit_outlined),
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
final bool? enabled;
|
||||
|
||||
/// Determines whether this widget ignores pointer events.
|
||||
@@ -683,12 +686,12 @@ class RichTextField extends StatefulWidget {
|
||||
/// Controls how tall the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||
final ui.BoxHeightStyle selectionHeightStyle;
|
||||
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;
|
||||
final ui.BoxWidthStyle? selectionWidthStyle;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
@@ -703,6 +706,9 @@ class RichTextField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectAllOnFocus}
|
||||
final bool? selectAllOnFocus;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||
final TextSelectionControls? selectionControls;
|
||||
|
||||
@@ -837,7 +843,7 @@ class RichTextField extends StatefulWidget {
|
||||
/// offset and - if no [controller] has been provided - the content of the
|
||||
/// text field. If a [controller] has been provided, it is the responsibility
|
||||
/// of the owner of that controller to persist and restore it, e.g. by using
|
||||
/// a [RestorableTextEditingController].
|
||||
/// a [RestorableRichTextEditingController].
|
||||
///
|
||||
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||
@@ -883,12 +889,14 @@ class RichTextField extends StatefulWidget {
|
||||
/// be possible to move the focus to the text field with tab key.
|
||||
final bool canRequestFocus;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.hintLocales}
|
||||
final List<Locale>? hintLocales;
|
||||
|
||||
static Widget _defaultContextMenuBuilder(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
SystemContextMenu.isSupported(context)) {
|
||||
if (SystemContextMenu.isSupportedByField(editableTextState)) {
|
||||
return SystemContextMenu.editableText(
|
||||
editableTextState: editableTextState,
|
||||
);
|
||||
@@ -991,7 +999,9 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null))
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<InputDecoration>(
|
||||
'decoration',
|
||||
@@ -1006,7 +1016,9 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: TextInputType.text,
|
||||
),
|
||||
)
|
||||
..add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null))
|
||||
..add(
|
||||
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false),
|
||||
)
|
||||
@@ -1028,7 +1040,7 @@ class RichTextField extends StatefulWidget {
|
||||
DiagnosticsProperty<bool>(
|
||||
'autocorrect',
|
||||
autocorrect,
|
||||
defaultValue: true,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
@@ -1058,7 +1070,9 @@ class RichTextField extends StatefulWidget {
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, defaultValue: 1))
|
||||
..add(IntProperty('minLines', minLines, defaultValue: null))
|
||||
..add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false))
|
||||
..add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false),
|
||||
)
|
||||
..add(IntProperty('maxLength', maxLength, defaultValue: null))
|
||||
..add(
|
||||
EnumProperty<MaxLengthEnforcement>(
|
||||
@@ -1102,8 +1116,12 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0))
|
||||
..add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null))
|
||||
..add(
|
||||
DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0),
|
||||
)
|
||||
..add(
|
||||
DoubleProperty('cursorHeight', cursorHeight, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Radius>(
|
||||
'cursorRadius',
|
||||
@@ -1118,7 +1136,9 @@ class RichTextField extends StatefulWidget {
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(ColorProperty('cursorColor', cursorColor, defaultValue: null))
|
||||
..add(
|
||||
ColorProperty('cursorColor', cursorColor, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
ColorProperty('cursorErrorColor', cursorErrorColor, defaultValue: null),
|
||||
)
|
||||
@@ -1208,6 +1228,13 @@ class RichTextField extends StatefulWidget {
|
||||
? const <String>[]
|
||||
: kDefaultContentInsertionMimeTypes,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<List<Locale>?>(
|
||||
'hintLocales',
|
||||
hintLocales,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1215,9 +1242,7 @@ class RichTextField extends StatefulWidget {
|
||||
class RichTextFieldState extends State<RichTextField>
|
||||
with RestorationMixin
|
||||
implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
|
||||
// RestorableRichTextEditingController? _controller;
|
||||
RichTextEditingController get _effectiveController => widget.controller;
|
||||
// widget.controller ?? _controller!.value;
|
||||
|
||||
FocusNode? _focusNode;
|
||||
FocusNode get _effectiveFocusNode =>
|
||||
@@ -1260,13 +1285,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
bool get _hasIntrinsicError =>
|
||||
widget.maxLength != null &&
|
||||
widget.maxLength! > 0 &&
|
||||
(
|
||||
// widget.controller == null
|
||||
// ? !restorePending &&
|
||||
// _effectiveController.value.text.characters.length >
|
||||
// widget.maxLength!
|
||||
// :
|
||||
_effectiveController.value.text.characters.length > widget.maxLength!);
|
||||
_effectiveController.value.text.characters.length > widget.maxLength!;
|
||||
|
||||
bool get _hasError =>
|
||||
widget.decoration?.errorText != null ||
|
||||
@@ -1283,12 +1302,18 @@ class RichTextFieldState extends State<RichTextField>
|
||||
context,
|
||||
);
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final InputDecorationThemeData decorationTheme = InputDecorationTheme.of(
|
||||
context,
|
||||
);
|
||||
final InputDecoration effectiveDecoration =
|
||||
(widget.decoration ?? const InputDecoration())
|
||||
.applyDefaults(themeData.inputDecorationTheme)
|
||||
.applyDefaults(decorationTheme)
|
||||
.copyWith(
|
||||
enabled: _isEnabled,
|
||||
hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
|
||||
hintMaxLines:
|
||||
widget.decoration?.hintMaxLines ??
|
||||
decorationTheme.hintMaxLines ??
|
||||
widget.maxLines,
|
||||
);
|
||||
|
||||
// No need to build anything if counter or counterText were given directly.
|
||||
@@ -1325,8 +1350,8 @@ class RichTextFieldState extends State<RichTextField>
|
||||
return effectiveDecoration;
|
||||
} // No counter widget
|
||||
|
||||
String counterText = '$currentLength';
|
||||
String semanticCounterText = '';
|
||||
var counterText = '$currentLength';
|
||||
var semanticCounterText = '';
|
||||
|
||||
// Handle a real maxLength (positive number)
|
||||
if (widget.maxLength! > 0) {
|
||||
@@ -1368,9 +1393,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
state: this,
|
||||
controller: widget.controller,
|
||||
);
|
||||
// if (widget.controller == null) {
|
||||
// _createLocalController();
|
||||
// }
|
||||
_effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
|
||||
_effectiveFocusNode.addListener(_handleFocusChanged);
|
||||
_initStatesController();
|
||||
@@ -1394,13 +1416,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
@override
|
||||
void didUpdateWidget(RichTextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// if (widget.controller == null && oldWidget.controller != null) {
|
||||
// _createLocalController(oldWidget.controller!.value);
|
||||
// } else if (widget.controller != null && oldWidget.controller == null) {
|
||||
// unregisterFromRestoration(_controller!);
|
||||
// _controller!.dispose();
|
||||
// _controller = null;
|
||||
// }
|
||||
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
|
||||
@@ -1434,26 +1449,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
// if (_controller != null) {
|
||||
// _registerController();
|
||||
// }
|
||||
}
|
||||
|
||||
// void _registerController() {
|
||||
// assert(_controller != null);
|
||||
// registerForRestoration(_controller!, 'controller');
|
||||
// }
|
||||
|
||||
// void _createLocalController([TextEditingValue? value]) {
|
||||
// assert(_controller == null);
|
||||
// _controller = value == null
|
||||
// ? RestorableRichTextEditingController()
|
||||
// : RestorableRichTextEditingController.fromValue(value);
|
||||
// if (!restorePending) {
|
||||
// _registerController();
|
||||
// }
|
||||
// }
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {}
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
@@ -1462,7 +1458,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
void dispose() {
|
||||
_effectiveFocusNode.removeListener(_handleFocusChanged);
|
||||
_focusNode?.dispose();
|
||||
// _controller?.dispose();
|
||||
_statesController.removeListener(_handleStatesControllerChange);
|
||||
_internalStatesController?.dispose();
|
||||
super.dispose();
|
||||
@@ -1476,8 +1471,9 @@ class RichTextFieldState extends State<RichTextField>
|
||||
|
||||
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) {
|
||||
// selection toolbar, we shouldn't show the handles either.
|
||||
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar ||
|
||||
!_selectionGestureDetectorBuilder.shouldShowSelectionHandles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1570,7 +1566,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
WidgetStatesController? _internalStatesController;
|
||||
|
||||
void _handleStatesControllerChange() {
|
||||
// Force a rebuild to resolve MaterialStateProperty properties.
|
||||
// Force a rebuild to resolve WidgetStateProperty properties.
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -1581,11 +1577,12 @@ class RichTextFieldState extends State<RichTextField>
|
||||
if (widget.statesController == null) {
|
||||
_internalStatesController = WidgetStatesController();
|
||||
}
|
||||
_statesController.update(WidgetState.disabled, !_isEnabled);
|
||||
_statesController.update(WidgetState.hovered, _isHovering);
|
||||
_statesController.update(WidgetState.focused, _effectiveFocusNode.hasFocus);
|
||||
_statesController.update(WidgetState.error, _hasError);
|
||||
_statesController.addListener(_handleStatesControllerChange);
|
||||
_statesController
|
||||
..update(WidgetState.disabled, !_isEnabled)
|
||||
..update(WidgetState.hovered, _isHovering)
|
||||
..update(WidgetState.focused, _effectiveFocusNode.hasFocus)
|
||||
..update(WidgetState.error, _hasError)
|
||||
..addListener(_handleStatesControllerChange);
|
||||
}
|
||||
|
||||
// AutofillClient implementation start.
|
||||
@@ -1661,7 +1658,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
widget.keyboardAppearance ?? theme.brightness;
|
||||
final RichTextEditingController controller = _effectiveController;
|
||||
final FocusNode focusNode = _effectiveFocusNode;
|
||||
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
||||
final formatters = <TextInputFormatter>[
|
||||
...?widget.inputFormatters,
|
||||
if (widget.maxLength != null)
|
||||
LengthLimitingTextInputFormatter(
|
||||
@@ -1716,7 +1713,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
cupertinoTheme.primaryColor;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||
cupertinoTheme.primaryColor.withValues(alpha: 0.40);
|
||||
cursorRadius ??= const Radius.circular(2.0);
|
||||
cursorOffset = Offset(
|
||||
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
|
||||
@@ -1737,7 +1734,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
cupertinoTheme.primaryColor;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||
cupertinoTheme.primaryColor.withValues(alpha: 0.40);
|
||||
cursorRadius ??= const Radius.circular(2.0);
|
||||
cursorOffset = Offset(
|
||||
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
|
||||
@@ -1767,7 +1764,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withOpacity(0.40);
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
|
||||
case TargetPlatform.linux:
|
||||
forcePressEnabled = false;
|
||||
@@ -1781,7 +1778,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withOpacity(0.40);
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
handleDidGainAccessibilityFocus = () {
|
||||
// Automatically activate the TextField when it receives accessibility focus.
|
||||
if (!_effectiveFocusNode.hasFocus &&
|
||||
@@ -1805,7 +1802,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
theme.colorScheme.primary;
|
||||
selectionColor =
|
||||
selectionStyle.selectionColor ??
|
||||
theme.colorScheme.primary.withOpacity(0.40);
|
||||
theme.colorScheme.primary.withValues(alpha: 0.40);
|
||||
handleDidGainAccessibilityFocus = () {
|
||||
// Automatically activate the TextField when it receives accessibility focus.
|
||||
if (!_effectiveFocusNode.hasFocus &&
|
||||
@@ -1876,9 +1873,11 @@ class RichTextFieldState extends State<RichTextField>
|
||||
scrollPadding: widget.scrollPadding,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
selectAllOnFocus: widget.selectAllOnFocus,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
autofillClient: this,
|
||||
autocorrectionTextRectColor: autocorrectionTextRectColor,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
@@ -1892,6 +1891,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
magnifierConfiguration:
|
||||
widget.magnifierConfiguration ??
|
||||
TextMagnifier.adaptiveMagnifierConfiguration,
|
||||
hintLocales: widget.hintLocales,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2026,26 +2026,17 @@ TextStyle _m2CounterErrorStyle(BuildContext context) => Theme.of(
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
TextStyle? _m3StateInputStyle(BuildContext context) =>
|
||||
WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return TextStyle(
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.color
|
||||
?.withOpacity(0.38));
|
||||
}
|
||||
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color);
|
||||
});
|
||||
TextStyle? _m3StateInputStyle(BuildContext context) => WidgetStateTextStyle.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withValues(alpha:0.38));
|
||||
}
|
||||
return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color);
|
||||
});
|
||||
|
||||
TextStyle _m3InputStyle(BuildContext context) =>
|
||||
Theme.of(context).textTheme.bodyLarge!;
|
||||
TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
|
||||
|
||||
TextStyle _m3CounterErrorStyle(BuildContext context) => Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.error);
|
||||
TextStyle _m3CounterErrorStyle(BuildContext context) =>
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - TextField
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
// 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/cupertino.dart';
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' show kMinInteractiveDimension;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart' hide EditableText, EditableTextState;
|
||||
|
||||
/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
|
||||
///
|
||||
/// The interface is usually implemented by the [State] of text field
|
||||
/// implementations wrapping [EditableText], so that they can use a
|
||||
/// [TextSelectionGestureDetectorBuilder] to build a
|
||||
/// [TextSelectionGestureDetector] for their [EditableText]. The delegate
|
||||
/// provides the builder with information about the current state of the text
|
||||
/// field. Based on that information, the builder adds the correct gesture
|
||||
/// handlers to the gesture detector.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextField], which implements this delegate for the Material text field.
|
||||
/// * [CupertinoTextField], which implements this delegate for the Cupertino
|
||||
/// text field.
|
||||
abstract class TextSelectionGestureDetectorBuilderDelegate {
|
||||
/// [GlobalKey] to the [EditableText] for which the
|
||||
/// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
|
||||
@@ -83,6 +104,10 @@ class TextSelectionGestureDetectorBuilder {
|
||||
|
||||
// Hides the magnifier on supported platforms, currently only Android and iOS.
|
||||
void _hideMagnifierIfSupportedByPlatform() {
|
||||
if (!_isEditableTextMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
@@ -181,6 +206,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
offset,
|
||||
);
|
||||
final TextSelection selection = renderEditable.selection!;
|
||||
// bggRGjQaUbCoE on select
|
||||
final TextSelection nextSelection = selection.copyWith(
|
||||
extentOffset: controller.tapOffsetSimple(tappedPosition.offset),
|
||||
);
|
||||
@@ -193,12 +219,23 @@ class TextSelectionGestureDetectorBuilder {
|
||||
|
||||
/// Whether to show the selection toolbar.
|
||||
///
|
||||
/// It is based on the signal source when a [onTapDown] is called. This getter
|
||||
/// will return true if current [onTapDown] event is triggered by a touch or
|
||||
/// a stylus.
|
||||
/// It is based on the signal source when [onTapDown], [onSecondaryTapDown],
|
||||
/// [onDragSelectionStart], or [onForcePressStart] is called. This getter
|
||||
/// will return true if the current [onTapDown], or [onDragSelectionStart] event
|
||||
/// is triggered by a touch or a stylus. It will always return true for the
|
||||
/// current [onSecondaryTapDown] or [onForcePressStart] event.
|
||||
bool get shouldShowSelectionToolbar => _shouldShowSelectionToolbar;
|
||||
bool _shouldShowSelectionToolbar = true;
|
||||
|
||||
/// Whether to show the selection handles.
|
||||
///
|
||||
/// It is based on the signal source when [onTapDown], [onSecondaryTapDown],
|
||||
/// [onDragSelectionStart], is called. This getter will return true if the
|
||||
/// current [onTapDown], [onSecondaryTapDown], or [onDragSelectionStart] event
|
||||
/// is triggered by a touch or a stylus.
|
||||
bool get shouldShowSelectionHandles => _shouldShowSelectionHandles;
|
||||
bool _shouldShowSelectionHandles = true;
|
||||
|
||||
/// The [State] of the [EditableText] for which the builder will provide a
|
||||
/// [TextSelectionGestureDetector].
|
||||
@protected
|
||||
@@ -209,6 +246,13 @@ class TextSelectionGestureDetectorBuilder {
|
||||
@protected
|
||||
RenderEditable get renderEditable => editableText.renderEditable;
|
||||
|
||||
/// Returns `true` if a widget with the global key [delegate.editableTextKey]
|
||||
/// is in the tree and the widget is mounted.
|
||||
///
|
||||
/// Otherwise returns `false`.
|
||||
bool get _isEditableTextMounted =>
|
||||
delegate.editableTextKey.currentContext?.mounted ?? false;
|
||||
|
||||
/// Whether the Shift key was pressed when the most recent [PointerDownEvent]
|
||||
/// was tracked by the [BaseTapAndDragGestureRecognizer].
|
||||
bool _isShiftPressed = false;
|
||||
@@ -311,6 +355,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
kind == null ||
|
||||
kind == PointerDeviceKind.touch ||
|
||||
kind == PointerDeviceKind.stylus;
|
||||
_shouldShowSelectionHandles = _shouldShowSelectionToolbar;
|
||||
|
||||
// It is impossible to extend the selection when the shift key is pressed, if the
|
||||
// renderEditable.selection is invalid.
|
||||
@@ -504,6 +549,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
// Precise devices should place the cursor at a precise position if the
|
||||
// word at the text position is not misspelled.
|
||||
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
||||
editableText.hideToolbar();
|
||||
case PointerDeviceKind.touch:
|
||||
case PointerDeviceKind.unknown:
|
||||
// If the word that was tapped is misspelled, select the word and show the spell check suggestions
|
||||
@@ -719,22 +765,23 @@ class TextSelectionGestureDetectorBuilder {
|
||||
/// callback.
|
||||
@protected
|
||||
void onSingleLongTapEnd(LongPressEndDetails details) {
|
||||
_hideMagnifierIfSupportedByPlatform();
|
||||
_onSingleLongTapEndOrCancel();
|
||||
if (shouldShowSelectionToolbar) {
|
||||
editableText.showToolbar();
|
||||
}
|
||||
_longPressStartedWithoutFocus = false;
|
||||
_dragStartViewportOffset = 0.0;
|
||||
_dragStartScrollOffset = 0.0;
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
delegate.selectionEnabled &&
|
||||
editableText.textEditingValue.selection.isCollapsed) {
|
||||
// Update the floating cursor.
|
||||
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
|
||||
state: FloatingCursorDragState.End,
|
||||
);
|
||||
editableText.updateFloatingCursor(cursorPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onSingleLongTapCancel].
|
||||
///
|
||||
/// By default, it hides the magnifier and the floating cursor if necessary.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextSelectionGestureDetector.onSingleLongTapCancel], which triggers
|
||||
/// this callback.
|
||||
@protected
|
||||
void onSingleLongTapCancel() {
|
||||
_onSingleLongTapEndOrCancel();
|
||||
}
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onSecondaryTap].
|
||||
@@ -785,6 +832,10 @@ class TextSelectionGestureDetectorBuilder {
|
||||
TapDownDetails(globalPosition: details.globalPosition),
|
||||
);
|
||||
_shouldShowSelectionToolbar = true;
|
||||
_shouldShowSelectionHandles =
|
||||
details.kind == null ||
|
||||
details.kind == PointerDeviceKind.touch ||
|
||||
details.kind == PointerDeviceKind.stylus;
|
||||
}
|
||||
|
||||
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
|
||||
@@ -806,6 +857,23 @@ class TextSelectionGestureDetectorBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
void _onSingleLongTapEndOrCancel() {
|
||||
_hideMagnifierIfSupportedByPlatform();
|
||||
_longPressStartedWithoutFocus = false;
|
||||
_dragStartViewportOffset = 0.0;
|
||||
_dragStartScrollOffset = 0.0;
|
||||
if (_isEditableTextMounted &&
|
||||
defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
delegate.selectionEnabled &&
|
||||
editableText.textEditingValue.selection.isCollapsed) {
|
||||
// Update the floating cursor.
|
||||
final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint(
|
||||
state: FloatingCursorDragState.End,
|
||||
);
|
||||
editableText.updateFloatingCursor(cursorPoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Selects the set of paragraphs in a document that intersect a given range of
|
||||
// global positions.
|
||||
void _selectParagraphsInRange({
|
||||
@@ -954,6 +1022,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
kind == null ||
|
||||
kind == PointerDeviceKind.touch ||
|
||||
kind == PointerDeviceKind.stylus;
|
||||
_shouldShowSelectionHandles = _shouldShowSelectionToolbar;
|
||||
|
||||
_dragStartSelection = renderEditable.selection;
|
||||
_dragStartScrollOffset = _scrollPosition;
|
||||
@@ -1300,6 +1369,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
onSingleLongTapStart: onSingleLongTapStart,
|
||||
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
|
||||
onSingleLongTapEnd: onSingleLongTapEnd,
|
||||
onSingleLongTapCancel: onSingleLongTapCancel,
|
||||
onDoubleTapDown: onDoubleTapDown,
|
||||
onTripleTapDown: onTripleTapDown,
|
||||
onDragSelectionStart: onDragSelectionStart,
|
||||
@@ -1312,6 +1382,149 @@ class TextSelectionGestureDetectorBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -1411,21 +1624,19 @@ class _TextSelectionGestureDetectorState
|
||||
}
|
||||
|
||||
void _handleLongPressStart(LongPressStartDetails details) {
|
||||
if (widget.onSingleLongTapStart != null) {
|
||||
widget.onSingleLongTapStart!(details);
|
||||
}
|
||||
widget.onSingleLongTapStart?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||
if (widget.onSingleLongTapMoveUpdate != null) {
|
||||
widget.onSingleLongTapMoveUpdate!(details);
|
||||
}
|
||||
widget.onSingleLongTapMoveUpdate?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressEnd(LongPressEndDetails details) {
|
||||
if (widget.onSingleLongTapEnd != null) {
|
||||
widget.onSingleLongTapEnd!(details);
|
||||
}
|
||||
widget.onSingleLongTapEnd?.call(details);
|
||||
}
|
||||
|
||||
void _handleLongPressCancel() {
|
||||
widget.onSingleLongTapCancel?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1445,7 +1656,8 @@ class _TextSelectionGestureDetectorState
|
||||
|
||||
if (widget.onSingleLongTapStart != null ||
|
||||
widget.onSingleLongTapMoveUpdate != null ||
|
||||
widget.onSingleLongTapEnd != null) {
|
||||
widget.onSingleLongTapEnd != null ||
|
||||
widget.onSingleLongTapCancel != null) {
|
||||
gestures[LongPressGestureRecognizer] =
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(
|
||||
@@ -1456,7 +1668,8 @@ class _TextSelectionGestureDetectorState
|
||||
instance
|
||||
..onLongPressStart = _handleLongPressStart
|
||||
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
|
||||
..onLongPressEnd = _handleLongPressEnd;
|
||||
..onLongPressEnd = _handleLongPressEnd
|
||||
..onLongPressCancel = _handleLongPressCancel;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1824,8 +2037,11 @@ class TextSelectionOverlay {
|
||||
/// specifically is visible.
|
||||
bool get toolbarIsVisible => _selectionOverlay.toolbarIsVisible;
|
||||
|
||||
/// Whether the magnifier is currently visible.
|
||||
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
|
||||
/// {@macro flutter.widgets.SelectionOverlay.magnifierIsVisible}
|
||||
bool get magnifierIsVisible => _selectionOverlay.magnifierIsVisible;
|
||||
|
||||
/// {@macro flutter.widgets.SelectionOverlay.magnifierExists}
|
||||
bool get magnifierExists => _selectionOverlay.magnifierExists;
|
||||
|
||||
/// Whether the spell check menu is currently visible.
|
||||
///
|
||||
@@ -1965,6 +2181,16 @@ class TextSelectionOverlay {
|
||||
late double _endHandleDragTarget;
|
||||
|
||||
// The initial selection when a selection handle drag has started.
|
||||
//
|
||||
// This is used on Apple platforms to:
|
||||
//
|
||||
// 1. Preserve a collapsed selection: if the selection was collapsed when the drag
|
||||
// began, then it should remain collapsed throughout the entire drag.
|
||||
// 2. Anchor the non-dragged end of a non-collapsed selection: On Apple platforms,
|
||||
// the dragged handle always defines the selection's new extent. The drag start
|
||||
// selection provides the original position for the selection's new base. This
|
||||
// allows the selection handles to correctly swap their logical order (invert)
|
||||
// during the drag.
|
||||
TextSelection? _dragStartSelection;
|
||||
|
||||
void _handleSelectionEndHandleDragStart(DragStartDetails details) {
|
||||
@@ -1990,7 +2216,12 @@ class TextSelectionOverlay {
|
||||
final TextPosition position = renderObject.getPositionForPoint(
|
||||
Offset(details.globalPosition.dx, centerOfLineGlobal),
|
||||
);
|
||||
_dragStartSelection ??= _selection;
|
||||
|
||||
// The drag start selection is only utilized on Apple platforms.
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
_dragStartSelection ??= _selection;
|
||||
}
|
||||
|
||||
_selectionOverlay.showMagnifier(
|
||||
_buildMagnifier(
|
||||
@@ -2031,7 +2262,6 @@ class TextSelectionOverlay {
|
||||
if (!renderObject.attached) {
|
||||
return;
|
||||
}
|
||||
assert(_dragStartSelection != null);
|
||||
|
||||
// This is NOT the same as details.localPosition. That is relative to the
|
||||
// selection handle, whereas this is relative to the RenderEditable.
|
||||
@@ -2059,27 +2289,27 @@ class TextSelectionOverlay {
|
||||
// bggRGjQaUbCoE right drag
|
||||
position = controller.dragOffset(position);
|
||||
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
final TextSelection newSelection;
|
||||
switch (defaultTargetPlatform) {
|
||||
// On Apple platforms, dragging the base handle makes it the extent.
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
assert(_dragStartSelection != null);
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
// Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized
|
||||
// always returns true for a TextSelection.
|
||||
final bool dragStartSelectionNormalized =
|
||||
@@ -2095,6 +2325,21 @@ class TextSelectionOverlay {
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
if (_selection.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
newSelection = TextSelection(
|
||||
baseOffset: _selection.baseOffset,
|
||||
extentOffset: position.offset,
|
||||
@@ -2146,7 +2391,12 @@ class TextSelectionOverlay {
|
||||
final TextPosition position = renderObject.getPositionForPoint(
|
||||
Offset(details.globalPosition.dx, centerOfLineGlobal),
|
||||
);
|
||||
_dragStartSelection ??= _selection;
|
||||
|
||||
// The drag start selection is only utilized on Apple platforms.
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
_dragStartSelection ??= _selection;
|
||||
}
|
||||
|
||||
_selectionOverlay.showMagnifier(
|
||||
_buildMagnifier(
|
||||
@@ -2161,7 +2411,6 @@ class TextSelectionOverlay {
|
||||
if (!renderObject.attached) {
|
||||
return;
|
||||
}
|
||||
assert(_dragStartSelection != null);
|
||||
|
||||
// This is NOT the same as details.localPosition. That is relative to the
|
||||
// selection handle, whereas this is relative to the RenderEditable.
|
||||
@@ -2186,27 +2435,27 @@ class TextSelectionOverlay {
|
||||
// bggRGjQaUbCoE single drag, left drag
|
||||
position = controller.dragOffset(position);
|
||||
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
final TextSelection newSelection;
|
||||
switch (defaultTargetPlatform) {
|
||||
// On Apple platforms, dragging the base handle makes it the extent.
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
assert(_dragStartSelection != null);
|
||||
if (_dragStartSelection!.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
// Use this instead of _dragStartSelection.isNormalized because TextRange.isNormalized
|
||||
// always returns true for a TextSelection.
|
||||
final bool dragStartSelectionNormalized =
|
||||
@@ -2222,6 +2471,21 @@ class TextSelectionOverlay {
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
if (_selection.isCollapsed) {
|
||||
_selectionOverlay.updateMagnifier(
|
||||
_buildMagnifier(
|
||||
currentTextPosition: position,
|
||||
globalGesturePosition: details.globalPosition,
|
||||
renderEditable: renderObject,
|
||||
),
|
||||
);
|
||||
|
||||
final TextSelection currentSelection = TextSelection.fromPosition(
|
||||
position,
|
||||
);
|
||||
_handleSelectionHandleChanged(currentSelection);
|
||||
return;
|
||||
}
|
||||
newSelection = TextSelection(
|
||||
baseOffset: position.offset,
|
||||
extentOffset: _selection.extentOffset,
|
||||
@@ -2250,19 +2514,26 @@ class TextSelectionOverlay {
|
||||
return;
|
||||
}
|
||||
_dragStartSelection = null;
|
||||
final bool draggingHandles =
|
||||
_selectionOverlay.isDraggingStartHandle ||
|
||||
_selectionOverlay.isDraggingEndHandle;
|
||||
if (selectionControls is! TextSelectionHandleControls) {
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar();
|
||||
if (!draggingHandles) {
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar(
|
||||
context: context,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
);
|
||||
if (!draggingHandles) {
|
||||
_selectionOverlay.hideMagnifier();
|
||||
if (!_selection.isCollapsed) {
|
||||
_selectionOverlay.showToolbar(
|
||||
context: context,
|
||||
contextMenuBuilder: contextMenuBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2375,6 +2646,19 @@ class SelectionOverlay {
|
||||
: _toolbar != null || _spellCheckToolbarController.isShown;
|
||||
}
|
||||
|
||||
/// {@template flutter.widgets.SelectionOverlay.magnifierIsVisible}
|
||||
/// Whether the magnifier is currently visible.
|
||||
/// {@endtemplate}
|
||||
bool get magnifierIsVisible => _magnifierController.shown;
|
||||
|
||||
/// {@template flutter.widgets.SelectionOverlay.magnifierExists}
|
||||
/// Whether the magnifier currently exists.
|
||||
///
|
||||
/// This differs from [magnifierIsVisible] in that the magnifier may exist
|
||||
/// in the overlay, but not be shown.
|
||||
/// {@endtemplate}
|
||||
bool get magnifierExists => _magnifierController.overlayEntry != null;
|
||||
|
||||
/// {@template flutter.widgets.SelectionOverlay.showMagnifier}
|
||||
/// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier]
|
||||
/// was called. This is safe to call on platforms not mobile, since
|
||||
@@ -2386,6 +2670,10 @@ class SelectionOverlay {
|
||||
/// [MagnifierController.shown].
|
||||
/// {@endtemplate}
|
||||
void showMagnifier(MagnifierInfo initialMagnifierInfo) {
|
||||
// Do not show the magnifier if one already exists.
|
||||
if (_magnifierController.overlayEntry != null) {
|
||||
return;
|
||||
}
|
||||
if (toolbarIsVisible) {
|
||||
hideToolbar();
|
||||
}
|
||||
@@ -2459,8 +2747,26 @@ class SelectionOverlay {
|
||||
markNeedsBuild();
|
||||
}
|
||||
|
||||
// Whether a drag is in progress on the start handle. This differs from
|
||||
// `_isDraggingStartHandle` in that it is not blocked by `_canDragStartHandle`.
|
||||
bool _startHandleDragInProgress = false;
|
||||
|
||||
/// Whether the selection start handle is currently being dragged.
|
||||
bool get isDraggingStartHandle =>
|
||||
_isDraggingStartHandle || _startHandleDragInProgress;
|
||||
bool _isDraggingStartHandle = false;
|
||||
|
||||
// Whether the start handle can be dragged.
|
||||
//
|
||||
// On Apple and web platforms only one selection handle can be dragged
|
||||
// at a time, so when the end handle is being dragged on these platforms
|
||||
// the the start handle cannot be dragged.
|
||||
bool get _canDragStartHandle =>
|
||||
!_isDraggingEndHandle ||
|
||||
(defaultTargetPlatform != TargetPlatform.iOS &&
|
||||
defaultTargetPlatform != TargetPlatform.macOS &&
|
||||
!kIsWeb);
|
||||
|
||||
/// Whether the start handle is visible.
|
||||
///
|
||||
/// If the value changes, the start handle uses [FadeTransition] to transition
|
||||
@@ -2480,6 +2786,10 @@ class SelectionOverlay {
|
||||
_isDraggingStartHandle = false;
|
||||
return;
|
||||
}
|
||||
_startHandleDragInProgress = true;
|
||||
if (!_canDragStartHandle) {
|
||||
return;
|
||||
}
|
||||
_isDraggingStartHandle = details.kind == PointerDeviceKind.touch;
|
||||
onStartHandleDragStart?.call(details);
|
||||
}
|
||||
@@ -2491,6 +2801,22 @@ class SelectionOverlay {
|
||||
_isDraggingStartHandle = false;
|
||||
return;
|
||||
}
|
||||
if (!_canDragStartHandle) {
|
||||
return;
|
||||
}
|
||||
// The handle drag may have been blocked before on Apple platforms and the web
|
||||
// while the opposite handle was being dragged. Ensure that any logic that was
|
||||
// meant to be run in onStartHandleDragStart is still run.
|
||||
if (!_isDraggingStartHandle) {
|
||||
_isDraggingStartHandle = details.kind == PointerDeviceKind.touch;
|
||||
final DragStartDetails startDetails = DragStartDetails(
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
sourceTimeStamp: details.sourceTimeStamp,
|
||||
kind: details.kind,
|
||||
);
|
||||
onStartHandleDragStart?.call(startDetails);
|
||||
}
|
||||
onStartHandleDragUpdate?.call(details);
|
||||
}
|
||||
|
||||
@@ -2508,6 +2834,10 @@ class SelectionOverlay {
|
||||
if (_handles == null) {
|
||||
return;
|
||||
}
|
||||
_startHandleDragInProgress = false;
|
||||
if (!_canDragStartHandle) {
|
||||
return;
|
||||
}
|
||||
onStartHandleDragEnd?.call(details);
|
||||
}
|
||||
|
||||
@@ -2539,8 +2869,26 @@ class SelectionOverlay {
|
||||
markNeedsBuild();
|
||||
}
|
||||
|
||||
// Whether a drag is in progress on the start handle. This differs from
|
||||
// `_isDraggingEndHandle` in that it is not blocked by `_canDragEndHandle`.
|
||||
bool _endHandleDragInProgress = false;
|
||||
|
||||
/// Whether the selection end handle is currently being dragged.
|
||||
bool get isDraggingEndHandle =>
|
||||
_isDraggingEndHandle || _endHandleDragInProgress;
|
||||
bool _isDraggingEndHandle = false;
|
||||
|
||||
// Whether the end handle can be dragged.
|
||||
//
|
||||
// On Apple and web platforms only one selection handle can be dragged
|
||||
// at a time, so when the start handle is being dragged on these platforms
|
||||
// the the end handle cannot be dragged.
|
||||
bool get _canDragEndHandle =>
|
||||
!_isDraggingStartHandle ||
|
||||
(defaultTargetPlatform != TargetPlatform.iOS &&
|
||||
defaultTargetPlatform != TargetPlatform.macOS &&
|
||||
!kIsWeb);
|
||||
|
||||
/// Whether the end handle is visible.
|
||||
///
|
||||
/// If the value changes, the end handle uses [FadeTransition] to transition
|
||||
@@ -2560,6 +2908,10 @@ class SelectionOverlay {
|
||||
_isDraggingEndHandle = false;
|
||||
return;
|
||||
}
|
||||
_endHandleDragInProgress = true;
|
||||
if (!_canDragEndHandle) {
|
||||
return;
|
||||
}
|
||||
_isDraggingEndHandle = details.kind == PointerDeviceKind.touch;
|
||||
onEndHandleDragStart?.call(details);
|
||||
}
|
||||
@@ -2571,6 +2923,22 @@ class SelectionOverlay {
|
||||
_isDraggingEndHandle = false;
|
||||
return;
|
||||
}
|
||||
if (!_canDragEndHandle) {
|
||||
return;
|
||||
}
|
||||
// The handle drag may have been blocked before on Apple platforms and the web
|
||||
// while the opposite handle was being dragged. Ensure that any logic that was
|
||||
// meant to be run in onStartHandleDragStart is still run.
|
||||
if (!_isDraggingEndHandle) {
|
||||
_isDraggingEndHandle = details.kind == PointerDeviceKind.touch;
|
||||
final DragStartDetails startDetails = DragStartDetails(
|
||||
globalPosition: details.globalPosition,
|
||||
localPosition: details.localPosition,
|
||||
sourceTimeStamp: details.sourceTimeStamp,
|
||||
kind: details.kind,
|
||||
);
|
||||
onEndHandleDragStart?.call(startDetails);
|
||||
}
|
||||
onEndHandleDragUpdate?.call(details);
|
||||
}
|
||||
|
||||
@@ -2588,6 +2956,10 @@ class SelectionOverlay {
|
||||
if (_handles == null) {
|
||||
return;
|
||||
}
|
||||
_endHandleDragInProgress = false;
|
||||
if (!_canDragEndHandle) {
|
||||
return;
|
||||
}
|
||||
onEndHandleDragEnd?.call(details);
|
||||
}
|
||||
|
||||
@@ -3201,7 +3573,7 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay>
|
||||
final Size handleSize = widget.selectionControls.getHandleSize(
|
||||
preferredLineHeight,
|
||||
);
|
||||
return Rect.fromLTWH(0.0, 0.0, handleSize.width, handleSize.height);
|
||||
return Rect.fromLTRB(0.0, 0.0, handleSize.width, handleSize.height);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
2402
lib/common/widgets/flutter/vertical_tabs.dart
Normal file
2402
lib/common/widgets/flutter/vertical_tabs.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
class CustomHorizontalDragGestureRecognizer
|
||||
extends HorizontalDragGestureRecognizer {
|
||||
CustomHorizontalDragGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
Offset? _initialPosition;
|
||||
Offset? get initialPosition => _initialPosition;
|
||||
|
||||
@override
|
||||
DeviceGestureSettings get gestureSettings => _gestureSettings;
|
||||
final _gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH);
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
_initialPosition = event.position;
|
||||
}
|
||||
|
||||
@override
|
||||
bool hasSufficientGlobalDistanceToAccept(
|
||||
PointerDeviceKind pointerDeviceKind,
|
||||
double? deviceTouchSlop,
|
||||
) {
|
||||
return _computeHitSlop(
|
||||
globalDistanceMoved.abs(),
|
||||
gestureSettings,
|
||||
pointerDeviceKind,
|
||||
_initialPosition,
|
||||
lastPosition.global,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double touchSlopH = Pref.touchSlopH;
|
||||
|
||||
bool _computeHitSlop(
|
||||
double globalDistanceMoved,
|
||||
DeviceGestureSettings? settings,
|
||||
PointerDeviceKind kind,
|
||||
Offset? initialPosition,
|
||||
Offset lastPosition,
|
||||
) {
|
||||
switch (kind) {
|
||||
case PointerDeviceKind.mouse:
|
||||
return globalDistanceMoved > kPrecisePointerHitSlop;
|
||||
case PointerDeviceKind.stylus:
|
||||
case PointerDeviceKind.invertedStylus:
|
||||
case PointerDeviceKind.unknown:
|
||||
case PointerDeviceKind.touch:
|
||||
return globalDistanceMoved > touchSlopH &&
|
||||
_calc(initialPosition!, lastPosition);
|
||||
case PointerDeviceKind.trackpad:
|
||||
return globalDistanceMoved > (settings?.touchSlop ?? kTouchSlop);
|
||||
}
|
||||
}
|
||||
|
||||
bool _calc(Offset initialPosition, Offset lastPosition) {
|
||||
final offset = lastPosition - initialPosition;
|
||||
return offset.dx.abs() > offset.dy.abs() * 3;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
mixin ImageGestureRecognizerMixin on GestureRecognizer {
|
||||
int? _pointer;
|
||||
|
||||
@override
|
||||
void addPointer(PointerDownEvent event) {
|
||||
if (_pointer == event.pointer) {
|
||||
return;
|
||||
}
|
||||
_pointer = event.pointer;
|
||||
super.addPointer(event);
|
||||
}
|
||||
}
|
||||
|
||||
typedef IsBoundaryAllowed =
|
||||
bool Function(Offset? initialPosition, OffsetPair lastPosition);
|
||||
|
||||
class ImageHorizontalDragGestureRecognizer
|
||||
extends CustomHorizontalDragGestureRecognizer
|
||||
with ImageGestureRecognizerMixin {
|
||||
ImageHorizontalDragGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
IsBoundaryAllowed? isBoundaryAllowed;
|
||||
|
||||
@override
|
||||
bool hasSufficientGlobalDistanceToAccept(
|
||||
PointerDeviceKind pointerDeviceKind,
|
||||
double? deviceTouchSlop,
|
||||
) {
|
||||
return super.hasSufficientGlobalDistanceToAccept(
|
||||
pointerDeviceKind,
|
||||
deviceTouchSlop,
|
||||
) &&
|
||||
(isBoundaryAllowed?.call(initialPosition, lastPosition) ?? true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isBoundaryAllowed = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -5,53 +5,60 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
ImmediateTapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
|
||||
this.onTapDown,
|
||||
required this.onTapUp,
|
||||
this.onTapUp,
|
||||
this.onTapCancel,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
static bool _defaultButtonAcceptBehavior(int buttons) =>
|
||||
buttons == kPrimaryButton;
|
||||
|
||||
GestureTapDownCallback? onTapDown;
|
||||
|
||||
final GestureTapUpCallback onTapUp;
|
||||
GestureTapUpCallback? onTapUp;
|
||||
|
||||
final GestureTapCancelCallback? onTapCancel;
|
||||
GestureTapCancelCallback? onTapCancel;
|
||||
|
||||
final GestureTapCallback? onTap;
|
||||
GestureTapCallback? onTap;
|
||||
|
||||
PointerUpEvent? _up;
|
||||
int _activePointer = 0;
|
||||
int? _activePointer;
|
||||
bool _sentTapDown = false;
|
||||
bool _wonArena = false;
|
||||
Offset? _initialPosition;
|
||||
|
||||
@override
|
||||
bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => false;
|
||||
|
||||
@override
|
||||
bool isPointerAllowed(PointerDownEvent event) =>
|
||||
_activePointer == 0 && super.isPointerAllowed(event);
|
||||
_activePointer == null && super.isPointerAllowed(event);
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
_reset(event.pointer);
|
||||
_handleTapDown(event);
|
||||
_initialPosition = event.position;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event) {
|
||||
if (event.pointer != _activePointer) {
|
||||
resolvePointer(event.pointer, GestureDisposition.rejected);
|
||||
stopTrackingPointer(event.pointer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event is PointerDownEvent) {
|
||||
_handleTapDown(event);
|
||||
} else if (event is PointerMoveEvent) {
|
||||
if (event is PointerMoveEvent) {
|
||||
_handlePointerMove(event);
|
||||
} else if (event is PointerUpEvent) {
|
||||
_up = event;
|
||||
_handlePointerUp(event);
|
||||
} else if (event is PointerCancelEvent) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
}
|
||||
|
||||
stopTrackingIfPointerNoLongerDown(event);
|
||||
@@ -59,9 +66,9 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
void _handleTapDown(PointerDownEvent event) {
|
||||
if (_sentTapDown) return;
|
||||
_sentTapDown = true;
|
||||
|
||||
if (onTapDown != null) {
|
||||
_sentTapDown = true;
|
||||
final details = TapDownDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
@@ -72,8 +79,8 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
}
|
||||
|
||||
void _handlePointerMove(PointerMoveEvent event) {
|
||||
if (event.delta.distanceSquared > 2.0) {
|
||||
_cancelGesture('pointer moved');
|
||||
if ((event.position - _initialPosition!).distanceSquared > 4.0) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
}
|
||||
@@ -85,12 +92,14 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
}
|
||||
|
||||
void _handleTapUp(PointerUpEvent event) {
|
||||
final details = TapUpDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
kind: event.kind,
|
||||
);
|
||||
invokeCallback<void>('onTapUp', () => onTapUp(details));
|
||||
if (onTapUp != null) {
|
||||
final details = TapUpDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
kind: event.kind,
|
||||
);
|
||||
invokeCallback<void>('onTapUp', () => onTapUp!(details));
|
||||
}
|
||||
|
||||
if (onTap != null) {
|
||||
invokeCallback<void>('onTap', onTap!);
|
||||
@@ -106,7 +115,7 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
_reset();
|
||||
}
|
||||
|
||||
void _reset([int pointer = 0]) {
|
||||
void _reset([int? pointer]) {
|
||||
_activePointer = pointer;
|
||||
_up = null;
|
||||
_sentTapDown = false;
|
||||
@@ -138,13 +147,7 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
@override
|
||||
void didStopTrackingLastPointer(int pointer) {
|
||||
// wait for arena
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelGesture('disposed');
|
||||
super.dispose();
|
||||
_initialPosition = null;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -36,9 +36,9 @@ class MouseInteractiveViewer extends StatefulWidget {
|
||||
this.transformationController,
|
||||
this.alignment,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
|
||||
required this.childKey,
|
||||
required this.child,
|
||||
required this.onTranslate,
|
||||
}) : assert(minScale > 0),
|
||||
assert(interactionEndFrictionCoefficient > 0),
|
||||
assert(maxScale > 0),
|
||||
@@ -66,6 +66,7 @@ class MouseInteractiveViewer extends StatefulWidget {
|
||||
final GestureScaleUpdateCallback? onInteractionUpdate;
|
||||
final TransformationController? transformationController;
|
||||
final GlobalKey childKey;
|
||||
final VoidCallback onTranslate;
|
||||
|
||||
static const double _kDrag = 0.0000135;
|
||||
|
||||
@@ -95,17 +96,7 @@ class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
touchSlop: Platform.isIOS ? 9 : 4,
|
||||
);
|
||||
|
||||
late final _scaleGestureRecognizer =
|
||||
ScaleGestureRecognizer(
|
||||
debugOwner: this,
|
||||
allowedButtonsFilter: (buttons) => buttons == kPrimaryButton,
|
||||
trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor),
|
||||
trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
|
||||
)
|
||||
..gestureSettings = gestureSettings
|
||||
..onStart = _onScaleStart
|
||||
..onUpdate = _onScaleUpdate
|
||||
..onEnd = _onScaleEnd;
|
||||
late final ScaleGestureRecognizer _scaleGestureRecognizer;
|
||||
|
||||
final bool _rotateEnabled = false;
|
||||
|
||||
@@ -442,17 +433,15 @@ class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
details.velocity.pixelsPerSecond.distance,
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
);
|
||||
_animation =
|
||||
Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(
|
||||
frictionSimulationX.finalX,
|
||||
frictionSimulationY.finalX,
|
||||
),
|
||||
).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
|
||||
)
|
||||
..addListener(_handleInertiaAnimation);
|
||||
_animation = _controller.drive(
|
||||
Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(
|
||||
frictionSimulationX.finalX,
|
||||
frictionSimulationY.finalX,
|
||||
),
|
||||
).chain(CurveTween(curve: Curves.decelerate)),
|
||||
)..addListener(_handleInertiaAnimation);
|
||||
_controller
|
||||
..duration = Duration(milliseconds: (tFinal * 1000).round())
|
||||
..forward();
|
||||
@@ -472,17 +461,12 @@ class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
effectivelyMotionless: 0.1,
|
||||
);
|
||||
_scaleAnimation =
|
||||
Tween<double>(
|
||||
begin: scale,
|
||||
end: frictionSimulation.x(tFinal),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.decelerate,
|
||||
),
|
||||
)
|
||||
..addListener(_handleScaleAnimation);
|
||||
_scaleAnimation = _scaleController.drive(
|
||||
Tween<double>(
|
||||
begin: scale,
|
||||
end: frictionSimulation.x(tFinal),
|
||||
).chain(CurveTween(curve: Curves.decelerate)),
|
||||
)..addListener(_handleScaleAnimation);
|
||||
_scaleController
|
||||
..duration = Duration(milliseconds: (tFinal * 1000).round())
|
||||
..forward();
|
||||
@@ -641,6 +625,8 @@ class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
_transformer.value,
|
||||
newFocalPointScene - focalPointScene,
|
||||
);
|
||||
|
||||
widget.onTranslate();
|
||||
}
|
||||
|
||||
void _handleInertiaAnimation() {
|
||||
@@ -697,6 +683,18 @@ class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scaleGestureRecognizer =
|
||||
ScaleGestureRecognizer(
|
||||
debugOwner: this,
|
||||
dragStartBehavior: .start,
|
||||
allowedButtonsFilter: (buttons) => buttons == kPrimaryButton,
|
||||
trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor),
|
||||
trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
|
||||
)
|
||||
..gestureSettings = gestureSettings
|
||||
..onStart = _onScaleStart
|
||||
..onUpdate = _onScaleUpdate
|
||||
..onEnd = _onScaleEnd;
|
||||
_controller = AnimationController(vsync: this);
|
||||
_scaleController = AnimationController(vsync: this);
|
||||
|
||||
|
||||
14
lib/common/widgets/gesture/tap_gesture_recognizer.dart
Normal file
14
lib/common/widgets/gesture/tap_gesture_recognizer.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
|
||||
|
||||
class NoDeadlineTapGestureRecognizer extends TapGestureRecognizer {
|
||||
NoDeadlineTapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
super.preAcceptSlopTolerance,
|
||||
super.postAcceptSlopTolerance,
|
||||
});
|
||||
|
||||
@override
|
||||
Duration? get deadline => null;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
@@ -67,31 +68,6 @@ class CachedNetworkSVGImage extends StatefulWidget {
|
||||
@override
|
||||
State<CachedNetworkSVGImage> createState() => _CachedNetworkSVGImageState();
|
||||
|
||||
static Future<void> preCache(
|
||||
String imageUrl, {
|
||||
String? cacheKey,
|
||||
BaseCacheManager? cacheManager,
|
||||
}) {
|
||||
final key = cacheKey ?? _generateKeyFromUrl(imageUrl);
|
||||
cacheManager ??= DefaultCacheManager();
|
||||
return cacheManager.downloadFile(key);
|
||||
}
|
||||
|
||||
static Future<void> clearCacheForUrl(
|
||||
String imageUrl, {
|
||||
String? cacheKey,
|
||||
BaseCacheManager? cacheManager,
|
||||
}) {
|
||||
final key = cacheKey ?? _generateKeyFromUrl(imageUrl);
|
||||
cacheManager ??= DefaultCacheManager();
|
||||
return cacheManager.removeFile(key);
|
||||
}
|
||||
|
||||
static Future<void> clearCache({BaseCacheManager? cacheManager}) {
|
||||
cacheManager ??= DefaultCacheManager();
|
||||
return cacheManager.emptyCache();
|
||||
}
|
||||
|
||||
static String _generateKeyFromUrl(String url) => url.split('?').first;
|
||||
}
|
||||
|
||||
@@ -124,12 +100,10 @@ class _CachedNetworkSVGImageState extends State<CachedNetworkSVGImage> {
|
||||
|
||||
Future<void> _loadImage() async {
|
||||
try {
|
||||
var file = (await widget._cacheManager.getFileFromCache(_cacheKey))?.file;
|
||||
|
||||
file ??= await widget._cacheManager.getSingleFile(
|
||||
final file = await widget._cacheManager.getSingleFile(
|
||||
widget._url,
|
||||
key: _cacheKey,
|
||||
headers: widget._headers ?? {},
|
||||
headers: widget._headers ?? const {},
|
||||
);
|
||||
final svg = await file.readAsString();
|
||||
_svgString = svg;
|
||||
@@ -158,7 +132,7 @@ class _CachedNetworkSVGImageState extends State<CachedNetworkSVGImage> {
|
||||
|
||||
_setState();
|
||||
} catch (e) {
|
||||
log('CachedNetworkSVGImage: $e');
|
||||
if (kDebugMode) log('CachedNetworkSVGImage: $e');
|
||||
|
||||
_isError = true;
|
||||
_isLoading = false;
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
/*
|
||||
* 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' show min;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/custom_layout.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/utils/context_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide CustomMultiChildLayout, MultiChildLayoutDelegate;
|
||||
|
||||
class ImageModel {
|
||||
ImageModel({
|
||||
required num? width,
|
||||
required num? height,
|
||||
required this.url,
|
||||
this.liveUrl,
|
||||
}) {
|
||||
this.width = width == null || width == 0 ? 1 : width;
|
||||
this.height = height == null || height == 0 ? 1 : height;
|
||||
}
|
||||
|
||||
late num width;
|
||||
late num height;
|
||||
String url;
|
||||
String? liveUrl;
|
||||
bool? _isLongPic;
|
||||
bool? _isLivePhoto;
|
||||
|
||||
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
|
||||
bool get isLivePhoto =>
|
||||
_isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
|
||||
|
||||
static bool enableLivePhoto = Pref.enableLivePhoto;
|
||||
}
|
||||
|
||||
const double _maxRatio = 22 / 9;
|
||||
|
||||
class CustomGridView extends StatelessWidget {
|
||||
const CustomGridView({
|
||||
super.key,
|
||||
this.space = 5,
|
||||
required this.maxWidth,
|
||||
required this.picArr,
|
||||
this.onViewImage,
|
||||
this.fullScreen = false,
|
||||
});
|
||||
|
||||
final double maxWidth;
|
||||
final double space;
|
||||
final List<ImageModel> picArr;
|
||||
final VoidCallback? onViewImage;
|
||||
final bool fullScreen;
|
||||
|
||||
static bool horizontalPreview = Pref.horizontalPreview;
|
||||
|
||||
void onTap(BuildContext context, int index) {
|
||||
final imgList = picArr.map(
|
||||
(item) {
|
||||
bool isLive = item.isLivePhoto;
|
||||
return SourceModel(
|
||||
sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage,
|
||||
url: item.url,
|
||||
liveUrl: isLive ? item.liveUrl : null,
|
||||
width: isLive ? item.width.toInt() : null,
|
||||
height: isLive ? item.height.toInt() : null,
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
if (horizontalPreview &&
|
||||
!fullScreen &&
|
||||
!context.mediaQuerySize.isPortrait) {
|
||||
final scaffoldState = Scaffold.maybeOf(context);
|
||||
if (scaffoldState != null) {
|
||||
onViewImage?.call();
|
||||
PageUtils.onHorizontalPreviewState(
|
||||
scaffoldState,
|
||||
imgList,
|
||||
index,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
PageUtils.imageView(
|
||||
initialPage: index,
|
||||
imgList: imgList,
|
||||
);
|
||||
}
|
||||
|
||||
static BorderRadius borderRadius(
|
||||
int col,
|
||||
int length,
|
||||
int index, {
|
||||
Radius r = StyleString.imgRadius,
|
||||
}) {
|
||||
if (length == 1) return StyleString.mdRadius;
|
||||
|
||||
final bool hasUp = index - col >= 0;
|
||||
final bool hasDown = index + col < length;
|
||||
|
||||
final bool isRowStart = (index % col) == 0;
|
||||
final bool isRowEnd = (index % col) == col - 1 || index == length - 1;
|
||||
|
||||
final bool hasLeft = !isRowStart;
|
||||
final bool hasRight = !isRowEnd && (index + 1) < length;
|
||||
|
||||
return BorderRadius.only(
|
||||
topLeft: !hasUp && !hasLeft ? r : Radius.zero,
|
||||
topRight: !hasUp && !hasRight ? r : Radius.zero,
|
||||
bottomLeft: !hasDown && !hasLeft ? r : Radius.zero,
|
||||
bottomRight: !hasDown && !hasRight ? r : Radius.zero,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double imageWidth;
|
||||
double imageHeight;
|
||||
final length = picArr.length;
|
||||
final isSingle = length == 1;
|
||||
final isFour = length == 4;
|
||||
if (length == 2) {
|
||||
imageWidth = imageHeight = (maxWidth - space) / 2;
|
||||
} else {
|
||||
imageHeight = imageWidth = (maxWidth - 2 * space) / 3;
|
||||
if (isSingle) {
|
||||
final img = picArr.first;
|
||||
final width = img.width;
|
||||
final height = img.height;
|
||||
final ratioWH = width / height;
|
||||
final ratioHW = height / width;
|
||||
imageWidth = ratioWH > 1.5
|
||||
? maxWidth
|
||||
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
|
||||
? 2 * imageWidth
|
||||
: 1.5 * imageWidth;
|
||||
if (width != 1) {
|
||||
imageWidth = min(imageWidth, width.toDouble());
|
||||
}
|
||||
imageHeight = imageWidth * min(ratioHW, _maxRatio);
|
||||
}
|
||||
}
|
||||
|
||||
final int column = isFour ? 2 : 3;
|
||||
final int row = isFour ? 2 : (length / 3).ceil();
|
||||
late final placeHolder = Container(
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/loading.png',
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
cacheWidth: imageWidth.cacheSize(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: SizedBox(
|
||||
width: maxWidth,
|
||||
height: imageHeight * row + space * (row - 1),
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _CustomGridViewDelegate(
|
||||
space: space,
|
||||
itemCount: length,
|
||||
column: column,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
),
|
||||
children: List.generate(length, (index) {
|
||||
final item = picArr[index];
|
||||
final radius = borderRadius(column, length, index);
|
||||
return LayoutId(
|
||||
id: index,
|
||||
child: Hero(
|
||||
tag: item.url,
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(context, index),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: NetworkImgLayer(
|
||||
radius: 0,
|
||||
src: item.url,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
isLongPic: item.isLongPic,
|
||||
forceUseCacheWidth: item.width <= item.height,
|
||||
getPlaceHolder: () => placeHolder,
|
||||
),
|
||||
),
|
||||
if (item.isLivePhoto)
|
||||
const PBadge(
|
||||
text: 'Live',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
type: PBadgeType.gray,
|
||||
)
|
||||
else if (item.isLongPic)
|
||||
const PBadge(
|
||||
text: '长图',
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomGridViewDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomGridViewDelegate({
|
||||
required this.space,
|
||||
required this.itemCount,
|
||||
required this.column,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final double space;
|
||||
final int itemCount;
|
||||
final int column;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
final constraints = BoxConstraints.expand(width: width, height: height);
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
layoutChild(i, constraints);
|
||||
positionChild(
|
||||
i,
|
||||
Offset(
|
||||
(space + width) * (i % column),
|
||||
(space + height) * (i ~/ column),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_CustomGridViewDelegate oldDelegate) {
|
||||
return space != oldDelegate.space ||
|
||||
itemCount != oldDelegate.itemCount ||
|
||||
column != oldDelegate.column ||
|
||||
width != oldDelegate.width ||
|
||||
height != oldDelegate.height;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/http/user.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -14,28 +14,15 @@ void imageSaveDialog({
|
||||
dynamic aid,
|
||||
String? bvid,
|
||||
}) {
|
||||
final double imgWidth = Get.mediaQuery.size.shortestSide - 8 * 2;
|
||||
final double imgWidth = MediaQuery.sizeOf(Get.context!).shortestSide - 16;
|
||||
SmartDialog.show(
|
||||
animationType: SmartAnimationType.centerScale_otherSlide,
|
||||
builder: (context) {
|
||||
const iconSize = 20.0;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
Widget iconBtn({
|
||||
String? tooltip,
|
||||
required Icon icon,
|
||||
required VoidCallback? onPressed,
|
||||
}) {
|
||||
return iconButton(
|
||||
icon: icon,
|
||||
iconSize: 20,
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: imgWidth,
|
||||
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
|
||||
margin: const .symmetric(horizontal: StyleString.safeSpace),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
@@ -49,32 +36,29 @@ void imageSaveDialog({
|
||||
GestureDetector(
|
||||
onTap: SmartDialog.dismiss,
|
||||
child: NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: cover,
|
||||
quality: 100,
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio16x9,
|
||||
borderRadius: const .vertical(top: StyleString.imgRadius),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: SizedBox(
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: IconButton(
|
||||
tooltip: '关闭',
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
Colors.black.withValues(alpha: 0.3),
|
||||
),
|
||||
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: IconButton(
|
||||
tooltip: '关闭',
|
||||
style: IconButton.styleFrom(
|
||||
padding: .zero,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.3),
|
||||
),
|
||||
onPressed: SmartDialog.dismiss,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -94,33 +78,31 @@ void imageSaveDialog({
|
||||
else
|
||||
const Spacer(),
|
||||
if (aid != null || bvid != null)
|
||||
iconBtn(
|
||||
iconButton(
|
||||
iconSize: iconSize,
|
||||
tooltip: '稍后再看',
|
||||
onPressed: () => {
|
||||
SmartDialog.dismiss(),
|
||||
UserHttp.toViewLater(aid: aid, bvid: bvid).then(
|
||||
(res) => SmartDialog.showToast(res['msg']),
|
||||
),
|
||||
UserHttp.toViewLater(aid: aid, bvid: bvid),
|
||||
},
|
||||
icon: const Icon(Icons.watch_later_outlined),
|
||||
),
|
||||
if (cover?.isNotEmpty == true) ...[
|
||||
if (Utils.isMobile)
|
||||
iconBtn(
|
||||
if (cover != null && cover.isNotEmpty) ...[
|
||||
if (PlatformUtils.isMobile)
|
||||
iconButton(
|
||||
iconSize: iconSize,
|
||||
tooltip: '分享',
|
||||
onPressed: () {
|
||||
SmartDialog.dismiss();
|
||||
ImageUtils.onShareImg(cover!);
|
||||
ImageUtils.onShareImg(cover);
|
||||
},
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
iconBtn(
|
||||
iconButton(
|
||||
iconSize: iconSize,
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
bool saveStatus = await ImageUtils.downloadImg(
|
||||
context,
|
||||
[cover!],
|
||||
);
|
||||
bool saveStatus = await ImageUtils.downloadImg([cover]);
|
||||
if (saveStatus) {
|
||||
SmartDialog.dismiss();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/extension/num_ext.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
@@ -11,76 +11,63 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
super.key,
|
||||
required this.src,
|
||||
required this.width,
|
||||
this.height,
|
||||
this.type = ImageType.def,
|
||||
this.fadeOutDuration,
|
||||
this.fadeInDuration,
|
||||
// 图片质量 默认1%
|
||||
this.quality,
|
||||
this.semanticsLabel,
|
||||
this.radius,
|
||||
this.imageBuilder,
|
||||
this.isLongPic = false,
|
||||
this.forceUseCacheWidth = false,
|
||||
required this.height,
|
||||
this.type = .def,
|
||||
this.fadeOutDuration = const Duration(milliseconds: 120),
|
||||
this.fadeInDuration = const Duration(milliseconds: 120),
|
||||
this.quality = 1,
|
||||
this.borderRadius = StyleString.mdRadius,
|
||||
this.getPlaceHolder,
|
||||
this.boxFit,
|
||||
this.fit = .cover,
|
||||
this.alignment = .center,
|
||||
this.cacheWidth,
|
||||
});
|
||||
|
||||
final String? src;
|
||||
final double width;
|
||||
final double? height;
|
||||
final double height;
|
||||
final ImageType type;
|
||||
final Duration? fadeOutDuration;
|
||||
final Duration? fadeInDuration;
|
||||
final int? quality;
|
||||
final String? semanticsLabel;
|
||||
final double? radius;
|
||||
final ImageWidgetBuilder? imageBuilder;
|
||||
final bool isLongPic;
|
||||
final bool forceUseCacheWidth;
|
||||
final Widget Function()? getPlaceHolder;
|
||||
final BoxFit? boxFit;
|
||||
final Duration fadeOutDuration;
|
||||
final Duration fadeInDuration;
|
||||
final int quality;
|
||||
final BorderRadius borderRadius;
|
||||
final ValueGetter<Widget>? getPlaceHolder;
|
||||
final BoxFit fit;
|
||||
final Alignment alignment;
|
||||
final bool? cacheWidth;
|
||||
|
||||
static Color? reduceLuxColor = Pref.reduceLuxColor;
|
||||
static bool reduce = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final noRadius = type == ImageType.emote || radius == 0;
|
||||
final Widget child;
|
||||
|
||||
final isEmote = type == ImageType.emote;
|
||||
final isAvatar = type == ImageType.avatar;
|
||||
if (src?.isNotEmpty == true) {
|
||||
child = noRadius
|
||||
? _buildImage(context, noRadius)
|
||||
: type == ImageType.avatar
|
||||
? ClipOval(child: _buildImage(context, noRadius))
|
||||
: ClipRRect(
|
||||
borderRadius: radius != null
|
||||
? BorderRadius.circular(radius!)
|
||||
: StyleString.mdRadius,
|
||||
child: _buildImage(context, noRadius),
|
||||
);
|
||||
Widget child = _buildImage(context, isEmote: isEmote, isAvatar: isAvatar);
|
||||
if (isEmote) {
|
||||
return child;
|
||||
} else if (isAvatar) {
|
||||
return ClipOval(child: child);
|
||||
} else {
|
||||
return ClipRRect(borderRadius: borderRadius, child: child);
|
||||
}
|
||||
} else {
|
||||
child = getPlaceHolder?.call() ?? _placeholder(context, noRadius);
|
||||
return getPlaceHolder?.call() ??
|
||||
_placeholder(context, isEmote: isEmote, isAvatar: isAvatar);
|
||||
}
|
||||
|
||||
return semanticsLabel?.isNotEmpty == true
|
||||
? Semantics(
|
||||
container: true,
|
||||
image: true,
|
||||
excludeSemantics: true,
|
||||
label: semanticsLabel,
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
}
|
||||
|
||||
Widget _buildImage(BuildContext context, bool noRadius) {
|
||||
Widget _buildImage(
|
||||
BuildContext context, {
|
||||
required bool isEmote,
|
||||
required bool isAvatar,
|
||||
}) {
|
||||
int? memCacheWidth, memCacheHeight;
|
||||
if (height == null || forceUseCacheWidth || width <= height!) {
|
||||
if (cacheWidth ?? width <= height) {
|
||||
memCacheWidth = width.cacheSize(context);
|
||||
} else {
|
||||
memCacheHeight = height?.cacheSize(context);
|
||||
memCacheHeight = height.cacheSize(context);
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
imageUrl: ImageUtils.thumbnailUrl(src, quality),
|
||||
@@ -88,36 +75,36 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
height: height,
|
||||
memCacheWidth: memCacheWidth,
|
||||
memCacheHeight: memCacheHeight,
|
||||
fit: boxFit ?? BoxFit.cover,
|
||||
alignment: isLongPic ? Alignment.topCenter : Alignment.center,
|
||||
fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120),
|
||||
fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120),
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
fadeOutDuration: fadeOutDuration,
|
||||
fadeInDuration: fadeInDuration,
|
||||
filterQuality: FilterQuality.low,
|
||||
placeholder: (BuildContext context, String url) =>
|
||||
getPlaceHolder?.call() ?? _placeholder(context, noRadius),
|
||||
imageBuilder: imageBuilder,
|
||||
errorWidget: (context, url, error) => _placeholder(context, noRadius),
|
||||
placeholder: (_, _) =>
|
||||
getPlaceHolder?.call() ??
|
||||
_placeholder(context, isEmote: isEmote, isAvatar: isAvatar),
|
||||
errorWidget: (_, _, _) =>
|
||||
_placeholder(context, isEmote: isEmote, isAvatar: isAvatar),
|
||||
colorBlendMode: reduce ? BlendMode.modulate : null,
|
||||
color: reduce ? reduceLuxColor : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _placeholder(BuildContext context, bool noRadius) {
|
||||
final isAvatar = type == ImageType.avatar;
|
||||
Widget _placeholder(
|
||||
BuildContext context, {
|
||||
required bool isEmote,
|
||||
required bool isAvatar,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
clipBehavior: noRadius ? Clip.none : Clip.antiAlias,
|
||||
clipBehavior: isEmote ? Clip.none : Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
shape: isAvatar ? BoxShape.circle : BoxShape.rectangle,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
||||
borderRadius: noRadius || isAvatar
|
||||
? null
|
||||
: radius != null
|
||||
? BorderRadius.circular(radius!)
|
||||
: StyleString.mdRadius,
|
||||
borderRadius: isEmote || isAvatar ? null : borderRadius,
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user