Compare commits
487 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ac855d393 | ||
|
|
7381939c0f | ||
|
|
a380bcd96a | ||
|
|
d253ef468b | ||
|
|
e8145ef65a | ||
|
|
0c175abc0b | ||
|
|
946a5a1e47 | ||
|
|
29e7e0e556 | ||
|
|
cc1704a021 | ||
|
|
7ab2cf973f | ||
|
|
32386bf146 | ||
|
|
40269da391 | ||
|
|
42e082bbc6 | ||
|
|
1ad710c1cf | ||
|
|
cfa925549e | ||
|
|
ca387787b3 | ||
|
|
29a9b22c29 | ||
|
|
672375b925 | ||
|
|
c099738802 | ||
|
|
50561b8dc1 | ||
|
|
2596859778 | ||
|
|
3d453bafdb | ||
|
|
18e0b93ca7 | ||
|
|
7260a387f9 | ||
|
|
37fa165f59 | ||
|
|
8f08104f37 | ||
|
|
6ee4deab05 | ||
|
|
77fff92939 | ||
|
|
8964197b73 | ||
|
|
dbc7bcd0dd | ||
|
|
207ad2753c | ||
|
|
d6e6e52df2 | ||
|
|
9442b17d63 | ||
|
|
058ff44e39 | ||
|
|
48c7dc0eed | ||
|
|
99634a66ab | ||
|
|
21fad89cde | ||
|
|
5979ddb60c | ||
|
|
bcbfe5c849 | ||
|
|
1640732f5d | ||
|
|
9567910611 | ||
|
|
d1713504a0 | ||
|
|
bce73d9f16 | ||
|
|
6f30d2e331 | ||
|
|
556bda0d68 | ||
|
|
9d5eb55e26 | ||
|
|
110469961d | ||
|
|
fa348db7c5 | ||
|
|
3eac565b5e | ||
|
|
af40e489bc | ||
|
|
361eb4c614 | ||
|
|
7ace981f24 | ||
|
|
bfb2becb2d | ||
|
|
038f03a4e7 | ||
|
|
219228f8b5 | ||
|
|
1f64de5954 | ||
|
|
e9b5cffa91 | ||
|
|
68872f7b14 | ||
|
|
bd158619a4 | ||
|
|
310f497c30 | ||
|
|
30ee413852 | ||
|
|
0ab07a713e | ||
|
|
7eaf05839a | ||
|
|
777c3c2278 | ||
|
|
b9b54ce4f7 | ||
|
|
92e5fae29c | ||
|
|
05e8ded86a | ||
|
|
7a65b777c9 | ||
|
|
0b1f6c4d0e | ||
|
|
923af32c96 | ||
|
|
4eae7e698f | ||
|
|
5a61dbe30c | ||
|
|
036dbcaf21 | ||
|
|
bd97f9a500 | ||
|
|
33278a74b2 | ||
|
|
397f887b91 | ||
|
|
ebe793ccfc | ||
|
|
68464e4e34 | ||
|
|
395893fc7d | ||
|
|
f5657d2d4c | ||
|
|
a3ddc83430 | ||
|
|
d2f8aff421 | ||
|
|
25148509d2 | ||
|
|
2879d0dc00 | ||
|
|
90349189ee | ||
|
|
bdc524e486 | ||
|
|
cb58822009 | ||
|
|
4a2679a589 | ||
|
|
09bd1edeb3 | ||
|
|
00da3c4a0e | ||
|
|
c40d794180 | ||
|
|
34a839d9e2 | ||
|
|
f06d0605ce | ||
|
|
ef975de624 | ||
|
|
d10c737a38 | ||
|
|
28b69a06fa | ||
|
|
069cf555ea | ||
|
|
836ab311d6 | ||
|
|
dbc11c36df | ||
|
|
fffce10b31 | ||
|
|
de85e82bfa | ||
|
|
9855b35b65 | ||
|
|
5a0b045a1f | ||
|
|
c226f8f6df | ||
|
|
fd06fa9cc4 | ||
|
|
2b5f111fb1 | ||
|
|
9f5ce5ae37 | ||
|
|
3d95165d46 | ||
|
|
cfb72f27ac | ||
|
|
bcacc41db3 | ||
|
|
b2da99e334 | ||
|
|
041af37bb0 | ||
|
|
80e007bac6 | ||
|
|
87c7699324 | ||
|
|
11912c5f62 | ||
|
|
236a8b3023 | ||
|
|
63e4bac204 | ||
|
|
2e11247af4 | ||
|
|
13f377f680 | ||
|
|
b9d594bc8b | ||
|
|
2a52157c3f | ||
|
|
a037d8e793 | ||
|
|
49b7ea14c3 | ||
|
|
0a40d11133 | ||
|
|
dff6b6486d | ||
|
|
b51c646415 | ||
|
|
25acf3a9bb | ||
|
|
7ec90e9a22 | ||
|
|
645ce0b7b3 | ||
|
|
864fef5881 | ||
|
|
eea232c6db | ||
|
|
25fca498fc | ||
|
|
c9a02f9c74 | ||
|
|
99602eea95 | ||
|
|
b5fe0faeec | ||
|
|
20a36e8f9a | ||
|
|
161bf2eedb | ||
|
|
fcf4e72d8e | ||
|
|
b46cb69df4 | ||
|
|
43c7620b4c | ||
|
|
1a8f65b075 | ||
|
|
259e7080f8 | ||
|
|
7da6f05a50 | ||
|
|
521ca3ad18 | ||
|
|
31e5692dff | ||
|
|
191bcbc525 | ||
|
|
a0f3b3e442 | ||
|
|
5bcd822251 | ||
|
|
d80324655e | ||
|
|
952d168022 | ||
|
|
af723e161c | ||
|
|
3ff521e103 | ||
|
|
b4a5d985f5 | ||
|
|
1e0e2d2d6e | ||
|
|
d7f7611af4 | ||
|
|
11cdb67050 | ||
|
|
53cf9d54c4 | ||
|
|
2e73688688 | ||
|
|
ce5e85e64b | ||
|
|
02e0d34127 | ||
|
|
830f3b60e0 | ||
|
|
b4fb7d14d4 | ||
|
|
ab1e5cb62a | ||
|
|
348a9e014e | ||
|
|
0baf3fcd36 | ||
|
|
13818533a7 | ||
|
|
0dd3689d65 | ||
|
|
23b6850778 | ||
|
|
d8ca89ac8f | ||
|
|
ae06d5f7f2 | ||
|
|
62506d3eb5 | ||
|
|
f7c61d63a0 | ||
|
|
f46437f891 | ||
|
|
1cd949c365 | ||
|
|
bc5ce11449 | ||
|
|
cef4beaa0c | ||
|
|
02bd68f697 | ||
|
|
2bc3275c1f | ||
|
|
ec107063c3 | ||
|
|
4c2fd38d6c | ||
|
|
1a6653ba93 | ||
|
|
74d5e03a34 | ||
|
|
2b4b1debe6 | ||
|
|
17883eb77e | ||
|
|
3741fe54ff | ||
|
|
ec11af3827 | ||
|
|
890dc58dc3 | ||
|
|
b12bdf2eb8 | ||
|
|
59c7f8a030 | ||
|
|
50cf74ccf7 | ||
|
|
15b5c0a874 | ||
|
|
244ef22f54 | ||
|
|
b4daf5fbd8 | ||
|
|
0519ec0e4b | ||
|
|
ff4f97de1a | ||
|
|
773bdafec3 | ||
|
|
3787f99d35 | ||
|
|
2cb8331528 | ||
|
|
5b6443cfa4 | ||
|
|
6fd8212d8b | ||
|
|
0d273f6909 | ||
|
|
255e39b709 | ||
|
|
ea52dd4484 | ||
|
|
b4a46133be | ||
|
|
7c1644efc4 | ||
|
|
775e1aa97d | ||
|
|
2a55d4390a | ||
|
|
d57a34a4e1 | ||
|
|
2785248615 | ||
|
|
c42468e2c8 | ||
|
|
196ddf3f5f | ||
|
|
27302435be | ||
|
|
2b3ec77e92 | ||
|
|
b7a277a57c | ||
|
|
9c8e5b53e7 | ||
|
|
001b746f65 | ||
|
|
a78214de3c | ||
|
|
d88ffb1127 | ||
|
|
f05b901009 | ||
|
|
430837eef6 | ||
|
|
fa583ebd0f | ||
|
|
d2dcba5a59 | ||
|
|
fb5116d525 | ||
|
|
a48f6b1ca5 | ||
|
|
fc0af3f284 | ||
|
|
2288e11398 | ||
|
|
d95283c4ac | ||
|
|
4b56bd5a87 | ||
|
|
62bb605ee8 | ||
|
|
0f8da1999a | ||
|
|
21a2373a5c | ||
|
|
2ca5310825 | ||
|
|
9ccaa3072b | ||
|
|
ded78e534f | ||
|
|
9b0a43efc9 | ||
|
|
10808c2a84 | ||
|
|
38a7afd63a | ||
|
|
54b26d20fa | ||
|
|
ad2bc78ebd | ||
|
|
c4aca389a8 | ||
|
|
cb8333d4c0 | ||
|
|
2f5eed6998 | ||
|
|
935c53e452 | ||
|
|
dd0ccb327b | ||
|
|
919134759b | ||
|
|
c1d42b498a | ||
|
|
a7e67796f1 | ||
|
|
6692c9e851 | ||
|
|
ace949aaa0 | ||
|
|
fbd9687432 | ||
|
|
460a8262c1 | ||
|
|
c8de503fae | ||
|
|
a60cd51ff4 | ||
|
|
aad980ce23 | ||
|
|
e7cda7b9fa | ||
|
|
1d368b7a8b | ||
|
|
725d7055bf | ||
|
|
1fb798db4e | ||
|
|
8e1d5e0dd5 | ||
|
|
2d9a1310b9 | ||
|
|
588ec7babd | ||
|
|
2be13e7283 | ||
|
|
d5d95671ff | ||
|
|
a0eccda6ff | ||
|
|
ec82c86210 | ||
|
|
de03bef226 | ||
|
|
0f8166620e | ||
|
|
76c2de4394 | ||
|
|
0d38ded981 | ||
|
|
646888c06f | ||
|
|
332f6f1bb4 | ||
|
|
aaab5371b2 | ||
|
|
ad931d7ea2 | ||
|
|
377e430d74 | ||
|
|
a797467606 | ||
|
|
5ee83d902d | ||
|
|
27ae296b28 | ||
|
|
e589f27195 | ||
|
|
c89d6a5a59 | ||
|
|
861365930d | ||
|
|
0d4d92a202 | ||
|
|
4c6ad0e385 | ||
|
|
ad45e995e2 | ||
|
|
50a035a479 | ||
|
|
c0dbd6cbb2 | ||
|
|
686af4a330 | ||
|
|
46aad06e34 | ||
|
|
3921b2304d | ||
|
|
bca5b0419c | ||
|
|
9754b061dd | ||
|
|
407b31c5c1 | ||
|
|
37b1228552 | ||
|
|
0acd9ca767 | ||
|
|
8f3c9f029c | ||
|
|
9310732343 | ||
|
|
e767e506f3 | ||
|
|
ef3a612338 | ||
|
|
d66a42a0aa | ||
|
|
0f06de0047 | ||
|
|
963181fef2 | ||
|
|
ffd4f9ee73 | ||
|
|
976622df89 | ||
|
|
13c220338c | ||
|
|
1291dc77c8 | ||
|
|
08e5477e74 | ||
|
|
c4c6a2243e | ||
|
|
58791e3e91 | ||
|
|
d5bb4bc149 | ||
|
|
3d1199363b | ||
|
|
f225fa33e1 | ||
|
|
e85c8b3dde | ||
|
|
737be8dcac | ||
|
|
77dd939172 | ||
|
|
0a5965a423 | ||
|
|
a53be6814c | ||
|
|
415b8e9da3 | ||
|
|
f034c24d13 | ||
|
|
1ac93d6269 | ||
|
|
906c8f7999 | ||
|
|
c904a5ded8 | ||
|
|
0c9486f6b4 | ||
|
|
576740a502 | ||
|
|
b3f9f43b57 | ||
|
|
e7424bcc66 | ||
|
|
209ec70ea9 | ||
|
|
3b4e251034 | ||
|
|
86beb879a2 | ||
|
|
321d434141 | ||
|
|
b9d17e27b1 | ||
|
|
2f6f6da6c0 | ||
|
|
c3d3fa67f7 | ||
|
|
032dfd69be | ||
|
|
e9dc154642 | ||
|
|
b43840b636 | ||
|
|
1a9d8e35ba | ||
|
|
ccb61415f5 | ||
|
|
08944241bb | ||
|
|
63030147ea | ||
|
|
8ff71c44ca | ||
|
|
4eaf16f500 | ||
|
|
1a9c8a62f2 | ||
|
|
4256c2b023 | ||
|
|
bbcf0dec1b | ||
|
|
da52cac2c6 | ||
|
|
e8a32a6149 | ||
|
|
a71a7b66f8 | ||
|
|
9808f50816 | ||
|
|
cf86bb7e13 | ||
|
|
ff065254ae | ||
|
|
39b4c1a59b | ||
|
|
28f10e0a4b | ||
|
|
12c0ed5baf | ||
|
|
23272d285b | ||
|
|
67b4ed65ab | ||
|
|
7524b3d168 | ||
|
|
340a933e70 | ||
|
|
488ca29fc1 | ||
|
|
cc00b2cc39 | ||
|
|
287cea4d6c | ||
|
|
39e556891a | ||
|
|
0ae4157384 | ||
|
|
6e1ceb1277 | ||
|
|
71a170deb5 | ||
|
|
9482a706da | ||
|
|
0804484a49 | ||
|
|
cdb9bb3dbc | ||
|
|
6ca0de96f4 | ||
|
|
d908f58528 | ||
|
|
1368733a24 | ||
|
|
32e71dbf65 | ||
|
|
c9ce1af2c6 | ||
|
|
416f9e6a8d | ||
|
|
3ae3955f53 | ||
|
|
464f008023 | ||
|
|
52498b3e34 | ||
|
|
57c57b02a5 | ||
|
|
b8c6868043 | ||
|
|
8200fbf512 | ||
|
|
8650c96b7b | ||
|
|
15fe7787ba | ||
|
|
d83076cb07 | ||
|
|
8b3b4c28a5 | ||
|
|
740c001e2f | ||
|
|
096b057f81 | ||
|
|
a161fa5e58 | ||
|
|
bebf34db23 | ||
|
|
b95061434a | ||
|
|
f2a05bb970 | ||
|
|
6c361a047b | ||
|
|
3fb9e22378 | ||
|
|
b2fb4c9afe | ||
|
|
0862c0fc87 | ||
|
|
77ec78e3fe | ||
|
|
fb59c208e3 | ||
|
|
112a06f92a | ||
|
|
c10c4a6f89 | ||
|
|
669c807b23 | ||
|
|
c9de79532a | ||
|
|
32ce2b87db | ||
|
|
4cfcf18bc9 | ||
|
|
14ae61f891 | ||
|
|
a2d5ecc51e | ||
|
|
84f972a3ab | ||
|
|
5249ceccdb | ||
|
|
5035495043 | ||
|
|
25483d71e9 | ||
|
|
c3fa976b26 | ||
|
|
43beb518f4 | ||
|
|
11edabb890 | ||
|
|
019cd9fda0 | ||
|
|
9d747c8e2c | ||
|
|
4cf1c25b36 | ||
|
|
6c6ed46aea | ||
|
|
e1473a453e | ||
|
|
9f6ef0281a | ||
|
|
84d5a24bc3 | ||
|
|
ed8c39aa76 | ||
|
|
23d235b8f4 | ||
|
|
8bea09b78a | ||
|
|
897fda875a | ||
|
|
510bfe01be | ||
|
|
f6ca007815 | ||
|
|
35b34cb2d4 | ||
|
|
5197cca69c | ||
|
|
e5f0742bf6 | ||
|
|
88d207cc24 | ||
|
|
931fcb6f8f | ||
|
|
e4a960ecf9 | ||
|
|
e44419e088 | ||
|
|
16f577f3fd | ||
|
|
a65edab7d1 | ||
|
|
c0bbf8400a | ||
|
|
1dc2da68ac | ||
|
|
3d49529272 | ||
|
|
41768656b4 | ||
|
|
c7e7b3f9c5 | ||
|
|
e0b0a98f0f | ||
|
|
ca0eb1716f | ||
|
|
06d8296939 | ||
|
|
322885f284 | ||
|
|
4553b86cb4 | ||
|
|
904756b6ea | ||
|
|
2bfa1bb6c2 | ||
|
|
8439a3d85c | ||
|
|
454d6b9de1 | ||
|
|
44c7c44a27 | ||
|
|
40e5e2f372 | ||
|
|
138739781c | ||
|
|
355d897ef0 | ||
|
|
a06aef2b25 | ||
|
|
6ef9a24ed1 | ||
|
|
4df2bb0073 | ||
|
|
f93753ccfd | ||
|
|
52373dc540 | ||
|
|
203a997583 | ||
|
|
b22a406471 | ||
|
|
a441759eb6 | ||
|
|
9057401b16 | ||
|
|
6d0017c256 | ||
|
|
12b27b1d8d | ||
|
|
884bb53d6f | ||
|
|
aa356b5376 | ||
|
|
2aa9b46433 | ||
|
|
42f5a42dd9 | ||
|
|
74f0fb471c | ||
|
|
c31e772a63 | ||
|
|
32f6d97256 | ||
|
|
a28db0dd98 | ||
|
|
aba9493ae0 | ||
|
|
4973176868 | ||
|
|
a000e2262c | ||
|
|
a5715868b3 | ||
|
|
a928e48159 | ||
|
|
16c152d306 | ||
|
|
5747dee03d | ||
|
|
06c545acd4 | ||
|
|
54c3c314e1 | ||
|
|
11c4fae547 | ||
|
|
8f87d248a1 | ||
|
|
ec37af5900 | ||
|
|
1b213793d4 | ||
|
|
aaa8998cb1 | ||
|
|
94760a4136 | ||
|
|
bdbd6cd377 | ||
|
|
d69d81912d | ||
|
|
198a38b103 | ||
|
|
750e67d835 |
16
.github/ISSUE_TEMPLATE/bug-反馈.yml
vendored
@@ -9,10 +9,24 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的 bug report。
|
||||
- label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同问题
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
- label: 已排除网络问题
|
||||
required: true
|
||||
- label: 已排除账号问题
|
||||
required: true
|
||||
- label: 已排除设置问题
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: assign
|
||||
attributes:
|
||||
label: Assign
|
||||
options:
|
||||
- label: self-assign
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/功能请求.yml
vendored
@@ -9,10 +9,20 @@ body:
|
||||
attributes:
|
||||
label: 检查清单
|
||||
options:
|
||||
- label: 之前没有人提交过类似或相同的功能请求。
|
||||
- label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同功能请求
|
||||
required: true
|
||||
- label: 正在使用最新版本。
|
||||
required: true
|
||||
- label: 设置中未搜索到该功能
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: assign
|
||||
attributes:
|
||||
label: Assign
|
||||
options:
|
||||
- label: self-assign
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: desc
|
||||
|
||||
92
.github/workflows/android.yml
vendored
@@ -1,92 +0,0 @@
|
||||
name: Android Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 构建Java环境
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "17"
|
||||
|
||||
- name: 检查缓存
|
||||
uses: actions/cache@v4
|
||||
id: cache-flutter
|
||||
with:
|
||||
path: /root/flutter-sdk # Flutter SDK 的路径
|
||||
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
|
||||
|
||||
- name: 安装Flutter
|
||||
if: steps.cache-flutter.outputs.cache-hit != 'true'
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: apply bottom sheet patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: |
|
||||
git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "bottom sheet patch"
|
||||
|
||||
- name: 下载项目依赖
|
||||
run: flutter pub get
|
||||
|
||||
- name: Write key
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
|
||||
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
|
||||
echo storeFile='key.jks' >> android/key.properties
|
||||
echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties
|
||||
echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties
|
||||
echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties
|
||||
fi
|
||||
|
||||
- name: flutter build apk
|
||||
run: |
|
||||
chmod +x lib/scripts/build.dart
|
||||
dart lib/scripts/build.dart "true"
|
||||
flutter build apk --release --split-per-abi
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-arm64-v8a
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-armeabi-v7a
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-x86_64
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/app-x86_64-release.apk
|
||||
173
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_android:
|
||||
description: "Build Android"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_ios:
|
||||
description: "Build iOS"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_mac:
|
||||
description: "Build Mac"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_win_x64:
|
||||
description: "Build Win-x64"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
build_linux_x64:
|
||||
description: "Build Linux-x64"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
android:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_android == 'true' }}
|
||||
name: Release Android
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
|
||||
steps:
|
||||
- name: 代码迁出
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 构建Java环境
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "17"
|
||||
cache: "gradle"
|
||||
cache-dependency-path: |
|
||||
android/*.gradle*
|
||||
android/**/gradle-wrapper.properties
|
||||
|
||||
- name: 安装Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
id: flutter-action
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: apply bottom sheet patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
|
||||
continue-on-error: true
|
||||
|
||||
- name: apply modal barrier patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
|
||||
continue-on-error: true
|
||||
|
||||
- name: Write key
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
|
||||
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
|
||||
echo storeFile='key.jks' >> android/key.properties
|
||||
echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties
|
||||
echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties
|
||||
echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties
|
||||
fi
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1 android
|
||||
|
||||
- name: flutter build apk
|
||||
run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub
|
||||
|
||||
- name: rename
|
||||
run: |
|
||||
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
||||
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
|
||||
mv "$file" "PiliPlus_android_${{ env.version }}_${abi}.apk"
|
||||
done
|
||||
shell: bash
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_android_*.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Android_arm64-v8a
|
||||
path: |
|
||||
PiliPlus_android_*_arm64-v8a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Android_armeabi-v7a
|
||||
path: |
|
||||
PiliPlus_android_*_armeabi-v7a.apk
|
||||
|
||||
- name: 上传
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Android_x86_64
|
||||
path: |
|
||||
PiliPlus_android_*_x86_64.apk
|
||||
|
||||
ios:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_ios == 'true' }}
|
||||
uses: ./.github/workflows/ios.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
mac:
|
||||
if: ${{ github.event.inputs.build_mac == 'true' }}
|
||||
uses: ./.github/workflows/mac.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
win_x64:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
|
||||
uses: ./.github/workflows/win_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
|
||||
linux_x64:
|
||||
if: ${{ github.event.inputs.build_linux_x64 == 'true' }}
|
||||
uses: ./.github/workflows/linux_x64.yml
|
||||
permissions: write-all
|
||||
with:
|
||||
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
|
||||
52
.github/workflows/ios.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: Build for iOS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: "main"
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-macos-app:
|
||||
@@ -21,9 +16,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
@@ -32,16 +26,34 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: apply modal barrier patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build iOS
|
||||
run: |
|
||||
chmod +x lib/scripts/build.dart
|
||||
dart lib/scripts/build.dart
|
||||
flutter build ios --release --no-codesign
|
||||
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
|
||||
ln -sf ./build/ios/iphoneos Payload
|
||||
zip -r9 ios-release-no-sign.ipa Payload/runner.app
|
||||
# make AltSign happy...
|
||||
find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \;
|
||||
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_ios_*.ipa
|
||||
|
||||
- name: Upload ios release
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ios-release
|
||||
path: ios-release-no-sign.ipa
|
||||
name: iOS-release
|
||||
path: PiliPlus_ios_*.ipa
|
||||
|
||||
78
.github/workflows/linux.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Build for Linux
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
required: false
|
||||
default: "main"
|
||||
|
||||
jobs:
|
||||
build-linux-app:
|
||||
name: Release Linux
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev
|
||||
sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \
|
||||
libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \
|
||||
libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \
|
||||
libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \
|
||||
libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \
|
||||
libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \
|
||||
libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \
|
||||
libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \
|
||||
libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \
|
||||
libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \
|
||||
linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \
|
||||
x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \
|
||||
unixodbc-dev libpq-dev libxxhash-dev libaom-dev \
|
||||
libgtk-3-0 libblkid1 liblzma5 libmpv-dev
|
||||
shell: bash
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Get Flutter dependencies
|
||||
run: flutter pub get
|
||||
shell: bash
|
||||
|
||||
- name: Set and Extract version
|
||||
run: |
|
||||
dart lib/scripts/build.dart
|
||||
VERSION=$(cat pubspec.yaml | grep 'version:' | sed 's/version: //g' | tr -d '[:space:]')
|
||||
echo "version=$VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
#TODO: deb and rpm packages need to be build
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release -v
|
||||
|
||||
- name: Package linux build output
|
||||
run: tar -zcvf PiliPlus_linux_${{ env.version }}_amd64.tar.gz -C build/linux/x64/release/bundle .
|
||||
|
||||
- name: Upload linux outputs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux_outputs
|
||||
path: |
|
||||
PiliPlus_linux_*.tar.gz
|
||||
265
.github/workflows/linux_x64.yml
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
name: Build for Linux x64
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux-app:
|
||||
name: Release Linux x64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev rpm patchelf
|
||||
sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \
|
||||
libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \
|
||||
libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \
|
||||
libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \
|
||||
libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \
|
||||
libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \
|
||||
libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \
|
||||
libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \
|
||||
libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \
|
||||
libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \
|
||||
linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \
|
||||
x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \
|
||||
unixodbc-dev libpq-dev libxxhash-dev libaom-dev \
|
||||
libgtk-3-0 libblkid1 liblzma5 libmpv-dev
|
||||
shell: bash
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
cache: true
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: apply modal barrier patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
|
||||
continue-on-error: true
|
||||
|
||||
#TODO: deb and rpm packages need to be build
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
|
||||
|
||||
- name: Package .tar.gz
|
||||
run: tar -zcvf PiliPlus_linux_${{ env.version }}_amd64.tar.gz -C build/linux/x64/release/bundle .
|
||||
|
||||
- name: Packege deb
|
||||
run: |
|
||||
printf "建立构建目录...\n"
|
||||
mkdir "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
pushd "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
mkdir -p opt/PiliPlus
|
||||
mkdir -p usr/share/applications
|
||||
mkdir -p usr/share/icons/hicolor/512x512/apps
|
||||
|
||||
printf "复制文件...\n"
|
||||
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
|
||||
cp -r ../assets/linux/DEBIAN .
|
||||
cp ../assets/linux/piliplus.desktop usr/share/applications
|
||||
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
printf "修改控制文件...\n"
|
||||
# 替换版本号
|
||||
sed -i "2s/version_need_change/${{ env.version }}/g" DEBIAN/control
|
||||
# 计算安装大小并替换
|
||||
SIZE_KB=$(du -s -b --apparent-size . | awk '{print int($1)}')
|
||||
SIZE_KB=$(($SIZE_KB - $(du -s -b --apparent-size DEBIAN | awk '{print int($1)}')))
|
||||
SIZE_KB=$(echo $SIZE_KB | awk '{print int($1/1024 + 0.999)}')
|
||||
printf "\t安装大小: %s KB\n" "$SIZE_KB"
|
||||
sed -i "9s/size_need_change/${SIZE_KB}/g" DEBIAN/control
|
||||
|
||||
printf "生成并写入 md5sums ...\n"
|
||||
md5sum opt/PiliPlus/piliplus >> DEBIAN/md5sums
|
||||
md5sum opt/PiliPlus/lib/* >> DEBIAN/md5sums
|
||||
md5sum opt/PiliPlus/data/icudtl.dat >> DEBIAN/md5sums
|
||||
|
||||
printf "设置权限...\n"
|
||||
chmod 0644 DEBIAN/control
|
||||
chmod 0644 DEBIAN/md5sums
|
||||
chmod 0755 DEBIAN/postinst
|
||||
chmod 0755 DEBIAN/postrm
|
||||
chmod 0755 DEBIAN/prerm
|
||||
|
||||
printf "打包 deb 文件...\n"
|
||||
popd
|
||||
dpkg-deb --build --verbose --root-owner-group "PiliPlus_linux_${{ env.version }}_amd64"
|
||||
printf "完成: PiliPlus_linux_%s_amd64.deb\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
- name: Packege rpm
|
||||
run: |
|
||||
printf "建立 RPM 构建目录...\n"
|
||||
RPM_BUILD_ROOT="$PWD/rpm_build"
|
||||
mkdir -p "$RPM_BUILD_ROOT/BUILD" "$RPM_BUILD_ROOT/RPMS" "$RPM_BUILD_ROOT/SOURCES" "$RPM_BUILD_ROOT/SPECS" "$RPM_BUILD_ROOT/SRPMS"
|
||||
|
||||
printf "准备源码归档(仅包含运行时与元数据)...\n"
|
||||
DATE="$(date '+%a %b %d %Y')"
|
||||
SRC_DIR="$PWD/piliplus-${{ env.version }}"
|
||||
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
|
||||
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
|
||||
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
|
||||
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
|
||||
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
|
||||
|
||||
printf "生成 spec 文件...\n"
|
||||
cat > "$RPM_BUILD_ROOT/SPECS/piliplus.spec" <<EOF
|
||||
Name: piliplus
|
||||
Version: ${{ env.version }}
|
||||
Release: 1%{?dist}
|
||||
Summary: PiliPlus Linux Version
|
||||
License: GPL-3.0
|
||||
Source0: piliplus-${{ env.version }}.tar.gz
|
||||
Requires: desktop-file-utils, hicolor-icon-theme
|
||||
|
||||
%description
|
||||
使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
|
||||
%prep
|
||||
%setup -q -n piliplus-${{ env.version }}
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}/opt/PiliPlus
|
||||
cp -r bundle/* %{buildroot}/opt/PiliPlus/
|
||||
|
||||
# 二进制权限与命令行入口
|
||||
chmod 755 %{buildroot}/opt/PiliPlus/piliplus
|
||||
mkdir -p %{buildroot}/usr/bin
|
||||
ln -sf /opt/PiliPlus/piliplus %{buildroot}/usr/bin/piliplus
|
||||
|
||||
# 桌面集成
|
||||
mkdir -p %{buildroot}/usr/share/applications
|
||||
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
|
||||
|
||||
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
|
||||
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
%post
|
||||
update-desktop-database -q || true
|
||||
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
|
||||
|
||||
%postun
|
||||
update-desktop-database -q || true
|
||||
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
|
||||
|
||||
%files
|
||||
/opt/PiliPlus
|
||||
/usr/bin/piliplus
|
||||
/usr/share/applications/piliplus.desktop
|
||||
/usr/share/icons/hicolor/512x512/apps/piliplus.png
|
||||
|
||||
%changelog
|
||||
* DATE - ${{ env.version }}-1
|
||||
- Initial RPM release
|
||||
EOF
|
||||
|
||||
sed -i "s/DATE/${DATE}/g" "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
|
||||
|
||||
printf "构建 RPM 包...\n"
|
||||
rpmbuild --define "_topdir $RPM_BUILD_ROOT" -bb "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
|
||||
|
||||
printf "移动生成的 RPM...\n"
|
||||
find "$RPM_BUILD_ROOT/RPMS" -name "*.rpm" -exec mv {} "PiliPlus_linux_${{ env.version }}_amd64.rpm" \;
|
||||
|
||||
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
|
||||
shell: bash
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
printf "下载 appimagetool...\n"
|
||||
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
|
||||
printf "建立 AppDir 目录结构...\n"
|
||||
APPDIR="PiliPlus.AppDir"
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
mkdir -p "$APPDIR/usr/lib"
|
||||
mkdir -p "$APPDIR/usr/share/applications"
|
||||
mkdir -p "$APPDIR/usr/share/icons/hicolor/512x512/apps"
|
||||
|
||||
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
|
||||
SELF=$(readlink -f "$0")
|
||||
HERE=${SELF%/*}
|
||||
export PATH="${HERE}/usr/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
|
||||
exec "${HERE}/usr/bin/piliplus" "$@"
|
||||
APPRUN_EOF
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
printf "修改桌面文件中的 Exec 路径...\n"
|
||||
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/piliplus.desktop"
|
||||
|
||||
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
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_linux_*.tar.gz
|
||||
PiliPlus_linux_*.deb
|
||||
PiliPlus_linux_*.rpm
|
||||
PiliPlus_linux_*.AppImage
|
||||
|
||||
- name: Upload linux targz package
|
||||
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@v5
|
||||
with:
|
||||
name: Linux_deb_amd64_package
|
||||
path: PiliPlus_linux_*.deb
|
||||
|
||||
- name: Upload linux rpm package
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Linux_rpm_amd64_package
|
||||
path: PiliPlus_linux_*.rpm
|
||||
|
||||
- name: Upload linux AppImage package
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Linux_AppImage_amd64_package
|
||||
path: PiliPlus_linux_*.AppImage
|
||||
52
.github/workflows/mac.yml
vendored
@@ -1,19 +1,14 @@
|
||||
name: Build for Mac
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: 'main'
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-mac-app:
|
||||
@@ -21,9 +16,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
@@ -32,12 +26,17 @@ jobs:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: apply modal barrier patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build Mac
|
||||
run: |
|
||||
dart lib/scripts/build.dart
|
||||
VERSION=$(cat pubspec.yaml | grep 'version:' | sed 's/version: //g' | tr -d '[:space:]')
|
||||
echo "version=$VERSION" >> $GITHUB_ENV
|
||||
flutter build macos --release
|
||||
run: flutter build macos --release --dart-define-from-file=pili_release.json
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
@@ -48,8 +47,17 @@ jobs:
|
||||
- name: Rename DMG
|
||||
run: mv PiliPlus*.dmg PiliPlus_macos_${{ env.version }}.dmg
|
||||
|
||||
- name: Upload macos release
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: macos-release
|
||||
path: PiliPlus*.dmg
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_macos_*.dmg
|
||||
|
||||
- name: Upload macos release
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: macOS-release
|
||||
path: PiliPlus_macos_*.dmg
|
||||
|
||||
49
.github/workflows/win.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Build for Windows
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
required: false
|
||||
default: 'main'
|
||||
|
||||
jobs:
|
||||
build-windows-app:
|
||||
name: Release Windows
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: Build Windows
|
||||
run: |
|
||||
dart lib/scripts/build.dart
|
||||
flutter build windows --release
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
mkdir -p Release/PiliPlus-Win
|
||||
mv build/windows/x64/runner/Release/* Release/PiliPlus-Win/
|
||||
|
||||
- name: Upload windows release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-release
|
||||
path: Release
|
||||
86
.github/workflows/win_x64.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Build for Windows x64
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "tag"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-windows-app:
|
||||
name: Release Windows x64
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version-file: pubspec.yaml
|
||||
|
||||
- name: apply modal barrier patch
|
||||
working-directory: ${{ env.FLUTTER_ROOT }}
|
||||
run: git apply $env:GITHUB_WORKSPACE\lib\scripts\modal-barrier-patch.diff
|
||||
shell: pwsh
|
||||
continue-on-error: true
|
||||
|
||||
- name: Add fastforge and Inno Setup
|
||||
run: |
|
||||
dart pub global activate fastforge
|
||||
choco install innosetup
|
||||
|
||||
- name: Add Chinese language file for Inno Setup
|
||||
run: |
|
||||
Copy-Item "windows/packaging/exe/ChineseSimplified.isl" "C:\Program Files (x86)\Inno Setup 6\Languages\ChineseSimplified.isl"
|
||||
shell: pwsh
|
||||
|
||||
- name: Set and Extract version
|
||||
shell: pwsh
|
||||
run: lib/scripts/build.ps1
|
||||
|
||||
- name: Build Windows
|
||||
run: |
|
||||
fastforge package --platform windows --targets exe --flutter-build-args="dart-define-from-file=pili_release.json"
|
||||
|
||||
- name: Prepare Upload
|
||||
run: |
|
||||
mkdir -p Release/PiliPlus-Win
|
||||
mkdir -p PiliPlus-Win-Setup
|
||||
mv build/windows/x64/runner/Release/* Release/PiliPlus-Win/
|
||||
mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe
|
||||
|
||||
- name: Compress
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
run: |
|
||||
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64.zip"
|
||||
shell: pwsh
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.tag != '' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
PiliPlus_windows_*.zip
|
||||
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
|
||||
|
||||
- name: Upload windows file release
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Windows-file-x64-release
|
||||
path: Release
|
||||
|
||||
- name: Upload windows setup release
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Windows-setup-x64-release
|
||||
path: PiliPlus-Win-Setup
|
||||
15
.gitignore
vendored
@@ -19,7 +19,7 @@ migrate_working_dir/
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
.vscode/
|
||||
|
||||
# Flutter repo-specific
|
||||
/bin/cache/
|
||||
@@ -134,7 +134,16 @@ app.*.symbols
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
!.vscode/settings.json
|
||||
|
||||
/lib/build_config.dart
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
|
||||
devtools_options.yaml
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
|
||||
pili_release.json
|
||||
|
||||
dist
|
||||
|
||||
test.dart
|
||||
8
.vscode/launch.json
vendored
@@ -1,22 +1,22 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "piliplus",
|
||||
"name": "PiliPlus",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "piliplus (profile mode)",
|
||||
"name": "PiliPlus (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "piliplus (release mode)",
|
||||
"name": "PiliPlus (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
|
||||
15
README.md
@@ -43,8 +43,15 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 编辑动态
|
||||
- [x] DLNA 投屏
|
||||
- [x] 离线缓存/播放
|
||||
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 播放音频
|
||||
- [x] 跳过番剧片头/片尾
|
||||
- [x] 安卓端 `loudnorm` 适配 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] Win/Mac 支持极验、短信登录 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 视频截取 GIF by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] 视频截取动图 by [@My-Responsitories](https://github.com/My-Responsitories)
|
||||
- [x] AI 原声翻译
|
||||
- [x] SuperChat
|
||||
- [x] 播放课堂视频
|
||||
@@ -147,7 +154,7 @@
|
||||
- [x] 粉丝、关注用户、拉黑用户查看
|
||||
- [x] 用户主页查看
|
||||
- [x] 关注/取关用户
|
||||
- [ ] 离线缓存
|
||||
- [x] 离线缓存
|
||||
- [x] 稍后再看
|
||||
- [x] 观看记录
|
||||
- [x] 我的收藏
|
||||
@@ -212,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
|
||||
|
||||
@@ -210,4 +210,5 @@
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||
</manifest>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.piliplus
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.SearchManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
@@ -42,7 +43,10 @@ class MainActivity : AudioServiceActivity() {
|
||||
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
|
||||
|
||||
val intent = Intent().apply {
|
||||
component = ComponentName("icu.freedomIntrovert.biliSendCommAntifraud", "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity")
|
||||
component = ComponentName(
|
||||
"icu.freedomIntrovert.biliSendCommAntifraud",
|
||||
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
|
||||
)
|
||||
putExtra("action", action)
|
||||
putExtra("oid", oid.toLong())
|
||||
putExtra("type", type)
|
||||
@@ -51,23 +55,27 @@ class MainActivity : AudioServiceActivity() {
|
||||
putExtra("parent", parent.toLong())
|
||||
putExtra("ctime", ctime.toLong())
|
||||
putExtra("comment_text", commentText)
|
||||
if(pictures != null)
|
||||
if (pictures != null)
|
||||
putExtra("pictures", pictures)
|
||||
putExtra("source_id", sourceId)
|
||||
putExtra("uid", uid.toLong())
|
||||
putStringArrayListExtra("cookies", ArrayList(cookies))
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (_: Exception) {}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
"linkVerifySettings" -> {
|
||||
val uri = ("package:" + context.packageName).toUri()
|
||||
try {
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
|
||||
} else {
|
||||
Intent("android.intent.action.MAIN", uri).setClassName("com.android.settings",
|
||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity")
|
||||
Intent("android.intent.action.MAIN", uri).setClassName(
|
||||
"com.android.settings",
|
||||
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
|
||||
)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (_: Throwable) {
|
||||
@@ -75,33 +83,56 @@ class MainActivity : AudioServiceActivity() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
"music" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, title)
|
||||
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
|
||||
call.argument<String?>("artist")?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
||||
call.argument<String?>("album")?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
||||
call.argument<String?>("artist")
|
||||
?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
|
||||
call.argument<String?>("album")
|
||||
?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
|
||||
|
||||
addCategory(Intent.CATEGORY_DEFAULT)
|
||||
}
|
||||
try {
|
||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
if (packageManager.resolveActivity(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
) != null
|
||||
) {
|
||||
startActivity(intent)
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
try {
|
||||
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
|
||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
||||
if (packageManager.resolveActivity(
|
||||
intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
) != null
|
||||
) {
|
||||
startActivity(intent)
|
||||
result.success(true)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
} catch (_: Throwable) {}
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
result.success(false)
|
||||
}
|
||||
|
||||
"setPipAutoEnterEnabled" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAutoEnterEnabled(call.argument<Boolean>("autoEnable") ?: false)
|
||||
.build()
|
||||
setPictureInPictureParams(params)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
@@ -124,6 +155,7 @@ class MainActivity : AudioServiceActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java))
|
||||
super.onDestroy()
|
||||
android.os.Process.killProcess(android.os.Process.myPid())
|
||||
exitProcess(0)
|
||||
@@ -134,7 +166,10 @@ class MainActivity : AudioServiceActivity() {
|
||||
methodChannel.invokeMethod("onUserLeaveHint", null)
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration?
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
MethodChannel(
|
||||
flutterEngine!!.dartExecutor.binaryMessenger,
|
||||
@@ -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")
|
||||
|
||||
16
assets/linux/DEBIAN/control
Normal file
@@ -0,0 +1,16 @@
|
||||
Package: PiliPlus
|
||||
Version: version_need_change
|
||||
Maintainer: gh-MzA4Nzk <githubaccount2333@proton.me>
|
||||
Original-Maintainer: bggRGjQaUbCoE <githubaccount56556@proton.me>
|
||||
Section: x11
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Essential: no
|
||||
Installed-Size: size_need_change
|
||||
Description: third-party Bilibili client developed in Flutter
|
||||
Homepage: https://github.com/bggRGjQaUbCoE/PiliPlus
|
||||
Depends: libgtk-3-0t64,
|
||||
libmpv2,
|
||||
gir1.2-ayatanaappindicator3-0.1,
|
||||
libayatana-appindicator3-1
|
||||
|
||||
21
assets/linux/DEBIAN/postinst
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus
|
||||
chmod +x /usr/bin/piliplus
|
||||
|
||||
if [ $1 == "configure" ] && [ -x /usr/bin/update-mime-database ]; then
|
||||
echo "updating mime database..."
|
||||
update-mime-database /usr/share/mime || true
|
||||
fi
|
||||
|
||||
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 == "configure" ] && [ -x /usr/bin/update-desktop-database ]; then
|
||||
echo "configure desktop database..."
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
26
assets/linux/DEBIAN/postrm
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
rm /usr/bin/piliplus
|
||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
||||
if [ -x /usr/bin/update-desktop-database ]; then
|
||||
echo "updating desktop database..."
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
|
||||
if [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
echo "updating icon cache..."
|
||||
gtk-update-icon-cache -q -t /usr/share/icons/hicolor || true
|
||||
fi
|
||||
|
||||
if [ -x /usr/bin/update-mime-database ]; then
|
||||
echo "updating mime database..."
|
||||
update-mime-database /usr/share/mime || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $1 = "purge" ]; then
|
||||
echo "Removing user data..."
|
||||
rm -rf /home/*/.local/share/com.example.PiliPlus || true
|
||||
rm -rf /root/.local/share/com.example.PiliPlus || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
8
assets/linux/DEBIAN/prerm
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
|
||||
echo "Stopping PiliPlus if running..."
|
||||
pkill -x piliplus || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
10
assets/linux/piliplus.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=PiliPlus
|
||||
Comment=A third-party Bilibili Client developed in Flutter
|
||||
Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
Exec=piliplus
|
||||
Icon=piliplus
|
||||
Terminal=false
|
||||
StartupWMClass=com.example.piliplus
|
||||
Categories=Video;AudioVideo;Player;
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1.0.0
|
||||
|
||||
### 初始版本
|
||||
+ 直播、推荐、动态功能
|
||||
+ 投稿、番剧播放功能
|
||||
+ 播放器手势支持
|
||||
+ 画质、音质、解码格式支持
|
||||
+ 点赞、投币、收藏功能
|
||||
+ 关注/取关、用户主页功能
|
||||
+ 评论功能
|
||||
+ 历史记录、稍后再看功能
|
||||
@@ -1,7 +0,0 @@
|
||||
## 1.0.1
|
||||
|
||||
### 修复
|
||||
+ 升级播放器依赖
|
||||
+ android平台 AV1格式视频支持
|
||||
+ 视频全屏功能
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
## 1.0.10
|
||||
|
||||
### 修复
|
||||
+ 长按倍速抬起后未恢复默认倍速
|
||||
@@ -1,26 +0,0 @@
|
||||
## 1.0.11
|
||||
|
||||
### 新功能
|
||||
+ 适配了原生媒体通知栏 @Daydreamer-riri
|
||||
+ 视频主题图标 @Daydreamer-riri
|
||||
+ 关闭软件后自动画中画播放
|
||||
+ UP主分组管理
|
||||
+ md2样式底栏
|
||||
+
|
||||
|
||||
|
||||
### 修复
|
||||
+ 历史记录记忆播放
|
||||
+ 部分类型视频连播
|
||||
+ 播放速度选择框不支持返回手势
|
||||
+ 播放速度选择框不支持返回手势
|
||||
+ 视频播放速度总是显示1.0X
|
||||
+ 评论页面计数错误
|
||||
+ 退出视频还有声音
|
||||
|
||||
|
||||
### 优化
|
||||
+ 视频加载速度
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1.0.12
|
||||
|
||||
|
||||
### 修复
|
||||
+ iOS端视频播放时没有声音
|
||||
+ 超过6分钟弹幕不显示
|
||||
+ 视频详情页网络异常
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1.0.13
|
||||
|
||||
|
||||
### 新功能
|
||||
+ 视频详情页稍后再看
|
||||
+ 发送弹幕 感谢@orz12
|
||||
+ 消息展示
|
||||
+ up主页显示获赞数
|
||||
+ up主页显示合集
|
||||
+ 视频详情页「ai总结」增加开关
|
||||
|
||||
### 修复
|
||||
+ 首页推荐问题(需要重新登录)
|
||||
+ 长按倍速逻辑
|
||||
+ 视频详情页网络异常
|
||||
|
||||
### 优化
|
||||
+ 设置面板样式 感谢@GuMengYu @KoolShow
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,28 +0,0 @@
|
||||
## 1.0.14
|
||||
|
||||
圣诞节快乐~ 🎉
|
||||
|
||||
大部分内容由@orz12提供,感谢👏
|
||||
|
||||
### 修复
|
||||
+ 全屏弹幕消失
|
||||
+ iOS全屏/退出全屏视频暂停
|
||||
+ 个人主页关注状态
|
||||
+ 视频合集向下滑动UI问题
|
||||
+ 媒体库滑动底栏不隐藏
|
||||
+ 个人主页动态加载问题 * 2
|
||||
+ 未登录状态访问个人主页异常
|
||||
+ 视频搜索标题特殊字符转义
|
||||
+ iOS闪退
|
||||
+ 消息页面夜间模式异常
|
||||
+ 消息页面含有撤回消息时异常
|
||||
+ 弹幕速度
|
||||
|
||||
### 优化
|
||||
+ 全屏播放方案优化
|
||||
+ 弹幕加载逻辑优化
|
||||
+ 点赞、投币逻辑优化
|
||||
+ 进度条及播放时间渲染优化
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1.0.15
|
||||
|
||||
元旦快乐~ 🎉
|
||||
|
||||
### 功能
|
||||
+ 转发动态评论展示
|
||||
+ 推荐、最热、收藏视频增肌日期显示
|
||||
|
||||
### 修复
|
||||
+ 全屏播放相关问题
|
||||
+ 评论区@用户展示问题
|
||||
+ 登录状态闪退问题
|
||||
+ pip意外触发问题
|
||||
+ 动态页tab切换样式问题
|
||||
|
||||
### 优化
|
||||
+ 首页默认使用web端推荐
|
||||
+ 取消iOS路由切换效果
|
||||
+ 视频分享中添加Up主
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,15 +0,0 @@
|
||||
## 1.0.16
|
||||
|
||||
|
||||
### 功能
|
||||
+ toast 背景支持透明度调节
|
||||
|
||||
### 修复
|
||||
+ web端推荐未展示【已关注】
|
||||
+ up主动态页异常
|
||||
+ 未打开自动播放时,视频详情页异常
|
||||
+ 视频暂停状态取消自动ip
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,39 +0,0 @@
|
||||
## 1.0.17
|
||||
|
||||
|
||||
### 功能
|
||||
+ 视频全屏时隐藏进度条
|
||||
+ 动态内容增加投稿跳转
|
||||
+ 未开启自动播放时点击封面播放
|
||||
+ 弹幕发送标识
|
||||
+ 定时关闭
|
||||
+ 推荐视频卡片拉黑up功能
|
||||
+ 首页tabbar编辑排序
|
||||
|
||||
### 修复
|
||||
+ 连续跳转搜索页未刷新
|
||||
+ 搜索结果为空时页面异常
|
||||
+ 评论区链接解析
|
||||
+ 视频全屏状态栏背景色
|
||||
+ 私信对话气泡位置
|
||||
+ 设置up关注分组样式
|
||||
+ 每次推荐请求数据相同
|
||||
+ iOS代理网络异常
|
||||
+ 双击切换播放状态无声
|
||||
+ 设置自定义倍速白屏
|
||||
+ 免登录查看1080p
|
||||
|
||||
### 优化
|
||||
+ 首页web端推荐观看数展示
|
||||
+ 首页web端推荐接口更新
|
||||
+ 首页样式
|
||||
+ 搜索页跳转
|
||||
+ 弹幕资源优化
|
||||
+ 图片渲染占用内存优化(部分)
|
||||
+ 两次返回退出应用
|
||||
+ schame 补充
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,16 +0,0 @@
|
||||
## 1.0.18
|
||||
|
||||
|
||||
### 功能
|
||||
|
||||
|
||||
### 修复
|
||||
|
||||
|
||||
### 优化
|
||||
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,15 +0,0 @@
|
||||
## 1.0.19
|
||||
|
||||
|
||||
### 修复
|
||||
+ 视频404、评论加载错误
|
||||
+ bvav转换
|
||||
|
||||
### 优化
|
||||
+ 视频详情页内存占用
|
||||
|
||||
|
||||
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1.0.2
|
||||
|
||||
### 新功能
|
||||
+ 自动检查更新
|
||||
+ 封面图片保存
|
||||
+ 动态跳转番剧
|
||||
+ 历史记录番剧记忆播放
|
||||
+ 一键清空稍后再看
|
||||
|
||||
### 修复
|
||||
+ 切换分P cid未切换
|
||||
+ cookie存储问题
|
||||
+ 登录/退出登录问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 退出登录提示
|
||||
+ 请求节流
|
||||
+ 全屏播放
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1.0.3
|
||||
|
||||
建议卸载1.0.2版本,重新安装
|
||||
### 新功能
|
||||
+ 底部播放进度条设置
|
||||
+ 复制图片链接
|
||||
|
||||
|
||||
### 修复
|
||||
+ 用户数据格式修改
|
||||
+ video Fit
|
||||
+ 没有audio 资源的视频异常
|
||||
+ 评论区域图片无法点击
|
||||
+ 视频进度条拖动问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 部分页面样式
|
||||
+ 图片预览页面样式
|
||||
@@ -1,21 +0,0 @@
|
||||
## 1.0.4
|
||||
|
||||
### 新功能
|
||||
+ 热搜刷新
|
||||
+ 视频搜索排序、筛选
|
||||
+ app字体大小自定义
|
||||
+ app主题色自定义
|
||||
+ 「课堂」类动态渲染
|
||||
|
||||
|
||||
### 修复
|
||||
+ 搜索词联想richText渲染异常
|
||||
+ 部分动态点赞异常
|
||||
+ 默认视频解码格式
|
||||
+ 搜索页面返回搜索词未清空
|
||||
+ 动态详情评论加载异常
|
||||
+ 动态页面下拉刷新数据异常
|
||||
|
||||
### 优化
|
||||
+ 一些样式修改
|
||||
+ 取消热搜词缓存
|
||||
@@ -1,30 +0,0 @@
|
||||
## 1.0.5
|
||||
|
||||
主要是bug修复跟一部分小功能,弹幕功能需要下一版。
|
||||
问题反馈请前往QQ频道或提交issues。
|
||||
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
|
||||
|
||||
### 新功能
|
||||
+ 高帧率支持
|
||||
+ 默认评论排序设置
|
||||
+ 默认动态类别设置
|
||||
+ 动态合集查看
|
||||
+ 同时观看人数
|
||||
+ iOS路由切换效果
|
||||
|
||||
|
||||
### 修复
|
||||
+ 收藏夹翻页
|
||||
+ 首页搜索框频繁点击消失
|
||||
+ 评论排序切换空白
|
||||
+ 快速返回首页
|
||||
+ 重复进入个人中心页面数据未刷新
|
||||
+ 动态goods数据异常
|
||||
+ 大会员切换番剧
|
||||
+ 高画质codes匹配
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择
|
||||
+ 播放器亮度记忆
|
||||
+ 下载对应版本apk
|
||||
@@ -1,34 +0,0 @@
|
||||
## 1.0.6
|
||||
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
|
||||
### 新功能
|
||||
+ 首页单列布局
|
||||
+ 首页推荐展示播放量、弹幕数
|
||||
+ 简单弹幕功能实现(持续开发中...)
|
||||
+ 评论区搜索关键词开关 issues#46
|
||||
+ 热搜榜隐藏功能 issues#35
|
||||
+ 自动全屏 issues#37
|
||||
+ 快速收藏功能
|
||||
+ 双击快进/快退开关
|
||||
+ 评论链接跳转视频
|
||||
+ 支持移除单个稍后再看
|
||||
+ app scheme外链跳转
|
||||
|
||||
|
||||
### 修复
|
||||
+ 杜比、无损音频切换
|
||||
+ 收藏夹展示 issues#42
|
||||
+ 搜索建议次 issues#47
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择优化
|
||||
+ 导航条沉浸
|
||||
+ 取消Hero动画
|
||||
+ 视频锁定逻辑
|
||||
+ 登录逻辑优化
|
||||
+ 图片预览样式
|
||||
+ +评论区用户点击范围
|
||||
+ 关注、粉丝页面优化
|
||||
+ 关闭自动播放时播放器初始化逻辑
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1.0.7
|
||||
|
||||
默认倍速、直播弹幕、专栏等功能开发中
|
||||
|
||||
### 新功能
|
||||
+ 弹幕设置、屏蔽功能
|
||||
+ 不是很完美的后台播放功能
|
||||
+ 不是很完美的画中画(pip)功能(Android端)
|
||||
|
||||
### 修复
|
||||
+ 动态页面加载异常
|
||||
+ 网络异常时页面空白
|
||||
+ 竖屏全屏状态栏问题
|
||||
+ iOS端代理请求异常
|
||||
|
||||
### 优化
|
||||
+ 图片预览
|
||||
+ 全屏播放时自动旋转
|
||||
+ 转发内容增加视频标题
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,24 +0,0 @@
|
||||
## 1.0.8
|
||||
|
||||
直播弹幕、循环播放等功能开发中
|
||||
|
||||
### 新功能
|
||||
+ 用户拉黑功能
|
||||
+ gif图片保存
|
||||
+ 删除已看历史记录
|
||||
|
||||
### 修复
|
||||
+ 弹幕数量较少
|
||||
+ 弹幕屏蔽设置自动记忆
|
||||
+ 动态页面渲染
|
||||
+ 用户主页数据错乱
|
||||
+ 大家都在搜空白
|
||||
+ 默认自动全屏,顶部操作栏丢失
|
||||
|
||||
|
||||
### 优化
|
||||
+ 全屏状态栏区域显示优化
|
||||
+ 图片保存至PiliPala文件夹
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
@@ -1,28 +0,0 @@
|
||||
## 1.0.9
|
||||
|
||||
|
||||
### 新功能
|
||||
+ 自定义倍速、默认倍速
|
||||
+ 历史记录搜索
|
||||
+ 收藏夹搜索
|
||||
+ 历史记录多选删除
|
||||
+ 视频循环播放
|
||||
+ 免登录看1080P
|
||||
+ 评论区视频链接跳转
|
||||
+ up主分组
|
||||
+ up主投稿搜索
|
||||
|
||||
### 修复
|
||||
+ 搜索视频标题乱码
|
||||
+ 屏幕帧率
|
||||
+ 动态页面渲染
|
||||
|
||||
|
||||
|
||||
### 优化
|
||||
+ 快进手势
|
||||
+ 视频简介链接匹配
|
||||
+ 视频全屏时安全区域
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
1
distribute_options.yaml
Normal file
@@ -0,0 +1 @@
|
||||
output: dist/
|
||||
@@ -1,10 +0,0 @@
|
||||
PiliPlus is a third-party Bilibili client developed in Flutter,
|
||||
fork from PiliPalaX (https://github.com/orz12/PiliPalaX).
|
||||
|
||||
Top Features:
|
||||
|
||||
* List of recommended videos
|
||||
* List of hottest videos
|
||||
* Popular live streams
|
||||
* List of bangumis
|
||||
* Block videos from blacklisted users
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +0,0 @@
|
||||
A third-party Bilibili client developed in Flutter
|
||||
@@ -1 +0,0 @@
|
||||
PiliPlus
|
||||
@@ -1,10 +0,0 @@
|
||||
PiliPlus 是使用 Flutter 开发的 BiliBili 第三方客户端,
|
||||
是由PiliPalaX仓库fork并进行了差异化开发的版本
|
||||
|
||||
主要功能:
|
||||
|
||||
* 推荐视频列表
|
||||
* 最热视频列表
|
||||
* 热门直播
|
||||
* 番剧列表
|
||||
* 屏蔽黑名单内用户视频
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1 +0,0 @@
|
||||
使用 Flutter 开发的 BiliBili 第三方客户端
|
||||
@@ -1 +0,0 @@
|
||||
PiliPlus
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
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">
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import UIKit
|
||||
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>
|
||||
|
||||
16
lib/build_config.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
abstract final class BuildConfig {
|
||||
static const int versionCode = int.fromEnvironment(
|
||||
'pili.code',
|
||||
defaultValue: 1,
|
||||
);
|
||||
static const String versionName = String.fromEnvironment(
|
||||
'pili.name',
|
||||
defaultValue: 'SNAPSHOT',
|
||||
);
|
||||
|
||||
static const int buildTime = int.fromEnvironment('pili.time');
|
||||
static const String commitHash = String.fromEnvironment(
|
||||
'pili.hash',
|
||||
defaultValue: 'N/A',
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
import 'package:PiliPlus/http/constants.dart';
|
||||
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';
|
||||
|
||||
@@ -21,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';
|
||||
@@ -41,9 +46,7 @@ class Constants {
|
||||
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
||||
|
||||
static const baseHeaders = {
|
||||
'connection': 'keep-alive',
|
||||
'accept-encoding': 'br,gzip',
|
||||
'referer': HttpString.baseUrl,
|
||||
// 'referer': HttpString.baseUrl,
|
||||
'env': 'prod',
|
||||
'app-key': 'android64',
|
||||
'x-bili-aurora-zone': 'sh001',
|
||||
|
||||
@@ -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),
|
||||
|
||||
56
lib/common/widgets/avatars.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||
import 'package:PiliPlus/models/model_owner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget avatars({
|
||||
required ColorScheme colorScheme,
|
||||
required Iterable<Owner> users,
|
||||
}) {
|
||||
const gap = 6.0;
|
||||
const size = 22.0;
|
||||
const padding = 0.8;
|
||||
const offset = size - gap;
|
||||
const imgSize = size - 2 * padding;
|
||||
if (users.length == 1) {
|
||||
return NetworkImgLayer(
|
||||
src: users.first.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
);
|
||||
} else {
|
||||
final decoration = BoxDecoration(
|
||||
shape: .circle,
|
||||
border: Border.all(color: colorScheme.surface),
|
||||
);
|
||||
return SizedBox(
|
||||
height: size,
|
||||
width: offset * users.length + gap,
|
||||
child: Stack(
|
||||
clipBehavior: .none,
|
||||
children: users.indexed
|
||||
.map(
|
||||
(e) => Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: size,
|
||||
left: e.$1 * offset,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: Padding(
|
||||
padding: const .all(padding),
|
||||
child: NetworkImgLayer(
|
||||
src: e.$2.face,
|
||||
width: imgSize,
|
||||
height: imgSize,
|
||||
type: .avatar,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
lib/common/widgets/back_detector.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/gestures.dart' show kBackMouseButton;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show KeyDownEvent;
|
||||
|
||||
class BackDetector extends StatelessWidget {
|
||||
const BackDetector({
|
||||
super.key,
|
||||
required this.onBack,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final VoidCallback onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
onKeyEvent: _onKeyEvent,
|
||||
child: Listener(
|
||||
behavior: .translucent,
|
||||
onPointerDown: _onPointerDown,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event.logicalKey == .escape && event is KeyDownEvent) {
|
||||
onBack();
|
||||
return .handled;
|
||||
}
|
||||
return .ignored;
|
||||
}
|
||||
|
||||
void _onPointerDown(PointerDownEvent event) {
|
||||
if (event.buttons == kBackMouseButton) {
|
||||
onBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:PiliPlus/models/common/badge_type.dart';
|
||||
import 'package:PiliPlus/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,50 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget iconButton({
|
||||
required BuildContext context,
|
||||
BuildContext? context,
|
||||
String? tooltip,
|
||||
required IconData icon,
|
||||
required Widget icon,
|
||||
required VoidCallback? onPressed,
|
||||
double size = 36,
|
||||
double? iconSize,
|
||||
Color? bgColor,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
late final theme = Theme.of(context);
|
||||
Color? backgroundColor = bgColor;
|
||||
Color? foregroundColor = iconColor;
|
||||
if (context != null) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
backgroundColor = colorScheme.secondaryContainer;
|
||||
foregroundColor = colorScheme.onSecondaryContainer;
|
||||
}
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: IconButton(
|
||||
icon: icon,
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: iconSize ?? size / 2,
|
||||
color: iconColor ?? theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: bgColor ?? theme.colorScheme.secondaryContainer,
|
||||
iconSize: iconSize ?? size / 2,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget mediumButton({
|
||||
String? tooltip,
|
||||
IconData? icon,
|
||||
VoidCallback? onPressed,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
icon: Icon(icon),
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
34
lib/common/widgets/button/more_btn.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget moreTextButton({
|
||||
String text = '查看更多',
|
||||
required VoidCallback onTap,
|
||||
EdgeInsets? padding,
|
||||
Color? color,
|
||||
}) {
|
||||
Widget child = Text.rich(
|
||||
style: TextStyle(color: color, height: 1),
|
||||
strutStyle: const StrutStyle(leading: 0, height: 1),
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: text),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
size: 22,
|
||||
color: color,
|
||||
Icons.keyboard_arrow_right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (padding != null) {
|
||||
child = Padding(padding: padding, child: child);
|
||||
}
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@@ -1,39 +1,32 @@
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
class ColorPalette extends StatelessWidget {
|
||||
final Color color;
|
||||
final ColorScheme colorScheme;
|
||||
final bool selected;
|
||||
final bool showBgColor;
|
||||
|
||||
const ColorPalette({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.colorScheme,
|
||||
required this.selected,
|
||||
this.showBgColor = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final Hct hct = Hct.fromInt(color.toARGB32());
|
||||
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());
|
||||
final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
|
||||
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());
|
||||
Widget coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
final primary = colorScheme.primary;
|
||||
final tertiary = colorScheme.tertiary;
|
||||
final primaryContainer = colorScheme.primaryContainer;
|
||||
Widget child = ClipOval(
|
||||
child: Column(
|
||||
children: [
|
||||
coloredBox(primary),
|
||||
_coloredBox(primary),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
coloredBox(tertiary),
|
||||
coloredBox(primaryContainer),
|
||||
_coloredBox(tertiary),
|
||||
_coloredBox(primaryContainer),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -50,7 +43,7 @@ class ColorPalette extends StatelessWidget {
|
||||
width: 23,
|
||||
height: 23,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(Hct.from(hct.hue, 30.0, 40.0).toInt()),
|
||||
color: colorScheme.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
@@ -62,15 +55,25 @@ class ColorPalette extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
if (showBgColor) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onInverseSurface,
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
static Widget _coloredBox(Color color) => Expanded(
|
||||
child: ColoredBox(
|
||||
color: color,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
158
lib/common/widgets/cropped_image.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* This file is part of PiliPlus
|
||||
*
|
||||
* PiliPlus is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* PiliPlus is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CroppedImage extends LeafRenderObjectWidget {
|
||||
const CroppedImage({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.image,
|
||||
required this.srcRect,
|
||||
required this.dstRect,
|
||||
required this.rrect,
|
||||
required this.imgPaint,
|
||||
required this.borderPaint,
|
||||
});
|
||||
|
||||
final Size size;
|
||||
final ui.Image image;
|
||||
final Rect srcRect;
|
||||
final Rect dstRect;
|
||||
final RRect rrect;
|
||||
final Paint imgPaint;
|
||||
final Paint borderPaint;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderCroppedImage(
|
||||
preferredSize: size,
|
||||
image: image,
|
||||
srcRect: srcRect,
|
||||
dstRect: dstRect,
|
||||
rrect: rrect,
|
||||
imgPaint: imgPaint,
|
||||
borderPaint: borderPaint,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderCroppedImage renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..preferredSize = size
|
||||
..image = image
|
||||
..srcRect = srcRect
|
||||
..dstRect = dstRect
|
||||
..rrect = rrect
|
||||
..imgPaint = imgPaint
|
||||
..borderPaint = borderPaint;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderCroppedImage extends RenderBox {
|
||||
RenderCroppedImage({
|
||||
required Size preferredSize,
|
||||
required ui.Image image,
|
||||
required Rect srcRect,
|
||||
required Rect dstRect,
|
||||
required RRect rrect,
|
||||
required Paint imgPaint,
|
||||
required Paint borderPaint,
|
||||
}) : _preferredSize = preferredSize,
|
||||
_image = image,
|
||||
_srcRect = srcRect,
|
||||
_dstRect = dstRect,
|
||||
_rrect = rrect,
|
||||
_imgPaint = imgPaint,
|
||||
_borderPaint = borderPaint;
|
||||
|
||||
Size _preferredSize;
|
||||
Size get preferredSize => _preferredSize;
|
||||
set preferredSize(Size value) {
|
||||
if (_preferredSize == value) return;
|
||||
_preferredSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
ui.Image _image;
|
||||
ui.Image get image => _image;
|
||||
set image(ui.Image value) {
|
||||
if (_image == value) return;
|
||||
_image = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Rect _srcRect;
|
||||
Rect get srcRect => _srcRect;
|
||||
set srcRect(Rect value) {
|
||||
if (_srcRect == value) return;
|
||||
_srcRect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Rect _dstRect;
|
||||
Rect get dstRect => _dstRect;
|
||||
set dstRect(Rect value) {
|
||||
if (_dstRect == value) return;
|
||||
_dstRect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
RRect _rrect;
|
||||
RRect get rrect => _rrect;
|
||||
set rrect(RRect value) {
|
||||
if (_rrect == value) return;
|
||||
_rrect = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Paint _imgPaint;
|
||||
Paint get imgPaint => _imgPaint;
|
||||
set imgPaint(Paint value) {
|
||||
if (_imgPaint == value) return;
|
||||
_imgPaint = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Paint _borderPaint;
|
||||
Paint get borderPaint => _borderPaint;
|
||||
set borderPaint(Paint value) {
|
||||
if (_borderPaint == value) return;
|
||||
_borderPaint = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrain(_preferredSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.canvas
|
||||
..drawImageRect(image, srcRect, dstRect, _imgPaint)
|
||||
..drawRRect(rrect, _borderPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
113
lib/common/widgets/custom_arc.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'dart:math' show pi;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class Arc extends LeafRenderObjectWidget {
|
||||
const Arc({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.color,
|
||||
required this.progress,
|
||||
this.strokeWidth = 2,
|
||||
});
|
||||
|
||||
final double size;
|
||||
final Color color;
|
||||
final double progress;
|
||||
final double strokeWidth;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderArc(
|
||||
preferredSize: size,
|
||||
color: color,
|
||||
progress: progress,
|
||||
strokeWidth: strokeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderArc renderObject,
|
||||
) {
|
||||
renderObject
|
||||
..preferredSize = size
|
||||
..color = color
|
||||
..progress = progress
|
||||
..strokeWidth = strokeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
class RenderArc extends RenderBox {
|
||||
RenderArc({
|
||||
required double preferredSize,
|
||||
required Color color,
|
||||
required double progress,
|
||||
required double strokeWidth,
|
||||
}) : _preferredSize = preferredSize,
|
||||
_color = color,
|
||||
_progress = progress,
|
||||
_strokeWidth = strokeWidth;
|
||||
|
||||
Color _color;
|
||||
Color get color => _color;
|
||||
set color(Color value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _progress;
|
||||
double get progress => _progress;
|
||||
set progress(double value) {
|
||||
if (_progress == value) return;
|
||||
_progress = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _strokeWidth;
|
||||
double get strokeWidth => _strokeWidth;
|
||||
set strokeWidth(double value) {
|
||||
if (_strokeWidth == value) return;
|
||||
_strokeWidth = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double _preferredSize;
|
||||
double get preferredSize => _preferredSize;
|
||||
set preferredSize(double value) {
|
||||
if (_preferredSize == value) return;
|
||||
_preferredSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = constraints.constrainDimensions(_preferredSize, _preferredSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (progress == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final radius = size.width / 2;
|
||||
final rect = Rect.fromCircle(
|
||||
center: Offset(radius, radius),
|
||||
radius: radius,
|
||||
);
|
||||
|
||||
const startAngle = -pi / 2;
|
||||
context.canvas.drawArc(rect, startAngle, progress * 2 * pi, false, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
@@ -10,19 +10,24 @@ class CustomIcons {
|
||||
static const IconData dyn = _CustomIconData(0xe804);
|
||||
static const IconData fav = _CustomIconData(0xe805);
|
||||
static const IconData live_reserve = _CustomIconData(0xe806);
|
||||
static const IconData share = _CustomIconData(0xe807);
|
||||
static const IconData share_line = _CustomIconData(0xe808);
|
||||
static const IconData share_node = _CustomIconData(0xe809);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80a);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe80b);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe80c);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe80d);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe80e);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe80f);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe811);
|
||||
static const IconData topic_tag = _CustomIconData(0xe812);
|
||||
static const IconData watch_later = _CustomIconData(0xe813);
|
||||
static const IconData player_dm_tip_back = _CustomIconData(0xe807);
|
||||
static const IconData player_dm_tip_copy = _CustomIconData(0xe808);
|
||||
static const IconData player_dm_tip_like = _CustomIconData(0xe809);
|
||||
static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a);
|
||||
static const IconData player_dm_tip_recall = _CustomIconData(0xe80b);
|
||||
static const IconData share = _CustomIconData(0xe80c);
|
||||
static const IconData share_line = _CustomIconData(0xe80d);
|
||||
static const IconData share_node = _CustomIconData(0xe80e);
|
||||
static const IconData star_favorite_line = _CustomIconData(0xe80f);
|
||||
static const IconData star_favorite_solid = _CustomIconData(0xe810);
|
||||
static const IconData thumbs_down = _CustomIconData(0xe811);
|
||||
static const IconData thumbs_down_outline = _CustomIconData(0xe812);
|
||||
static const IconData thumbs_up = _CustomIconData(0xe813);
|
||||
static const IconData thumbs_up_fill = _CustomIconData(0xe814);
|
||||
static const IconData thumbs_up_line = _CustomIconData(0xe815);
|
||||
static const IconData thumbs_up_outline = _CustomIconData(0xe816);
|
||||
static const IconData topic_tag = _CustomIconData(0xe817);
|
||||
static const IconData watch_later = _CustomIconData(0xe818);
|
||||
}
|
||||
|
||||
class _CustomIconData extends IconData {
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/widgets.dart';
|
||||
///
|
||||
/// @docImport 'stack.dart';
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class CustomMultiChildLayout extends MultiChildRenderObjectWidget {
|
||||
/// Creates a custom multi-child layout.
|
||||
const CustomMultiChildLayout({
|
||||
super.key,
|
||||
required this.delegate,
|
||||
super.children,
|
||||
});
|
||||
|
||||
/// The delegate that controls the layout of the children.
|
||||
final MultiChildLayoutDelegate delegate;
|
||||
|
||||
@override
|
||||
RenderCustomMultiChildLayoutBox createRenderObject(BuildContext context) {
|
||||
return RenderCustomMultiChildLayoutBox(delegate: delegate);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderCustomMultiChildLayoutBox renderObject,
|
||||
) {
|
||||
renderObject.delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
/// A delegate that controls the layout of multiple children.
|
||||
///
|
||||
/// Used with [CustomMultiChildLayout] (in the widgets library) and
|
||||
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
|
||||
///
|
||||
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
|
||||
/// they must produce the same layout. To change the layout, replace the
|
||||
/// delegate with a different instance whose [shouldRelayout] returns true when
|
||||
/// given the previous instance.
|
||||
///
|
||||
/// Override [getSize] to control the overall size of the layout. The size of
|
||||
/// the layout cannot depend on layout properties of the children. This was
|
||||
/// a design decision to simplify the delegate implementations: This way,
|
||||
/// the delegate implementations do not have to also handle various intrinsic
|
||||
/// sizing functions if the parent's size depended on the children.
|
||||
/// If you want to build a custom layout where you define the size of that widget
|
||||
/// based on its children, then you will have to create a custom render object.
|
||||
/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and
|
||||
/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an
|
||||
/// example implementation.
|
||||
///
|
||||
/// Override [performLayout] to size and position the children. An
|
||||
/// implementation of [performLayout] must call [layoutChild] exactly once for
|
||||
/// each child, but it may call [layoutChild] on children in an arbitrary order.
|
||||
/// Typically a delegate will use the size returned from [layoutChild] on one
|
||||
/// child to determine the constraints for [performLayout] on another child or
|
||||
/// to determine the offset for [positionChild] for that child or another child.
|
||||
///
|
||||
/// Override [shouldRelayout] to determine when the layout of the children needs
|
||||
/// to be recomputed when the delegate changes.
|
||||
///
|
||||
/// The most efficient way to trigger a relayout is to supply a `relayout`
|
||||
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
|
||||
/// layout will listen to this value and relayout whenever the Listenable
|
||||
/// notifies its listeners, such as when an [Animation] ticks. This allows
|
||||
/// the custom layout to avoid the build phase of the pipeline.
|
||||
///
|
||||
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
|
||||
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
|
||||
/// the children that the [CustomMultiChildLayout] manages.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// Below is an example implementation of [performLayout] that causes one widget
|
||||
/// (the follower) to be the same size as another (the leader):
|
||||
///
|
||||
/// ```dart
|
||||
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
|
||||
/// // Typical usage is to define an enum like the one below, and use those
|
||||
/// // values as the ids.
|
||||
/// enum _Slot {
|
||||
/// leader,
|
||||
/// follower,
|
||||
/// }
|
||||
///
|
||||
/// class FollowTheLeader extends MultiChildLayoutDelegate {
|
||||
/// @override
|
||||
/// void performLayout(Size size) {
|
||||
/// Size leaderSize = Size.zero;
|
||||
///
|
||||
/// if (hasChild(_Slot.leader)) {
|
||||
/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
|
||||
/// positionChild(_Slot.leader, Offset.zero);
|
||||
/// }
|
||||
///
|
||||
/// if (hasChild(_Slot.follower)) {
|
||||
/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
|
||||
/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
|
||||
/// size.height - leaderSize.height));
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// The delegate gives the leader widget loose constraints, which means the
|
||||
/// child determines what size to be (subject to fitting within the given size).
|
||||
/// The delegate then remembers the size of that child and places it in the
|
||||
/// upper left corner.
|
||||
///
|
||||
/// The delegate then gives the follower widget tight constraints, forcing it to
|
||||
/// match the size of the leader widget. The delegate then places the follower
|
||||
/// widget in the bottom right corner.
|
||||
///
|
||||
/// The leader and follower widget will paint in the order they appear in the
|
||||
/// child list, regardless of the order in which [layoutChild] is called on
|
||||
/// them.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomMultiChildLayout], the widget that uses this delegate.
|
||||
/// * [RenderCustomMultiChildLayoutBox], render object that uses this
|
||||
/// delegate.
|
||||
abstract class MultiChildLayoutDelegate {
|
||||
/// Creates a layout delegate.
|
||||
///
|
||||
/// The layout will update whenever [relayout] notifies its listeners.
|
||||
MultiChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout;
|
||||
|
||||
final Listenable? _relayout;
|
||||
|
||||
Map<Object, RenderBox>? _idToChild;
|
||||
Set<RenderBox>? _debugChildrenNeedingLayout;
|
||||
|
||||
/// True if a non-null LayoutChild was provided for the specified id.
|
||||
///
|
||||
/// Call this from the [performLayout] method to determine which children
|
||||
/// are available, if the child list might vary.
|
||||
///
|
||||
/// This method cannot be called from [getSize] as the size is not allowed
|
||||
/// to depend on the children.
|
||||
bool hasChild(Object childId) => _idToChild![childId] != null;
|
||||
|
||||
/// Ask the child to update its layout within the limits specified by
|
||||
/// the constraints parameter. The child's size is returned.
|
||||
///
|
||||
/// Call this from your [performLayout] function to lay out each
|
||||
/// child. Every child must be laid out using this function exactly
|
||||
/// once each time the [performLayout] function is called.
|
||||
Size layoutChild(Object childId, BoxConstraints constraints) {
|
||||
final RenderBox? child = _idToChild![childId];
|
||||
assert(() {
|
||||
if (child == null) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
|
||||
'There is no child with the id "$childId".',
|
||||
);
|
||||
}
|
||||
if (!_debugChildrenNeedingLayout!.remove(child)) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
|
||||
'Each child must be laid out exactly once.',
|
||||
);
|
||||
}
|
||||
try {
|
||||
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
|
||||
} on AssertionError catch (exception) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".',
|
||||
),
|
||||
DiagnosticsProperty<AssertionError>(
|
||||
'Exception',
|
||||
exception,
|
||||
showName: false,
|
||||
),
|
||||
ErrorDescription(
|
||||
'The minimum width and height must be greater than or equal to zero.\n'
|
||||
'The maximum width must be greater than or equal to the minimum width.\n'
|
||||
'The maximum height must be greater than or equal to the minimum height.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
child!.layout(constraints, parentUsesSize: true);
|
||||
return child.size;
|
||||
}
|
||||
|
||||
/// Specify the child's origin relative to this origin.
|
||||
///
|
||||
/// Call this from your [performLayout] function to position each
|
||||
/// child. If you do not call this for a child, its position will
|
||||
/// remain unchanged. Children initially have their position set to
|
||||
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
|
||||
void positionChild(Object childId, Offset offset) {
|
||||
final RenderBox? child = _idToChild![childId];
|
||||
assert(() {
|
||||
if (child == null) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
|
||||
'There is no child with the id "$childId".',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child!.parentData! as MultiChildLayoutParentData;
|
||||
childParentData.offset = offset;
|
||||
}
|
||||
|
||||
DiagnosticsNode _debugDescribeChild(RenderBox child) {
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child.parentData! as MultiChildLayoutParentData;
|
||||
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
|
||||
}
|
||||
|
||||
void _callPerformLayout(Size size, RenderBox? firstChild) {
|
||||
// A particular layout delegate could be called reentrantly, e.g. if it used
|
||||
// by both a parent and a child. So, we must restore the _idToChild map when
|
||||
// we return.
|
||||
final Map<Object, RenderBox>? previousIdToChild = _idToChild;
|
||||
|
||||
Set<RenderBox>? debugPreviousChildrenNeedingLayout;
|
||||
assert(() {
|
||||
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
|
||||
_debugChildrenNeedingLayout = <RenderBox>{};
|
||||
return true;
|
||||
}());
|
||||
|
||||
try {
|
||||
_idToChild = <Object, RenderBox>{};
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child.parentData! as MultiChildLayoutParentData;
|
||||
assert(() {
|
||||
if (childParentData.id == null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.',
|
||||
),
|
||||
child!.describeForError('The following child has no ID'),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_idToChild![childParentData.id!] = child;
|
||||
assert(() {
|
||||
_debugChildrenNeedingLayout!.add(child!);
|
||||
return true;
|
||||
}());
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
performLayout(size);
|
||||
assert(() {
|
||||
if (_debugChildrenNeedingLayout!.isNotEmpty) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('Each child must be laid out exactly once.'),
|
||||
DiagnosticsBlock(
|
||||
name:
|
||||
'The $this custom multichild layout delegate forgot '
|
||||
'to lay out the following '
|
||||
'${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}',
|
||||
properties: _debugChildrenNeedingLayout!
|
||||
.map<DiagnosticsNode>(_debugDescribeChild)
|
||||
.toList(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
} finally {
|
||||
_idToChild = previousIdToChild;
|
||||
assert(() {
|
||||
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Override this method to return the size of this object given the
|
||||
/// incoming constraints.
|
||||
///
|
||||
/// The size cannot reflect the sizes of the children. If this layout has a
|
||||
/// fixed width or height the returned size can reflect that; the size will be
|
||||
/// constrained to the given constraints.
|
||||
///
|
||||
/// By default, attempts to size the box to the biggest size
|
||||
/// possible given the constraints.
|
||||
Size getSize(BoxConstraints constraints) => constraints.biggest;
|
||||
|
||||
/// Override this method to lay out and position all children given this
|
||||
/// widget's size.
|
||||
///
|
||||
/// This method must call [layoutChild] for each child. It should also specify
|
||||
/// the final position of each child with [positionChild].
|
||||
void performLayout(Size size);
|
||||
|
||||
/// Override this method to return true when the children need to be
|
||||
/// laid out.
|
||||
///
|
||||
/// This should compare the fields of the current delegate and the given
|
||||
/// `oldDelegate` and return true if the fields are such that the layout would
|
||||
/// be different.
|
||||
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
|
||||
|
||||
/// Override this method to include additional information in the
|
||||
/// debugging data printed by [debugDumpRenderTree] and friends.
|
||||
///
|
||||
/// By default, returns the [runtimeType] of the class.
|
||||
@override
|
||||
String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate');
|
||||
}
|
||||
|
||||
/// Defers the layout of multiple children to a delegate.
|
||||
///
|
||||
/// The delegate can determine the layout constraints for each child and can
|
||||
/// decide where to position each child. The delegate can also determine the
|
||||
/// size of the parent, but the size of the parent cannot depend on the sizes of
|
||||
/// the children.
|
||||
class RenderCustomMultiChildLayoutBox extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
/// Creates a render object that customizes the layout of multiple children.
|
||||
RenderCustomMultiChildLayoutBox({
|
||||
List<RenderBox>? children,
|
||||
required MultiChildLayoutDelegate delegate,
|
||||
}) : _delegate = delegate {
|
||||
addAll(children);
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
/// The delegate that controls the layout of the children.
|
||||
MultiChildLayoutDelegate get delegate => _delegate;
|
||||
MultiChildLayoutDelegate _delegate;
|
||||
set delegate(MultiChildLayoutDelegate newDelegate) {
|
||||
if (_delegate == newDelegate) {
|
||||
return;
|
||||
}
|
||||
final MultiChildLayoutDelegate oldDelegate = _delegate;
|
||||
if (newDelegate.runtimeType != oldDelegate.runtimeType ||
|
||||
newDelegate.shouldRelayout(oldDelegate)) {
|
||||
markNeedsLayout();
|
||||
}
|
||||
_delegate = newDelegate;
|
||||
if (attached) {
|
||||
oldDelegate._relayout?.removeListener(markNeedsLayout);
|
||||
newDelegate._relayout?.addListener(markNeedsLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
super.attach(owner);
|
||||
_delegate._relayout?.addListener(markNeedsLayout);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_delegate._relayout?.removeListener(markNeedsLayout);
|
||||
super.detach();
|
||||
}
|
||||
|
||||
Size _getSize(BoxConstraints constraints) {
|
||||
assert(constraints.debugAssertIsValid());
|
||||
return constraints.constrain(_delegate.getSize(constraints));
|
||||
}
|
||||
|
||||
// TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
|
||||
// figure out the intrinsic dimensions. We really should either not support intrinsics,
|
||||
// or we should expose intrinsic delegate callbacks and throw if they're not implemented.
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
Size computeDryLayout(covariant BoxConstraints constraints) {
|
||||
return _getSize(constraints);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _getSize(constraints);
|
||||
delegate._callPerformLayout(size, firstChild);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show RenderProxyBox;
|
||||
|
||||
class CustomSliverPersistentHeaderDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
CustomSliverPersistentHeaderDelegate({
|
||||
const CustomSliverPersistentHeaderDelegate({
|
||||
required this.child,
|
||||
required this.bgColor,
|
||||
double extent = 45,
|
||||
this.bgColor,
|
||||
this.extent = 45,
|
||||
this.needRebuild = false,
|
||||
}) : _minExtent = extent,
|
||||
_maxExtent = extent;
|
||||
final double _minExtent;
|
||||
final double _maxExtent;
|
||||
});
|
||||
final double extent;
|
||||
final Widget child;
|
||||
final Color? bgColor;
|
||||
final bool needRebuild;
|
||||
@@ -26,31 +23,16 @@ class CustomSliverPersistentHeaderDelegate
|
||||
//创建child子组件
|
||||
//shrinkOffset:child偏移值minExtent~maxExtent
|
||||
//overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false
|
||||
return bgColor != null
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
boxShadow: Platform.isIOS
|
||||
? null
|
||||
: [
|
||||
BoxShadow(
|
||||
color: bgColor!,
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
return _DecoratedBox(color: bgColor, child: child);
|
||||
}
|
||||
|
||||
//SliverPersistentHeader最大高度
|
||||
@override
|
||||
double get maxExtent => _maxExtent;
|
||||
double get maxExtent => extent;
|
||||
|
||||
//SliverPersistentHeader最小高度
|
||||
@override
|
||||
double get minExtent => _minExtent;
|
||||
double get minExtent => extent;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
|
||||
@@ -58,3 +40,62 @@ class CustomSliverPersistentHeaderDelegate
|
||||
(needRebuild && oldDelegate.child != child);
|
||||
}
|
||||
}
|
||||
|
||||
class _DecoratedBox extends SingleChildRenderObjectWidget {
|
||||
const _DecoratedBox({
|
||||
this.color,
|
||||
super.child,
|
||||
});
|
||||
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderDecoratedBox(color: color);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
_RenderDecoratedBox renderObject,
|
||||
) {
|
||||
renderObject.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderDecoratedBox extends RenderProxyBox {
|
||||
_RenderDecoratedBox({
|
||||
Color? color,
|
||||
}) : _color = color;
|
||||
|
||||
Color? _color;
|
||||
Color? get color => _color;
|
||||
set color(Color? value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (_color case final color?) {
|
||||
final size = this.size;
|
||||
context.canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
offset.dx,
|
||||
offset.dy - 2,
|
||||
size.width,
|
||||
size.height + 2,
|
||||
),
|
||||
Paint()..color = color,
|
||||
);
|
||||
}
|
||||
super.paint(context, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => true;
|
||||
|
||||
@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,135 +1,63 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show clampDouble;
|
||||
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum TooltipType { top, right }
|
||||
import 'package:flutter/rendering.dart'
|
||||
show
|
||||
ContainerRenderObjectMixin,
|
||||
RenderBoxContainerDefaultsMixin,
|
||||
MultiChildLayoutParentData;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CustomTooltip extends StatefulWidget {
|
||||
const CustomTooltip({
|
||||
super.key,
|
||||
this.type = TooltipType.top,
|
||||
required this.overlayWidget,
|
||||
required this.child,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
final Widget child;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
|
||||
static final List<CustomTooltipState> _openedTooltips =
|
||||
<CustomTooltipState>[];
|
||||
|
||||
static bool dismissAllToolTips() {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
|
||||
for (final CustomTooltipState state in openedTooltips) {
|
||||
assert(state.mounted);
|
||||
state._scheduleDismissTooltip();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
State<CustomTooltip> createState() => CustomTooltipState();
|
||||
State<CustomTooltip> createState() => _CustomTooltipState();
|
||||
}
|
||||
|
||||
class CustomTooltipState extends State<CustomTooltip>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const Duration _fadeInDuration = Duration(milliseconds: 150);
|
||||
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
|
||||
|
||||
class _CustomTooltipState extends State<CustomTooltip> {
|
||||
final OverlayPortalController _overlayController = OverlayPortalController();
|
||||
|
||||
AnimationController? _backingController;
|
||||
AnimationController get _controller {
|
||||
return _backingController ??= AnimationController(
|
||||
duration: _fadeInDuration,
|
||||
reverseDuration: _fadeOutDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener(_handleStatusChanged);
|
||||
}
|
||||
|
||||
CurvedAnimation? _backingOverlayAnimation;
|
||||
CurvedAnimation get _overlayAnimation {
|
||||
return _backingOverlayAnimation ??= CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
}
|
||||
|
||||
LongPressGestureRecognizer? _longPressRecognizer;
|
||||
|
||||
AnimationStatus _animationStatus = AnimationStatus.dismissed;
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
assert(mounted);
|
||||
switch ((_animationStatus.isDismissed, status.isDismissed)) {
|
||||
case (false, true):
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_overlayController.hide();
|
||||
case (true, false):
|
||||
_overlayController.show();
|
||||
CustomTooltip._openedTooltips.add(this);
|
||||
case (true, true) || (false, false):
|
||||
break;
|
||||
}
|
||||
_animationStatus = status;
|
||||
}
|
||||
LongPressGestureRecognizer get longPressRecognizer =>
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer()
|
||||
..onLongPress = _scheduleShowTooltip;
|
||||
|
||||
void _scheduleShowTooltip() {
|
||||
_controller.forward();
|
||||
_overlayController.show();
|
||||
}
|
||||
|
||||
void _scheduleDismissTooltip() {
|
||||
_controller.reverse();
|
||||
_overlayController.hide();
|
||||
}
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
assert(mounted);
|
||||
const Set<PointerDeviceKind> triggerModeDeviceKinds = <PointerDeviceKind>{
|
||||
PointerDeviceKind.invertedStylus,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.unknown,
|
||||
PointerDeviceKind.trackpad,
|
||||
};
|
||||
_longPressRecognizer ??= LongPressGestureRecognizer(
|
||||
debugOwner: this,
|
||||
supportedDevices: triggerModeDeviceKinds,
|
||||
);
|
||||
_longPressRecognizer!
|
||||
..onLongPress = _scheduleShowTooltip
|
||||
..addPointer(event);
|
||||
longPressRecognizer.addPointer(event);
|
||||
}
|
||||
|
||||
Widget _buildCustomTooltipOverlay(BuildContext context) {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
Widget _buildCustomTooltipOverlay(
|
||||
BuildContext context,
|
||||
OverlayChildLayoutInfo layoutInfo,
|
||||
) {
|
||||
final target = MatrixUtils.transformPoint(
|
||||
layoutInfo.childPaintTransform,
|
||||
layoutInfo.childSize.topCenter(Offset.zero),
|
||||
);
|
||||
final RenderBox box = this.context.findRenderObject()! as RenderBox;
|
||||
final Offset target = box.localToGlobal(
|
||||
box.size.center(Offset.zero),
|
||||
ancestor: overlayState.context.findRenderObject(),
|
||||
);
|
||||
|
||||
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
|
||||
verticalOffset: box.size.height / 2,
|
||||
horizontslOffset: box.size.width / 2,
|
||||
type: widget.type,
|
||||
animation: _overlayAnimation,
|
||||
target: target,
|
||||
onDismiss: _scheduleDismissTooltip,
|
||||
overlayWidget: widget.overlayWidget,
|
||||
indicator: widget.indicator,
|
||||
);
|
||||
|
||||
return SelectionContainer.maybeOf(context) == null
|
||||
? overlayChild
|
||||
: SelectionContainer.disabled(child: overlayChild);
|
||||
@@ -138,11 +66,10 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
CustomTooltip._openedTooltips.remove(this);
|
||||
_longPressRecognizer?.onLongPressCancel = null;
|
||||
_longPressRecognizer?.dispose();
|
||||
_backingController?.dispose();
|
||||
_backingOverlayAnimation?.dispose();
|
||||
_longPressRecognizer
|
||||
?..onLongPress = null
|
||||
..dispose();
|
||||
_longPressRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -150,7 +77,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget result;
|
||||
if (Utils.isMobile) {
|
||||
if (PlatformUtils.isMobile) {
|
||||
result = Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -164,7 +91,7 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
return OverlayPortal(
|
||||
return OverlayPortal.overlayChildLayoutBuilder(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildCustomTooltipOverlay,
|
||||
child: result,
|
||||
@@ -174,232 +101,241 @@ class CustomTooltipState extends State<CustomTooltip>
|
||||
|
||||
class _CustomTooltipOverlay extends StatelessWidget {
|
||||
const _CustomTooltipOverlay({
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.type,
|
||||
required this.animation,
|
||||
required this.target,
|
||||
required this.onDismiss,
|
||||
required this.overlayWidget,
|
||||
this.indicator,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
final double verticalOffset;
|
||||
final double horizontslOffset;
|
||||
final TooltipType type;
|
||||
final Animation<double> animation;
|
||||
final Offset target;
|
||||
final VoidCallback onDismiss;
|
||||
final Widget Function() overlayWidget;
|
||||
final Widget Function()? indicator;
|
||||
final ValueGetter<Widget> overlayWidget;
|
||||
final ValueGetter<Widget> indicator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = CustomMultiChildLayout(
|
||||
delegate: _CustomMultiTooltipPositionDelegate(
|
||||
type: type,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: false,
|
||||
),
|
||||
return _ToolTip(
|
||||
target: target,
|
||||
preferBelow: false,
|
||||
onTap: PlatformUtils.isMobile ? onDismiss : null,
|
||||
children: [
|
||||
LayoutId(
|
||||
id: 'overlay',
|
||||
child: overlayWidget(),
|
||||
),
|
||||
if (indicator != null)
|
||||
LayoutId(
|
||||
id: 'indicator',
|
||||
child: indicator!(),
|
||||
),
|
||||
indicator(),
|
||||
overlayWidget(),
|
||||
],
|
||||
);
|
||||
if (Utils.isMobile) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onDismiss,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
|
||||
_CustomMultiTooltipPositionDelegate({
|
||||
required this.type,
|
||||
class _ToolTip extends MultiChildRenderObjectWidget {
|
||||
const _ToolTip({
|
||||
super.children,
|
||||
this.onTap,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.horizontslOffset,
|
||||
required this.preferBelow,
|
||||
});
|
||||
|
||||
final TooltipType type;
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final Offset target;
|
||||
|
||||
final double verticalOffset;
|
||||
|
||||
final double horizontslOffset;
|
||||
|
||||
final bool preferBelow;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderToolTip(
|
||||
onTap: onTap,
|
||||
target: target,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
case TooltipType.right:
|
||||
Size? indicatorSize;
|
||||
if (hasChild('indicator')) {
|
||||
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
|
||||
}
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, _RenderToolTip renderObject) {
|
||||
renderObject
|
||||
..onTap = onTap
|
||||
..target = target
|
||||
..preferBelow = preferBelow;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChild('overlay')) {
|
||||
final overlaySize = layoutChild(
|
||||
'overlay',
|
||||
BoxConstraints.loose(size),
|
||||
);
|
||||
Offset offset = positionDependentBox(
|
||||
type: type,
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontslOffset: horizontslOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
if (indicatorSize != null) {
|
||||
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
|
||||
positionChild(
|
||||
'indicator',
|
||||
Offset(
|
||||
offset.dx - indicatorSize.width + 1,
|
||||
target.dy - indicatorSize.height / 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
positionChild('overlay', offset);
|
||||
}
|
||||
class _RenderToolTip extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
_RenderToolTip({
|
||||
VoidCallback? onTap,
|
||||
required Offset target,
|
||||
required bool preferBelow,
|
||||
}) : _target = target,
|
||||
_preferBelow = preferBelow,
|
||||
_hitTestSelf = onTap != null {
|
||||
if (onTap != null) {
|
||||
_tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap;
|
||||
}
|
||||
}
|
||||
|
||||
TapGestureRecognizer? _tapGestureRecognizer;
|
||||
|
||||
set onTap(VoidCallback? value) {
|
||||
_tapGestureRecognizer?.onTap = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapGestureRecognizer
|
||||
?..onTap = null
|
||||
..dispose();
|
||||
_tapGestureRecognizer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final bool _hitTestSelf;
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => _hitTestSelf;
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry) {
|
||||
if (event is PointerDownEvent) {
|
||||
_tapGestureRecognizer?.addPointer(event);
|
||||
}
|
||||
}
|
||||
|
||||
Offset _target;
|
||||
Offset get target => _target;
|
||||
set target(Offset value) {
|
||||
if (_target == value) return;
|
||||
_target = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
bool _preferBelow;
|
||||
bool get preferBelow => _preferBelow;
|
||||
set preferBelow(bool value) {
|
||||
if (_preferBelow == value) return;
|
||||
_preferBelow = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) {
|
||||
return target != oldDelegate.target ||
|
||||
verticalOffset != oldDelegate.verticalOffset ||
|
||||
preferBelow != oldDelegate.preferBelow;
|
||||
void performLayout() {
|
||||
size = constraints.constrain(constraints.biggest);
|
||||
|
||||
final c = BoxConstraints.loose(size);
|
||||
RenderBox indicator = firstChild!..layout(c, parentUsesSize: true);
|
||||
RenderBox overlay = lastChild!..layout(c, parentUsesSize: true);
|
||||
|
||||
final indicatorSize = indicator.size;
|
||||
final overlaySize = overlay.size;
|
||||
|
||||
final indicatorParentData =
|
||||
indicator.parentData as MultiChildLayoutParentData;
|
||||
final overlayParentData = overlay.parentData as MultiChildLayoutParentData;
|
||||
|
||||
Offset offset = positionDependentBox(
|
||||
size: size,
|
||||
childSize: overlaySize,
|
||||
target: target,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
|
||||
overlayParentData.offset = offset;
|
||||
indicatorParentData.offset = Offset(
|
||||
target.dx - indicatorSize.width / 2,
|
||||
offset.dy + overlaySize.height - 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
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,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return RenderTriangle(
|
||||
color: color,
|
||||
preferredSize: size,
|
||||
);
|
||||
}
|
||||
|
||||
@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,
|
||||
}) : _color = color,
|
||||
_preferredSize = preferredSize;
|
||||
|
||||
Color _color;
|
||||
Color get color => _color;
|
||||
set color(Color value) {
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Size _preferredSize;
|
||||
set preferredSize(Size value) {
|
||||
if (_preferredSize == value) return;
|
||||
_preferredSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void 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) {
|
||||
case TooltipType.top:
|
||||
path = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width / 2, size.height)
|
||||
..close();
|
||||
case TooltipType.right:
|
||||
path = Path()
|
||||
..moveTo(0, size.height / 2)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width, size.height)
|
||||
..close();
|
||||
}
|
||||
final path = Path()
|
||||
..moveTo(0, 0)
|
||||
..lineTo(size.width, 0)
|
||||
..lineTo(size.width / 2, size.height)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
context.canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color;
|
||||
}
|
||||
|
||||
Offset positionDependentBox({
|
||||
required TooltipType type,
|
||||
required Size size,
|
||||
required Size childSize,
|
||||
required Offset target,
|
||||
required bool preferBelow,
|
||||
double verticalOffset = 0.0,
|
||||
double horizontslOffset = 0.0,
|
||||
double margin = 10.0,
|
||||
}) {
|
||||
switch (type) {
|
||||
case TooltipType.top:
|
||||
// VERTICAL DIRECTION
|
||||
final bool fitsBelow =
|
||||
target.dy + verticalOffset + childSize.height <= size.height - margin;
|
||||
final bool fitsAbove =
|
||||
target.dy - verticalOffset - childSize.height >= margin;
|
||||
final bool tooltipBelow = fitsAbove == fitsBelow
|
||||
? preferBelow
|
||||
: fitsBelow;
|
||||
final double y;
|
||||
if (tooltipBelow) {
|
||||
y = math.min(target.dy + verticalOffset, size.height - margin);
|
||||
} else {
|
||||
y = math.max(target.dy - verticalOffset - childSize.height, margin);
|
||||
} // HORIZONTAL DIRECTION
|
||||
final double flexibleSpace = size.width - childSize.width;
|
||||
final double x = flexibleSpace <= 2 * margin
|
||||
// If there's not enough horizontal space for margin + child, center the
|
||||
// child.
|
||||
? flexibleSpace / 2.0
|
||||
: clampDouble(
|
||||
target.dx - childSize.width / 2,
|
||||
margin,
|
||||
flexibleSpace - margin,
|
||||
);
|
||||
return Offset(x, y);
|
||||
case TooltipType.right:
|
||||
final double dy = math.max(margin, target.dy - childSize.height / 2);
|
||||
final double dx = math.min(
|
||||
target.dx + horizontslOffset,
|
||||
size.width - childSize.width - margin,
|
||||
);
|
||||
return Offset(dx, dy);
|
||||
}
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
Future<void> showConfirmDialog({
|
||||
Future<bool> showConfirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
dynamic content,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
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),
|
||||
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) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: content is String
|
||||
? Text(content)
|
||||
: content is Widget
|
||||
? content
|
||||
: null,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back(result: true);
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
void showPgcFollowDialog({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:PiliPlus/common/widgets/radio_widget.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -7,92 +9,81 @@ import 'package:get/get.dart';
|
||||
Future<void> autoWrapReportDialog(
|
||||
BuildContext context,
|
||||
Map<String, Map<int, String>> options,
|
||||
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess,
|
||||
) {
|
||||
Future<LoadingState> Function(int reasonType, String? reasonDesc, bool banUid)
|
||||
onSuccess, {
|
||||
bool ban = true,
|
||||
}) {
|
||||
int? reasonType;
|
||||
String? reasonDesc;
|
||||
bool banUid = false;
|
||||
late final key = GlobalKey<FormState>();
|
||||
late final key = GlobalKey<FormFieldState<String>>();
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('举报'),
|
||||
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 5),
|
||||
actionsPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 22,
|
||||
right: 22,
|
||||
bottom: 5,
|
||||
),
|
||||
child: Text('请选择举报的理由:'),
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('举报'),
|
||||
titlePadding: const .only(left: 22, top: 16, right: 22),
|
||||
contentPadding: const .symmetric(vertical: 5),
|
||||
actionsPadding: const .only(left: 16, right: 16, bottom: 10),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Builder(
|
||||
builder: (context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: .only(left: 22, right: 22, bottom: 5),
|
||||
child: Text('请选择举报的理由:'),
|
||||
),
|
||||
RadioGroup(
|
||||
onChanged: (value) {
|
||||
reasonType = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonType,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.entries.map((entry) {
|
||||
return WrapRadioOptionsGroup<int>(
|
||||
groupTitle: entry.key,
|
||||
options: entry.value,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
RadioGroup(
|
||||
onChanged: (value) {
|
||||
reasonType = value;
|
||||
(context as Element).markNeedsBuild();
|
||||
},
|
||||
groupValue: reasonType,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.entries.map((entry) {
|
||||
return WrapRadioOptionsGroup<int>(
|
||||
groupTitle: entry.key,
|
||||
options: entry.value,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const .only(left: 22, top: 5, right: 22),
|
||||
child: TextFormField(
|
||||
key: key,
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
initialValue: reasonDesc,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: .all(10),
|
||||
labelStyle: TextStyle(fontSize: 14),
|
||||
floatingLabelStyle: TextStyle(fontSize: 14),
|
||||
),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
validator: (value) =>
|
||||
value.isNullOrEmpty ? '理由不能为空' : null,
|
||||
),
|
||||
),
|
||||
if (reasonType == 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 22,
|
||||
top: 5,
|
||||
right: 22,
|
||||
),
|
||||
child: Form(
|
||||
key: key,
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
initialValue: reasonDesc,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.all(10),
|
||||
),
|
||||
onChanged: (value) => reasonDesc = value,
|
||||
validator: (value) =>
|
||||
value.isNullOrEmpty ? '理由不能为空' : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (ban)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 14, top: 6),
|
||||
child: CheckBoxText(
|
||||
@@ -100,42 +91,42 @@ Future<void> autoWrapReportDialog(
|
||||
onChanged: (value) => banUid = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (reasonType == null ||
|
||||
(reasonType == 0 && key.currentState?.validate() != true)) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final data = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (data['code'] == 0) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
SmartDialog.showToast(data['message']);
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: ColorScheme.of(context).outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (reasonType == null ||
|
||||
(reasonType == 0 && key.currentState?.validate() != true)) {
|
||||
return;
|
||||
}
|
||||
SmartDialog.showLoading();
|
||||
try {
|
||||
final res = await onSuccess(reasonType!, reasonDesc, banUid);
|
||||
SmartDialog.dismiss();
|
||||
if (res.isSuccess) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('举报成功');
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
} catch (e, s) {
|
||||
SmartDialog.dismiss();
|
||||
SmartDialog.showToast('提交失败:$e');
|
||||
Utils.reportError(e, s);
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +157,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -199,7 +190,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
|
||||
}
|
||||
}
|
||||
|
||||
class ReportOptions {
|
||||
abstract final class ReportOptions {
|
||||
// from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js
|
||||
static Map<String, Map<int, String>> get commentReport => const {
|
||||
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
|
||||
@@ -231,4 +222,46 @@ class ReportOptions {
|
||||
0: '其他',
|
||||
},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get danmakuReport => const {
|
||||
'': {
|
||||
1: '违法违禁',
|
||||
2: '色情低俗',
|
||||
3: '赌博诈骗',
|
||||
4: '人身攻击',
|
||||
5: '侵犯隐私',
|
||||
6: '垃圾广告',
|
||||
7: '引战',
|
||||
8: '剧透',
|
||||
9: '恶意刷屏',
|
||||
10: '视频无关',
|
||||
12: '青少年不良信息',
|
||||
13: '违法信息外链',
|
||||
0: '其它', // 11
|
||||
},
|
||||
};
|
||||
|
||||
static Map<String, Map<int, String>> get liveDanmakuReport => const {
|
||||
'': {
|
||||
1: '违法违规',
|
||||
2: '低俗色情',
|
||||
3: '垃圾广告',
|
||||
4: '辱骂引战',
|
||||
5: '政治敏感',
|
||||
6: '青少年不良信息',
|
||||
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.onPerformLayout,
|
||||
});
|
||||
|
||||
final ValueChanged<double>? callback;
|
||||
final ValueChanged<double>? onPerformLayout;
|
||||
final Widget? flexibleSpace;
|
||||
final Widget? leading;
|
||||
final bool automaticallyImplyLeading;
|
||||
@@ -93,56 +93,43 @@ 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return SliverToBoxAdapter(
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
key: _childKey,
|
||||
width: _width,
|
||||
child: widget.flexibleSpace,
|
||||
child: OnlyLayoutWidget(
|
||||
onPerformLayout: (Size size) {
|
||||
if (!mounted) return;
|
||||
_height = size.height;
|
||||
widget.onPerformLayout?.call(_height!);
|
||||
setState(() {});
|
||||
},
|
||||
child: UnconstrainedBox(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: _width,
|
||||
child: widget.flexibleSpace,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final padding = MediaQuery.viewPaddingOf(context).top;
|
||||
return SliverAppBar.medium(
|
||||
leading: widget.leading,
|
||||
automaticallyImplyLeading: widget.automaticallyImplyLeading,
|
||||
@@ -170,8 +157,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;
|
||||
|
||||
@@ -316,6 +316,7 @@ class ListTile extends StatelessWidget {
|
||||
/// Requires one of its ancestors to be a [Material] widget.
|
||||
const ListTile({
|
||||
super.key,
|
||||
this.safeArea = false,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
@@ -334,7 +335,10 @@ class ListTile extends StatelessWidget {
|
||||
this.contentPadding,
|
||||
this.enabled = true,
|
||||
this.onTap,
|
||||
this.onTapUp,
|
||||
this.onLongPress,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onFocusChange,
|
||||
this.mouseCursor,
|
||||
this.selected = false,
|
||||
@@ -355,6 +359,8 @@ class ListTile extends StatelessWidget {
|
||||
this.statesController,
|
||||
}) : assert(isThreeLine != true || subtitle != null);
|
||||
|
||||
final bool safeArea;
|
||||
|
||||
/// A widget to display before the title.
|
||||
///
|
||||
/// Typically an [Icon] or a [CircleAvatar] widget.
|
||||
@@ -558,11 +564,17 @@ class ListTile extends StatelessWidget {
|
||||
/// Inoperative if [enabled] is false.
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
final GestureTapUpCallback? onTapUp;
|
||||
|
||||
/// Called when the user long-presses on this list tile.
|
||||
///
|
||||
/// Inoperative if [enabled] is false.
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
|
||||
final GestureTapUpCallback? onSecondaryTapUp;
|
||||
|
||||
/// {@macro flutter.material.inkwell.onFocusChange}
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
@@ -904,14 +916,16 @@ class ListTile extends StatelessWidget {
|
||||
|
||||
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
|
||||
final Set<WidgetState> mouseStates = <WidgetState>{
|
||||
if (!enabled || (onTap == null && onLongPress == null))
|
||||
if (!enabled ||
|
||||
(onTap == null &&
|
||||
onTapUp == null &&
|
||||
onLongPress == null &&
|
||||
onSecondaryTap == null &&
|
||||
onSecondaryTapUp == null))
|
||||
WidgetState.disabled,
|
||||
};
|
||||
final MouseCursor effectiveMouseCursor =
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(
|
||||
mouseCursor,
|
||||
mouseStates,
|
||||
) ??
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates) ??
|
||||
tileTheme.mouseCursor?.resolve(mouseStates) ??
|
||||
WidgetStateMouseCursor.clickable.resolve(mouseStates);
|
||||
|
||||
@@ -922,10 +936,66 @@ class ListTile extends StatelessWidget {
|
||||
? ListTileTitleAlignment.threeLine
|
||||
: ListTileTitleAlignment.titleHeight);
|
||||
|
||||
Widget child = IconTheme.merge(
|
||||
data: iconThemeData,
|
||||
child: IconButtonTheme(
|
||||
data: iconButtonThemeData,
|
||||
child: _ListTile(
|
||||
leading: leadingIcon,
|
||||
title: titleText,
|
||||
subtitle: subtitleText,
|
||||
trailing: trailingIcon,
|
||||
isDense: _isDenseLayout(theme, tileTheme),
|
||||
visualDensity:
|
||||
visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity,
|
||||
isThreeLine:
|
||||
isThreeLine ??
|
||||
tileTheme.isThreeLine ??
|
||||
theme.listTileTheme.isThreeLine ??
|
||||
false,
|
||||
textDirection: textDirection,
|
||||
titleBaselineType:
|
||||
titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!,
|
||||
subtitleBaselineType:
|
||||
subtitleStyle?.textBaseline ??
|
||||
defaults.subtitleTextStyle!.textBaseline!,
|
||||
horizontalTitleGap:
|
||||
horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
|
||||
minVerticalPadding:
|
||||
minVerticalPadding ??
|
||||
tileTheme.minVerticalPadding ??
|
||||
defaults.minVerticalPadding!,
|
||||
minLeadingWidth:
|
||||
minLeadingWidth ??
|
||||
tileTheme.minLeadingWidth ??
|
||||
defaults.minLeadingWidth!,
|
||||
minTileHeight: minTileHeight ?? tileTheme.minTileHeight,
|
||||
titleAlignment: effectiveTitleAlignment,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (safeArea) {
|
||||
child = SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
minimum: resolvedContentPadding,
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
child = Padding(
|
||||
padding: resolvedContentPadding,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
customBorder: shape ?? tileTheme.shape,
|
||||
onTap: enabled ? onTap : null,
|
||||
onTapUp: enabled ? onTapUp : null,
|
||||
onLongPress: enabled ? onLongPress : null,
|
||||
onSecondaryTap: enabled ? onSecondaryTap : null,
|
||||
onSecondaryTapUp: enabled ? onSecondaryTapUp : null,
|
||||
onFocusChange: onFocusChange,
|
||||
mouseCursor: effectiveMouseCursor,
|
||||
canRequestFocus: enabled,
|
||||
@@ -947,50 +1017,7 @@ class ListTile extends StatelessWidget {
|
||||
shape: shape ?? tileTheme.shape ?? const Border(),
|
||||
color: _tileBackgroundColor(theme, tileTheme, defaults),
|
||||
),
|
||||
child: Padding(
|
||||
padding: resolvedContentPadding,
|
||||
child: IconTheme.merge(
|
||||
data: iconThemeData,
|
||||
child: IconButtonTheme(
|
||||
data: iconButtonThemeData,
|
||||
child: _ListTile(
|
||||
leading: leadingIcon,
|
||||
title: titleText,
|
||||
subtitle: subtitleText,
|
||||
trailing: trailingIcon,
|
||||
isDense: _isDenseLayout(theme, tileTheme),
|
||||
visualDensity:
|
||||
visualDensity ??
|
||||
tileTheme.visualDensity ??
|
||||
theme.visualDensity,
|
||||
isThreeLine:
|
||||
isThreeLine ??
|
||||
tileTheme.isThreeLine ??
|
||||
theme.listTileTheme.isThreeLine ??
|
||||
false,
|
||||
textDirection: textDirection,
|
||||
titleBaselineType:
|
||||
titleStyle.textBaseline ??
|
||||
defaults.titleTextStyle!.textBaseline!,
|
||||
subtitleBaselineType:
|
||||
subtitleStyle?.textBaseline ??
|
||||
defaults.subtitleTextStyle!.textBaseline!,
|
||||
horizontalTitleGap:
|
||||
horizontalTitleGap ?? tileTheme.horizontalTitleGap ?? 16,
|
||||
minVerticalPadding:
|
||||
minVerticalPadding ??
|
||||
tileTheme.minVerticalPadding ??
|
||||
defaults.minVerticalPadding!,
|
||||
minLeadingWidth:
|
||||
minLeadingWidth ??
|
||||
tileTheme.minLeadingWidth ??
|
||||
defaults.minLeadingWidth!,
|
||||
minTileHeight: minTileHeight ?? tileTheme.minTileHeight,
|
||||
titleAlignment: effectiveTitleAlignment,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1313,12 +1340,7 @@ class _RenderListTile extends RenderBox
|
||||
@override
|
||||
Iterable<RenderBox> get children {
|
||||
final RenderBox? title = childForSlot(_ListTileSlot.title);
|
||||
return <RenderBox>[
|
||||
?leading,
|
||||
?title,
|
||||
?subtitle,
|
||||
?trailing,
|
||||
];
|
||||
return <RenderBox>[?leading, ?title, ?subtitle, ?trailing];
|
||||
}
|
||||
|
||||
bool get isDense => _isDense;
|
||||
@@ -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,14 +657,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
_fallbackScrollController = ScrollController();
|
||||
}
|
||||
super.initState();
|
||||
_animController = AnimationController(
|
||||
vsync: this,
|
||||
reverseDuration: const Duration(milliseconds: 500),
|
||||
);
|
||||
_anim = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0, 1),
|
||||
).animate(_animController);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -681,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;
|
||||
}
|
||||
@@ -708,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) {
|
||||
@@ -750,8 +739,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
|
||||
position.dispose();
|
||||
_persistedScrollOffset.dispose();
|
||||
|
||||
_animController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -781,13 +768,6 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
bool? _lastCanDrag;
|
||||
Axis? _lastAxisDirection;
|
||||
|
||||
late bool _isRTL = false;
|
||||
Offset? _downPos;
|
||||
bool? _isSliding;
|
||||
|
||||
late AnimationController _animController;
|
||||
late Animation<Offset> _anim;
|
||||
|
||||
@override
|
||||
@protected
|
||||
void setCanDrag(bool value) {
|
||||
@@ -834,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;
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -893,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.
|
||||
@@ -965,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);
|
||||
@@ -986,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
|
||||
@@ -1103,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,
|
||||
@@ -1182,25 +1090,7 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
_maxWidth = constraints.maxWidth;
|
||||
return SlideTransition(
|
||||
position: _anim,
|
||||
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
|
||||
@@ -1248,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;
|
||||
@@ -1321,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;
|
||||
@@ -1680,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);
|
||||
}
|
||||
|
||||
@@ -1773,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),
|
||||
@@ -1792,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.
|
||||
@@ -1935,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);
|
||||
@@ -1980,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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||