mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-14 05:03:57 +08:00
Compare commits
237 Commits
1.1.5-pre2
...
1.1.5.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
038f03a4e7 | ||
|
|
219228f8b5 | ||
|
|
1f64de5954 | ||
|
|
e9b5cffa91 | ||
|
|
68872f7b14 | ||
|
|
bd158619a4 | ||
|
|
310f497c30 | ||
|
|
30ee413852 | ||
|
|
0ab07a713e | ||
|
|
7eaf05839a | ||
|
|
777c3c2278 | ||
|
|
b9b54ce4f7 | ||
|
|
92e5fae29c | ||
|
|
05e8ded86a | ||
|
|
7a65b777c9 | ||
|
|
0b1f6c4d0e | ||
|
|
923af32c96 | ||
|
|
4eae7e698f | ||
|
|
5a61dbe30c | ||
|
|
036dbcaf21 | ||
|
|
bd97f9a500 | ||
|
|
33278a74b2 | ||
|
|
397f887b91 | ||
|
|
ebe793ccfc | ||
|
|
68464e4e34 | ||
|
|
395893fc7d | ||
|
|
f5657d2d4c | ||
|
|
a3ddc83430 | ||
|
|
d2f8aff421 | ||
|
|
25148509d2 | ||
|
|
2879d0dc00 | ||
|
|
90349189ee | ||
|
|
bdc524e486 | ||
|
|
cb58822009 | ||
|
|
4a2679a589 | ||
|
|
09bd1edeb3 | ||
|
|
00da3c4a0e | ||
|
|
c40d794180 | ||
|
|
34a839d9e2 | ||
|
|
f06d0605ce | ||
|
|
ef975de624 | ||
|
|
d10c737a38 | ||
|
|
28b69a06fa | ||
|
|
069cf555ea | ||
|
|
836ab311d6 | ||
|
|
dbc11c36df | ||
|
|
fffce10b31 | ||
|
|
de85e82bfa | ||
|
|
9855b35b65 | ||
|
|
5a0b045a1f | ||
|
|
c226f8f6df | ||
|
|
fd06fa9cc4 | ||
|
|
2b5f111fb1 | ||
|
|
9f5ce5ae37 | ||
|
|
3d95165d46 | ||
|
|
cfb72f27ac | ||
|
|
bcacc41db3 | ||
|
|
b2da99e334 | ||
|
|
041af37bb0 | ||
|
|
80e007bac6 | ||
|
|
87c7699324 | ||
|
|
11912c5f62 | ||
|
|
236a8b3023 | ||
|
|
63e4bac204 | ||
|
|
2e11247af4 | ||
|
|
13f377f680 | ||
|
|
b9d594bc8b | ||
|
|
2a52157c3f | ||
|
|
a037d8e793 | ||
|
|
49b7ea14c3 | ||
|
|
0a40d11133 | ||
|
|
dff6b6486d | ||
|
|
b51c646415 | ||
|
|
25acf3a9bb | ||
|
|
7ec90e9a22 | ||
|
|
645ce0b7b3 | ||
|
|
864fef5881 | ||
|
|
eea232c6db | ||
|
|
25fca498fc | ||
|
|
c9a02f9c74 | ||
|
|
99602eea95 | ||
|
|
b5fe0faeec | ||
|
|
20a36e8f9a | ||
|
|
161bf2eedb | ||
|
|
fcf4e72d8e | ||
|
|
b46cb69df4 | ||
|
|
43c7620b4c | ||
|
|
1a8f65b075 | ||
|
|
259e7080f8 | ||
|
|
7da6f05a50 | ||
|
|
521ca3ad18 | ||
|
|
31e5692dff | ||
|
|
191bcbc525 | ||
|
|
a0f3b3e442 | ||
|
|
5bcd822251 | ||
|
|
d80324655e | ||
|
|
952d168022 | ||
|
|
af723e161c | ||
|
|
3ff521e103 | ||
|
|
b4a5d985f5 | ||
|
|
1e0e2d2d6e | ||
|
|
d7f7611af4 | ||
|
|
11cdb67050 | ||
|
|
53cf9d54c4 | ||
|
|
2e73688688 | ||
|
|
ce5e85e64b | ||
|
|
02e0d34127 | ||
|
|
830f3b60e0 | ||
|
|
b4fb7d14d4 | ||
|
|
ab1e5cb62a | ||
|
|
348a9e014e | ||
|
|
0baf3fcd36 | ||
|
|
13818533a7 | ||
|
|
0dd3689d65 | ||
|
|
23b6850778 | ||
|
|
d8ca89ac8f | ||
|
|
ae06d5f7f2 | ||
|
|
62506d3eb5 | ||
|
|
f7c61d63a0 | ||
|
|
f46437f891 | ||
|
|
1cd949c365 | ||
|
|
bc5ce11449 | ||
|
|
cef4beaa0c | ||
|
|
02bd68f697 | ||
|
|
2bc3275c1f | ||
|
|
ec107063c3 | ||
|
|
4c2fd38d6c | ||
|
|
1a6653ba93 | ||
|
|
74d5e03a34 | ||
|
|
2b4b1debe6 | ||
|
|
17883eb77e | ||
|
|
3741fe54ff | ||
|
|
ec11af3827 | ||
|
|
890dc58dc3 | ||
|
|
b12bdf2eb8 | ||
|
|
59c7f8a030 | ||
|
|
50cf74ccf7 | ||
|
|
15b5c0a874 | ||
|
|
244ef22f54 | ||
|
|
b4daf5fbd8 | ||
|
|
0519ec0e4b | ||
|
|
ff4f97de1a | ||
|
|
773bdafec3 | ||
|
|
3787f99d35 | ||
|
|
2cb8331528 | ||
|
|
5b6443cfa4 | ||
|
|
6fd8212d8b | ||
|
|
0d273f6909 | ||
|
|
255e39b709 | ||
|
|
ea52dd4484 | ||
|
|
b4a46133be | ||
|
|
7c1644efc4 | ||
|
|
775e1aa97d | ||
|
|
2a55d4390a | ||
|
|
d57a34a4e1 | ||
|
|
2785248615 | ||
|
|
c42468e2c8 | ||
|
|
196ddf3f5f | ||
|
|
27302435be | ||
|
|
2b3ec77e92 | ||
|
|
b7a277a57c | ||
|
|
9c8e5b53e7 | ||
|
|
001b746f65 | ||
|
|
a78214de3c | ||
|
|
d88ffb1127 | ||
|
|
f05b901009 | ||
|
|
430837eef6 | ||
|
|
fa583ebd0f | ||
|
|
d2dcba5a59 | ||
|
|
fb5116d525 | ||
|
|
a48f6b1ca5 | ||
|
|
fc0af3f284 | ||
|
|
2288e11398 | ||
|
|
d95283c4ac | ||
|
|
4b56bd5a87 | ||
|
|
62bb605ee8 | ||
|
|
0f8da1999a | ||
|
|
21a2373a5c | ||
|
|
2ca5310825 | ||
|
|
9ccaa3072b | ||
|
|
ded78e534f | ||
|
|
9b0a43efc9 | ||
|
|
10808c2a84 | ||
|
|
38a7afd63a | ||
|
|
54b26d20fa | ||
|
|
ad2bc78ebd | ||
|
|
c4aca389a8 | ||
|
|
cb8333d4c0 | ||
|
|
2f5eed6998 | ||
|
|
935c53e452 | ||
|
|
dd0ccb327b | ||
|
|
919134759b | ||
|
|
c1d42b498a | ||
|
|
a7e67796f1 | ||
|
|
6692c9e851 | ||
|
|
ace949aaa0 | ||
|
|
fbd9687432 | ||
|
|
460a8262c1 | ||
|
|
c8de503fae | ||
|
|
a60cd51ff4 | ||
|
|
aad980ce23 | ||
|
|
e7cda7b9fa | ||
|
|
1d368b7a8b | ||
|
|
725d7055bf | ||
|
|
1fb798db4e | ||
|
|
8e1d5e0dd5 | ||
|
|
2d9a1310b9 | ||
|
|
588ec7babd | ||
|
|
2be13e7283 | ||
|
|
d5d95671ff | ||
|
|
a0eccda6ff | ||
|
|
ec82c86210 | ||
|
|
de03bef226 | ||
|
|
0f8166620e | ||
|
|
76c2de4394 | ||
|
|
0d38ded981 | ||
|
|
646888c06f | ||
|
|
332f6f1bb4 | ||
|
|
aaab5371b2 | ||
|
|
ad931d7ea2 | ||
|
|
377e430d74 | ||
|
|
a797467606 | ||
|
|
5ee83d902d | ||
|
|
27ae296b28 | ||
|
|
e589f27195 | ||
|
|
c89d6a5a59 | ||
|
|
861365930d | ||
|
|
0d4d92a202 | ||
|
|
4c6ad0e385 | ||
|
|
ad45e995e2 | ||
|
|
50a035a479 | ||
|
|
c0dbd6cbb2 | ||
|
|
686af4a330 | ||
|
|
46aad06e34 | ||
|
|
3921b2304d | ||
|
|
bca5b0419c | ||
|
|
9754b061dd |
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -119,21 +119,21 @@ jobs:
|
||||
PiliPlus_android_*.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Android_arm64-v8a
|
||||
path: |
|
||||
PiliPlus_android_*_arm64-v8a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Android_armeabi-v7a
|
||||
path: |
|
||||
PiliPlus_android_*_armeabi-v7a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Android_x86_64
|
||||
path: |
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
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:
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
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:
|
||||
|
||||
6
.github/workflows/ios.yml
vendored
6
.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
|
||||
|
||||
@@ -34,6 +34,8 @@ jobs:
|
||||
run: |
|
||||
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
# 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
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
PiliPlus_ios_*.ipa
|
||||
|
||||
- name: Upload ios release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: iOS-release
|
||||
path: PiliPlus_ios_*.ipa
|
||||
|
||||
20
.github/workflows/linux_x64.yml
vendored
20
.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
|
||||
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
|
||||
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
printf "下载 appimagetool...\n"
|
||||
@@ -195,13 +195,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/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
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
|
||||
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 +236,25 @@ jobs:
|
||||
PiliPlus_linux_*.AppImage
|
||||
|
||||
- name: Upload linux targz package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Linux_targz_amd64_packege
|
||||
path: PiliPlus_linux_*.tar.gz
|
||||
|
||||
- name: Upload linux deb package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Linux_deb_amd64_package
|
||||
path: PiliPlus_linux_*.deb
|
||||
|
||||
- name: Upload linux rpm package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Linux_rpm_amd64_package
|
||||
path: PiliPlus_linux_*.rpm
|
||||
|
||||
|
||||
- name: Upload linux AppImage package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Linux_AppImage_amd64_package
|
||||
path: PiliPlus_linux_*.AppImage
|
||||
|
||||
4
.github/workflows/mac.yml
vendored
4
.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
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
PiliPlus_macos_*.dmg
|
||||
|
||||
- name: Upload macos release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: macOS-release
|
||||
path: PiliPlus_macos_*.dmg
|
||||
|
||||
6
.github/workflows/win_x64.yml
vendored
6
.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
|
||||
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
- name: Upload windows file release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Windows-file-x64-release
|
||||
path: Release
|
||||
|
||||
- name: Upload windows setup release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Windows-setup-x64-release
|
||||
path: PiliPlus-Win-Setup
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 编辑动态
|
||||
- [x] DLNA 投屏
|
||||
- [x] 离线缓存/播放
|
||||
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
@@ -218,8 +219,8 @@
|
||||
|
||||
## 声明
|
||||
|
||||
此项目(PiliPlus)是个人为了兴趣而开发, 仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
||||
此项目(PiliPlus)是个人为了兴趣而开发,仅用于学习和测试,请于下载后24小时内删除。
|
||||
所用API皆从官方网站收集,不提供任何破解内容。
|
||||
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
|
||||
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
|
||||
本仓库做了更激进的修改,感谢原作者的开源精神。
|
||||
|
||||
@@ -12,7 +12,8 @@ include: package:flutter_lints/flutter.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/grpc/bilibili/**
|
||||
- lib/grpc/google/**
|
||||
# - lib/grpc/google/**
|
||||
# - lib/common/widgets/flutter/**
|
||||
|
||||
formatter:
|
||||
trailing_commas: preserve
|
||||
@@ -63,5 +64,13 @@ 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
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,23 @@
|
||||
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 = 22 / 9;
|
||||
static const bottomSheetRadius = BorderRadius.vertical(
|
||||
top: Radius.circular(18),
|
||||
);
|
||||
static const dialogFixedConstraints = BoxConstraints(
|
||||
minWidth: 420,
|
||||
maxWidth: 420,
|
||||
);
|
||||
}
|
||||
|
||||
class Constants {
|
||||
abstract final class Constants {
|
||||
static const appName = 'PiliPlus';
|
||||
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
|
||||
|
||||
@@ -20,9 +26,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 +46,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',
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
54
lib/common/widgets/avatars.dart
Normal file
54
lib/common/widgets/avatars.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
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 offset = size - gap;
|
||||
if (users.length == 1) {
|
||||
return NetworkImgLayer(
|
||||
src: users.first.face,
|
||||
width: size,
|
||||
height: size,
|
||||
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(.8),
|
||||
child: NetworkImgLayer(
|
||||
src: e.$2.face,
|
||||
width: size - .8,
|
||||
height: size - .8,
|
||||
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(
|
||||
@@ -66,11 +59,20 @@ class ColorPalette extends StatelessWidget {
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
decoration: showBgColor
|
||||
? BoxDecoration(
|
||||
color: colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
)
|
||||
: null,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,9 +1,14 @@
|
||||
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';
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
ContainerRenderObjectMixin,
|
||||
RenderBoxContainerDefaultsMixin,
|
||||
MultiChildLayoutParentData;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum TooltipType { top, right }
|
||||
|
||||
@@ -13,99 +18,37 @@ class CustomTooltip extends StatefulWidget {
|
||||
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) {
|
||||
@@ -121,9 +64,8 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
|
||||
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
|
||||
verticalOffset: box.size.height / 2,
|
||||
horizontslOffset: box.size.width / 2,
|
||||
horizontalOffset: box.size.width / 2,
|
||||
type: widget.type,
|
||||
animation: _overlayAnimation,
|
||||
target: target,
|
||||
onDismiss: _scheduleDismissTooltip,
|
||||
overlayWidget: widget.overlayWidget,
|
||||
@@ -138,11 +80,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 +91,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,
|
||||
@@ -172,170 +113,317 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
}
|
||||
}
|
||||
|
||||
enum _ChildType { overlay, indicator }
|
||||
|
||||
class _CustomTooltipOverlay extends StatelessWidget {
|
||||
const _CustomTooltipOverlay({
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.horizontalOffset,
|
||||
required this.type,
|
||||
required this.animation,
|
||||
required this.target,
|
||||
required this.onDismiss,
|
||||
required this.overlayWidget,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final double verticalOffset;
|
||||
final double horizontslOffset;
|
||||
final double horizontalOffset;
|
||||
final TooltipType type;
|
||||
final Animation<double> animation;
|
||||
final Offset target;
|
||||
final VoidCallback onDismiss;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = CustomMultiChildLayout(
|
||||
delegate: _CustomMultiTooltipPositionDelegate(
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: false,
|
||||
),
|
||||
return _ToolTip(
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontalOffset: horizontalOffset,
|
||||
preferBelow: false,
|
||||
onTap: PlatformUtils.isMobile ? onDismiss : null,
|
||||
children: [
|
||||
LayoutId(
|
||||
id: 'overlay',
|
||||
id: _ChildType.indicator,
|
||||
child: indicator(),
|
||||
),
|
||||
LayoutId(
|
||||
id: _ChildType.overlay,
|
||||
child: overlayWidget(),
|
||||
),
|
||||
if (indicator != null)
|
||||
LayoutId(
|
||||
id: 'indicator',
|
||||
child: indicator!(),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (Utils.isMobile) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomMultiTooltipPositionDelegate({
|
||||
class _ToolTip extends MultiChildRenderObjectWidget {
|
||||
const _ToolTip({
|
||||
super.children,
|
||||
this.onTap,
|
||||
required this.type,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.horizontalOffset,
|
||||
required this.preferBelow,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final TooltipType type;
|
||||
|
||||
final Offset target;
|
||||
|
||||
final double verticalOffset;
|
||||
|
||||
final double horizontslOffset;
|
||||
|
||||
final double horizontalOffset;
|
||||
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,
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontalOffset: horizontalOffset,
|
||||
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
|
||||
..verticalOffset = verticalOffset
|
||||
..horizontalOffset = horizontalOffset
|
||||
..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 TooltipType type,
|
||||
required Offset target,
|
||||
required double verticalOffset,
|
||||
required double horizontalOffset,
|
||||
required bool preferBelow,
|
||||
}) : _type = type,
|
||||
_target = target,
|
||||
_verticalOffset = verticalOffset,
|
||||
_horizontalOffset = horizontalOffset,
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
final TooltipType _type;
|
||||
|
||||
Offset _target;
|
||||
Offset get target => _target;
|
||||
set target(Offset value) {
|
||||
if (_target == value) return;
|
||||
_target = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _verticalOffset;
|
||||
double get verticalOffset => _verticalOffset;
|
||||
set verticalOffset(double value) {
|
||||
if (_verticalOffset == value) return;
|
||||
_verticalOffset = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _horizontalOffset;
|
||||
double get horizontalOffset => _horizontalOffset;
|
||||
set horizontalOffset(double value) {
|
||||
if (_horizontalOffset == value) return;
|
||||
_horizontalOffset = 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;
|
||||
|
||||
switch (_type) {
|
||||
case TooltipType.top:
|
||||
Offset offset = _positionDependentBox(
|
||||
type: _type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontalOffset: horizontalOffset,
|
||||
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,
|
||||
);
|
||||
case TooltipType.right:
|
||||
Offset offset = _positionDependentBox(
|
||||
type: _type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontalOffset: horizontalOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
|
||||
overlayParentData.offset = offset;
|
||||
Offset(
|
||||
offset.dx - indicatorSize.width + 1,
|
||||
target.dy - indicatorSize.height / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final childParentData = child.parentData as MultiChildLayoutParentData;
|
||||
context.paintChild(child, childParentData.offset + offset);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
class Triangle extends LeafRenderObjectWidget {
|
||||
const Triangle({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.size,
|
||||
this.type = .top,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final Size size;
|
||||
final TooltipType type;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderTriangle(
|
||||
color: color,
|
||||
preferredSize: size,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderTriangle renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..color = color
|
||||
..preferredSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
class TrianglePainter extends CustomPainter {
|
||||
TrianglePainter(this.color, {this.type = TooltipType.top});
|
||||
final TooltipType type;
|
||||
final Color color;
|
||||
class RenderTriangle extends RenderBox {
|
||||
RenderTriangle({
|
||||
required Color color,
|
||||
required Size preferredSize,
|
||||
required TooltipType type,
|
||||
}) : _color = color,
|
||||
_preferredSize = preferredSize,
|
||||
_type = type;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
final TooltipType _type;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
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) {
|
||||
switch (_type) {
|
||||
case TooltipType.top:
|
||||
path = Path()
|
||||
..moveTo(0, 0)
|
||||
@@ -350,21 +438,21 @@ class TrianglePainter extends CustomPainter {
|
||||
..close();
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
context.canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color;
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
Offset positionDependentBox({
|
||||
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 horizontalOffset = 0.0,
|
||||
double margin = 10.0,
|
||||
}) {
|
||||
switch (type) {
|
||||
@@ -397,7 +485,7 @@ Offset positionDependentBox({
|
||||
case TooltipType.right:
|
||||
final double dy = math.max(margin, target.dy - childSize.height / 2);
|
||||
final double dx = math.min(
|
||||
target.dx + horizontslOffset,
|
||||
target.dx + horizontalOffset,
|
||||
size.width - childSize.width - margin,
|
||||
);
|
||||
return Offset(dx, dy);
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
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,
|
||||
}) {
|
||||
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),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
Object? content,
|
||||
// @Deprecated('use `bool result = await showConfirmDialog()` instead')
|
||||
VoidCallback? onConfirm,
|
||||
}) async {
|
||||
assert(content is String? || content is Widget);
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: true);
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
void showPgcFollowDialog({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -8,25 +9,22 @@ 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,
|
||||
),
|
||||
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,
|
||||
@@ -40,11 +38,7 @@ Future<void> autoWrapReportDialog(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 22,
|
||||
right: 22,
|
||||
bottom: 5,
|
||||
),
|
||||
padding: .only(left: 22, right: 22, bottom: 5),
|
||||
child: Text('请选择举报的理由:'),
|
||||
),
|
||||
RadioGroup(
|
||||
@@ -65,27 +59,23 @@ Future<void> autoWrapReportDialog(
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 22,
|
||||
top: 5,
|
||||
right: 22,
|
||||
),
|
||||
child: Form(
|
||||
padding: const .only(left: 22, top: 5, right: 22),
|
||||
child: TextFormField(
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -94,13 +84,14 @@ Future<void> autoWrapReportDialog(
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
text: '拉黑该用户',
|
||||
onChanged: (value) => banUid = value,
|
||||
if (ban)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
text: '拉黑该用户',
|
||||
onChanged: (value) => banUid = value,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -108,7 +99,7 @@ Future<void> autoWrapReportDialog(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
style: TextStyle(color: ColorScheme.of(context).outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
@@ -119,18 +110,18 @@ Future<void> autoWrapReportDialog(
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final data = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
final res = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (data['code'] == 0) {
|
||||
if (res.isSuccess) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
SmartDialog.showToast(data['message'].toString());
|
||||
res.toast();
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
if (kDebugMode) rethrow;
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
@@ -168,7 +159,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 +192,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 +254,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('确定'),
|
||||
|
||||
@@ -4,61 +4,141 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
|
||||
final Color? color;
|
||||
final double lineLengthScale;
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
const DisabledIcon({
|
||||
super.key,
|
||||
required T child,
|
||||
this.disable = false,
|
||||
this.color,
|
||||
this.iconSize,
|
||||
double? lineLengthScale,
|
||||
StrokeCap? strokeCap,
|
||||
}) : lineLengthScale = lineLengthScale ?? 0.9,
|
||||
strokeCap = strokeCap ?? StrokeCap.butt,
|
||||
super(child: child);
|
||||
|
||||
final bool disable;
|
||||
final Color? color;
|
||||
final double? iconSize;
|
||||
final StrokeCap strokeCap;
|
||||
final double lineLengthScale;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
late final iconTheme = IconTheme.of(context);
|
||||
return RenderMaskedIcon(
|
||||
color ??
|
||||
(child is Icon
|
||||
? (child as Icon).color ?? IconTheme.of(context).color!
|
||||
: IconTheme.of(context).color!),
|
||||
lineLengthScale,
|
||||
strokeCap,
|
||||
disable: disable,
|
||||
iconSize:
|
||||
iconSize ??
|
||||
(child is Icon ? (child as Icon).size : null) ??
|
||||
iconTheme.size ??
|
||||
24.0,
|
||||
color:
|
||||
color ??
|
||||
(child is Icon ? (child as Icon).color : null) ??
|
||||
iconTheme.color!,
|
||||
strokeCap: strokeCap,
|
||||
lineLengthScale: lineLengthScale,
|
||||
);
|
||||
}
|
||||
|
||||
T enable() => child as T;
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) {
|
||||
late final iconTheme = IconTheme.of(context);
|
||||
renderObject
|
||||
..disable = disable
|
||||
..iconSize =
|
||||
iconSize ??
|
||||
(child is Icon ? (child as Icon?)?.size : null) ??
|
||||
iconTheme.size ??
|
||||
24.0
|
||||
..color =
|
||||
color ??
|
||||
(child is Icon ? (child as Icon?)?.color : null) ??
|
||||
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;
|
||||
|
||||
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);
|
||||
offset = Offset(dx, dy);
|
||||
} else if (size.width < _iconSize && size.height < _iconSize) {
|
||||
size = Size.square(_iconSize);
|
||||
}
|
||||
|
||||
final strokeWidth = size.width / 12;
|
||||
|
||||
var rect = offset & size;
|
||||
|
||||
final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4
|
||||
|
||||
// final path = Path.combine(
|
||||
// PathOperation.difference,
|
||||
// Path()..addRect(rect),
|
||||
// Path()..moveTo(rect.left, rect.top)
|
||||
// ..relativeLineTo(sqrt2Width, 0)
|
||||
// ..lineTo(rect.right, rect.bottom - sqrt2Width)
|
||||
// ..lineTo(rect.right, rect.bottom)
|
||||
// ..close(),
|
||||
// );
|
||||
|
||||
final path = Path.combine(
|
||||
PathOperation.union,
|
||||
Path() // bottom
|
||||
@@ -75,9 +155,9 @@ class RenderMaskedIcon extends RenderProxyBox {
|
||||
canvas
|
||||
..save()
|
||||
..clipPath(path, doAntiAlias: false);
|
||||
super.paint(context, offset);
|
||||
super.paint(context, .zero);
|
||||
|
||||
context.canvas.restore();
|
||||
canvas.restore();
|
||||
|
||||
final linePaint = Paint()
|
||||
..color = color
|
||||
@@ -94,9 +174,7 @@ class RenderMaskedIcon extends RenderProxyBox {
|
||||
linePaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DisabledIconExt on Icon {
|
||||
DisabledIcon<Icon> disable([double? lineLengthScale]) =>
|
||||
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
|
||||
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,
|
||||
@@ -43,10 +43,10 @@ class DynamicSliverAppBarMedium extends StatefulWidget {
|
||||
this.forceMaterialTransparency = false,
|
||||
this.clipBehavior,
|
||||
this.appBarClipper,
|
||||
this.callback,
|
||||
this.afterCalc,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? callback;
|
||||
final ValueChanged<double>? afterCalc;
|
||||
final Widget? flexibleSpace;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
@@ -93,56 +93,45 @@ class DynamicSliverAppBarMedium extends StatefulWidget {
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final GlobalKey _key = GlobalKey();
|
||||
double? _height;
|
||||
double? _width;
|
||||
late double _topPadding;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_topPadding = MediaQuery.viewPaddingOf(context).top;
|
||||
final width = MediaQuery.widthOf(context);
|
||||
if (_width != width) {
|
||||
_width = width;
|
||||
_height = 0;
|
||||
_updateHeight();
|
||||
_height = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
|
||||
if (_height == 0) {
|
||||
if (_height == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_height =
|
||||
(_key.currentContext!.findRenderObject() as RenderBox).size.height;
|
||||
widget.afterCalc?.call(_height!);
|
||||
setState(() {});
|
||||
});
|
||||
return SliverToBoxAdapter(
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
width: _width,
|
||||
child: widget.flexibleSpace,
|
||||
child: OnlyLayoutWidget(
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
key: _key,
|
||||
width: _width,
|
||||
child: widget.flexibleSpace,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final padding = MediaQuery.viewPaddingOf(context).top;
|
||||
return SliverAppBar.medium(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
@@ -170,8 +159,8 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
|
||||
onStretchTrigger: widget.onStretchTrigger,
|
||||
shape: widget.shape,
|
||||
toolbarHeight: kToolbarHeight,
|
||||
collapsedHeight: kToolbarHeight + padding + 1,
|
||||
expandedHeight: _height - padding,
|
||||
collapsedHeight: kToolbarHeight + _topPadding + 1,
|
||||
expandedHeight: _height! - _topPadding,
|
||||
leadingWidth: widget.leadingWidth,
|
||||
toolbarTextStyle: widget.toolbarTextStyle,
|
||||
titleTextStyle: widget.titleTextStyle,
|
||||
|
||||
@@ -13,7 +13,7 @@ library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dyn/ink_well.dart';
|
||||
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';
|
||||
@@ -21,9 +21,6 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
|
||||
abstract class _ParentInkResponseState {
|
||||
void markChildInkResponsePressed(
|
||||
_ParentInkResponseState childState,
|
||||
@@ -144,6 +141,7 @@ class InkResponse extends StatelessWidget {
|
||||
this.onTapCancel,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.onLongPressUp,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onSecondaryTapDown,
|
||||
@@ -197,6 +195,19 @@ class InkResponse extends StatelessWidget {
|
||||
/// Called when the user long-presses on this part of the material.
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
|
||||
/// Called when the user lifts their finger after a long press on the button.
|
||||
///
|
||||
/// This callback is triggered at the end of a long press gesture, specifically
|
||||
/// after the user holds a long press and then releases it. It does not include
|
||||
/// position details.
|
||||
///
|
||||
/// Common use cases include performing an action only after the long press completes,
|
||||
/// such as displaying a context menu or confirming a held gesture.
|
||||
///
|
||||
/// See also:
|
||||
/// * [onLongPress], which is triggered when the long press gesture is first recognized.
|
||||
final GestureLongPressUpCallback? onLongPressUp;
|
||||
|
||||
/// Called when the user taps this part of the material with a secondary button.
|
||||
///
|
||||
/// See also:
|
||||
@@ -237,7 +248,7 @@ class InkResponse extends StatelessWidget {
|
||||
/// become highlighted and false if this part of the material has stopped
|
||||
/// being highlighted.
|
||||
///
|
||||
/// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a
|
||||
/// If all of [onTap], [onDoubleTap], [onLongPress], and [onLongPressUp] become null while a
|
||||
/// gesture is ongoing, then [onTapCancel] will be fired and
|
||||
/// [onHighlightChanged] will be fired with the value false _during the
|
||||
/// build_. This means, for instance, that in that scenario [State.setState]
|
||||
@@ -493,6 +504,7 @@ class InkResponse extends StatelessWidget {
|
||||
onTapCancel: onTapCancel,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
onLongPressUp: onLongPressUp,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapUp: onSecondaryTapUp,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
@@ -550,6 +562,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||
this.onTapCancel,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.onLongPressUp,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onSecondaryTapDown,
|
||||
@@ -588,6 +601,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||
final GestureTapCallback? onTapCancel;
|
||||
final GestureTapCallback? onDoubleTap;
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
final GestureLongPressUpCallback? onLongPressUp;
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
final GestureTapUpCallback? onSecondaryTapUp;
|
||||
final GestureTapDownCallback? onSecondaryTapDown;
|
||||
@@ -628,6 +642,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||
if (onTap != null) 'tap',
|
||||
if (onDoubleTap != null) 'double tap',
|
||||
if (onLongPress != null) 'long press',
|
||||
if (onLongPressUp != null) 'long press up',
|
||||
if (onTapDown != null) 'tap down',
|
||||
if (onTapUp != null) 'tap up',
|
||||
if (onTapCancel != null) 'tap cancel',
|
||||
@@ -1099,6 +1114,12 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||
}
|
||||
}
|
||||
|
||||
void handleLongPressUp() {
|
||||
_currentSplash?.confirm();
|
||||
_currentSplash = null;
|
||||
widget.onLongPressUp?.call();
|
||||
}
|
||||
|
||||
void handleSecondaryTap() {
|
||||
_currentSplash?.confirm();
|
||||
_currentSplash = null;
|
||||
@@ -1140,6 +1161,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||
return widget.onTap != null ||
|
||||
widget.onDoubleTap != null ||
|
||||
widget.onLongPress != null ||
|
||||
widget.onLongPressUp != null ||
|
||||
widget.onTapUp != null ||
|
||||
widget.onTapDown != null;
|
||||
}
|
||||
@@ -1276,6 +1298,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||
onLongPress: widget.onLongPress != null
|
||||
? handleLongPress
|
||||
: null,
|
||||
onLongPressUp: widget.onLongPressUp != null
|
||||
? handleLongPressUp
|
||||
: null,
|
||||
onSecondaryTapDown: _secondaryEnabled
|
||||
? handleSecondaryTapDown
|
||||
: null,
|
||||
@@ -1387,6 +1412,7 @@ class InkWell extends InkResponse {
|
||||
super.onTap,
|
||||
super.onDoubleTap,
|
||||
super.onLongPress,
|
||||
super.onLongPressUp,
|
||||
super.onTapDown,
|
||||
super.onTapUp,
|
||||
super.onTapCancel,
|
||||
@@ -12,7 +12,7 @@ library;
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dyn/button.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
|
||||
|
||||
@@ -337,6 +337,7 @@ class ListTile extends StatelessWidget {
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onFocusChange,
|
||||
this.mouseCursor,
|
||||
this.selected = false,
|
||||
@@ -569,6 +570,8 @@ class ListTile extends StatelessWidget {
|
||||
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
final GestureTapUpCallback? onSecondaryTapUp;
|
||||
|
||||
/// {@macro flutter.material.inkwell.onFocusChange}
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
@@ -914,10 +917,7 @@ class ListTile extends StatelessWidget {
|
||||
WidgetState.disabled,
|
||||
};
|
||||
final MouseCursor effectiveMouseCursor =
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(
|
||||
mouseCursor,
|
||||
mouseStates,
|
||||
) ??
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates) ??
|
||||
tileTheme.mouseCursor?.resolve(mouseStates) ??
|
||||
WidgetStateMouseCursor.clickable.resolve(mouseStates);
|
||||
|
||||
@@ -986,6 +986,7 @@ class ListTile extends StatelessWidget {
|
||||
onTap: enabled ? onTap : null,
|
||||
onLongPress: enabled ? onLongPress : null,
|
||||
onSecondaryTap: enabled ? onSecondaryTap : null,
|
||||
onSecondaryTapUp: enabled ? onSecondaryTapUp : null,
|
||||
onFocusChange: onFocusChange,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
canRequestFocus: enabled,
|
||||
@@ -1330,12 +1331,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;
|
||||
@@ -10,8 +10,9 @@
|
||||
/// @docImport 'text.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart'
|
||||
show DragStartBehavior, HorizontalDragGestureRecognizer;
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
@@ -41,18 +42,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 +62,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 +75,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 +90,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 +111,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 T horizontalDragGestureRecognizer;
|
||||
|
||||
/// Creates a scrollable list that works page by page using widgets that are
|
||||
/// created on demand.
|
||||
@@ -123,7 +123,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 +141,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 +159,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 +170,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 +193,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 +263,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 +302,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 +331,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();
|
||||
@@ -388,9 +387,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 +416,7 @@ class _CustomPageViewState extends State<CustomPageView> {
|
||||
],
|
||||
);
|
||||
},
|
||||
horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -19,40 +19,39 @@ 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 +61,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 +69,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 +85,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 +102,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 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 +133,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 +153,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 +189,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 +200,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 +215,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 +300,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 +310,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 +330,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;
|
||||
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 +378,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 +435,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 +466,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(
|
||||
@@ -491,7 +487,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 +502,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 +524,7 @@ class _ScrollableScope extends InheritedWidget {
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final CustomScrollableState scrollable;
|
||||
final ScrollableState scrollable;
|
||||
final ScrollPosition position;
|
||||
|
||||
@override
|
||||
@@ -537,30 +533,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 +657,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
_fallbackScrollController = ScrollController();
|
||||
}
|
||||
super.initState();
|
||||
_animController = AnimationController(
|
||||
vsync: this,
|
||||
reverseDuration: const Duration(milliseconds: 500),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -677,7 +670,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 +697,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 +739,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
|
||||
position.dispose();
|
||||
_persistedScrollOffset.dispose();
|
||||
|
||||
_animController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -777,12 +768,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 +814,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;
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -888,65 +868,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 +887,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 +900,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,8 +1013,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
return false;
|
||||
}
|
||||
|
||||
late double _maxWidth;
|
||||
|
||||
Widget _buildChrome(BuildContext context, Widget child) {
|
||||
final ScrollableDetails details = ScrollableDetails(
|
||||
direction: widget.axisDirection,
|
||||
@@ -1177,32 +1090,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 +1138,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 +1211,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;
|
||||
@@ -1682,7 +1570,7 @@ class _ScrollableSelectionContainerDelegate
|
||||
bool _globalPositionInScrollable(Offset globalPosition) {
|
||||
final RenderBox 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 = Rect.fromLTRB(0, 0, box.size.width, box.size.height);
|
||||
return rect.contains(localPosition);
|
||||
}
|
||||
|
||||
@@ -1775,7 +1663,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 +1682,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.
|
||||
@@ -1937,7 +1825,7 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
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 +1870,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
210
lib/common/widgets/flutter/page/scrollable_helpers.dart
Normal file
210
lib/common/widgets/flutter/page/scrollable_helpers.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
// 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 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 [CustomScrollable] 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 RenderBox 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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/page/page_view.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide TabBarView, PageView;
|
||||
import 'package:flutter/gestures.dart'
|
||||
show DragStartBehavior, HorizontalDragGestureRecognizer;
|
||||
import 'package:flutter/material.dart' hide PageView;
|
||||
|
||||
/// A page view that displays the widget which corresponds to the currently
|
||||
/// selected tab.
|
||||
@@ -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 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(),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
44
lib/common/widgets/flutter/pop_scope.dart
Normal file
44
lib/common/widgets/flutter/pop_scope.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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';
|
||||
|
||||
abstract class PopScopeState<T extends StatefulWidget> extends State<T>
|
||||
implements PopEntry<T> {
|
||||
ModalRoute<dynamic>? _route;
|
||||
|
||||
@override
|
||||
void onPopInvoked(bool didPop) {}
|
||||
|
||||
@override
|
||||
late final ValueNotifier<bool> canPopNotifier;
|
||||
|
||||
void initCanPopNotifier() {
|
||||
canPopNotifier = ValueNotifier<bool>(false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initCanPopNotifier();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
|
||||
if (nextRoute != _route) {
|
||||
_route?.unregisterPopEntry(this);
|
||||
_route = nextRoute;
|
||||
_route?.registerPopEntry(this);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_route?.unregisterPopEntry(this);
|
||||
canPopNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
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,3 +1,12 @@
|
||||
// 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 'color_scheme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@@ -6,25 +15,10 @@ 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.
|
||||
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
|
||||
@@ -403,15 +397,15 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
_effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
final Color color = _effectiveValueColor;
|
||||
if (color.alpha == 0x00) {
|
||||
if (color.a == 0) {
|
||||
// Set an always stopped animation instead of a driven tween.
|
||||
_valueColor = AlwaysStoppedAnimation<Color>(color);
|
||||
} else {
|
||||
// Respect the alpha of the given color.
|
||||
_valueColor = _positionController.drive(
|
||||
ColorTween(
|
||||
begin: color.withAlpha(0),
|
||||
end: color.withAlpha(color.alpha),
|
||||
begin: color.withValues(alpha: 0),
|
||||
end: color,
|
||||
).chain(
|
||||
CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)),
|
||||
),
|
||||
@@ -555,7 +549,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
1.0,
|
||||
); // This triggers various rebuilds.
|
||||
if (_status == RefreshIndicatorStatus.drag &&
|
||||
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
|
||||
_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
_status = RefreshIndicatorStatus.armed;
|
||||
widget.onStatusChange?.call(_status);
|
||||
}
|
||||
@@ -749,7 +743,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
|
||||
case _IndicatorType.noSpinner:
|
||||
return Container();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -762,3 +756,14 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget refreshIndicator({
|
||||
required RefreshCallback onRefresh,
|
||||
required Widget child,
|
||||
}) {
|
||||
return RefreshIndicator(
|
||||
displacement: displacement,
|
||||
onRefresh: onRefresh,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
/// @docImport 'editable.dart';
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
@@ -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,6 +1,10 @@
|
||||
import 'dart:ui' as ui;
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
|
||||
@@ -260,7 +264,7 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
|
||||
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||
renderObject
|
||||
..text = text
|
||||
..text = (text: text, primary: primary)
|
||||
..textAlign = textAlign
|
||||
..textDirection = textDirection ?? Directionality.of(context)
|
||||
..softWrap = softWrap
|
||||
@@ -273,75 +277,75 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
..locale = locale ?? Localizations.maybeLocaleOf(context)
|
||||
..registrar = selectionRegistrar
|
||||
..selectionColor = selectionColor
|
||||
..primary = primary
|
||||
..onShowMore = onShowMore;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
properties.add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
);
|
||||
properties.add(StringProperty('text', text.toPlainText()));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties
|
||||
..add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'))
|
||||
..add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
)
|
||||
..add(StringProperty('text', text.toPlainText()))
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ library;
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/text/rich_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide RichText;
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
@@ -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
|
||||
@@ -410,6 +416,7 @@ class Text extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
locale: locale,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
primary: primary,
|
||||
@@ -442,6 +449,7 @@ class Text extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
locale: locale,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
onShowMore: onShowMore,
|
||||
@@ -1105,7 +1113,7 @@ class _SelectableTextContainerDelegate
|
||||
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()!;
|
||||
@@ -9,7 +9,7 @@
|
||||
/// @docImport 'text_field.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -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;
|
||||
@@ -588,7 +588,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 +599,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 +704,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 +736,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 +787,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
width: 22, // emote.width,
|
||||
height: 22, // emote.height,
|
||||
type: ImageType.emote,
|
||||
boxFit: BoxFit.contain,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -846,7 +846,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 +866,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 +891,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 +902,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 +930,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 +963,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 +994,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 +1046,7 @@ class RichTextEditingController extends TextEditingController {
|
||||
String text = '';
|
||||
final start = selection.start;
|
||||
final end = selection.end;
|
||||
for (var e in items) {
|
||||
for (final e in items) {
|
||||
final range = e.range;
|
||||
if (start >= range.end) {
|
||||
continue;
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/rendering.dart';
|
||||
@@ -209,7 +209,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children?.isEmpty ?? false) || (buttonItems?.isEmpty ?? false)) {
|
||||
if ((children ?? buttonItems)?.isEmpty ?? true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
/// @docImport 'package:flutter/cupertino.dart';
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
@@ -16,10 +15,10 @@ import 'dart:ui'
|
||||
SemanticsInputType,
|
||||
TextBox;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -153,7 +152,10 @@ class VerticalCaretMovementRun implements Iterator<TextPosition> {
|
||||
final TextPosition closestPosition = _editable._textPainter
|
||||
.getPositionForOffset(newOffset);
|
||||
final MapEntry<Offset, TextPosition> position =
|
||||
MapEntry<Offset, TextPosition>(newOffset, closestPosition);
|
||||
MapEntry<Offset, TextPosition>(
|
||||
newOffset,
|
||||
closestPosition,
|
||||
);
|
||||
_positionCache[lineNumber] = position;
|
||||
return position;
|
||||
}
|
||||
@@ -294,8 +296,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,
|
||||
@@ -1301,7 +1303,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 +1313,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,
|
||||
@@ -1381,6 +1387,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.
|
||||
@@ -1437,8 +1444,7 @@ class RenderEditable 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!) {
|
||||
@@ -1507,8 +1513,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,
|
||||
@@ -2203,17 +2210,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;
|
||||
@@ -2265,7 +2269,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 +2281,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 +2473,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 +2527,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)),
|
||||
@@ -2603,8 +2602,9 @@ class RenderEditable extends RenderBox
|
||||
_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
|
||||
@@ -2942,28 +2942,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,11 +3155,13 @@ 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) {
|
||||
canvas.drawRect(
|
||||
@@ -3166,7 +3169,7 @@ class _TextHighlightPainter extends RenderEditablePainter {
|
||||
.toRect()
|
||||
.shift(renderEditable._paintOffset)
|
||||
.intersect(
|
||||
Rect.fromLTWH(0, 0, textPainter.width, textPainter.height),
|
||||
Rect.fromLTRB(0, 0, textPainter.width, textPainter.height),
|
||||
),
|
||||
highlightPaint,
|
||||
);
|
||||
@@ -3215,7 +3218,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 +3249,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;
|
||||
}
|
||||
|
||||
@@ -3318,7 +3321,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 ||
|
||||
@@ -20,44 +20,25 @@ import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui hide TextStyle;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_selection.dart';
|
||||
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/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide
|
||||
EditableText,
|
||||
EditableTextState,
|
||||
SpellCheckConfiguration,
|
||||
buildTextSpanWithSpellCheckSuggestions,
|
||||
TextSelectionOverlay,
|
||||
TextSelectionGestureDetectorBuilder;
|
||||
TextSelectionGestureDetectorBuilder,
|
||||
TextSelectionOverlay;
|
||||
import 'package:flutter/rendering.dart'
|
||||
hide RenderEditable, VerticalCaretMovementRun;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
export 'package:flutter/services.dart'
|
||||
show
|
||||
KeyboardInsertedContent,
|
||||
SelectionChangedCause,
|
||||
SmartDashesType,
|
||||
SmartQuotesType,
|
||||
TextEditingValue,
|
||||
TextInputType,
|
||||
TextSelection;
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
// late WidgetTester tester;
|
||||
|
||||
/// Signature for the callback that reports when the user changes the selection
|
||||
/// (including the cursor location).
|
||||
typedef SelectionChangedCallback =
|
||||
void Function(TextSelection selection, SelectionChangedCause? cause);
|
||||
|
||||
/// Signature for a widget builder that builds a context menu for the given
|
||||
/// [EditableTextState].
|
||||
///
|
||||
@@ -135,54 +116,6 @@ class _RenderCompositionCallback extends RenderProxyBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// A controller for an editable text field.
|
||||
///
|
||||
/// Whenever the user modifies a text field with an associated
|
||||
/// [RichTextEditingController], the text field updates [value] and the controller
|
||||
/// notifies its listeners. Listeners can then read the [text] and [selection]
|
||||
/// properties to learn what the user has typed or how the selection has been
|
||||
/// updated.
|
||||
///
|
||||
/// Similarly, if you modify the [text] or [selection] properties, the text
|
||||
/// field will be notified and will update itself appropriately.
|
||||
///
|
||||
/// A [RichTextEditingController] can also be used to provide an initial value for a
|
||||
/// text field. If you build a text field with a controller that already has
|
||||
/// [text], the text field will use that text as its initial value.
|
||||
///
|
||||
/// The [value] (as well as [text] and [selection]) of this controller can be
|
||||
/// updated from within a listener added to this controller. Be aware of
|
||||
/// infinite loops since the listener will also be notified of the changes made
|
||||
/// from within itself. Modifying the composing region from within a listener
|
||||
/// can also have a bad interaction with some input methods. Gboard, for
|
||||
/// example, will try to restore the composing region of the text if it was
|
||||
/// modified programmatically, creating an infinite loop of communications
|
||||
/// between the framework and the input method. Consider using
|
||||
/// [TextInputFormatter]s instead for as-you-type text modification.
|
||||
///
|
||||
/// If both the [text] and [selection] properties need to be changed, set the
|
||||
/// controller's [value] instead. Setting [text] will clear the selection
|
||||
/// and composing range.
|
||||
///
|
||||
/// Remember to [dispose] of the [RichTextEditingController] when it is no longer
|
||||
/// needed. This will ensure we discard any resources used by the object.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example creates a [TextField] with a [RichTextEditingController] whose
|
||||
/// change listener forces the entered text to be lower case and keeps the
|
||||
/// cursor at the end of the input.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextField], which is a Material Design text field that can be controlled
|
||||
/// with a [RichTextEditingController].
|
||||
/// * [EditableText], which is a raw region of editable text that can be
|
||||
/// controlled with a [RichTextEditingController].
|
||||
/// * Learn how to use a [RichTextEditingController] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
|
||||
|
||||
// A time-value pair that represents a key frame in an animation.
|
||||
class _KeyFrame {
|
||||
const _KeyFrame(this.time, this.value);
|
||||
@@ -506,7 +439,7 @@ class EditableText extends StatefulWidget {
|
||||
this.readOnly = false,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
bool? autocorrect,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
@@ -556,12 +489,13 @@ class EditableText extends StatefulWidget {
|
||||
this.cursorOpacityAnimates = false,
|
||||
this.cursorOffset,
|
||||
this.paintCursorAboveText = false,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
ui.BoxHeightStyle? selectionHeightStyle,
|
||||
ui.BoxWidthStyle? selectionWidthStyle,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.keyboardAppearance = Brightness.light,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
bool? enableInteractiveSelection,
|
||||
bool? selectAllOnFocus,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autocorrectionTextRectColor,
|
||||
@@ -586,7 +520,10 @@ class EditableText extends StatefulWidget {
|
||||
this.contextMenuBuilder,
|
||||
this.spellCheckConfiguration,
|
||||
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
|
||||
this.hintLocales,
|
||||
}) : assert(obscuringCharacter.length == 1),
|
||||
autocorrect =
|
||||
autocorrect ?? _inferAutocorrect(autofillHints: autofillHints),
|
||||
smartDashesType =
|
||||
smartDashesType ??
|
||||
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
@@ -608,6 +545,7 @@ class EditableText extends StatefulWidget {
|
||||
),
|
||||
enableInteractiveSelection =
|
||||
enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||
selectAllOnFocus = selectAllOnFocus ?? _defaultSelectAllOnFocus,
|
||||
toolbarOptions =
|
||||
selectionControls is TextSelectionHandleControls &&
|
||||
toolbarOptions == null
|
||||
@@ -647,7 +585,10 @@ class EditableText extends StatefulWidget {
|
||||
...inputFormatters ?? const Iterable<TextInputFormatter>.empty(),
|
||||
]
|
||||
: inputFormatters,
|
||||
showCursor = showCursor ?? !readOnly;
|
||||
showCursor = showCursor ?? !readOnly,
|
||||
selectionHeightStyle =
|
||||
selectionHeightStyle ?? defaultSelectionHeightStyle,
|
||||
selectionWidthStyle = selectionWidthStyle ?? defaultSelectionWidthStyle;
|
||||
|
||||
/// Controls the text being edited.
|
||||
final RichTextEditingController controller;
|
||||
@@ -737,7 +678,7 @@ class EditableText extends StatefulWidget {
|
||||
/// {@template flutter.widgets.editableText.autocorrect}
|
||||
/// Whether to enable autocorrection.
|
||||
///
|
||||
/// Defaults to true.
|
||||
/// False on iOS if [autofillHints] contains password-related hints, otherwise true.
|
||||
/// {@endtemplate}
|
||||
final bool autocorrect;
|
||||
|
||||
@@ -1302,7 +1243,7 @@ class EditableText extends StatefulWidget {
|
||||
/// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set
|
||||
/// [rendererIgnoresPointer] to true.
|
||||
///
|
||||
/// When [rendererIgnoresPointer] is true true, the [RenderEditable] created
|
||||
/// When [rendererIgnoresPointer] is true, the [RenderEditable] created
|
||||
/// by this widget will not handle pointer events.
|
||||
///
|
||||
/// This property is false by default.
|
||||
@@ -1396,8 +1337,9 @@ class EditableText extends StatefulWidget {
|
||||
/// cut/copy/paste menu, and tapping to move the text caret.
|
||||
///
|
||||
/// When this is false, the text selection cannot be adjusted by
|
||||
/// the user, text cannot be copied, and the user cannot paste into
|
||||
/// the text field from the clipboard.
|
||||
/// the user, the cut/copy/paste menu is hidden, and the shortcuts to
|
||||
/// cut/copy/paste text do nothing but stop propagation of the key event
|
||||
/// to other key event handlers in the focus chain.
|
||||
///
|
||||
/// Defaults to true.
|
||||
/// {@endtemplate}
|
||||
@@ -1480,6 +1422,16 @@ class EditableText extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// {@template flutter.widgets.editableText.selectAllOnFocus}
|
||||
/// Whether this field should select all text when gaining focus.
|
||||
///
|
||||
/// When false, focusing this text field will leave its
|
||||
/// existing text selection unchanged.
|
||||
///
|
||||
/// Defaults to true on web and desktop platforms, and false on mobile platforms.
|
||||
/// {@endtemplate}
|
||||
final bool selectAllOnFocus;
|
||||
|
||||
/// {@template flutter.widgets.editableText.autofillHints}
|
||||
/// A list of strings that helps the autofill service identify the type of this
|
||||
/// text input.
|
||||
@@ -1714,12 +1666,62 @@ class EditableText extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.magnifier.intro}
|
||||
final TextMagnifierConfiguration magnifierConfiguration;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.hintLocales}
|
||||
final List<Locale>? hintLocales;
|
||||
|
||||
/// The default value for [selectionHeightStyle].
|
||||
///
|
||||
/// On web platforms, this defaults to [ui.BoxHeightStyle.max].
|
||||
///
|
||||
/// On native platforms, this defaults to [ui.BoxHeightStyle.includeLineSpacingMiddle] for all
|
||||
/// platforms.
|
||||
static ui.BoxHeightStyle get defaultSelectionHeightStyle {
|
||||
if (kIsWeb) {
|
||||
return ui.BoxHeightStyle.max;
|
||||
}
|
||||
return ui.BoxHeightStyle.includeLineSpacingMiddle;
|
||||
}
|
||||
|
||||
/// The default value for [selectionWidthStyle].
|
||||
///
|
||||
/// On web platforms, this defaults to [ui.BoxWidthStyle.max] for Apple platforms running
|
||||
/// Safari (webkit) based browsers and [ui.BoxWidthStyle.tight] for all others.
|
||||
///
|
||||
/// On non-web platforms, this defaults to [ui.BoxWidthStyle.max].
|
||||
static ui.BoxWidthStyle get defaultSelectionWidthStyle {
|
||||
// if (kIsWeb) {
|
||||
// if (defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
// WebBrowserDetection.isSafari) {
|
||||
// // On macOS web, the selection width behavior differs when running on
|
||||
// // Chrom(e|ium) (blink) or Safari (webkit).
|
||||
// return ui.BoxWidthStyle.max;
|
||||
// }
|
||||
// return ui.BoxWidthStyle.tight;
|
||||
// }
|
||||
return ui.BoxWidthStyle.max;
|
||||
}
|
||||
|
||||
/// The default value for [stylusHandwritingEnabled].
|
||||
static const bool defaultStylusHandwritingEnabled = true;
|
||||
|
||||
bool get _userSelectionEnabled =>
|
||||
enableInteractiveSelection && (!readOnly || !obscureText);
|
||||
|
||||
/// The default value for [selectAllOnFocus].
|
||||
static bool get _defaultSelectAllOnFocus {
|
||||
if (kIsWeb) {
|
||||
return true;
|
||||
}
|
||||
return switch (defaultTargetPlatform) {
|
||||
TargetPlatform.android => false,
|
||||
TargetPlatform.iOS => false,
|
||||
TargetPlatform.fuchsia => false,
|
||||
TargetPlatform.linux => true,
|
||||
TargetPlatform.macOS => true,
|
||||
TargetPlatform.windows => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
|
||||
/// platform's default selection menu for an editable field.
|
||||
///
|
||||
@@ -1818,6 +1820,38 @@ class EditableText extends StatefulWidget {
|
||||
return resultButtonItem;
|
||||
}
|
||||
|
||||
// Infer the value of autocorrect from autofillHints.
|
||||
static bool _inferAutocorrect({required Iterable<String>? autofillHints}) {
|
||||
if (autofillHints == null || autofillHints.isEmpty || kIsWeb) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
// username, password and newPassword are password related hint.
|
||||
// newUsername is not supported on iOS.
|
||||
final bool passwordRelatedHint = autofillHints.any(
|
||||
(String hint) =>
|
||||
hint == AutofillHints.username ||
|
||||
hint == AutofillHints.password ||
|
||||
hint == AutofillHints.newPassword,
|
||||
);
|
||||
if (passwordRelatedHint) {
|
||||
// https://github.com/flutter/flutter/issues/134723
|
||||
// Set autocorrect to false to prevent password bar from flashing.
|
||||
return false;
|
||||
}
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Infer the keyboard type of an `EditableText` if it's not specified.
|
||||
static TextInputType _inferKeyboardType({
|
||||
required Iterable<String>? autofillHints,
|
||||
@@ -2000,7 +2034,7 @@ class EditableText extends StatefulWidget {
|
||||
DiagnosticsProperty<bool>(
|
||||
'autocorrect',
|
||||
autocorrect,
|
||||
defaultValue: true,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
@@ -2136,6 +2170,13 @@ class EditableText extends StatefulWidget {
|
||||
? const <String>[]
|
||||
: kDefaultContentInsertionMimeTypes,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<List<Locale>?>(
|
||||
'hintLocales',
|
||||
hintLocales,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2438,6 +2479,7 @@ class EditableTextState extends State<EditableText>
|
||||
if (selection.isCollapsed || widget.obscureText) {
|
||||
return;
|
||||
}
|
||||
// bggRGjQaUbCoE copySelection
|
||||
final String text =
|
||||
widget.controller.getSelectionText(selection) ??
|
||||
selection.textInside(textEditingValue.text);
|
||||
@@ -2455,7 +2497,6 @@ class EditableTextState extends State<EditableText>
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
// Collapse the selection and hide the toolbar and handles.
|
||||
|
||||
userUpdateTextEditingValue(
|
||||
TextEditingValue(
|
||||
text: textEditingValue.text,
|
||||
@@ -2480,6 +2521,7 @@ class EditableTextState extends State<EditableText>
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
// bggRGjQaUbCoE cutSelection
|
||||
final String text =
|
||||
widget.controller.getSelectionText(selection) ??
|
||||
selection.textInside(textEditingValue.text);
|
||||
@@ -2528,12 +2570,7 @@ class EditableTextState extends State<EditableText>
|
||||
selection.baseOffset,
|
||||
selection.extentOffset,
|
||||
);
|
||||
// final TextEditingValue collapsedTextEditingValue =
|
||||
// textEditingValue.copyWith(
|
||||
// selection: TextSelection.collapsed(offset: lastSelectionIndex),
|
||||
// );
|
||||
// final newValue = collapsedTextEditingValue.replaced(selection, text);
|
||||
|
||||
// bggRGjQaUbCoE _pasteText
|
||||
widget.controller.syncRichText(
|
||||
selection.isCollapsed
|
||||
? TextEditingDeltaInsertion(
|
||||
@@ -2551,15 +2588,12 @@ class EditableTextState extends State<EditableText>
|
||||
composing: TextRange.empty,
|
||||
),
|
||||
);
|
||||
|
||||
final newValue = _value.copyWith(
|
||||
text: widget.controller.plainText,
|
||||
selection: widget.controller.newSelection,
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
|
||||
userUpdateTextEditingValue(newValue, cause);
|
||||
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -2579,7 +2613,6 @@ class EditableTextState extends State<EditableText>
|
||||
// selecting it.
|
||||
return;
|
||||
}
|
||||
|
||||
userUpdateTextEditingValue(
|
||||
textEditingValue.copyWith(
|
||||
selection: TextSelection(
|
||||
@@ -3368,6 +3401,19 @@ class EditableTextState extends State<EditableText>
|
||||
// editing.
|
||||
if (!_isMultiline) {
|
||||
_finalizeEditing(action, shouldUnfocus: true);
|
||||
} else if (HardwareKeyboard.instance.isControlPressed) {
|
||||
final ctr = widget.controller;
|
||||
final offset = ctr.selection.end;
|
||||
// delete newline
|
||||
ctr.syncRichText(
|
||||
TextEditingDeltaDeletion(
|
||||
composing: TextRange.empty,
|
||||
selection: TextSelection.collapsed(offset: offset - 1),
|
||||
deletedRange: TextRange(start: offset - 1, end: offset),
|
||||
oldText: ctr.text,
|
||||
),
|
||||
);
|
||||
_finalizeEditing(action, shouldUnfocus: true);
|
||||
}
|
||||
case TextInputAction.done:
|
||||
case TextInputAction.go:
|
||||
@@ -3520,7 +3566,9 @@ class EditableTextState extends State<EditableText>
|
||||
);
|
||||
case FloatingCursorDragState.End:
|
||||
// Resume cursor blinking.
|
||||
_startCursorBlink();
|
||||
if (_hasFocus) {
|
||||
_startCursorBlink();
|
||||
}
|
||||
// We skip animation if no update has happened.
|
||||
if (_lastTextPosition != null && _lastBoundedOffset != null) {
|
||||
_floatingCursorResetController!.value = 0.0;
|
||||
@@ -4417,15 +4465,6 @@ class EditableTextState extends State<EditableText>
|
||||
final bool textCommitted =
|
||||
!oldValue.composing.isCollapsed && value.composing.isCollapsed;
|
||||
final bool selectionChanged = oldValue.selection != value.selection;
|
||||
// if (!textChanged && selectionChanged) {
|
||||
// value = value.copyWith(
|
||||
// selection: widget.controller.updateSelection(
|
||||
// oldSelection: _value.selection,
|
||||
// newSelection: value.selection,
|
||||
// cause: cause,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
if (textChanged || textCommitted) {
|
||||
// Only apply input formatters if the text has changed (including uncommitted
|
||||
@@ -4689,17 +4728,9 @@ class EditableTextState extends State<EditableText>
|
||||
|
||||
TextSelection? _adjustedSelectionWhenFocused() {
|
||||
TextSelection? selection;
|
||||
final bool isDesktop = switch (defaultTargetPlatform) {
|
||||
TargetPlatform.android ||
|
||||
TargetPlatform.iOS ||
|
||||
TargetPlatform.fuchsia => false,
|
||||
TargetPlatform.macOS ||
|
||||
TargetPlatform.linux ||
|
||||
TargetPlatform.windows => true,
|
||||
};
|
||||
final bool shouldSelectAll =
|
||||
widget.selectAllOnFocus &&
|
||||
widget.selectionEnabled &&
|
||||
(kIsWeb || isDesktop) &&
|
||||
!_isMultiline &&
|
||||
!_nextFocusChangeIsInternal &&
|
||||
!_justResumed;
|
||||
@@ -5043,10 +5074,10 @@ class EditableTextState extends State<EditableText>
|
||||
}
|
||||
|
||||
/// Shows the magnifier at the position given by `positionToShow`,
|
||||
/// if there is no magnifier visible.
|
||||
/// if no magnifier exists.
|
||||
///
|
||||
/// Updates the magnifier to the position given by `positionToShow`,
|
||||
/// if there is a magnifier visible.
|
||||
/// if a magnifier exits.
|
||||
///
|
||||
/// Does nothing if a magnifier couldn't be shown, such as when the selection
|
||||
/// overlay does not currently exist.
|
||||
@@ -5055,22 +5086,20 @@ class EditableTextState extends State<EditableText>
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectionOverlay!.magnifierIsVisible) {
|
||||
if (_selectionOverlay!.magnifierExists) {
|
||||
_selectionOverlay!.updateMagnifier(positionToShow);
|
||||
} else {
|
||||
_selectionOverlay!.showMagnifier(positionToShow);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the magnifier if it is visible.
|
||||
/// Hides the magnifier.
|
||||
void hideMagnifier() {
|
||||
if (_selectionOverlay == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectionOverlay!.magnifierIsVisible) {
|
||||
_selectionOverlay!.hideMagnifier();
|
||||
}
|
||||
_selectionOverlay!.hideMagnifier();
|
||||
}
|
||||
|
||||
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
|
||||
@@ -5161,6 +5190,7 @@ class EditableTextState extends State<EditableText>
|
||||
allowedMimeTypes: widget.contentInsertionConfiguration == null
|
||||
? const <String>[]
|
||||
: widget.contentInsertionConfiguration!.allowedMimeTypes,
|
||||
hintLocales: widget.hintLocales,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5357,10 +5387,7 @@ class EditableTextState extends State<EditableText>
|
||||
|
||||
void _replaceText(ReplaceTextIntent intent) {
|
||||
final TextEditingValue oldValue = _value;
|
||||
// final TextEditingValue newValue = intent.currentTextEditingValue.replaced(
|
||||
// intent.replacementRange,
|
||||
// intent.replacementText,
|
||||
// );
|
||||
// bggRGjQaUbCoE _replaceText
|
||||
widget.controller.syncRichText(
|
||||
intent.replacementText.isEmpty
|
||||
? TextEditingDeltaDeletion(
|
||||
@@ -5387,7 +5414,6 @@ class EditableTextState extends State<EditableText>
|
||||
selection: widget.controller.newSelection,
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
|
||||
userUpdateTextEditingValue(newValue, intent.cause);
|
||||
|
||||
// If there's no change in text and selection (e.g. when selecting and
|
||||
@@ -5509,7 +5535,6 @@ class EditableTextState extends State<EditableText>
|
||||
}
|
||||
|
||||
bringIntoView(nextSelection.extent);
|
||||
|
||||
userUpdateTextEditingValue(
|
||||
_value.copyWith(selection: nextSelection),
|
||||
SelectionChangedCause.keyboard,
|
||||
@@ -5718,7 +5743,8 @@ class EditableTextState extends State<EditableText>
|
||||
),
|
||||
),
|
||||
ScrollToDocumentBoundaryIntent: _makeOverridable(
|
||||
CallbackAction<ScrollToDocumentBoundaryIntent>(
|
||||
_WebComposingDisablingCallbackAction<ScrollToDocumentBoundaryIntent>(
|
||||
this,
|
||||
onInvoke: _scrollToDocumentBoundary,
|
||||
),
|
||||
),
|
||||
@@ -5748,11 +5774,7 @@ class EditableTextState extends State<EditableText>
|
||||
// Copy Paste
|
||||
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
||||
CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
|
||||
PasteTextIntent: _makeOverridable(
|
||||
CallbackAction<PasteTextIntent>(
|
||||
onInvoke: (PasteTextIntent intent) => pasteText(intent.cause),
|
||||
),
|
||||
),
|
||||
PasteTextIntent: _makeOverridable(_PasteSelectionAction(this)),
|
||||
|
||||
TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction),
|
||||
EditableTextTapOutsideIntent: _makeOverridable(
|
||||
@@ -5826,7 +5848,13 @@ class EditableTextState extends State<EditableText>
|
||||
? AxisDirection.down
|
||||
: AxisDirection.right,
|
||||
controller: _scrollController,
|
||||
physics: widget.scrollPhysics,
|
||||
// On iOS a single-line TextField should not scroll.
|
||||
physics:
|
||||
widget.scrollPhysics ??
|
||||
(!_isMultiline &&
|
||||
defaultTargetPlatform == TargetPlatform.iOS
|
||||
? const _NeverUserScrollableScrollPhysics()
|
||||
: null),
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
restorationId: widget.restorationId,
|
||||
// If a ScrollBehavior is not provided, only apply scrollbars when
|
||||
@@ -5970,15 +5998,19 @@ class EditableTextState extends State<EditableText>
|
||||
final int placeholderLocation = _value.text.length - _placeholderLocation;
|
||||
if (_isMultiline) {
|
||||
// The zero size placeholder here allows the line to break and keep the caret on the first line.
|
||||
placeholders.add(
|
||||
const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size.zero),
|
||||
);
|
||||
placeholders.add(
|
||||
_ScribblePlaceholder(
|
||||
child: const SizedBox.shrink(),
|
||||
size: Size(renderEditable.size.width, 0.0),
|
||||
),
|
||||
);
|
||||
placeholders
|
||||
..add(
|
||||
const _ScribblePlaceholder(
|
||||
child: SizedBox.shrink(),
|
||||
size: Size.zero,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
_ScribblePlaceholder(
|
||||
child: const SizedBox.shrink(),
|
||||
size: Size(renderEditable.size.width, 0.0),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
placeholders.add(
|
||||
const _ScribblePlaceholder(
|
||||
@@ -6061,8 +6093,8 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
this.cursorRadius,
|
||||
required this.cursorOffset,
|
||||
required this.paintCursorAboveText,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
ui.BoxHeightStyle? selectionHeightStyle,
|
||||
ui.BoxWidthStyle? selectionWidthStyle,
|
||||
this.enableInteractiveSelection = true,
|
||||
required this.textSelectionDelegate,
|
||||
required this.devicePixelRatio,
|
||||
@@ -6070,7 +6102,11 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
this.promptRectColor,
|
||||
required this.clipBehavior,
|
||||
required this.controller,
|
||||
}) : super(
|
||||
}) : selectionHeightStyle =
|
||||
selectionHeightStyle ?? EditableText.defaultSelectionHeightStyle,
|
||||
selectionWidthStyle =
|
||||
selectionWidthStyle ?? EditableText.defaultSelectionWidthStyle,
|
||||
super(
|
||||
children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler),
|
||||
);
|
||||
|
||||
@@ -6203,6 +6239,20 @@ class _Editable extends MultiChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NeverUserScrollableScrollPhysics extends ScrollPhysics {
|
||||
/// Creates a scroll physics that prevents scrolling with user input, for example
|
||||
/// by dragging, but still allows for programmatic scrolling.
|
||||
const _NeverUserScrollableScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
_NeverUserScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return _NeverUserScrollableScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
bool get allowUserScrolling => false;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class _ScribbleCacheKey {
|
||||
const _ScribbleCacheKey({
|
||||
@@ -6349,7 +6399,7 @@ class _ScribbleFocusableState extends State<_ScribbleFocusable>
|
||||
final Matrix4 transform = box.getTransformTo(null);
|
||||
return MatrixUtils.transformRect(
|
||||
transform,
|
||||
Rect.fromLTWH(0, 0, box.size.width, box.size.height),
|
||||
Rect.fromLTRB(0, 0, box.size.width, box.size.height),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6454,11 +6504,7 @@ class _CodePointBoundary extends TextBoundary {
|
||||
// ------------------------------- Text Actions -------------------------------
|
||||
class _DeleteTextAction<T extends DirectionalTextEditingIntent>
|
||||
extends ContextAction<T> {
|
||||
_DeleteTextAction(
|
||||
this.state,
|
||||
this.getTextBoundary,
|
||||
this._applyTextBoundary,
|
||||
);
|
||||
_DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary);
|
||||
|
||||
final EditableTextState state;
|
||||
final TextBoundary Function() getTextBoundary;
|
||||
@@ -6658,7 +6704,15 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent>
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb &&
|
||||
state.widget.selectionEnabled &&
|
||||
state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state._value.selection.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateTextSelectionVerticallyAction<
|
||||
@@ -6745,7 +6799,33 @@ class _UpdateTextSelectionVerticallyAction<
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb &&
|
||||
state.widget.selectionEnabled &&
|
||||
state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state._value.selection.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
class _WebComposingDisablingCallbackAction<T extends Intent>
|
||||
extends CallbackAction<T> {
|
||||
_WebComposingDisablingCallbackAction(this.state, {required super.onInvoke});
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb &&
|
||||
state.widget.selectionEnabled &&
|
||||
state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isActionEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
@@ -6755,6 +6835,10 @@ class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
|
||||
@override
|
||||
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
|
||||
if (!state.widget.selectionEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Actions.invoke(
|
||||
context!,
|
||||
UpdateSelectionIntent(
|
||||
@@ -6764,9 +6848,6 @@ class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state.widget.selectionEnabled;
|
||||
}
|
||||
|
||||
class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
|
||||
@@ -6776,16 +6857,35 @@ class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
|
||||
|
||||
@override
|
||||
void invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
|
||||
if (!state._value.selection.isValid || state._value.selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.widget.selectionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.collapseSelection) {
|
||||
state.cutSelection(intent.cause);
|
||||
} else {
|
||||
state.copySelection(intent.cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PasteSelectionAction extends ContextAction<PasteTextIntent> {
|
||||
_PasteSelectionAction(this.state);
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
bool get isActionEnabled =>
|
||||
state._value.selection.isValid && !state._value.selection.isCollapsed;
|
||||
void invoke(PasteTextIntent intent, [BuildContext? context]) {
|
||||
if (!state.widget.selectionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.pasteText(intent.cause);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [ClipboardStatusNotifier] whose [value] is hardcoded to
|
||||
119
lib/common/widgets/flutter/text_field/spell_check.dart
Normal file
119
lib/common/widgets/flutter/text_field/spell_check.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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 'editable_text.dart';
|
||||
library;
|
||||
|
||||
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.
|
||||
///
|
||||
/// This configuration determines the [SpellCheckService] used to fetch the
|
||||
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
|
||||
/// mark misspelled words within text input.
|
||||
@immutable
|
||||
class SpellCheckConfiguration {
|
||||
/// Creates a configuration that specifies the service and suggestions handler
|
||||
/// for spell check.
|
||||
const SpellCheckConfiguration({
|
||||
this.spellCheckService,
|
||||
this.misspelledSelectionColor,
|
||||
this.misspelledTextStyle,
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
}) : _spellCheckEnabled = true;
|
||||
|
||||
/// Creates a configuration that disables spell check.
|
||||
const SpellCheckConfiguration.disabled()
|
||||
: _spellCheckEnabled = false,
|
||||
spellCheckService = null,
|
||||
spellCheckSuggestionsToolbarBuilder = null,
|
||||
misspelledTextStyle = null,
|
||||
misspelledSelectionColor = null;
|
||||
|
||||
/// The service used to fetch spell check results for text input.
|
||||
final SpellCheckService? spellCheckService;
|
||||
|
||||
/// The color the paint the selection highlight when spell check is showing
|
||||
/// suggestions for a misspelled word.
|
||||
///
|
||||
/// For example, on iOS, the selection appears red while the spell check menu
|
||||
/// is showing.
|
||||
final Color? misspelledSelectionColor;
|
||||
|
||||
/// Style used to indicate misspelled words.
|
||||
///
|
||||
/// This is nullable to allow style-specific wrappers of [EditableText]
|
||||
/// to infer this, but this must be specified if this configuration is
|
||||
/// provided directly to [EditableText] or its construction will fail with an
|
||||
/// assertion error.
|
||||
final TextStyle? misspelledTextStyle;
|
||||
|
||||
/// Builds the toolbar used to display spell check suggestions for misspelled
|
||||
/// words.
|
||||
final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
|
||||
|
||||
final bool _spellCheckEnabled;
|
||||
|
||||
/// Whether or not the configuration should enable or disable spell check.
|
||||
bool get spellCheckEnabled => _spellCheckEnabled;
|
||||
|
||||
/// Returns a copy of the current [SpellCheckConfiguration] instance with
|
||||
/// specified overrides.
|
||||
SpellCheckConfiguration copyWith({
|
||||
SpellCheckService? spellCheckService,
|
||||
Color? misspelledSelectionColor,
|
||||
TextStyle? misspelledTextStyle,
|
||||
EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder,
|
||||
}) {
|
||||
if (!_spellCheckEnabled) {
|
||||
// A new configuration should be constructed to enable spell check.
|
||||
return const SpellCheckConfiguration.disabled();
|
||||
}
|
||||
|
||||
return SpellCheckConfiguration(
|
||||
spellCheckService: spellCheckService ?? this.spellCheckService,
|
||||
misspelledSelectionColor:
|
||||
misspelledSelectionColor ?? this.misspelledSelectionColor,
|
||||
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder:
|
||||
spellCheckSuggestionsToolbarBuilder ??
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'SpellCheckConfiguration')}('
|
||||
'${_spellCheckEnabled ? 'enabled' : 'disabled'}, '
|
||||
'service: $spellCheckService, '
|
||||
'text style: $misspelledTextStyle, '
|
||||
'toolbar builder: $spellCheckSuggestionsToolbarBuilder'
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is SpellCheckConfiguration &&
|
||||
other.spellCheckService == spellCheckService &&
|
||||
other.misspelledTextStyle == misspelledTextStyle &&
|
||||
other.spellCheckSuggestionsToolbarBuilder ==
|
||||
spellCheckSuggestionsToolbarBuilder &&
|
||||
other._spellCheckEnabled == _spellCheckEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
spellCheckService,
|
||||
misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder,
|
||||
_spellCheckEnabled,
|
||||
);
|
||||
}
|
||||
@@ -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/text_field/editable_text.dart';
|
||||
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;
|
||||
211
lib/common/widgets/flutter/text_field/system_context_menu.dart
Normal file
211
lib/common/widgets/flutter/text_field/system_context_menu.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
// 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';
|
||||
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';
|
||||
|
||||
/// Displays the system context menu on top of the Flutter view.
|
||||
///
|
||||
/// Currently, only supports iOS 16.0 and above and displays nothing on other
|
||||
/// platforms.
|
||||
///
|
||||
/// The context menu is the menu that appears, for example, when doing text
|
||||
/// selection. Flutter typically draws this menu itself, but this class deals
|
||||
/// with the platform-rendered context menu instead.
|
||||
///
|
||||
/// There can only be one system context menu visible at a time. Building this
|
||||
/// widget when the system context menu is already visible will hide the old one
|
||||
/// and display this one. A system context menu that is hidden is informed via
|
||||
/// [onSystemHide].
|
||||
///
|
||||
/// Pass [items] to specify the buttons that will appear in the menu. Any items
|
||||
/// without a title will be given a default title from [WidgetsLocalizations].
|
||||
///
|
||||
/// By default, [items] will be set to the result of [getDefaultItems]. This
|
||||
/// method considers the state of the [EditableTextState] so that, for example,
|
||||
/// it will only include [IOSSystemContextMenuItemCopy] if there is currently a
|
||||
/// selection to copy.
|
||||
///
|
||||
/// To check if the current device supports showing the system context menu,
|
||||
/// call [isSupported].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to create a [TextField] that uses the system context
|
||||
/// menu where supported and does not show a system notification when the user
|
||||
/// presses the "Paste" button.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenuController], which directly controls the hiding and
|
||||
/// showing of the system context menu.
|
||||
class SystemContextMenu extends StatefulWidget {
|
||||
/// Creates an instance of [SystemContextMenu] that points to the given
|
||||
/// [anchor].
|
||||
const SystemContextMenu._({
|
||||
super.key,
|
||||
required this.anchor,
|
||||
required this.items,
|
||||
this.onSystemHide,
|
||||
});
|
||||
|
||||
/// Creates an instance of [SystemContextMenu] for the field indicated by the
|
||||
/// given [EditableTextState].
|
||||
factory SystemContextMenu.editableText({
|
||||
Key? key,
|
||||
required EditableTextState editableTextState,
|
||||
List<IOSSystemContextMenuItem>? items,
|
||||
}) {
|
||||
final (
|
||||
startGlyphHeight: double startGlyphHeight,
|
||||
endGlyphHeight: double endGlyphHeight,
|
||||
) = editableTextState
|
||||
.getGlyphHeights();
|
||||
|
||||
return SystemContextMenu._(
|
||||
key: key,
|
||||
anchor: TextSelectionToolbarAnchors.getSelectionRect(
|
||||
editableTextState.renderEditable,
|
||||
startGlyphHeight,
|
||||
endGlyphHeight,
|
||||
editableTextState.renderEditable.getEndpointsForSelection(
|
||||
editableTextState.textEditingValue.selection,
|
||||
),
|
||||
),
|
||||
items: items ?? getDefaultItems(editableTextState),
|
||||
onSystemHide: () => editableTextState.hideToolbar(false),
|
||||
);
|
||||
}
|
||||
|
||||
/// The [Rect] that the context menu should point to.
|
||||
final Rect anchor;
|
||||
|
||||
/// A list of the items to be displayed in the system context menu.
|
||||
///
|
||||
/// When passed, items will be shown regardless of the state of text input.
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will produce a copy button
|
||||
/// even when there is no selection to copy. Use [EditableTextState] and/or
|
||||
/// the result of [getDefaultItems] to add and remove items based on the state
|
||||
/// 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.
|
||||
///
|
||||
/// For example, tapping outside of the context menu typically causes the
|
||||
/// system to hide the menu.
|
||||
///
|
||||
/// This is not called when showing a new system context menu causes another
|
||||
/// to be hidden.
|
||||
final VoidCallback? onSystemHide;
|
||||
|
||||
/// 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 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].
|
||||
///
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will only be included when the
|
||||
/// field represented by the [EditableTextState] has a selection.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableTextState.contextMenuButtonItems], which provides the default
|
||||
/// [ContextMenuButtonItem]s for the Flutter-rendered context menu.
|
||||
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(),
|
||||
if (editableTextState.liveTextInputEnabled)
|
||||
const IOSSystemContextMenuItemLiveText(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
State<SystemContextMenu> createState() => _SystemContextMenuState();
|
||||
}
|
||||
|
||||
class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
late final SystemContextMenuController _systemContextMenuController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_systemContextMenuController = SystemContextMenuController(
|
||||
onSystemHide: widget.onSystemHide,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_systemContextMenuController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(SystemContextMenu.isSupported(context));
|
||||
|
||||
if (widget.items.isNotEmpty) {
|
||||
final WidgetsLocalizations localizations = WidgetsLocalizations.of(
|
||||
context,
|
||||
);
|
||||
final List<IOSSystemContextMenuItemData> itemDatas = widget.items
|
||||
.map((IOSSystemContextMenuItem item) => item.getData(localizations))
|
||||
.toList();
|
||||
_systemContextMenuController.showWithItems(widget.anchor, itemDatas);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
@@ -13,75 +13,44 @@ library;
|
||||
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/adaptive_text_selection_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/cupertino/cupertino_text_field.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/spell_check_suggestions_toolbar.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/system_context_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/text_selection.dart';
|
||||
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/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';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/system_context_menu.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart';
|
||||
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 ||
|
||||
@@ -1288,7 +1307,10 @@ class RichTextFieldState extends State<RichTextField>
|
||||
.applyDefaults(themeData.inputDecorationTheme)
|
||||
.copyWith(
|
||||
enabled: _isEnabled,
|
||||
hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
|
||||
hintMaxLines:
|
||||
widget.decoration?.hintMaxLines ??
|
||||
themeData.inputDecorationTheme.hintMaxLines ??
|
||||
widget.maxLines,
|
||||
);
|
||||
|
||||
// No need to build anything if counter or counterText were given directly.
|
||||
@@ -1368,9 +1390,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 +1413,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 +1446,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 +1455,6 @@ class RichTextFieldState extends State<RichTextField>
|
||||
void dispose() {
|
||||
_effectiveFocusNode.removeListener(_handleFocusChanged);
|
||||
_focusNode?.dispose();
|
||||
// _controller?.dispose();
|
||||
_statesController.removeListener(_handleStatesControllerChange);
|
||||
_internalStatesController?.dispose();
|
||||
super.dispose();
|
||||
@@ -1476,8 +1468,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 +1563,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 +1574,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.
|
||||
@@ -1716,7 +1710,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 +1731,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 +1761,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 +1775,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 +1799,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 +1870,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 +1888,7 @@ class RichTextFieldState extends State<RichTextField>
|
||||
magnifierConfiguration:
|
||||
widget.magnifierConfiguration ??
|
||||
TextMagnifier.adaptiveMagnifierConfiguration,
|
||||
hintLocales: widget.hintLocales,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2026,26 +2023,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 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/controller.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable.dart';
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
/// @docImport 'package:flutter/cupertino.dart';
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
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
|
||||
63
lib/common/widgets/flutter/time_picker.dart
Normal file
63
lib/common/widgets/flutter/time_picker.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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';
|
||||
|
||||
Future<TimeOfDay?> showTimePicker({
|
||||
required BuildContext context,
|
||||
required TimeOfDay initialTime,
|
||||
TransitionBuilder? builder,
|
||||
bool barrierDismissible = true,
|
||||
Color? barrierColor,
|
||||
String? barrierLabel,
|
||||
bool useRootNavigator = true,
|
||||
TimePickerEntryMode initialEntryMode = TimePickerEntryMode.dial,
|
||||
String? cancelText,
|
||||
String? confirmText,
|
||||
String? helpText,
|
||||
String? errorInvalidText,
|
||||
String? hourLabelText,
|
||||
String? minuteLabelText,
|
||||
RouteSettings? routeSettings,
|
||||
EntryModeChangeCallback? onEntryModeChanged,
|
||||
Offset? anchorPoint,
|
||||
Orientation? orientation,
|
||||
Icon? switchToInputEntryModeIcon,
|
||||
Icon? switchToTimerEntryModeIcon,
|
||||
bool emptyInitialInput = false,
|
||||
BoxConstraints? constraints,
|
||||
}) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
|
||||
final Widget dialog = DialogTheme(
|
||||
data: const DialogThemeData(constraints: BoxConstraints(minWidth: 280.0)),
|
||||
child: TimePickerDialog(
|
||||
initialTime: initialTime,
|
||||
initialEntryMode: initialEntryMode,
|
||||
cancelText: cancelText,
|
||||
confirmText: confirmText,
|
||||
helpText: helpText,
|
||||
errorInvalidText: errorInvalidText,
|
||||
hourLabelText: hourLabelText,
|
||||
minuteLabelText: minuteLabelText,
|
||||
orientation: orientation,
|
||||
onEntryModeChanged: onEntryModeChanged,
|
||||
switchToInputEntryModeIcon: switchToInputEntryModeIcon,
|
||||
switchToTimerEntryModeIcon: switchToTimerEntryModeIcon,
|
||||
emptyInitialInput: emptyInitialInput,
|
||||
),
|
||||
);
|
||||
return showDialog<TimeOfDay>(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierColor: barrierColor,
|
||||
barrierLabel: barrierLabel,
|
||||
useRootNavigator: useRootNavigator,
|
||||
builder: (BuildContext context) {
|
||||
return builder == null ? dialog : builder(context, dialog);
|
||||
},
|
||||
routeSettings: routeSettings,
|
||||
anchorPoint: anchorPoint,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -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,17 @@ class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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;
|
||||
_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;
|
||||
}
|
||||
@@ -124,12 +124,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;
|
||||
|
||||
@@ -15,20 +15,32 @@
|
||||
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math' show min;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||
import 'package:PiliPlus/common/widgets/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/extension/context_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension/num_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension/size_ext.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart'
|
||||
hide CustomMultiChildLayout, MultiChildLayoutDelegate;
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
ContainerRenderObjectMixin,
|
||||
RenderBoxContainerDefaultsMixin,
|
||||
MultiChildLayoutParentData,
|
||||
BoxHitTestResult;
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/get_navigation.dart';
|
||||
|
||||
class ImageModel {
|
||||
ImageModel({
|
||||
@@ -48,15 +60,14 @@ class ImageModel {
|
||||
bool? _isLongPic;
|
||||
bool? _isLivePhoto;
|
||||
|
||||
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
|
||||
bool get isLongPic =>
|
||||
_isLongPic ??= (height / width) > StyleString.imgMaxRatio;
|
||||
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,
|
||||
@@ -74,6 +85,7 @@ class CustomGridView extends StatelessWidget {
|
||||
final bool fullScreen;
|
||||
|
||||
static bool horizontalPreview = Pref.horizontalPreview;
|
||||
static const _routes = ['/videoV', '/dynamicDetail'];
|
||||
|
||||
void onTap(BuildContext context, int index) {
|
||||
final imgList = picArr.map(
|
||||
@@ -90,6 +102,7 @@ class CustomGridView extends StatelessWidget {
|
||||
).toList();
|
||||
if (horizontalPreview &&
|
||||
!fullScreen &&
|
||||
_routes.contains(Get.currentRoute) &&
|
||||
!context.mediaQuerySize.isPortrait) {
|
||||
final scaffoldState = Scaffold.maybeOf(context);
|
||||
if (scaffoldState != null) {
|
||||
@@ -108,7 +121,7 @@ class CustomGridView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static BorderRadius borderRadius(
|
||||
static BorderRadius _borderRadius(
|
||||
int col,
|
||||
int length,
|
||||
int index, {
|
||||
@@ -133,6 +146,56 @@ class CustomGridView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static bool enableImgMenu = Pref.enableImgMenu;
|
||||
|
||||
void _showMenu(BuildContext context, Offset offset, ImageModel item) {
|
||||
HapticFeedback.mediumImpact();
|
||||
showMenu(
|
||||
context: context,
|
||||
position: PageUtils.menuPosition(offset),
|
||||
items: [
|
||||
if (PlatformUtils.isMobile)
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => ImageUtils.onShareImg(item.url),
|
||||
child: const Text('分享', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => ImageUtils.downloadImg([item.url]),
|
||||
child: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (PlatformUtils.isDesktop)
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => PageUtils.launchURL(item.url),
|
||||
child: const Text('网页打开', style: TextStyle(fontSize: 14)),
|
||||
)
|
||||
else if (picArr.length > 1)
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () =>
|
||||
ImageUtils.downloadImg(picArr.map((item) => item.url).toList()),
|
||||
child: const Text('保存全部', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (item.isLivePhoto)
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => ImageUtils.downloadLivePhoto(
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width.toInt(),
|
||||
height: item.height.toInt(),
|
||||
),
|
||||
child: Text(
|
||||
'保存${Platform.isIOS ? '实况' : '视频'}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double imageWidth;
|
||||
@@ -158,7 +221,7 @@ class CustomGridView extends StatelessWidget {
|
||||
if (width != 1) {
|
||||
imageWidth = min(imageWidth, width.toDouble());
|
||||
}
|
||||
imageHeight = imageWidth * min(ratioHW, _maxRatio);
|
||||
imageHeight = imageWidth * min(ratioHW, StyleString.imgMaxRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,13 +235,11 @@ class CustomGridView extends StatelessWidget {
|
||||
context,
|
||||
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
'assets/images/loading.png',
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
cacheWidth: imageWidth.cacheSize(context),
|
||||
),
|
||||
child: Image.asset(
|
||||
'assets/images/loading.png',
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
cacheWidth: imageWidth.cacheSize(context),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -187,38 +248,40 @@ class CustomGridView extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: maxWidth,
|
||||
height: imageHeight * row + space * (row - 1),
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _CustomGridViewDelegate(
|
||||
space: space,
|
||||
itemCount: length,
|
||||
column: column,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
),
|
||||
child: ImageGrid(
|
||||
space: space,
|
||||
column: column,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
children: List.generate(length, (index) {
|
||||
final item = picArr[index];
|
||||
final radius = borderRadius(column, length, index);
|
||||
final borderRadius = _borderRadius(column, length, index);
|
||||
return LayoutId(
|
||||
id: index,
|
||||
child: Hero(
|
||||
tag: item.url,
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(context, index),
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(context, index),
|
||||
onSecondaryTapUp: enableImgMenu && PlatformUtils.isDesktop
|
||||
? (details) =>
|
||||
_showMenu(context, details.globalPosition, item)
|
||||
: null,
|
||||
onLongPressStart: enableImgMenu && PlatformUtils.isMobile
|
||||
? (details) =>
|
||||
_showMenu(context, details.globalPosition, item)
|
||||
: null,
|
||||
child: Hero(
|
||||
tag: item.url,
|
||||
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,
|
||||
),
|
||||
NetworkImgLayer(
|
||||
src: item.url,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
borderRadius: borderRadius,
|
||||
alignment: item.isLongPic ? .topCenter : .center,
|
||||
cacheWidth: item.width <= item.height,
|
||||
getPlaceHolder: () => placeHolder,
|
||||
),
|
||||
if (item.isLivePhoto)
|
||||
const PBadge(
|
||||
@@ -245,42 +308,132 @@ class CustomGridView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomGridViewDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomGridViewDelegate({
|
||||
class ImageGrid extends MultiChildRenderObjectWidget {
|
||||
const ImageGrid({
|
||||
super.key,
|
||||
super.children,
|
||||
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),
|
||||
),
|
||||
);
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderImageGrid(
|
||||
space: space,
|
||||
column: column,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderImageGrid renderObject) {
|
||||
renderObject
|
||||
..space = space
|
||||
..column = column
|
||||
..width = width
|
||||
..height = height;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderImageGrid extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
RenderImageGrid({
|
||||
required double space,
|
||||
required int column,
|
||||
required double width,
|
||||
required double height,
|
||||
}) : _space = space,
|
||||
_column = column,
|
||||
_width = width,
|
||||
_height = height;
|
||||
|
||||
double _space;
|
||||
double get space => _space;
|
||||
set space(double value) {
|
||||
if (_space == value) return;
|
||||
_space = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
int _column;
|
||||
int get column => _column;
|
||||
set column(int value) {
|
||||
if (_column == value) return;
|
||||
_column = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double _width;
|
||||
double get width => _width;
|
||||
set width(double value) {
|
||||
if (_width == value) return;
|
||||
_width = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double _height;
|
||||
double get height => _height;
|
||||
set height(double value) {
|
||||
if (_height == value) return;
|
||||
_height = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_CustomGridViewDelegate oldDelegate) {
|
||||
return space != oldDelegate.space ||
|
||||
itemCount != oldDelegate.itemCount ||
|
||||
column != oldDelegate.column ||
|
||||
width != oldDelegate.width ||
|
||||
height != oldDelegate.height;
|
||||
void performLayout() {
|
||||
size = constraints.constrain(constraints.biggest);
|
||||
|
||||
final itemConstraints = BoxConstraints(
|
||||
minWidth: width,
|
||||
maxWidth: width,
|
||||
minHeight: height,
|
||||
maxHeight: height,
|
||||
);
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final childParentData = child.parentData as MultiChildLayoutParentData;
|
||||
final index = childParentData.id as int;
|
||||
child.layout(itemConstraints, parentUsesSize: true);
|
||||
childParentData.offset = Offset(
|
||||
(space + width) * (index % column),
|
||||
(space + height) * (index ~/ column),
|
||||
);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final childParentData = child.parentData as MultiChildLayoutParentData;
|
||||
context.paintChild(child, childParentData.offset + offset);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -39,7 +39,7 @@ class HeroDialogRoute<T> extends PageRoute<T> {
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
||||
opacity: animation.drive(CurveTween(curve: Curves.easeOut)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
// with GestureDetector's scale gesture.
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
if (widget.isAnimating?.call() == true ||
|
||||
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
|
||||
(details.pointerCount < 2 && _transformer.value.storage[0] == 1.0)) {
|
||||
widget.onPanStart?.call(details);
|
||||
return;
|
||||
}
|
||||
@@ -773,7 +773,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
// handled with GestureDetector's scale gesture.
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
if (widget.isAnimating?.call() == true ||
|
||||
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
|
||||
(details.pointerCount < 2 && _transformer.value.storage[0] == 1.0)) {
|
||||
widget.onPanUpdate?.call(details);
|
||||
return;
|
||||
}
|
||||
@@ -873,11 +873,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
|
||||
// are handled with GestureDetector's scale gesture.
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
if (_transformer.value.row0.x == 1.0) {
|
||||
if (_transformer.value.storage[0] == 1.0) {
|
||||
widget.onReset?.call();
|
||||
}
|
||||
if (widget.isAnimating?.call() == true ||
|
||||
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
|
||||
(details.pointerCount < 2 && _transformer.value.storage[0] == 1.0)) {
|
||||
widget.onPanEnd?.call(details);
|
||||
return;
|
||||
}
|
||||
@@ -922,16 +922,15 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
details.velocity.pixelsPerSecond.distance,
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
);
|
||||
_animation =
|
||||
Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(
|
||||
frictionSimulationX.finalX,
|
||||
frictionSimulationY.finalX,
|
||||
),
|
||||
).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
|
||||
);
|
||||
_animation = _controller.drive(
|
||||
Tween<Offset>(
|
||||
begin: translation,
|
||||
end: Offset(
|
||||
frictionSimulationX.finalX,
|
||||
frictionSimulationY.finalX,
|
||||
),
|
||||
).chain(CurveTween(curve: Curves.decelerate)),
|
||||
);
|
||||
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
|
||||
_animation!.addListener(_handleInertiaAnimation);
|
||||
_controller.forward();
|
||||
@@ -951,16 +950,12 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
||||
widget.interactionEndFrictionCoefficient,
|
||||
effectivelyMotionless: 0.1,
|
||||
);
|
||||
_scaleAnimation =
|
||||
Tween<double>(
|
||||
begin: scale,
|
||||
end: frictionSimulation.x(tFinal),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.decelerate,
|
||||
),
|
||||
);
|
||||
_scaleAnimation = _scaleController.drive(
|
||||
Tween<double>(
|
||||
begin: scale,
|
||||
end: frictionSimulation.x(tFinal),
|
||||
).chain(CurveTween(curve: Curves.decelerate)),
|
||||
);
|
||||
_scaleController.duration = Duration(
|
||||
milliseconds: (tFinal * 1000).round(),
|
||||
);
|
||||
|
||||
@@ -114,7 +114,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
|
||||
|
||||
_scaleAnimation = _animateController.drive(
|
||||
Tween<double>(
|
||||
begin: 1,
|
||||
begin: 1.0,
|
||||
end: 0.25,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,14 +2,16 @@ import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart';
|
||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
@@ -122,9 +124,11 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
..removeListener(listener)
|
||||
..dispose();
|
||||
_transformationController.dispose();
|
||||
for (var item in widget.sources) {
|
||||
if (item.sourceType == SourceType.networkImage) {
|
||||
CachedNetworkImageProvider(_getActualUrl(item.url)).evict();
|
||||
if (widget.quality != _quality) {
|
||||
for (final item in widget.sources) {
|
||||
if (item.sourceType == SourceType.networkImage) {
|
||||
CachedNetworkImageProvider(_getActualUrl(item.url)).evict();
|
||||
}
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
@@ -196,20 +200,19 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
void _onPageChanged(int page) {
|
||||
_player?.pause();
|
||||
currentIndex.value = page;
|
||||
var item = widget.sources[page];
|
||||
final item = widget.sources[page];
|
||||
if (item.sourceType == SourceType.livePhoto) {
|
||||
_onPlay(item.liveUrl!);
|
||||
}
|
||||
if (_transformationController.value != Matrix4.identity()) {
|
||||
// animate the reset for the transformation of the interactive viewer
|
||||
|
||||
_animation =
|
||||
Matrix4Tween(
|
||||
begin: _transformationController.value,
|
||||
end: Matrix4.identity(),
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animation = _animationController.drive(
|
||||
Matrix4Tween(
|
||||
begin: _transformationController.value,
|
||||
end: Matrix4.identity(),
|
||||
).chain(CurveTween(curve: Curves.easeOut)),
|
||||
);
|
||||
|
||||
_animationController.forward(from: 0);
|
||||
}
|
||||
@@ -272,8 +275,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onDoubleTap,
|
||||
),
|
||||
onLongPress: !isFileImg ? () => onLongPress(item) : null,
|
||||
onSecondaryTap: !isFileImg && !Utils.isMobile
|
||||
? () => onLongPress(item)
|
||||
onSecondaryTapUp: PlatformUtils.isDesktop && !isFileImg
|
||||
? (e) => _showDesktopMenu(e.globalPosition, item)
|
||||
: null,
|
||||
child: widget.itemBuilder != null
|
||||
? widget.itemBuilder!(
|
||||
@@ -325,9 +328,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
child: Hero(
|
||||
tag: item.url,
|
||||
child: switch (item.sourceType) {
|
||||
SourceType.fileImage => Image(
|
||||
SourceType.fileImage => Image.file(
|
||||
File(item.url),
|
||||
filterQuality: FilterQuality.low,
|
||||
image: FileImage(File(item.url)),
|
||||
),
|
||||
SourceType.networkImage => CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
@@ -335,10 +338,14 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
imageUrl: _getActualUrl(item.url),
|
||||
placeholderFadeInDuration: Duration.zero,
|
||||
placeholder: (context, url) {
|
||||
if (widget.quality == _quality) {
|
||||
return const SizedBox.expand();
|
||||
}
|
||||
return CachedNetworkImage(
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
imageUrl: ImageUtils.thumbnailUrl(item.url, widget.quality),
|
||||
placeholder: (_, _) => const SizedBox.expand(),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -359,7 +366,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
|
||||
void onDoubleTap() {
|
||||
Matrix4 matrix = _transformationController.value.clone();
|
||||
double currentScale = matrix.row0.x;
|
||||
double currentScale = matrix.storage[0];
|
||||
|
||||
double targetScale = widget.minScale;
|
||||
|
||||
@@ -393,19 +400,19 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
matrix.row3.w,
|
||||
]);
|
||||
|
||||
_animation =
|
||||
Matrix4Tween(
|
||||
begin: _transformationController.value,
|
||||
end: matrix,
|
||||
).animate(
|
||||
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||
);
|
||||
_animation = _animationController.drive(
|
||||
Matrix4Tween(
|
||||
begin: _transformationController.value,
|
||||
end: matrix,
|
||||
).chain(CurveTween(curve: Curves.easeOut)),
|
||||
);
|
||||
_animationController
|
||||
.forward(from: 0)
|
||||
.whenComplete(() => _onScaleChanged(targetScale));
|
||||
}
|
||||
|
||||
void onLongPress(SourceModel item) {
|
||||
HapticFeedback.mediumImpact();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@@ -415,7 +422,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (Utils.isMobile)
|
||||
if (PlatformUtils.isMobile)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
@@ -435,15 +442,12 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
ImageUtils.downloadImg(
|
||||
this.context,
|
||||
[item.url],
|
||||
);
|
||||
ImageUtils.downloadImg([item.url]);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (Utils.isDesktop)
|
||||
if (PlatformUtils.isDesktop)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
@@ -457,7 +461,6 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onTap: () {
|
||||
Get.back();
|
||||
ImageUtils.downloadImg(
|
||||
this.context,
|
||||
widget.sources.map((item) => item.url).toList(),
|
||||
);
|
||||
},
|
||||
@@ -469,7 +472,6 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
onTap: () {
|
||||
Get.back();
|
||||
ImageUtils.downloadLivePhoto(
|
||||
context: this.context,
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width!,
|
||||
@@ -477,9 +479,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
);
|
||||
},
|
||||
dense: true,
|
||||
title: const Text(
|
||||
'保存 Live Photo',
|
||||
style: TextStyle(fontSize: 14),
|
||||
title: Text(
|
||||
'保存${Platform.isIOS ? ' Live Photo' : '视频'}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -488,4 +490,39 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showDesktopMenu(Offset offset, SourceModel item) {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: PageUtils.menuPosition(offset),
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => Utils.copyText(item.url),
|
||||
child: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => ImageUtils.downloadImg([item.url]),
|
||||
child: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => PageUtils.launchURL(item.url),
|
||||
child: const Text('网页打开', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
if (item.sourceType == SourceType.livePhoto)
|
||||
PopupMenuItem(
|
||||
height: 42,
|
||||
onTap: () => ImageUtils.downloadLivePhoto(
|
||||
url: item.url,
|
||||
liveUrl: item.liveUrl!,
|
||||
width: item.width!,
|
||||
height: item.height!,
|
||||
),
|
||||
child: const Text('保存视频', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
|
||||
import 'package:PiliPlus/common/widgets/custom_arc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -26,27 +24,20 @@ class LoadingWidget extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//loading animation
|
||||
RepaintBoundary.wrap(
|
||||
Obx(
|
||||
() => CustomPaint(
|
||||
size: const Size.square(40),
|
||||
painter: ArcPainter(
|
||||
color: onSurfaceVariant,
|
||||
strokeWidth: 3,
|
||||
sweepAngle: progress.value * 2 * pi,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Arc(
|
||||
size: 40,
|
||||
color: onSurfaceVariant,
|
||||
strokeWidth: 3,
|
||||
progress: progress.value,
|
||||
),
|
||||
0,
|
||||
),
|
||||
//msg
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
),
|
||||
Text(msg, style: TextStyle(color: onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,10 +47,10 @@ class HttpError extends StatelessWidget {
|
||||
if (onReload != null)
|
||||
FilledButton.tonal(
|
||||
onPressed: onReload,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.primary.withAlpha(20),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
backgroundColor: theme.colorScheme.primary.withAlpha(20),
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
child: Text(
|
||||
btnText ?? '点击重试',
|
||||
|
||||
@@ -6,13 +6,17 @@ Widget get loadingWidget => const Center(child: CircularProgressIndicator());
|
||||
Widget get linearLoading =>
|
||||
const SliverToBoxAdapter(child: LinearProgressIndicator());
|
||||
|
||||
Widget errorWidget({errMsg, onReload}) => HttpError(
|
||||
Widget errorWidget({String? errMsg, VoidCallback? onReload}) => HttpError(
|
||||
isSliver: false,
|
||||
errMsg: errMsg,
|
||||
onReload: onReload,
|
||||
);
|
||||
|
||||
Widget scrollErrorWidget({errMsg, onReload, controller}) => CustomScrollView(
|
||||
Widget scrollErrorWidget({
|
||||
String? errMsg,
|
||||
VoidCallback? onReload,
|
||||
ScrollController? controller,
|
||||
}) => CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
HttpError(
|
||||
|
||||
@@ -288,7 +288,7 @@ class _BounceMarqueeRender extends MarqueeRender {
|
||||
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
|
||||
}
|
||||
} else {
|
||||
paintCenter(context, offset);
|
||||
context.paintChild(child!, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ class _NormalMarqueeRender extends MarqueeRender {
|
||||
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
|
||||
}
|
||||
} else {
|
||||
paintCenter(context, offset);
|
||||
context.paintChild(child, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,12 +399,12 @@ class _MarqueeSimulation extends Simulation {
|
||||
class ContextSingleTicker implements TickerProvider {
|
||||
Ticker? _ticker;
|
||||
BuildContext context;
|
||||
final bool autoStart;
|
||||
final ValueGetter<bool>? autoStart;
|
||||
|
||||
ContextSingleTicker(this.context, {this.autoStart = true});
|
||||
ContextSingleTicker(this.context, {this.autoStart});
|
||||
|
||||
void initStart() {
|
||||
if (autoStart) {
|
||||
if (autoStart?.call() ?? true) {
|
||||
_ticker?.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MouseBackDetector extends StatelessWidget {
|
||||
const MouseBackDetector({
|
||||
super.key,
|
||||
required this.onTapDown,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final VoidCallback onTapDown;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (event.buttons == kBackMouseButton) {
|
||||
onTapDown();
|
||||
}
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
17
lib/common/widgets/only_layout_widget.dart
Normal file
17
lib/common/widgets/only_layout_widget.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show RenderProxyBox;
|
||||
|
||||
class OnlyLayoutWidget extends SingleChildRenderObjectWidget {
|
||||
const OnlyLayoutWidget({
|
||||
super.key,
|
||||
super.child,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => Layout();
|
||||
}
|
||||
|
||||
class Layout extends RenderProxyBox {
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/common/avatar_badge_type.dart';
|
||||
import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/extension/num_ext.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PendantAvatar extends StatelessWidget {
|
||||
@@ -44,6 +43,23 @@ class PendantAvatar extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isMemberAvatar = size == 80;
|
||||
Widget? pendant;
|
||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty) {
|
||||
final pendantSize = size * 1.75;
|
||||
pendant = Positioned(
|
||||
// -(size * 1.75 - size) / 2
|
||||
top: -0.375 * size + (size == 80 ? 2 : 0),
|
||||
child: IgnorePointer(
|
||||
child: NetworkImgLayer(
|
||||
type: .emote,
|
||||
width: pendantSize,
|
||||
height: pendantSize,
|
||||
src: garbPendantImage,
|
||||
getPlaceHolder: () => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
clipBehavior: Clip.none,
|
||||
@@ -55,19 +71,7 @@ class PendantAvatar extends StatelessWidget {
|
||||
onTap: onTap,
|
||||
child: _buildAvatar(colorScheme, isMemberAvatar),
|
||||
),
|
||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty)
|
||||
Positioned(
|
||||
top:
|
||||
-0.375 *
|
||||
(size == 80 ? size - 4 : size), // -(size * 1.75 - size) / 2
|
||||
child: IgnorePointer(
|
||||
child: CachedNetworkImage(
|
||||
width: size * 1.75,
|
||||
height: size * 1.75,
|
||||
imageUrl: ImageUtils.thumbnailUrl(garbPendantImage),
|
||||
),
|
||||
),
|
||||
),
|
||||
?pendant,
|
||||
if (roomId != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
@@ -83,8 +87,9 @@ class PendantAvatar extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 16,
|
||||
applyTextScaling: true,
|
||||
Icons.equalizer_rounded,
|
||||
size: MediaQuery.textScalerOf(context).scale(16),
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
Text(
|
||||
@@ -101,7 +106,7 @@ class PendantAvatar extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
else if (_badgeType != BadgeType.none)
|
||||
_buildBadge(colorScheme, isMemberAvatar),
|
||||
_buildBadge(context, colorScheme, isMemberAvatar),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -133,11 +138,17 @@ class PendantAvatar extends StatelessWidget {
|
||||
type: ImageType.avatar,
|
||||
);
|
||||
|
||||
Widget _buildBadge(ColorScheme colorScheme, bool isMemberAvatar) {
|
||||
Widget _buildBadge(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
bool isMemberAvatar,
|
||||
) {
|
||||
final child = switch (_badgeType) {
|
||||
BadgeType.vip => Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
width: badgeSize,
|
||||
height: badgeSize,
|
||||
cacheWidth: badgeSize.cacheSize(context),
|
||||
semanticLabel: _badgeType.desc,
|
||||
),
|
||||
_ => Icon(
|
||||
|
||||
@@ -4,15 +4,13 @@ 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'
|
||||
show
|
||||
MouseTrackerAnnotation,
|
||||
PointerEnterEventListener,
|
||||
PointerExitEventListener;
|
||||
|
||||
/// The shape of the progress bar at the left and right ends.
|
||||
enum BarCapShape {
|
||||
/// The left and right ends of the bar are round.
|
||||
round,
|
||||
|
||||
/// The left and right ends of the bar are square.
|
||||
square,
|
||||
}
|
||||
/// https://github.com/suragch/audio_video_progress_bar
|
||||
|
||||
/// A progress bar widget to show or set the location of the currently
|
||||
/// playing audio or video content.
|
||||
@@ -31,7 +29,7 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
super.key,
|
||||
required this.progress,
|
||||
required this.total,
|
||||
this.buffered,
|
||||
this.buffered = .zero,
|
||||
this.onSeek,
|
||||
this.onDragStart,
|
||||
this.onDragUpdate,
|
||||
@@ -40,7 +38,6 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
required this.baseBarColor,
|
||||
required this.progressBarColor,
|
||||
required this.bufferedBarColor,
|
||||
this.barCapShape = BarCapShape.round,
|
||||
this.thumbRadius = 10.0,
|
||||
required this.thumbColor,
|
||||
required this.thumbGlowColor,
|
||||
@@ -60,7 +57,7 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
///
|
||||
/// This is useful for streamed content. If you are playing a local file
|
||||
/// then you can leave this out.
|
||||
final Duration? buffered;
|
||||
final Duration buffered;
|
||||
|
||||
/// A callback when user moves the thumb.
|
||||
///
|
||||
@@ -137,12 +134,6 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
/// a shade darker than [baseBarColor].
|
||||
final Color bufferedBarColor;
|
||||
|
||||
/// The shape of the bar at the left and right ends.
|
||||
///
|
||||
/// This affects the base bar for the total time, the current progress bar,
|
||||
/// and the buffered progress bar. The default is [BarCapShape.round].
|
||||
final BarCapShape barCapShape;
|
||||
|
||||
/// The radius of the circle for the moveable progress bar thumb.
|
||||
final double thumbRadius;
|
||||
|
||||
@@ -181,10 +172,10 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderProgressBar(
|
||||
return RenderProgressBar(
|
||||
progress: progress,
|
||||
total: total,
|
||||
buffered: buffered ?? Duration.zero,
|
||||
buffered: buffered,
|
||||
onSeek: onSeek,
|
||||
onDragStart: onDragStart,
|
||||
onDragUpdate: onDragUpdate,
|
||||
@@ -193,7 +184,6 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
baseBarColor: baseBarColor,
|
||||
progressBarColor: progressBarColor,
|
||||
bufferedBarColor: bufferedBarColor,
|
||||
barCapShape: barCapShape,
|
||||
thumbRadius: thumbRadius,
|
||||
thumbColor: thumbColor,
|
||||
thumbGlowColor: thumbGlowColor,
|
||||
@@ -203,11 +193,14 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderObject renderObject) {
|
||||
(renderObject as _RenderProgressBar)
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderProgressBar renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..total = total
|
||||
..progress = progress
|
||||
..buffered = buffered ?? Duration.zero
|
||||
..buffered = buffered
|
||||
..onSeek = onSeek
|
||||
..onDragStart = onDragStart
|
||||
..onDragUpdate = onDragUpdate
|
||||
@@ -216,7 +209,6 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
..baseBarColor = baseBarColor
|
||||
..progressBarColor = progressBarColor
|
||||
..bufferedBarColor = bufferedBarColor
|
||||
..barCapShape = barCapShape
|
||||
..thumbRadius = thumbRadius
|
||||
..thumbColor = thumbColor
|
||||
..thumbGlowColor = thumbGlowColor
|
||||
@@ -263,7 +255,6 @@ class ProgressBar extends LeafRenderObjectWidget {
|
||||
..add(ColorProperty('baseBarColor', baseBarColor))
|
||||
..add(ColorProperty('progressBarColor', progressBarColor))
|
||||
..add(ColorProperty('bufferedBarColor', bufferedBarColor))
|
||||
..add(StringProperty('barCapShape', barCapShape.toString()))
|
||||
..add(DoubleProperty('thumbRadius', thumbRadius))
|
||||
..add(ColorProperty('thumbColor', thumbColor))
|
||||
..add(ColorProperty('thumbGlowColor', thumbGlowColor))
|
||||
@@ -327,8 +318,8 @@ class _EagerHorizontalDragGestureRecognizer
|
||||
String get debugDescription => '_EagerHorizontalDragGestureRecognizer';
|
||||
}
|
||||
|
||||
class _RenderProgressBar extends RenderBox {
|
||||
_RenderProgressBar({
|
||||
class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation {
|
||||
RenderProgressBar({
|
||||
required Duration progress,
|
||||
required Duration total,
|
||||
required Duration buffered,
|
||||
@@ -340,7 +331,6 @@ class _RenderProgressBar extends RenderBox {
|
||||
required Color baseBarColor,
|
||||
required Color progressBarColor,
|
||||
required Color bufferedBarColor,
|
||||
required BarCapShape barCapShape,
|
||||
double thumbRadius = 20.0,
|
||||
required Color thumbColor,
|
||||
required Color thumbGlowColor,
|
||||
@@ -356,18 +346,20 @@ class _RenderProgressBar extends RenderBox {
|
||||
_baseBarColor = baseBarColor,
|
||||
_progressBarColor = progressBarColor,
|
||||
_bufferedBarColor = bufferedBarColor,
|
||||
_barCapShape = barCapShape,
|
||||
_thumbRadius = thumbRadius,
|
||||
_thumbColor = thumbColor,
|
||||
_thumbGlowColor = thumbGlowColor,
|
||||
_thumbGlowRadius = thumbGlowRadius,
|
||||
_paintThumbGlow = thumbGlowRadius > thumbRadius,
|
||||
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar {
|
||||
_drag = _EagerHorizontalDragGestureRecognizer()
|
||||
..onStart = _onDragStart
|
||||
..onUpdate = _onDragUpdate
|
||||
..onEnd = _onDragEnd
|
||||
..onCancel = _finishDrag;
|
||||
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
|
||||
_hitTestSelf = onDragStart != null {
|
||||
if (onDragStart != null) {
|
||||
_drag = _EagerHorizontalDragGestureRecognizer()
|
||||
..onStart = _onDragStart
|
||||
..onUpdate = _onDragUpdate
|
||||
..onEnd = _onDragEnd
|
||||
..onCancel = _finishDrag;
|
||||
}
|
||||
if (!_userIsDraggingThumb) {
|
||||
_progress = progress;
|
||||
_thumbValue = _proportionOfTotal(_progress);
|
||||
@@ -377,6 +369,7 @@ class _RenderProgressBar extends RenderBox {
|
||||
@override
|
||||
void dispose() {
|
||||
_drag?.dispose();
|
||||
_drag = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -586,14 +579,6 @@ class _RenderProgressBar extends RenderBox {
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
BarCapShape get barCapShape => _barCapShape;
|
||||
BarCapShape _barCapShape;
|
||||
set barCapShape(BarCapShape value) {
|
||||
if (_barCapShape == value) return;
|
||||
_barCapShape = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
/// The color of the moveable thumb.
|
||||
Color get thumbColor => _thumbColor;
|
||||
Color _thumbColor;
|
||||
@@ -656,8 +641,9 @@ class _RenderProgressBar extends RenderBox {
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) => _heightWhenNoLabels();
|
||||
|
||||
final bool _hitTestSelf;
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => true;
|
||||
bool hitTestSelf(Offset position) => _hitTestSelf;
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
||||
@@ -676,8 +662,7 @@ class _RenderProgressBar extends RenderBox {
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
final desiredWidth = constraints.maxWidth;
|
||||
final desiredHeight = _heightWhenNoLabels();
|
||||
final desiredSize = Size(desiredWidth, desiredHeight);
|
||||
return constraints.constrain(desiredSize);
|
||||
return constraints.constrainDimensions(desiredWidth, desiredHeight);
|
||||
}
|
||||
|
||||
double _heightWhenNoLabels() {
|
||||
@@ -752,18 +737,15 @@ class _RenderProgressBar extends RenderBox {
|
||||
required double widthProportion,
|
||||
required Color color,
|
||||
}) {
|
||||
final strokeCap = (_barCapShape == BarCapShape.round)
|
||||
? StrokeCap.round
|
||||
: StrokeCap.square;
|
||||
final baseBarPaint = Paint()
|
||||
..color = color
|
||||
..strokeCap = strokeCap
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeWidth = _barHeight;
|
||||
final capRadius = _barHeight / 2;
|
||||
final adjustedWidth = availableSize.width - barHeight;
|
||||
final dx = widthProportion * adjustedWidth + capRadius;
|
||||
final startPoint = Offset(capRadius, availableSize.height / 2);
|
||||
var endPoint = Offset(dx, availableSize.height / 2);
|
||||
final endPoint = Offset(dx, availableSize.height / 2);
|
||||
canvas.drawLine(startPoint, endPoint, baseBarPaint);
|
||||
}
|
||||
|
||||
@@ -830,4 +812,16 @@ class _RenderProgressBar extends RenderBox {
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
MouseCursor get cursor => SystemMouseCursors.click;
|
||||
|
||||
@override
|
||||
PointerEnterEventListener? onEnter;
|
||||
|
||||
@override
|
||||
PointerExitEventListener? onExit;
|
||||
|
||||
@override
|
||||
bool get validForMouseTracker => false;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
class Segment {
|
||||
import 'package:flutter/foundation.dart' show listEquals, kDebugMode;
|
||||
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show BoxHitTestEntry;
|
||||
|
||||
sealed class BaseSegment {
|
||||
final double start;
|
||||
final double end;
|
||||
final Color color;
|
||||
final String? title;
|
||||
final String? url;
|
||||
final int? from;
|
||||
final int? to;
|
||||
|
||||
Segment(
|
||||
this.start,
|
||||
this.end,
|
||||
this.color, [
|
||||
this.title,
|
||||
this.url,
|
||||
this.from,
|
||||
this.to,
|
||||
]);
|
||||
BaseSegment({
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
}
|
||||
|
||||
class Segment extends BaseSegment {
|
||||
final Color color;
|
||||
|
||||
Segment({
|
||||
required super.start,
|
||||
required super.end,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
@@ -25,9 +45,38 @@ class Segment {
|
||||
return true;
|
||||
}
|
||||
if (other is Segment) {
|
||||
return start == other.start && end == other.end && color == other.color;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(start, end, color);
|
||||
}
|
||||
|
||||
class ViewPointSegment extends BaseSegment {
|
||||
final String? title;
|
||||
final String? url;
|
||||
final int? from;
|
||||
final int? to;
|
||||
|
||||
ViewPointSegment({
|
||||
required super.start,
|
||||
required super.end,
|
||||
this.title,
|
||||
this.url,
|
||||
this.from,
|
||||
this.to,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other is ViewPointSegment) {
|
||||
return start == other.start &&
|
||||
end == other.end &&
|
||||
color == other.color &&
|
||||
title == other.title &&
|
||||
url == other.url &&
|
||||
from == other.from &&
|
||||
@@ -37,116 +86,278 @@ class Segment {
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(start, end, color, title, url, from, to);
|
||||
int get hashCode => Object.hash(start, end, title, url, from, to);
|
||||
}
|
||||
|
||||
class SegmentProgressBar extends CustomPainter {
|
||||
final List<Segment> segmentColors;
|
||||
double? _defHeight;
|
||||
|
||||
SegmentProgressBar({
|
||||
required this.segmentColors,
|
||||
class SegmentProgressBar extends BaseSegmentProgressBar<Segment> {
|
||||
const SegmentProgressBar({
|
||||
super.key,
|
||||
super.height = 3.5,
|
||||
required super.segments,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderProgressBar(
|
||||
height: height,
|
||||
segments: segments,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RenderProgressBar extends BaseRenderProgressBar<Segment> {
|
||||
RenderProgressBar({
|
||||
required super.height,
|
||||
required super.segments,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final size = this.size;
|
||||
final canvas = context.canvas;
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
for (int i = 0; i < segmentColors.length; i++) {
|
||||
final item = segmentColors[i];
|
||||
paint.color = item.color;
|
||||
for (final segment in segments) {
|
||||
paint.color = segment.color;
|
||||
final segmentStart = segment.start * size.width;
|
||||
final segmentEnd = segment.end * size.width;
|
||||
|
||||
if (segmentEnd > segmentStart ||
|
||||
(segmentEnd == segmentStart && segmentStart > 0)) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTRB(
|
||||
segmentStart,
|
||||
0,
|
||||
segmentEnd == segmentStart ? segmentStart + 2 : segmentEnd,
|
||||
size.height,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewPointSegmentProgressBar
|
||||
extends BaseSegmentProgressBar<ViewPointSegment> {
|
||||
const ViewPointSegmentProgressBar({
|
||||
super.key,
|
||||
super.height = 3.5,
|
||||
required super.segments,
|
||||
this.onSeek,
|
||||
});
|
||||
|
||||
final ValueSetter<Duration>? onSeek;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderViewPointProgressBar(
|
||||
height: height,
|
||||
segments: segments,
|
||||
onSeek: onSeek,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderViewPointProgressBar renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..height = height
|
||||
..segments = segments
|
||||
..onSeek = onSeek;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderViewPointProgressBar
|
||||
extends BaseRenderProgressBar<ViewPointSegment> {
|
||||
RenderViewPointProgressBar({
|
||||
required super.height,
|
||||
required super.segments,
|
||||
ValueSetter<Duration>? onSeek,
|
||||
}) : _onSeek = onSeek,
|
||||
_hitTestSelf = onSeek != null {
|
||||
if (onSeek != null) {
|
||||
_tapGestureRecognizer = TapGestureRecognizer()..onTapUp = _onTapUp;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrainDimensions(constraints.maxWidth, _barHeight);
|
||||
}
|
||||
|
||||
static const double _barHeight = 15.0;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final size = this.size;
|
||||
final canvas = context.canvas;
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTRB(0, 0, size.width, _barHeight),
|
||||
paint..color = Colors.grey[600]!.withValues(alpha: 0.45),
|
||||
);
|
||||
|
||||
paint.color = Colors.black.withValues(alpha: 0.5);
|
||||
|
||||
for (int index = 0; index < segments.length; index++) {
|
||||
final isFirst = index == 0;
|
||||
final item = segments[index];
|
||||
final segmentStart = item.start * size.width;
|
||||
final segmentEnd = item.end * size.width;
|
||||
|
||||
if (segmentEnd > segmentStart ||
|
||||
(segmentEnd == segmentStart && segmentStart > 0)) {
|
||||
if (item.title != null) {
|
||||
double fontSize = 10;
|
||||
double fontSize = 10;
|
||||
|
||||
_defHeight ??=
|
||||
(TextPainter(
|
||||
text: TextSpan(
|
||||
text: item.title,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout()).height +
|
||||
2;
|
||||
|
||||
TextPainter getTextPainter() => TextPainter(
|
||||
text: TextSpan(
|
||||
text: item.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
height: 1,
|
||||
),
|
||||
TextPainter getTextPainter() => TextPainter(
|
||||
text: TextSpan(
|
||||
text: item.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: fontSize,
|
||||
height: 1,
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1, fontSize: fontSize),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
),
|
||||
strutStyle: StrutStyle(leading: 0, height: 1, fontSize: fontSize),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
TextPainter textPainter = getTextPainter();
|
||||
TextPainter textPainter = getTextPainter();
|
||||
|
||||
late double prevStart;
|
||||
if (i != 0) {
|
||||
prevStart = segmentColors[i - 1].start * size.width;
|
||||
}
|
||||
double width = i == 0 ? segmentStart : segmentStart - prevStart;
|
||||
|
||||
while (textPainter.width > width - 2 && fontSize >= 2) {
|
||||
fontSize -= 0.5;
|
||||
textPainter = getTextPainter();
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTRB(
|
||||
0,
|
||||
-_defHeight!,
|
||||
size.width,
|
||||
0,
|
||||
),
|
||||
Paint()..color = Colors.grey[600]!.withValues(alpha: 0.45),
|
||||
);
|
||||
}
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
segmentStart,
|
||||
-_defHeight!,
|
||||
segmentEnd == segmentStart ? 2 : segmentEnd - segmentStart,
|
||||
size.height + _defHeight!,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
|
||||
double textX = i == 0
|
||||
? (segmentStart - textPainter.width) / 2
|
||||
: (segmentStart - prevStart - textPainter.width) / 2 +
|
||||
prevStart +
|
||||
1;
|
||||
double textY = (-_defHeight! - textPainter.height) / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
} else {
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
segmentStart,
|
||||
0,
|
||||
segmentEnd == segmentStart ? 2 : segmentEnd - segmentStart,
|
||||
size.height,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
late double prevStart;
|
||||
if (!isFirst) {
|
||||
prevStart = segments[index - 1].start * size.width;
|
||||
}
|
||||
final width = isFirst ? segmentStart : segmentStart - prevStart;
|
||||
|
||||
while (textPainter.width > width - 2 && fontSize >= 2) {
|
||||
fontSize -= 0.5;
|
||||
textPainter.dispose();
|
||||
textPainter = getTextPainter();
|
||||
}
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTRB(
|
||||
segmentStart,
|
||||
0,
|
||||
segmentEnd == segmentStart ? segmentStart + 2 : segmentEnd,
|
||||
_barHeight + height,
|
||||
),
|
||||
paint,
|
||||
);
|
||||
|
||||
final textX = isFirst
|
||||
? (segmentStart - textPainter.width) / 2
|
||||
: (segmentStart - prevStart - textPainter.width) / 2 +
|
||||
prevStart +
|
||||
1;
|
||||
final textY = (_barHeight - textPainter.height) / 2;
|
||||
textPainter.paint(canvas, Offset(textX, textY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ValueSetter<Duration>? _onSeek;
|
||||
set onSeek(ValueSetter<Duration>? value) {
|
||||
if (_onSeek == value) {
|
||||
return;
|
||||
}
|
||||
_onSeek = value;
|
||||
}
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
@override
|
||||
bool shouldRepaint(SegmentProgressBar oldDelegate) {
|
||||
return segmentColors != oldDelegate.segmentColors;
|
||||
void dispose() {
|
||||
_onSeek = null;
|
||||
_tapGestureRecognizer
|
||||
?..onTapUp = null
|
||||
..dispose();
|
||||
_tapGestureRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final bool _hitTestSelf;
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => _hitTestSelf;
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
||||
if (event is PointerDownEvent) {
|
||||
_tapGestureRecognizer?.addPointer(event);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
try {
|
||||
final seg = details.localPosition.dx / size.width;
|
||||
final item = _segments
|
||||
.where((item) => item.start >= seg)
|
||||
.reduce((a, b) => a.start < b.start ? a : b);
|
||||
if (item.from case final from?) {
|
||||
_onSeek?.call(Duration(seconds: from));
|
||||
}
|
||||
// if (kDebugMode) debugPrint('${item.title},,${item.from}');
|
||||
} catch (e) {
|
||||
if (kDebugMode) rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseSegmentProgressBar<T extends BaseSegment>
|
||||
extends LeafRenderObjectWidget {
|
||||
const BaseSegmentProgressBar({
|
||||
super.key,
|
||||
this.height = 3.5,
|
||||
required this.segments,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final List<T> segments;
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
BaseRenderProgressBar renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..height = height
|
||||
..segments = segments;
|
||||
}
|
||||
}
|
||||
|
||||
class BaseRenderProgressBar<T extends BaseSegment> extends RenderBox {
|
||||
BaseRenderProgressBar({
|
||||
required double height,
|
||||
required List<T> segments,
|
||||
}) : _height = height,
|
||||
_segments = segments;
|
||||
|
||||
double _height;
|
||||
double get height => _height;
|
||||
set height(double value) {
|
||||
if (_height == value) return;
|
||||
_height = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
List<T> _segments;
|
||||
List<T> get segments => _segments;
|
||||
set segments(List<T> value) {
|
||||
if (listEquals(_segments, value)) return;
|
||||
_segments = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrainDimensions(constraints.maxWidth, height);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,154 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
Widget videoProgressIndicator(double progress) => ClipRect(
|
||||
clipper: ProgressClipper(),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(bottom: StyleString.imgRadius),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 10,
|
||||
value: progress,
|
||||
// ignore: deprecated_member_use
|
||||
year2023: true,
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class VideoProgressIndicator extends LeafRenderObjectWidget {
|
||||
const VideoProgressIndicator({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
this.radius = 10,
|
||||
this.height = 4,
|
||||
required this.progress,
|
||||
}) : assert(progress >= 0 && progress <= 1);
|
||||
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final double radius;
|
||||
final double height;
|
||||
final double progress;
|
||||
|
||||
class ProgressClipper extends CustomClipper<Rect> {
|
||||
@override
|
||||
Rect getClip(Size size) {
|
||||
return Rect.fromLTWH(0, 6, size.width, size.height - 6);
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderProgressBar(
|
||||
color: color,
|
||||
backgroundColor: backgroundColor,
|
||||
radius: radius,
|
||||
height: height,
|
||||
progress: progress,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Rect> oldClipper) {
|
||||
return false;
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderProgressBar renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..color = color
|
||||
..backgroundColor = backgroundColor
|
||||
..radius = radius
|
||||
..height = height
|
||||
..progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderProgressBar extends RenderBox {
|
||||
RenderProgressBar({
|
||||
required Color color,
|
||||
required Color backgroundColor,
|
||||
required double radius,
|
||||
required double height,
|
||||
required double progress,
|
||||
}) : _color = color,
|
||||
_backgroundColor = backgroundColor,
|
||||
_radius = radius,
|
||||
_height = height,
|
||||
_progress = progress;
|
||||
|
||||
Color _color;
|
||||
Color get color => _color;
|
||||
set color(Color value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Color _backgroundColor;
|
||||
Color get backgroundColor => _backgroundColor;
|
||||
set backgroundColor(Color value) {
|
||||
if (_backgroundColor == value) return;
|
||||
_backgroundColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _progress;
|
||||
double get progress => _progress;
|
||||
set progress(double value) {
|
||||
if (_progress == value) return;
|
||||
_progress = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _radius;
|
||||
double get radius => _radius;
|
||||
set radius(double value) {
|
||||
if (_radius == value) return;
|
||||
_radius = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double _height;
|
||||
double get height => _height;
|
||||
set height(double value) {
|
||||
if (_height == value) return;
|
||||
_height = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrainDimensions(constraints.maxWidth, _radius);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final size = this.size;
|
||||
final canvas = context.canvas;
|
||||
final paint = Paint()..style = .fill;
|
||||
|
||||
canvas.clipRect(
|
||||
.fromLTRB(0, size.height - height, size.width, size.height),
|
||||
);
|
||||
|
||||
final radius = Radius.circular(_radius);
|
||||
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
|
||||
final rrect = RRect.fromRectAndCorners(
|
||||
rect,
|
||||
bottomLeft: radius,
|
||||
bottomRight: radius,
|
||||
);
|
||||
|
||||
if (progress == 0) {
|
||||
canvas.drawRRect(rrect, paint..color = _backgroundColor);
|
||||
} else if (progress == 1) {
|
||||
canvas.drawRRect(rrect, paint..color = _color);
|
||||
} else {
|
||||
final w = size.width * progress;
|
||||
final left = Rect.fromLTRB(0, 0, w, size.height);
|
||||
final right = Rect.fromLTRB(w, 0, size.width, size.height);
|
||||
canvas
|
||||
..clipRRect(rrect)
|
||||
..drawRect(left, paint..color = _color)
|
||||
..drawRect(right, paint..color = _backgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
121
lib/common/widgets/scale_app.dart
Normal file
121
lib/common/widgets/scale_app.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'dart:async' show scheduleMicrotask;
|
||||
import 'dart:collection' show Queue;
|
||||
import 'dart:ui' show PointerDataPacket;
|
||||
|
||||
import 'package:flutter/gestures.dart' show PointerEventConverter;
|
||||
import 'package:flutter/rendering.dart' show RenderView, ViewConfiguration;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// ref https://github.com/LastMonopoly/scaled_app
|
||||
|
||||
/// Adapted from [WidgetsFlutterBinding]
|
||||
///
|
||||
class ScaledWidgetsFlutterBinding extends WidgetsFlutterBinding {
|
||||
ScaledWidgetsFlutterBinding._({double scaleFactor = 1.0})
|
||||
: _scaleFactor = scaleFactor;
|
||||
|
||||
/// Calculate scale factor from device size.
|
||||
double _scaleFactor;
|
||||
|
||||
/// Update scaleFactor callback, then rebuild layout
|
||||
void setScaleFactor(double scaleFactor) {
|
||||
if (_scaleFactor == scaleFactor) return;
|
||||
_scaleFactor = scaleFactor;
|
||||
handleMetricsChanged();
|
||||
}
|
||||
|
||||
double devicePixelRatioScaled = 0;
|
||||
|
||||
static ScaledWidgetsFlutterBinding? _binding;
|
||||
|
||||
static ScaledWidgetsFlutterBinding get instance => _binding!;
|
||||
|
||||
/// Scaling will be applied based on [scaleFactor] callback.
|
||||
///
|
||||
static WidgetsBinding ensureInitialized({double scaleFactor = 1.0}) =>
|
||||
_binding ??= ScaledWidgetsFlutterBinding._(scaleFactor: scaleFactor);
|
||||
|
||||
/// Override the method from [RendererBinding.createViewConfiguration] to
|
||||
/// change what size or device pixel ratio the [RenderView] will use.
|
||||
///
|
||||
/// See more:
|
||||
/// * [RendererBinding.createViewConfiguration]
|
||||
/// * [TestWidgetsFlutterBinding.createViewConfiguration]
|
||||
@override
|
||||
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
|
||||
final view = renderView.flutterView;
|
||||
final devicePixelRatio = view.devicePixelRatio;
|
||||
devicePixelRatioScaled = devicePixelRatio * _scaleFactor;
|
||||
final BoxConstraints physicalConstraints =
|
||||
BoxConstraints.fromViewConstraints(view.physicalConstraints);
|
||||
return ViewConfiguration(
|
||||
physicalConstraints: physicalConstraints,
|
||||
logicalConstraints: physicalConstraints / devicePixelRatioScaled,
|
||||
devicePixelRatio: devicePixelRatioScaled,
|
||||
);
|
||||
}
|
||||
|
||||
/// Adapted from [GestureBinding.initInstances]
|
||||
@override
|
||||
void initInstances() {
|
||||
super.initInstances();
|
||||
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
|
||||
}
|
||||
|
||||
@override
|
||||
void unlocked() {
|
||||
super.unlocked();
|
||||
_flushPointerEventQueue();
|
||||
}
|
||||
|
||||
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
|
||||
|
||||
/// When we scale UI using [ViewConfiguration], [ui.window] stays the same.
|
||||
///
|
||||
/// [GestureBinding] uses [platformDispatcher.implicitView.devicePixelRatio] for calculations,
|
||||
/// so we override corresponding methods.
|
||||
///
|
||||
void _handlePointerDataPacket(PointerDataPacket packet) {
|
||||
// We convert pointer data to logical pixels so that e.g. the touch slop can be
|
||||
// defined in a device-independent manner.
|
||||
try {
|
||||
_pendingPointerEvents.addAll(
|
||||
PointerEventConverter.expand(packet.data, _devicePixelRatioForView),
|
||||
);
|
||||
if (!locked) {
|
||||
_flushPointerEventQueue();
|
||||
}
|
||||
} catch (error, stack) {
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: error,
|
||||
stack: stack,
|
||||
library: 'gestures library',
|
||||
context: ErrorDescription('while handling a pointer data packet'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double _devicePixelRatioForView(int viewId) => devicePixelRatioScaled;
|
||||
|
||||
/// Dispatch a [PointerCancelEvent] for the given pointer soon.
|
||||
///
|
||||
/// The pointer event will be dispatched before the next pointer event and
|
||||
/// before the end of the microtask but not within this function call.
|
||||
@override
|
||||
void cancelPointer(int pointer) {
|
||||
if (_pendingPointerEvents.isEmpty && !locked) {
|
||||
scheduleMicrotask(_flushPointerEventQueue);
|
||||
}
|
||||
_pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer));
|
||||
}
|
||||
|
||||
void _flushPointerEventQueue() {
|
||||
assert(!locked);
|
||||
|
||||
while (_pendingPointerEvents.isNotEmpty) {
|
||||
handlePointerEvent(_pendingPointerEvents.removeFirst());
|
||||
}
|
||||
}
|
||||
}
|
||||
25
lib/common/widgets/scroll_behavior.dart
Normal file
25
lib/common/widgets/scroll_behavior.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/gestures.dart' show PointerDeviceKind;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomScrollBehavior extends MaterialScrollBehavior {
|
||||
const CustomScrollBehavior(this.dragDevices);
|
||||
|
||||
@override
|
||||
Widget buildScrollbar(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
ScrollableDetails details,
|
||||
) => child;
|
||||
|
||||
@override
|
||||
final Set<PointerDeviceKind> dragDevices;
|
||||
}
|
||||
|
||||
const Set<PointerDeviceKind> desktopDragDevices = <PointerDeviceKind>{
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.unknown,
|
||||
PointerDeviceKind.mouse,
|
||||
};
|
||||
@@ -1,24 +1,37 @@
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart';
|
||||
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide TabBarView;
|
||||
|
||||
Widget videoTabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) => TabBarView(
|
||||
physics: const CustomTabBarViewClampingScrollPhysics(),
|
||||
}) => TabBarView<CustomHorizontalDragGestureRecognizer>(
|
||||
controller: controller,
|
||||
physics: const CustomTabBarViewScrollPhysics(parent: ClampingScrollPhysics()),
|
||||
horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer(),
|
||||
children: children,
|
||||
);
|
||||
|
||||
Widget tabBarView({
|
||||
required List<Widget> children,
|
||||
TabController? controller,
|
||||
}) => TabBarView(
|
||||
}) => TabBarView<CustomHorizontalDragGestureRecognizer>(
|
||||
physics: const CustomTabBarViewScrollPhysics(),
|
||||
controller: controller,
|
||||
horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer(),
|
||||
children: children,
|
||||
);
|
||||
|
||||
SpringDescription _customSpringDescription() {
|
||||
final List<double> springDescription = Pref.springDescription;
|
||||
return SpringDescription(
|
||||
mass: springDescription[0],
|
||||
stiffness: springDescription[1],
|
||||
damping: springDescription[2],
|
||||
);
|
||||
}
|
||||
|
||||
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
|
||||
const CustomTabBarViewScrollPhysics({super.parent});
|
||||
|
||||
@@ -27,20 +40,10 @@ class CustomTabBarViewScrollPhysics extends ScrollPhysics {
|
||||
return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => CustomSpringDescription();
|
||||
}
|
||||
|
||||
class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
const CustomTabBarViewClampingScrollPhysics({super.parent});
|
||||
static final _springDescription = _customSpringDescription();
|
||||
|
||||
@override
|
||||
CustomTabBarViewClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return CustomTabBarViewClampingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => CustomSpringDescription();
|
||||
SpringDescription get spring => _springDescription;
|
||||
}
|
||||
|
||||
mixin ReloadMixin {
|
||||
@@ -79,30 +82,3 @@ class ReloadScrollPhysics extends AlwaysScrollableScrollPhysics {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSpringDescription implements SpringDescription {
|
||||
static final List<double> springDescription = Pref.springDescription;
|
||||
|
||||
@override
|
||||
final mass = springDescription[0];
|
||||
|
||||
@override
|
||||
final stiffness = springDescription[1];
|
||||
|
||||
@override
|
||||
final damping = springDescription[2];
|
||||
|
||||
CustomSpringDescription._();
|
||||
|
||||
static final _instance = CustomSpringDescription._();
|
||||
|
||||
factory CustomSpringDescription() => _instance;
|
||||
|
||||
/// Defaults to 0.
|
||||
@override
|
||||
double bounce = 0.0;
|
||||
|
||||
/// Defaults to 0.5 seconds.
|
||||
@override
|
||||
Duration duration = const Duration(milliseconds: 500);
|
||||
}
|
||||
|
||||
@@ -1,88 +1,61 @@
|
||||
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// https://stackoverflow.com/a/76605401
|
||||
|
||||
class SelfSizedHorizontalList extends StatefulWidget {
|
||||
final Widget Function(int index) childBuilder;
|
||||
final int itemCount;
|
||||
final double gapSize;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final ScrollController? controller;
|
||||
|
||||
const SelfSizedHorizontalList({
|
||||
super.key,
|
||||
required this.childBuilder,
|
||||
required this.itemCount,
|
||||
this.gapSize = 5,
|
||||
this.padding,
|
||||
required this.itemBuilder,
|
||||
required this.separatorBuilder,
|
||||
this.controller,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
final int itemCount;
|
||||
final EdgeInsets? padding;
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final IndexedWidgetBuilder separatorBuilder;
|
||||
final ScrollController? controller;
|
||||
|
||||
@override
|
||||
State<SelfSizedHorizontalList> createState() =>
|
||||
_SelfSizedHorizontalListState();
|
||||
}
|
||||
|
||||
class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
|
||||
final infoKey = GlobalKey();
|
||||
|
||||
double? prevHeight;
|
||||
double? get height {
|
||||
if (prevHeight != null) return prevHeight;
|
||||
prevHeight = infoKey.globalPaintBounds?.height;
|
||||
return prevHeight;
|
||||
}
|
||||
|
||||
bool get isInit => height == null;
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(SelfSizedHorizontalList oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
// if (BuildConfig.isDebug) {
|
||||
// prevHeight = null;
|
||||
// }
|
||||
// }
|
||||
final _key = GlobalKey();
|
||||
double? _height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (height == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((v) => setState(() {}));
|
||||
}
|
||||
if (widget.itemCount == 0) return const SizedBox.shrink();
|
||||
if (isInit) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
if (_height == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
_height = (_key.currentContext!.findRenderObject() as RenderBox)
|
||||
.size
|
||||
.height;
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
return OnlyLayoutWidget(
|
||||
key: _key,
|
||||
child: Padding(
|
||||
key: infoKey,
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
child: widget.childBuilder(0),
|
||||
padding: widget.padding ?? .zero,
|
||||
child: widget.itemBuilder(context, 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: height,
|
||||
height: _height,
|
||||
child: ListView.separated(
|
||||
controller: widget.controller,
|
||||
scrollDirection: .horizontal,
|
||||
padding: widget.padding,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.itemCount,
|
||||
itemBuilder: (c, i) => widget.childBuilder(i),
|
||||
separatorBuilder: (c, i) => SizedBox(width: widget.gapSize),
|
||||
controller: widget.controller,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
separatorBuilder: widget.separatorBuilder,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension GlobalKeyExtension on GlobalKey {
|
||||
Rect? get globalPaintBounds {
|
||||
final renderObject = currentContext?.findRenderObject();
|
||||
final translation = renderObject?.getTransformTo(null).getTranslation();
|
||||
if (translation != null && renderObject?.paintBounds != null) {
|
||||
final offset = Offset(translation.x, translation.y);
|
||||
return renderObject!.paintBounds.shift(offset);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
lib/common/widgets/stateful_builder.dart
Normal file
36
lib/common/widgets/stateful_builder.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatefulBuilder extends StatefulWidget {
|
||||
const StatefulBuilder({
|
||||
super.key,
|
||||
this.onInit,
|
||||
this.onDispose,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final VoidCallback? onInit;
|
||||
|
||||
final VoidCallback? onDispose;
|
||||
|
||||
final StatefulWidgetBuilder builder;
|
||||
|
||||
@override
|
||||
State<StatefulBuilder> createState() => _StatefulBuilderState();
|
||||
}
|
||||
|
||||
class _StatefulBuilderState extends State<StatefulBuilder> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.onInit?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.onDispose?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder(context, setState);
|
||||
}
|
||||
@@ -1,471 +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.
|
||||
|
||||
/// @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/text_field/editable_text.dart'
|
||||
show EditableTextContextMenuBuilder;
|
||||
|
||||
/// Controls how spell check is performed for text input.
|
||||
///
|
||||
/// This configuration determines the [SpellCheckService] used to fetch the
|
||||
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
|
||||
/// mark misspelled words within text input.
|
||||
@immutable
|
||||
class SpellCheckConfiguration {
|
||||
/// Creates a configuration that specifies the service and suggestions handler
|
||||
/// for spell check.
|
||||
const SpellCheckConfiguration({
|
||||
this.spellCheckService,
|
||||
this.misspelledSelectionColor,
|
||||
this.misspelledTextStyle,
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
}) : _spellCheckEnabled = true;
|
||||
|
||||
/// Creates a configuration that disables spell check.
|
||||
const SpellCheckConfiguration.disabled()
|
||||
: _spellCheckEnabled = false,
|
||||
spellCheckService = null,
|
||||
spellCheckSuggestionsToolbarBuilder = null,
|
||||
misspelledTextStyle = null,
|
||||
misspelledSelectionColor = null;
|
||||
|
||||
/// The service used to fetch spell check results for text input.
|
||||
final SpellCheckService? spellCheckService;
|
||||
|
||||
/// The color the paint the selection highlight when spell check is showing
|
||||
/// suggestions for a misspelled word.
|
||||
///
|
||||
/// For example, on iOS, the selection appears red while the spell check menu
|
||||
/// is showing.
|
||||
final Color? misspelledSelectionColor;
|
||||
|
||||
/// Style used to indicate misspelled words.
|
||||
///
|
||||
/// This is nullable to allow style-specific wrappers of [EditableText]
|
||||
/// to infer this, but this must be specified if this configuration is
|
||||
/// provided directly to [EditableText] or its construction will fail with an
|
||||
/// assertion error.
|
||||
final TextStyle? misspelledTextStyle;
|
||||
|
||||
/// Builds the toolbar used to display spell check suggestions for misspelled
|
||||
/// words.
|
||||
final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
|
||||
|
||||
final bool _spellCheckEnabled;
|
||||
|
||||
/// Whether or not the configuration should enable or disable spell check.
|
||||
bool get spellCheckEnabled => _spellCheckEnabled;
|
||||
|
||||
/// Returns a copy of the current [SpellCheckConfiguration] instance with
|
||||
/// specified overrides.
|
||||
SpellCheckConfiguration copyWith({
|
||||
SpellCheckService? spellCheckService,
|
||||
Color? misspelledSelectionColor,
|
||||
TextStyle? misspelledTextStyle,
|
||||
EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder,
|
||||
}) {
|
||||
if (!_spellCheckEnabled) {
|
||||
// A new configuration should be constructed to enable spell check.
|
||||
return const SpellCheckConfiguration.disabled();
|
||||
}
|
||||
|
||||
return SpellCheckConfiguration(
|
||||
spellCheckService: spellCheckService ?? this.spellCheckService,
|
||||
misspelledSelectionColor:
|
||||
misspelledSelectionColor ?? this.misspelledSelectionColor,
|
||||
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder:
|
||||
spellCheckSuggestionsToolbarBuilder ??
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'SpellCheckConfiguration')}('
|
||||
'${_spellCheckEnabled ? 'enabled' : 'disabled'}, '
|
||||
'service: $spellCheckService, '
|
||||
'text style: $misspelledTextStyle, '
|
||||
'toolbar builder: $spellCheckSuggestionsToolbarBuilder'
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is SpellCheckConfiguration &&
|
||||
other.spellCheckService == spellCheckService &&
|
||||
other.misspelledTextStyle == misspelledTextStyle &&
|
||||
other.spellCheckSuggestionsToolbarBuilder ==
|
||||
spellCheckSuggestionsToolbarBuilder &&
|
||||
other._spellCheckEnabled == _spellCheckEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
spellCheckService,
|
||||
misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder,
|
||||
_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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,435 +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.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/text_field/editable_text.dart';
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Displays the system context menu on top of the Flutter view.
|
||||
///
|
||||
/// Currently, only supports iOS 16.0 and above and displays nothing on other
|
||||
/// platforms.
|
||||
///
|
||||
/// The context menu is the menu that appears, for example, when doing text
|
||||
/// selection. Flutter typically draws this menu itself, but this class deals
|
||||
/// with the platform-rendered context menu instead.
|
||||
///
|
||||
/// There can only be one system context menu visible at a time. Building this
|
||||
/// widget when the system context menu is already visible will hide the old one
|
||||
/// and display this one. A system context menu that is hidden is informed via
|
||||
/// [onSystemHide].
|
||||
///
|
||||
/// Pass [items] to specify the buttons that will appear in the menu. Any items
|
||||
/// without a title will be given a default title from [WidgetsLocalizations].
|
||||
///
|
||||
/// By default, [items] will be set to the result of [getDefaultItems]. This
|
||||
/// method considers the state of the [EditableTextState] so that, for example,
|
||||
/// it will only include [IOSSystemContextMenuItemCopy] if there is currently a
|
||||
/// selection to copy.
|
||||
///
|
||||
/// To check if the current device supports showing the system context menu,
|
||||
/// call [isSupported].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to create a [TextField] that uses the system context
|
||||
/// menu where supported and does not show a system notification when the user
|
||||
/// presses the "Paste" button.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenuController], which directly controls the hiding and
|
||||
/// showing of the system context menu.
|
||||
class SystemContextMenu extends StatefulWidget {
|
||||
/// Creates an instance of [SystemContextMenu] that points to the given
|
||||
/// [anchor].
|
||||
const SystemContextMenu._({
|
||||
super.key,
|
||||
required this.anchor,
|
||||
required this.items,
|
||||
this.onSystemHide,
|
||||
});
|
||||
|
||||
/// Creates an instance of [SystemContextMenu] for the field indicated by the
|
||||
/// given [EditableTextState].
|
||||
factory SystemContextMenu.editableText({
|
||||
Key? key,
|
||||
required EditableTextState editableTextState,
|
||||
List<IOSSystemContextMenuItem>? items,
|
||||
}) {
|
||||
final (
|
||||
startGlyphHeight: double startGlyphHeight,
|
||||
endGlyphHeight: double endGlyphHeight,
|
||||
) = editableTextState
|
||||
.getGlyphHeights();
|
||||
|
||||
return SystemContextMenu._(
|
||||
key: key,
|
||||
anchor: TextSelectionToolbarAnchors.getSelectionRect(
|
||||
editableTextState.renderEditable,
|
||||
startGlyphHeight,
|
||||
endGlyphHeight,
|
||||
editableTextState.renderEditable.getEndpointsForSelection(
|
||||
editableTextState.textEditingValue.selection,
|
||||
),
|
||||
),
|
||||
items: items ?? getDefaultItems(editableTextState),
|
||||
onSystemHide: editableTextState.hideToolbar,
|
||||
);
|
||||
}
|
||||
|
||||
/// The [Rect] that the context menu should point to.
|
||||
final Rect anchor;
|
||||
|
||||
/// A list of the items to be displayed in the system context menu.
|
||||
///
|
||||
/// When passed, items will be shown regardless of the state of text input.
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will produce a copy button
|
||||
/// even when there is no selection to copy. Use [EditableTextState] and/or
|
||||
/// the result of [getDefaultItems] to add and remove items based on the state
|
||||
/// of the input.
|
||||
///
|
||||
/// Defaults to the result of [getDefaultItems].
|
||||
final List<IOSSystemContextMenuItem> items;
|
||||
|
||||
/// Called when the system hides this context menu.
|
||||
///
|
||||
/// For example, tapping outside of the context menu typically causes the
|
||||
/// system to hide the menu.
|
||||
///
|
||||
/// This is not called when showing a new system context menu causes another
|
||||
/// to be hidden.
|
||||
final VoidCallback? onSystemHide;
|
||||
|
||||
/// Whether the current device supports showing the system context menu.
|
||||
///
|
||||
/// Currently, this is only supported on newer versions of iOS.
|
||||
static bool isSupported(BuildContext context) {
|
||||
return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
|
||||
}
|
||||
|
||||
/// The default [items] for the given [EditableTextState].
|
||||
///
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will only be included when the
|
||||
/// field represented by the [EditableTextState] has a selection.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableTextState.contextMenuButtonItems], which provides the default
|
||||
/// [ContextMenuButtonItem]s for the Flutter-rendered context menu.
|
||||
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(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
State<SystemContextMenu> createState() => _SystemContextMenuState();
|
||||
}
|
||||
|
||||
class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
late final SystemContextMenuController _systemContextMenuController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_systemContextMenuController = SystemContextMenuController(
|
||||
onSystemHide: widget.onSystemHide,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_systemContextMenuController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(SystemContextMenu.isSupported(context));
|
||||
|
||||
if (widget.items.isNotEmpty) {
|
||||
final WidgetsLocalizations localizations = WidgetsLocalizations.of(
|
||||
context,
|
||||
);
|
||||
final List<IOSSystemContextMenuItemData> itemDatas = widget.items
|
||||
.map((IOSSystemContextMenuItem item) => item.getData(localizations))
|
||||
.toList();
|
||||
_systemContextMenuController.showWithItems(widget.anchor, itemDatas);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -14,7 +14,7 @@ import 'package:PiliPlus/models/search/result.dart';
|
||||
import 'package:PiliPlus/utils/date_utils.dart';
|
||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_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';
|
||||
|
||||
@@ -36,10 +36,10 @@ class VideoCardH extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
String type = 'video';
|
||||
String? badge;
|
||||
if (videoItem case SearchVideoItemModel item) {
|
||||
var typeOrNull = item.type;
|
||||
if (typeOrNull?.isNotEmpty == true) {
|
||||
type = typeOrNull!;
|
||||
if (videoItem case final SearchVideoItemModel item) {
|
||||
final typeOrNull = item.type;
|
||||
if (typeOrNull != null && typeOrNull.isNotEmpty) {
|
||||
type = typeOrNull;
|
||||
if (type == 'ketang') {
|
||||
badge = '课堂';
|
||||
} else if (type == 'live_room') {
|
||||
@@ -49,7 +49,7 @@ class VideoCardH extends StatelessWidget {
|
||||
if (item.isUnionVideo == 1) {
|
||||
badge = '合作';
|
||||
}
|
||||
} else if (videoItem case HotVideoItemModel item) {
|
||||
} else if (videoItem case final HotVideoItemModel item) {
|
||||
if (item.isCharging == true) {
|
||||
badge = '充电专属';
|
||||
} else if (item.isCooperation == 1) {
|
||||
@@ -63,6 +63,7 @@ class VideoCardH extends StatelessWidget {
|
||||
title: videoItem.title,
|
||||
cover: videoItem.cover,
|
||||
);
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
@@ -70,7 +71,7 @@ class VideoCardH extends StatelessWidget {
|
||||
children: [
|
||||
InkWell(
|
||||
onLongPress: onLongPress,
|
||||
onSecondaryTap: Utils.isMobile ? null : onLongPress,
|
||||
onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress,
|
||||
onTap:
|
||||
onTap ??
|
||||
() async {
|
||||
@@ -78,7 +79,7 @@ class VideoCardH extends StatelessWidget {
|
||||
PageUtils.viewPugv(seasonId: videoItem.aid);
|
||||
return;
|
||||
} else if (type == 'live_room') {
|
||||
if (videoItem case SearchVideoItemModel item) {
|
||||
if (videoItem case final SearchVideoItemModel item) {
|
||||
int? roomId = item.id;
|
||||
if (roomId != null) {
|
||||
PageUtils.toLiveRoom(roomId);
|
||||
@@ -90,7 +91,7 @@ class VideoCardH extends StatelessWidget {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (videoItem case HotVideoItemModel item) {
|
||||
if (videoItem case final HotVideoItemModel item) {
|
||||
if (item.redirectUrl?.isNotEmpty == true &&
|
||||
PageUtils.viewPgcFromUri(item.redirectUrl!)) {
|
||||
return;
|
||||
@@ -131,7 +132,7 @@ class VideoCardH extends StatelessWidget {
|
||||
final double maxWidth = boxConstraints.maxWidth;
|
||||
final double maxHeight = boxConstraints.maxHeight;
|
||||
num? progress;
|
||||
if (videoItem case HotVideoItemModel item) {
|
||||
if (videoItem case final HotVideoItemModel item) {
|
||||
progress = item.progress;
|
||||
}
|
||||
|
||||
@@ -166,8 +167,11 @@ class VideoCardH extends StatelessWidget {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: videoProgressIndicator(
|
||||
progress == -1
|
||||
child: VideoProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
backgroundColor:
|
||||
colorScheme.secondaryContainer,
|
||||
progress: progress == -1
|
||||
? 1
|
||||
: progress / videoItem.duration,
|
||||
),
|
||||
@@ -195,8 +199,9 @@ class VideoCardH extends StatelessWidget {
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 12,
|
||||
width: 29,
|
||||
height: 29,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
videoItem: videoItem,
|
||||
onRemove: onRemove,
|
||||
@@ -215,7 +220,7 @@ class VideoCardH extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (videoItem case SearchVideoItemModel item) ...[
|
||||
if (videoItem case final SearchVideoItemModel item) ...[
|
||||
if (item.titleList?.isNotEmpty == true)
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:PiliPlus/utils/date_utils.dart';
|
||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@@ -80,7 +81,7 @@ class VideoCardV extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.aid)),
|
||||
onLongPress: onLongPress,
|
||||
onSecondaryTap: Utils.isMobile ? null : onLongPress,
|
||||
onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -97,7 +98,7 @@ class VideoCardV extends StatelessWidget {
|
||||
src: videoItem.cover,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
radius: 0,
|
||||
type: .emote,
|
||||
),
|
||||
if (videoItem.duration > 0)
|
||||
PBadge(
|
||||
@@ -123,8 +124,9 @@ class VideoCardV extends StatelessWidget {
|
||||
Positioned(
|
||||
right: -5,
|
||||
bottom: -2,
|
||||
width: 29,
|
||||
height: 29,
|
||||
child: VideoPopupMenu(
|
||||
size: 29,
|
||||
iconSize: 17,
|
||||
videoItem: videoItem,
|
||||
onRemove: onRemove,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user