Compare commits

..

639 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
fc6f51787b upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 16:37:20 +08:00
bggRGjQaUbCoE
e72203afdb fix #1173
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 15:31:55 +08:00
bggRGjQaUbCoE
6741333367 opt marquee
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 15:31:55 +08:00
bggRGjQaUbCoE
6e1bc8d0e7 fix #1169
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 10:41:33 +08:00
bggRGjQaUbCoE
477b59ce89 remove audio_normalization
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 21:37:54 +08:00
bggRGjQaUbCoE
70881ead22 opt fav intro
Closes #1159

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 20:34:16 +08:00
bggRGjQaUbCoE
b09a41af24 opt slide dismiss
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 18:03:15 +08:00
My-Responsitories
08a33d9ce5 feat: musicDetail (#1157)
* feat: musicDetail

* opt: marquee
2025-08-28 17:40:12 +08:00
bggRGjQaUbCoE
84f7f14a29 fix #1156
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 17:15:10 +08:00
bggRGjQaUbCoE
331c9877a3 opt view pgc
Closes #1155

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 16:30:19 +08:00
bggRGjQaUbCoE
ac26022da1 feat: fold dyn
Closes #1153

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 16:07:38 +08:00
bggRGjQaUbCoE
7a5662c6ca feat: login devices
Closes #1140

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 16:07:14 +08:00
bggRGjQaUbCoE
659cff875f custom show fs lock btn
Closes #1150

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 16:06:41 +08:00
dom
06a5c2c63b Update ios.yml 2025-08-28 13:41:10 +08:00
bggRGjQaUbCoE
077293854c opt slide dismiss
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-27 15:28:03 +08:00
bggRGjQaUbCoE
cf24f851e8 fix #1143
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-27 13:36:11 +08:00
bggRGjQaUbCoE
01a8631e00 fix typo
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-27 12:08:57 +08:00
My-Responsitories
5f8313901b tweaks (#1142)
* opt: unused layout

* mod: semantics

* opt: DanmakuMsg type

* opt: avoid cast

* opt: unnecessary_lambdas

* opt: use isEven

* opt: logger

* opt: invalid common page

* tweak

* opt: unify DynController
2025-08-27 12:01:53 +08:00
bggRGjQaUbCoE
56ffc2781f RTF slide dismiss
Closes #1135

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-27 11:19:44 +08:00
bggRGjQaUbCoE
51d7e454de tweak
Closes #1139
Closes #1141

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-27 10:49:55 +08:00
bggRGjQaUbCoE
63419d5b1c charge btn
Closes #1123

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-26 18:16:39 +08:00
bggRGjQaUbCoE
91627df804 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-26 18:16:32 +08:00
bggRGjQaUbCoE
fb8a06787b handle music playlist uri
Closes #1130

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-26 14:42:01 +08:00
bggRGjQaUbCoE
dc7fe2cb3b tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-26 14:30:33 +08:00
bggRGjQaUbCoE
1f22dcd73f bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-26 12:12:26 +08:00
bggRGjQaUbCoE
09b0f19775 opt scheme
Closes #1126

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-26 12:12:20 +08:00
bggRGjQaUbCoE
8498ea0618 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-25 20:23:10 +08:00
bggRGjQaUbCoE
a366b8a9e4 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-25 15:45:31 +08:00
bggRGjQaUbCoE
461e91239e opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-25 12:52:31 +08:00
bggRGjQaUbCoE
4bba675063 persistent buvid
Closes #1110

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 20:19:17 +08:00
bggRGjQaUbCoE
08d64be5d4 opt check reply state
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 18:14:39 +08:00
bggRGjQaUbCoE
7d30c9c66a filter dyn arc title
Closes #1075

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 17:28:18 +08:00
bggRGjQaUbCoE
fe191ef934 opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 14:46:10 +08:00
bggRGjQaUbCoE
db8b5f5e66 custom silent down img
Closes #1030

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 14:30:53 +08:00
bggRGjQaUbCoE
6c52db1c6c custom show fs screenshot btn
Closes #1103

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 13:59:01 +08:00
bggRGjQaUbCoE
f942b2a7ee fix #1108
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 13:30:38 +08:00
bggRGjQaUbCoE
288d554de9 opt check reply note
Closes #1095

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 09:41:39 +08:00
bggRGjQaUbCoE
a274f5ae8b opt live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-24 09:41:19 +08:00
bggRGjQaUbCoE
ad0d9ecee0 fix #1098
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-23 22:07:26 +08:00
bggRGjQaUbCoE
ee819bb260 Revert "opt: non null (#1091)"
This reverts commit 3c34e43827.
2025-08-23 22:05:35 +08:00
My-Responsitories
b77403f03f fix: latex (#1094) 2025-08-23 12:45:19 +00:00
My-Responsitories
3c34e43827 opt: non null (#1091)
* opt: type

* opt: type 2

* opt: type 3
2025-08-23 10:25:41 +00:00
bggRGjQaUbCoE
6009668427 downgrade font_awesome_flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-23 09:50:47 +08:00
bggRGjQaUbCoE
16a3e21db4 opt qa btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-23 09:42:08 +08:00
bggRGjQaUbCoE
d69649f1b6 fix #1088
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-23 09:11:53 +08:00
bggRGjQaUbCoE
faaffd0f30 fix #1087
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-23 09:11:47 +08:00
bggRGjQaUbCoE
a9f1e3cf09 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-23 09:03:59 +08:00
bggRGjQaUbCoE
9e72fea67c opt opus item
opt live dm

add audio qa type

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-22 20:00:52 +08:00
bggRGjQaUbCoE
8fc8bd99e5 fix #1085
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-22 17:27:35 +08:00
bggRGjQaUbCoE
4d3a74f2e0 show fullscreen qa btn
Closes #1081

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-22 15:37:37 +08:00
bggRGjQaUbCoE
272cfcb829 opt loading res
Closes #1080

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-22 11:05:16 +08:00
bggRGjQaUbCoE
c7437225eb opt fan item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-22 10:32:46 +08:00
bggRGjQaUbCoE
d4a1568b28 opt dyn jump
tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-22 10:27:42 +08:00
bggRGjQaUbCoE
824ee53025 show dyn pugv
Closes #1064

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 20:30:11 +08:00
bggRGjQaUbCoE
ee142e5e1d opt search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 18:01:40 +08:00
bggRGjQaUbCoE
571bdb5eae opt block
Closes #1074

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 17:32:27 +08:00
bggRGjQaUbCoE
2e5cb324a1 opt article
Closes #1072

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 17:16:30 +08:00
bggRGjQaUbCoE
ed191e20b4 fix import history
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 15:12:40 +08:00
bggRGjQaUbCoE
ba14e56ceb fix #1061
opt `FollowingsFollowedUpper` url

Closes #1061
Closes #1062

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 14:51:06 +08:00
bggRGjQaUbCoE
b6ce93cbd2 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-21 10:58:48 +08:00
bggRGjQaUbCoE
76f1d0129b opt ui
Closes #1050

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-20 23:28:57 +08:00
bggRGjQaUbCoE
e096ebcbba deprecate account migration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-19 19:34:39 +08:00
bggRGjQaUbCoE
6c8baa5be5 opt video action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-19 18:27:22 +08:00
bggRGjQaUbCoE
4f2bfb8126 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-19 18:27:15 +08:00
bggRGjQaUbCoE
33738c90bc opt article code theme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 22:51:21 +08:00
bggRGjQaUbCoE
7ff95c00d2 opt import render theme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 22:37:51 +08:00
bggRGjQaUbCoE
fc4f92e0c0 set android minsdk 23
Closes #1045

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 22:21:28 +08:00
My-Responsitories
ed57697fdc feat: InportExportDialog (#1048) 2025-08-18 13:25:00 +00:00
bggRGjQaUbCoE
08c3789321 copy/cut rich text
Closes #1047

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 20:10:40 +08:00
bggRGjQaUbCoE
43fa00848d show followings_followed_upper
Closes #1033

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 18:31:56 +08:00
bggRGjQaUbCoE
8c38699334 opt filter dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 18:31:56 +08:00
bggRGjQaUbCoE
dcc5f51e6a tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-18 18:31:56 +08:00
bggRGjQaUbCoE
dc6b76812c custom video aspectRatio
Closes #1040

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-17 22:19:41 +08:00
bggRGjQaUbCoE
470545337d fix #1035
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-17 17:41:54 +08:00
bggRGjQaUbCoE
ab610e9da5 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-17 12:07:03 +08:00
bggRGjQaUbCoE
5420712bda tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-17 12:06:57 +08:00
bggRGjQaUbCoE
55733d30c5 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-16 18:34:55 +08:00
bggRGjQaUbCoE
2090fd2312 migrate gradle kts
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-16 15:59:52 +08:00
bggRGjQaUbCoE
f3bad60fb6 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-16 10:43:11 +08:00
dom
d805306d20 opt video seek preview (#1026)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-16 10:29:46 +08:00
bggRGjQaUbCoE
831a3052fa tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-16 10:27:20 +08:00
bggRGjQaUbCoE
52151765f8 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-15 11:18:17 +08:00
bggRGjQaUbCoE
422b413778 opt ui
opt req

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-15 11:18:10 +08:00
My-Responsitories
1943b65788 opt: initialScrollIndex (#1018) 2025-08-14 15:50:45 +00:00
bggRGjQaUbCoE
629be129ff opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-14 17:25:33 +08:00
bggRGjQaUbCoE
6ff256637a opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-14 17:01:33 +08:00
bggRGjQaUbCoE
34e9afd7ad tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-14 11:56:01 +08:00
bggRGjQaUbCoE
0cd57c9bb0 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-14 11:40:01 +08:00
bggRGjQaUbCoE
22d9fbddf9 fix #1015
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-14 11:29:54 +08:00
bggRGjQaUbCoE
65746ae2bd opt set dm/sub settings
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-13 20:57:46 +08:00
bggRGjQaUbCoE
685852c0a4 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-13 18:40:20 +08:00
bggRGjQaUbCoE
b2100f3872 opt live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-13 11:25:16 +08:00
bggRGjQaUbCoE
86125d5ecd fix live dm
Closes #1007

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-13 11:04:51 +08:00
bggRGjQaUbCoE
086c93d24f opt member page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-13 10:43:19 +08:00
bggRGjQaUbCoE
aea1992f5d upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-13 10:43:19 +08:00
dom
6b38322c3b Update android.yml 2025-08-12 18:52:39 +08:00
dom
865ddad147 Update android.yml 2025-08-12 18:48:41 +08:00
dom
6709fa4d21 Update ios.yml 2025-08-12 18:48:24 +08:00
My-Responsitories
705417f65b fix: check crossAxisExtent (#1005) 2025-08-12 10:24:54 +00:00
My-Responsitories
690c4f5786 feat: grid jump to index (#1004) 2025-08-12 09:06:33 +00:00
dom
e00c176bdf Update bug-反馈.yml 2025-08-12 16:57:43 +08:00
dom
8d14f42fd8 Update bug-反馈.yml 2025-08-12 16:53:42 +08:00
dom
6688fcf3e9 Update 功能请求.yml 2025-08-12 16:53:13 +08:00
bggRGjQaUbCoE
308bd26172 opt reply check
fix check dyn

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-12 16:45:25 +08:00
bggRGjQaUbCoE
a94493705d opt video bar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-12 15:04:52 +08:00
bggRGjQaUbCoE
e251eaf811 opt check dyn
Closes #996

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-12 13:50:36 +08:00
bggRGjQaUbCoE
1826b6a059 opt video bar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-12 13:50:30 +08:00
bggRGjQaUbCoE
be5a1af040 handle sub url
Closes #995

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-11 21:21:57 +08:00
bggRGjQaUbCoE
17b7eb7e0f tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-11 18:35:50 +08:00
bggRGjQaUbCoE
60c25e4b65 opt live
Closes #979

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-11 16:42:00 +08:00
bggRGjQaUbCoE
2c92845af0 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-11 14:52:28 +08:00
bggRGjQaUbCoE
4a4aa569ec check reply state
Closes #990

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-11 14:06:39 +08:00
bggRGjQaUbCoE
95f1d1485d tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-11 13:56:57 +08:00
My-Responsitories
e7f27e4913 mod: account (#989) 2025-08-11 03:01:00 +00:00
My-Responsitories
dc61d9007f feat: reduce luminosity in dark mode (#988) 2025-08-11 02:57:08 +00:00
bggRGjQaUbCoE
88c2ba8059 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 22:27:06 +08:00
bggRGjQaUbCoE
309c871919 handle dyn RICH_TEXT_NODE_TYPE_OGV_SEASON
Closes #983

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 20:44:54 +08:00
bggRGjQaUbCoE
745a510ffa opt find pgc episode
tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 19:03:35 +08:00
bggRGjQaUbCoE
8fbc8fda3d opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 14:59:51 +08:00
bggRGjQaUbCoE
dbde90459b opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 13:41:31 +08:00
bggRGjQaUbCoE
b788794f4b tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 12:31:33 +08:00
bggRGjQaUbCoE
06b433aa60 fix change episode
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 11:54:04 +08:00
bggRGjQaUbCoE
6093848811 opt video/intro page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-10 11:22:41 +08:00
My-Responsitories
34c5d6812f opt: settings (#977) 2025-08-10 03:05:36 +00:00
My-Responsitories
aaad7fc6dc opt: ActionItem (#974) 2025-08-09 16:29:58 +00:00
bggRGjQaUbCoE
fac37e59aa opt action item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 21:38:06 +08:00
bggRGjQaUbCoE
11c6745fd7 opt triple mixin
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 19:25:21 +08:00
bggRGjQaUbCoE
30aa29598b opt action item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 19:19:08 +08:00
bggRGjQaUbCoE
85c72731f6 refa video action item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 19:04:35 +08:00
bggRGjQaUbCoE
27c9c266c1 opt search page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 17:39:18 +08:00
bggRGjQaUbCoE
720f3e10e8 opt video appbar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 17:30:29 +08:00
bggRGjQaUbCoE
162a79145f opt query search rcmd
related #972

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 17:04:46 +08:00
bggRGjQaUbCoE
9e31326bf5 opt search bar
Closes #971

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 16:42:56 +08:00
My-Responsitories
e77fe2587c opt: userInfoCache (#968) 2025-08-09 05:36:48 +00:00
My-Responsitories
c75a68dacc opt: log page (#967) 2025-08-09 05:32:52 +00:00
bggRGjQaUbCoE
16fa47e8e9 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 12:12:25 +08:00
bggRGjQaUbCoE
2df6c91a3d feat: like live room
Closes #963

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 12:12:25 +08:00
bggRGjQaUbCoE
bd490b87ca opt live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 12:12:19 +08:00
bggRGjQaUbCoE
597fca9fbf lint
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-08 19:30:17 +08:00
bggRGjQaUbCoE
810505ea1d tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-08 19:30:06 +08:00
bggRGjQaUbCoE
d108373c33 opt set dynpage ratio
tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-08 18:11:25 +08:00
bggRGjQaUbCoE
c0287d05be live stream retry
related #936

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-08 12:00:09 +08:00
bggRGjQaUbCoE
be998b8ee1 export settings file
Closes #950

tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-08 11:56:19 +08:00
bggRGjQaUbCoE
ef1ccabc8a show livetime
tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-07 20:33:49 +08:00
bggRGjQaUbCoE
edb5ea7a7a opt search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-07 14:48:43 +08:00
bggRGjQaUbCoE
b4c1568869 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-07 13:49:12 +08:00
bggRGjQaUbCoE
83e25ec0bf opt sort search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-07 12:58:25 +08:00
bggRGjQaUbCoE
6d55321699 feat: member cheese
feat: fav pugv

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-07 12:58:19 +08:00
bggRGjQaUbCoE
26a5b7b7a7 opt sync history status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 21:36:11 +08:00
bggRGjQaUbCoE
f663301eae opt history account
Closes #948

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 21:25:50 +08:00
bggRGjQaUbCoE
eb9f3cd21c update part media item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 20:15:29 +08:00
bggRGjQaUbCoE
05119edacb opt live room
Closes #947

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 20:15:04 +08:00
bggRGjQaUbCoE
554e96c820 opt later item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 18:26:18 +08:00
bggRGjQaUbCoE
40a19f2766 opt pgc/pugv intro panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 17:47:53 +08:00
bggRGjQaUbCoE
b723529d7f tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-06 14:29:57 +08:00
My-Responsitories
9f33488248 revert: toSet (#941) 2025-08-05 21:36:21 +08:00
bggRGjQaUbCoE
80a4c8c24d opt change episode
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 21:30:52 +08:00
My-Responsitories
170b2aa6d9 opt: GroupPanel (#940)
* opt: GroupPanel

* mod: int? operator
2025-08-05 12:52:11 +00:00
bggRGjQaUbCoE
e2639b6951 opt del later view
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 19:08:27 +08:00
bggRGjQaUbCoE
b954c6f893 opt del later view
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 18:47:34 +08:00
bggRGjQaUbCoE
104d295389 opt multi del
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 18:15:06 +08:00
bggRGjQaUbCoE
3caa684b2e fix del history/later
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 16:30:48 +08:00
bggRGjQaUbCoE
af7a1a6ee9 opt context ext
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 15:58:48 +08:00
My-Responsitories
add519120c mod: hasLater (#938) 2025-08-05 05:45:49 +00:00
My-Responsitories
01552801f2 opt: select (#937) 2025-08-05 05:41:37 +00:00
bggRGjQaUbCoE
afb09e8a0a opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-05 11:57:46 +08:00
bggRGjQaUbCoE
deb48d1ada opt to live room
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 18:58:48 +08:00
bggRGjQaUbCoE
cf84a92808 refa video params
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 17:38:10 +08:00
bggRGjQaUbCoE
26ccb92b44 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 15:02:03 +08:00
bggRGjQaUbCoE
3fa697a037 remove appbar anim
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 14:27:06 +08:00
qpst4
f72c13df62 Fix intent-filter for bilibili://search deep link (#934)
Fix intent-filter for bilibili://search deep link

* update

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 14:07:32 +08:00
My-Responsitories
7b51f15753 opt: multiSelect (#935) 2025-08-04 04:57:37 +00:00
bggRGjQaUbCoE
d246462535 fix heartbeat
Closes #929

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 12:23:29 +08:00
bggRGjQaUbCoE
3208661a52 opt pugv
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-04 11:44:17 +08:00
bggRGjQaUbCoE
2e614fa03c upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-03 19:03:19 +08:00
bggRGjQaUbCoE
b7f70ee0b3 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-03 19:03:13 +08:00
bggRGjQaUbCoE
cb52840bad Release 1.1.4
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-03 15:39:48 +08:00
dom
bd3d6cf34c feat: pugv (#927)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-03 15:25:29 +08:00
bggRGjQaUbCoE
cf835e330b opt change episode
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-03 10:42:26 +08:00
bggRGjQaUbCoE
14fd660ce2 opt history pause tip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-03 09:47:50 +08:00
bggRGjQaUbCoE
0a8282d3e3 opt save pgc reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-02 22:50:35 +08:00
bggRGjQaUbCoE
574e432e09 fix get live second list
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-02 22:03:09 +08:00
bggRGjQaUbCoE
4b9f251dae opt live room
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-02 19:53:52 +08:00
bggRGjQaUbCoE
f0e2a63d11 opt pgc
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-02 18:00:17 +08:00
bggRGjQaUbCoE
3c964787df opt intro controller
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-02 15:24:45 +08:00
bggRGjQaUbCoE
199ddc0e7e fix disable search suggestion
related #923

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 21:10:06 +08:00
bggRGjQaUbCoE
1071a29b26 opt minepage
Closes #922

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 20:31:29 +08:00
bggRGjQaUbCoE
90ce74cf91 opt level indicator
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 18:57:56 +08:00
bggRGjQaUbCoE
05bb27ee2b show search rcmd reason
Closes #921

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 18:32:55 +08:00
bggRGjQaUbCoE
53ef4219eb opt live room
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 17:13:47 +08:00
bggRGjQaUbCoE
dd5c2229b3 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 15:45:40 +08:00
bggRGjQaUbCoE
5c28376210 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 13:37:42 +08:00
bggRGjQaUbCoE
aa8eef46da common dyn page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 12:43:49 +08:00
bggRGjQaUbCoE
f7d4db6aad tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-01 10:20:07 +08:00
My-Responsitories
edc9a1ca7b fix: save bangumi & fav pic (#917) 2025-07-31 12:31:39 +00:00
My-Responsitories
05c9269531 opt: unify fav & coin of video & pgc (#916) 2025-07-31 12:16:42 +00:00
bggRGjQaUbCoE
e945daba3a refa query follow up
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-31 17:11:10 +08:00
bggRGjQaUbCoE
1029621b63 update blackMids
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-31 17:11:03 +08:00
bggRGjQaUbCoE
c8613fbe07 show history pause tip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-31 17:08:10 +08:00
bggRGjQaUbCoE
c4e87925cf opt vote panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-30 21:06:12 +08:00
bggRGjQaUbCoE
83e5095cc3 opt segment post errmsg
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-30 18:23:37 +08:00
bggRGjQaUbCoE
a57323e5a8 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-30 12:47:27 +08:00
Tong xuewen
3eb9c5b8ba Add configurable scroll threshold (#910)
* Add configurable scroll threshold

* update

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-29 23:02:05 +08:00
Tong xuewen
cf403aaf78 Move a setting from “Other settings” to “Player settings” (#909) 2025-07-29 12:20:38 +08:00
bggRGjQaUbCoE
2325814f6d tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-28 20:42:07 +08:00
bggRGjQaUbCoE
e5c86e1d2e opt webdav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-28 19:23:38 +08:00
bggRGjQaUbCoE
26c420023f opt mine page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-28 17:49:10 +08:00
bggRGjQaUbCoE
cbb838fff8 opt mine page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-28 17:14:17 +08:00
bggRGjQaUbCoE
3c466d5748 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-28 16:54:38 +08:00
Tong xuewen
db79a03ec4 fix ios fullscreen when resuming from background (#902) 2025-07-28 16:53:17 +08:00
bggRGjQaUbCoE
65b432ed2c merge mine & media
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-28 16:44:33 +08:00
bggRGjQaUbCoE
6ca7efe8d1 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-27 12:36:43 +08:00
bggRGjQaUbCoE
916931dd11 update tmpPos on drag start
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-27 00:28:02 +08:00
bggRGjQaUbCoE
819a28c48c fix blackMids
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 23:51:08 +08:00
bggRGjQaUbCoE
f281e6e36a fix #897
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 23:36:08 +08:00
bggRGjQaUbCoE
c46058ef4d bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 21:59:32 +08:00
bggRGjQaUbCoE
39cc42d542 opt type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 21:18:43 +08:00
bggRGjQaUbCoE
3a78ead3a6 opt type
opt ua

opt subtitle

opt playertype

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 20:11:26 +08:00
bggRGjQaUbCoE
a05ecd020b opt episode
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 18:25:44 +08:00
bggRGjQaUbCoE
e00f009a64 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 17:35:25 +08:00
bggRGjQaUbCoE
b977f5228e tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 17:07:20 +08:00
bggRGjQaUbCoE
4003ca6c4d opt share img
opt block query

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 16:43:30 +08:00
bggRGjQaUbCoE
9072d6e051 upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 15:13:42 +08:00
bggRGjQaUbCoE
bb36876d1e opt search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 12:47:01 +08:00
bggRGjQaUbCoE
d17dbe139e opt uplist item
end align player duration

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 11:46:26 +08:00
bggRGjQaUbCoE
d567c296f8 opt video action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-24 20:12:15 +08:00
bggRGjQaUbCoE
0c6bc9d58a refa fav video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-24 13:09:42 +08:00
bggRGjQaUbCoE
6d48c70020 remove unused pkg
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-24 11:28:32 +08:00
bggRGjQaUbCoE
569484014e reformat
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-24 11:25:20 +08:00
bggRGjQaUbCoE
c89a39cf5c fix #887
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-24 09:14:05 +08:00
bggRGjQaUbCoE
418a1e8d39 reformat
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 16:47:11 +08:00
bggRGjQaUbCoE
148e0872b4 lint
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 16:21:50 +08:00
bggRGjQaUbCoE
b1432b5ff5 opt action item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 16:18:25 +08:00
bggRGjQaUbCoE
75e86952fd fix msg live status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 15:56:04 +08:00
bggRGjQaUbCoE
03b095905a upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 12:12:30 +08:00
bggRGjQaUbCoE
77a444b896 opt ugc intro
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 12:12:30 +08:00
bggRGjQaUbCoE
e770e39c8f tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-23 12:12:26 +08:00
bggRGjQaUbCoE
55bed2e830 opt ugc intro
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-22 18:30:04 +08:00
bggRGjQaUbCoE
a875ff3988 refa: ugc intro
Closes #879

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-22 17:16:37 +08:00
bggRGjQaUbCoE
a4a866d3f5 fix #878
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-22 12:58:14 +08:00
bggRGjQaUbCoE
4e5c4169fa opt get sdkInt
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-22 12:19:51 +08:00
bggRGjQaUbCoE
fbf47d7485 fix #877
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-22 11:53:59 +08:00
bggRGjQaUbCoE
ba16f3d597 fix update skip type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-22 11:01:44 +08:00
bggRGjQaUbCoE
8a62f5bbee fav order
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-21 17:49:18 +08:00
bggRGjQaUbCoE
042a7df7f3 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-21 13:55:37 +08:00
bggRGjQaUbCoE
610ed02dd4 show msg user live status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-21 13:55:37 +08:00
bggRGjQaUbCoE
f7184aff4e show video label
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-21 13:55:37 +08:00
bggRGjQaUbCoE
473515efc5 fix query follow up
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-21 13:55:37 +08:00
bggRGjQaUbCoE
aee65b0a9c bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-21 13:55:37 +08:00
dom
e46488d11e Update 功能请求.yml 2025-07-21 13:55:23 +08:00
dom
f43bc74868 Update bug-反馈.yml 2025-07-21 13:55:13 +08:00
bggRGjQaUbCoE
f223befad6 opt live onlyPlayAudio
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-16 17:44:06 +08:00
bggRGjQaUbCoE
e0243461bb opt scheme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-15 18:03:36 +08:00
bggRGjQaUbCoE
2877372f67 fix msg avatar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-15 12:47:06 +08:00
bggRGjQaUbCoE
d6c12195f8 opt member tab
try-catch handle live dm

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-15 12:37:51 +08:00
bggRGjQaUbCoE
e280f6ee4a login/exp log
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-14 15:57:55 +08:00
bggRGjQaUbCoE
4275719844 opt save dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-14 12:21:17 +08:00
bggRGjQaUbCoE
f41af00b31 fix live dm
opt live/article report

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-13 12:42:22 +08:00
Fengning Zhu
10ed5f2ea4 fix: resolve fullscreen UI offset issue on some Android tablets (#873) 2025-07-13 11:47:38 +08:00
bggRGjQaUbCoE
44ba554e0e fix save reply
opt profile page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-12 00:13:57 +08:00
bggRGjQaUbCoE
c346d586a5 opt reply item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-11 21:27:46 +08:00
bggRGjQaUbCoE
52fb332378 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-11 17:27:11 +08:00
bggRGjQaUbCoE
5f5387b941 show co/charging label
fix special dm

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-11 12:59:58 +08:00
Leung Ming
db682066ba Fix only_show_wearing (#872)
Signed-off-by: Leung Ming <165622843+leung-ming@users.noreply.github.com>
2025-07-11 11:34:58 +08:00
bggRGjQaUbCoE
3ee8c68eac show charging label
tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-10 17:32:18 +08:00
bggRGjQaUbCoE
a9ceb04d07 opt dyn post
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-10 12:46:05 +08:00
bggRGjQaUbCoE
f60a714c06 fix mention list header
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-10 12:31:09 +08:00
bggRGjQaUbCoE
e240a6caae opt scheme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 23:55:08 +08:00
bggRGjQaUbCoE
829b966382 opt multi mention
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 19:15:34 +08:00
bggRGjQaUbCoE
58f3949a22 check reply inputDisable
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 19:03:24 +08:00
bggRGjQaUbCoE
dfb823c30c multi mention
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 18:36:21 +08:00
bggRGjQaUbCoE
b32922af8f opt mention
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 18:05:21 +08:00
bggRGjQaUbCoE
753e10ef20 opt emote
opt live fav state

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 16:15:21 +08:00
bggRGjQaUbCoE
05153fda72 opt pub page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-09 12:02:28 +08:00
bggRGjQaUbCoE
8bf55ec95a opt pm msg
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-08 18:58:55 +08:00
bggRGjQaUbCoE
d2023b1750 show auto reply tip
opt pm msg

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-08 18:40:16 +08:00
bggRGjQaUbCoE
b51c6b65a1 custom emoji tooltip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-08 17:04:49 +08:00
bggRGjQaUbCoE
e3337f1e7c fix rm top dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-08 17:04:31 +08:00
bggRGjQaUbCoE
5ff6ef8801 opt reply hint
opt mention list header

tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-07 13:40:48 +08:00
bggRGjQaUbCoE
74f7c5d0ea fix video tab
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-07 13:39:24 +08:00
bggRGjQaUbCoE
b43c07bd51 opt dyn panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-06 22:36:54 +08:00
bggRGjQaUbCoE
7cdcd6df97 fix buvid3
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-06 22:36:54 +08:00
bggRGjQaUbCoE
7439160f03 fix imageview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-06 12:21:18 +08:00
bggRGjQaUbCoE
b496ea4da4 opt insert rich text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-05 13:12:39 +08:00
bggRGjQaUbCoE
0f1665bf08 fix update vote
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-04 23:17:56 +08:00
dom
83459df3b7 feat: create vote (#871)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-04 22:10:11 +08:00
bggRGjQaUbCoE
9ce84fb997 fix vote
fix filter dyn

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-04 17:36:54 +08:00
bggRGjQaUbCoE
708bf27710 remove silence endtime
deprecated

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-04 12:00:04 +08:00
bggRGjQaUbCoE
dae64e74d5 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-03 18:07:28 +08:00
bggRGjQaUbCoE
8414c0f71f tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-03 15:56:32 +08:00
bggRGjQaUbCoE
18f5ddd937 show pgc indexShow
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-02 21:44:30 +08:00
bggRGjQaUbCoE
a231492f49 fix richtextfield
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-02 21:44:18 +08:00
bggRGjQaUbCoE
6f2570c5be feat: richtextfield
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-01 10:54:31 +08:00
bggRGjQaUbCoE
721bf2d59f opt dyn pgc errmsg
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-27 14:02:09 +08:00
bggRGjQaUbCoE
e5301c3cf8 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 21:54:22 +08:00
bggRGjQaUbCoE
20893ef65f opt video card
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 17:27:01 +08:00
bggRGjQaUbCoE
12c13cd25a opt jump
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 17:11:08 +08:00
bggRGjQaUbCoE
81f72e2c4a opt reply item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 14:22:57 +08:00
bggRGjQaUbCoE
d2e5e71729 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 12:26:27 +08:00
bggRGjQaUbCoE
158e8f7cb8 fix reply action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 12:09:53 +08:00
6v
7886a901a3 feat: add configurable main page back behavior (#870)
* feat: add configurable main page back behavior

Add setting to control whether back button exits directly or returns to first tab

* update

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 11:40:44 +08:00
bggRGjQaUbCoE
0264a4c01f opt text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 10:59:39 +08:00
bggRGjQaUbCoE
2eb86658b7 opt show more text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-26 10:15:17 +08:00
bggRGjQaUbCoE
0b95476d8f opt pub textfield
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-25 15:19:45 +08:00
bggRGjQaUbCoE
27023a305d opt pub textfield
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-25 13:33:29 +08:00
bggRGjQaUbCoE
ef7cfdd92e opt pub insert text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-25 11:38:35 +08:00
bggRGjQaUbCoE
4b067c5ed2 del at user
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-25 00:00:27 +08:00
bggRGjQaUbCoE
7be3774675 feat: at user
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-24 23:13:31 +08:00
bggRGjQaUbCoE
fcf758e290 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-24 18:08:07 +08:00
bggRGjQaUbCoE
79e30047f5 member audio
member comic

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-23 13:26:52 +08:00
bggRGjQaUbCoE
c6a377b9d4 opt req
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-23 11:37:16 +08:00
bggRGjQaUbCoE
bc3ce66322 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-22 12:52:26 +08:00
bggRGjQaUbCoE
17568c8c27 opt item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-22 00:06:32 +08:00
bggRGjQaUbCoE
a1555826c3 opt reply item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-21 22:07:58 +08:00
bggRGjQaUbCoE
07b7c42f13 opt setting
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-21 21:07:08 +08:00
bggRGjQaUbCoE
2d66b1d8ca opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-21 20:16:26 +08:00
bggRGjQaUbCoE
604d78ad6a opt data
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-21 18:22:41 +08:00
bggRGjQaUbCoE
5f3f158932 opt filter dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-20 23:43:42 +08:00
bggRGjQaUbCoE
345402d2fe remove topic rcmd btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-20 14:39:58 +08:00
bggRGjQaUbCoE
0bc0c36f14 feat: live dm block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-20 14:08:49 +08:00
bggRGjQaUbCoE
dcb893ed07 opt introctr
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-19 17:18:06 +08:00
bggRGjQaUbCoE
3bfb0db307 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-19 16:47:55 +08:00
bggRGjQaUbCoE
9b8d4a62fa opt item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-19 14:13:38 +08:00
bggRGjQaUbCoE
6f48a97b4b opt item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-19 13:31:12 +08:00
bggRGjQaUbCoE
5644e9a0e1 refa fav/group panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-19 11:07:52 +08:00
bggRGjQaUbCoE
f440edf43b feat: msg like detail
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-19 10:42:56 +08:00
bggRGjQaUbCoE
30a8b4d25c opt req
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 22:46:23 +08:00
bggRGjQaUbCoE
41245d5256 dyn pic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 22:46:23 +08:00
bggRGjQaUbCoE
89b1a63946 opt forwarded dyn pic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 21:13:07 +08:00
bggRGjQaUbCoE
448d7c38db show forwarded dyn pic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 20:50:38 +08:00
bggRGjQaUbCoE
cc4100d74f upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 19:48:27 +08:00
bggRGjQaUbCoE
768f3e20b1 opt buildTime
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 19:48:02 +08:00
bggRGjQaUbCoE
91a1b77d83 opt live room
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 19:48:02 +08:00
bggRGjQaUbCoE
9d9784f3c2 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 13:51:16 +08:00
bggRGjQaUbCoE
6c6c4cffd2 opt fav folder sort
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-18 13:08:28 +08:00
bggRGjQaUbCoE
cb167dae29 opt member home fav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-17 18:47:56 +08:00
bggRGjQaUbCoE
0bf9d13967 reply/dyn appeal
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-17 18:32:29 +08:00
bggRGjQaUbCoE
0963713fad opt member fav
fix parse duration

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-16 21:31:10 +08:00
bggRGjQaUbCoE
d69a996be4 remove unused code
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-16 14:19:02 +08:00
bggRGjQaUbCoE
fcdb04b728 opt msg item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-16 13:44:23 +08:00
bggRGjQaUbCoE
a2c24fb33c feat: match info
opt dateformat

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-16 12:51:32 +08:00
bggRGjQaUbCoE
25f4ed6636 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-15 18:46:11 +08:00
bggRGjQaUbCoE
a0bed68c79 remove unused widget
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-15 17:54:57 +08:00
bggRGjQaUbCoE
75c2cf70a0 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-14 12:53:27 +08:00
bggRGjQaUbCoE
f3b9749a85 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-14 11:06:13 +08:00
bggRGjQaUbCoE
c05fbde3fa opt item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-13 17:36:03 +08:00
bggRGjQaUbCoE
f824477ddb opt dyn panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-12 16:07:04 +08:00
bggRGjQaUbCoE
54fe38047f update dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-12 16:06:46 +08:00
bggRGjQaUbCoE
de9d16cc61 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-12 10:16:55 +08:00
bggRGjQaUbCoE
7c6f82891d fix space setting
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-11 19:14:14 +08:00
bggRGjQaUbCoE
4e710fca79 fix skip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-11 18:25:16 +08:00
bggRGjQaUbCoE
4f3f01d80a opt dm filter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-11 18:25:16 +08:00
bggRGjQaUbCoE
4a4cd3017f show space setting
opt switch anonymity

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-11 14:25:46 +08:00
bggRGjQaUbCoE
89a418c7c5 custom enable log
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-11 11:40:57 +08:00
bggRGjQaUbCoE
f4d3ec39a0 opt reply item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-11 11:40:57 +08:00
My-Responsitories
3655c31a48 feat: millisecond skip (#869)
* feat: millisecond skip

* fix: formatDuration

* fix: post segment
2025-06-11 01:39:26 +00:00
bggRGjQaUbCoE
bc2de4828b refa dm block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-10 17:46:18 +08:00
bggRGjQaUbCoE
206602e49a Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-10 15:50:48 +08:00
bggRGjQaUbCoE
f5d52237c5 remove invalid later view type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-10 15:45:02 +08:00
bggRGjQaUbCoE
88288f4a7a opt model
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-10 15:39:51 +08:00
bggRGjQaUbCoE
bdf3cfc750 opt follow tab
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-10 13:51:48 +08:00
bggRGjQaUbCoE
4c758bb1a3 opt account
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-10 13:19:58 +08:00
bggRGjQaUbCoE
5f77a8aa19 check selfdm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 23:12:30 +08:00
bggRGjQaUbCoE
9fbe824d6d view live list
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 22:19:08 +08:00
bggRGjQaUbCoE
d61706d4f3 set tooltipTheme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 21:37:35 +08:00
bggRGjQaUbCoE
208db62d93 fix msg secondary
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 19:51:06 +08:00
bggRGjQaUbCoE
10efd96788 feat: dyn topic rcmd
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 19:50:34 +08:00
bggRGjQaUbCoE
f1e4130201 feat: coin log
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 19:50:04 +08:00
bggRGjQaUbCoE
63a286056c opt color select
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 14:19:31 +08:00
bggRGjQaUbCoE
dc9b345e99 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-09 14:19:12 +08:00
bggRGjQaUbCoE
c67866a148 fix video height
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-08 23:42:36 +08:00
bggRGjQaUbCoE
d893102939 update live roomid
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-08 17:14:51 +08:00
bggRGjQaUbCoE
82b4f76b95 opt msg unread
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-08 15:38:26 +08:00
bggRGjQaUbCoE
a6ac2c4522 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-08 14:07:54 +08:00
bggRGjQaUbCoE
fdb817cadd opt video page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-08 12:38:27 +08:00
bggRGjQaUbCoE
c7dabba3b2 opt pgc review score
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 23:00:27 +08:00
bggRGjQaUbCoE
19e4ae6c04 feat: space setting
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 21:50:30 +08:00
bggRGjQaUbCoE
6ec0d8f589 opt vote option
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 18:32:36 +08:00
bggRGjQaUbCoE
f151e63923 opt article pics
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 18:16:19 +08:00
bggRGjQaUbCoE
f77f853fd1 opt post preview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 17:45:15 +08:00
bggRGjQaUbCoE
930afa4c60 later view episode
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 15:45:59 +08:00
bggRGjQaUbCoE
bffcfd1f90 check cid
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 15:37:41 +08:00
bggRGjQaUbCoE
e3c920dc87 opt view later
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 15:15:12 +08:00
bggRGjQaUbCoE
13f1392821 fix create note
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 14:25:46 +08:00
bggRGjQaUbCoE
7376fc788a opt live room
tweak

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 14:07:45 +08:00
bggRGjQaUbCoE
5c1312bbcd opt send colorful dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-07 11:45:41 +08:00
bggRGjQaUbCoE
db4283af4a refa: coin/like arc
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-06 21:34:59 +08:00
bggRGjQaUbCoE
77e418e4b7 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-06 20:55:38 +08:00
bggRGjQaUbCoE
ccde326e38 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-06 16:54:32 +08:00
bggRGjQaUbCoE
4a00b45c5c lint
opt pages

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-06 16:54:25 +08:00
bggRGjQaUbCoE
b149ee4998 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-06 12:47:26 +08:00
bggRGjQaUbCoE
707d2f4b07 opt pages
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-05 18:39:02 +08:00
bggRGjQaUbCoE
b960359a39 opt models
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-05 14:44:56 +08:00
bggRGjQaUbCoE
f50b1d2beb opt live room
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-04 14:51:09 +08:00
bggRGjQaUbCoE
50efe1e24c fix push dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-04 14:33:30 +08:00
bggRGjQaUbCoE
daf5d302e3 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-04 14:33:24 +08:00
bggRGjQaUbCoE
84e24b5827 opt msg top
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-04 12:23:33 +08:00
bggRGjQaUbCoE
19cf085e3e feat: upower rank page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-02 13:43:21 +08:00
bggRGjQaUbCoE
459d7cb9f1 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-02 12:10:39 +08:00
bggRGjQaUbCoE
e56e216c59 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-02 11:50:01 +08:00
bggRGjQaUbCoE
08c9ebc42e fix ios applinks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 15:59:56 +08:00
bggRGjQaUbCoE
924fb4bf81 fix applinks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 15:30:26 +08:00
bggRGjQaUbCoE
f60c0b9a10 opt dyn topic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 15:02:38 +08:00
bggRGjQaUbCoE
7c0d161b9a fix live room
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 14:27:35 +08:00
bggRGjQaUbCoE
0a8b632200 fix route
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 12:29:31 +08:00
bggRGjQaUbCoE
401f5268a6 fix appscheme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 11:49:10 +08:00
bggRGjQaUbCoE
d508e0822e fix get video param
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-06-01 10:48:03 +08:00
bggRGjQaUbCoE
6147df2030 opt dyn badge
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-31 22:11:44 +08:00
bggRGjQaUbCoE
b990f9cf87 Revert "opt: account (#846)"
This reverts commit ab57aee8c1.
2025-05-31 21:23:46 +08:00
bggRGjQaUbCoE
0fb01f1b7c Revert "opt: buvid3"
This reverts commit 17ea416c98.
2025-05-31 21:23:46 +08:00
bggRGjQaUbCoE
91fe0492c1 opt msg text
opt noti

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-31 16:46:55 +08:00
bggRGjQaUbCoE
19bbdaac65 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-31 15:38:08 +08:00
bggRGjQaUbCoE
1462e6ecf1 opt view fav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-31 15:10:54 +08:00
bggRGjQaUbCoE
3364b52e33 opt color select
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-31 14:08:55 +08:00
bggRGjQaUbCoE
4ac05caa28 opt pages
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-31 11:39:08 +08:00
bggRGjQaUbCoE
132a7e15de bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-30 23:42:30 +08:00
bggRGjQaUbCoE
c2c8a5166b fix check reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-30 23:28:04 +08:00
bggRGjQaUbCoE
a260b1640a remove boolext
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-30 22:54:46 +08:00
bggRGjQaUbCoE
3031d5e3b0 opt nav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-30 14:40:13 +08:00
bggRGjQaUbCoE
5f2e863cc2 opt pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-30 14:09:12 +08:00
bggRGjQaUbCoE
9a63e23478 opt handle data
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-30 11:54:47 +08:00
bggRGjQaUbCoE
c9450992d9 opt episode panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 20:33:03 +08:00
bggRGjQaUbCoE
8aeb035e55 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 20:13:48 +08:00
bggRGjQaUbCoE
924d51d41b opt handle res
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 17:17:42 +08:00
bggRGjQaUbCoE
b643cb1bd0 opt create dyn menubtn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 10:21:20 +08:00
bggRGjQaUbCoE
1f77ee178e opt live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 10:21:07 +08:00
bggRGjQaUbCoE
6d599891dc lint
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 10:19:04 +08:00
bggRGjQaUbCoE
4e9fdfbfbd fix reply hint
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-29 10:18:10 +08:00
bggRGjQaUbCoE
d4ac9ab79a opt subtitle
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-28 16:34:58 +08:00
bggRGjQaUbCoE
ad4fba4f44 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-28 14:24:00 +08:00
bggRGjQaUbCoE
6092bab75c tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-28 14:17:21 +08:00
bggRGjQaUbCoE
365d9e1223 add android launcher
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-27 16:27:28 +08:00
bggRGjQaUbCoE
9c3b2717ac opt add reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-27 14:15:33 +08:00
bggRGjQaUbCoE
8b6320730c opt del reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-27 14:13:14 +08:00
bggRGjQaUbCoE
c34eeba859 fix get live dm token
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-27 11:39:24 +08:00
bggRGjQaUbCoE
d6914c42b3 opt create dyn panel drag
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-27 11:11:57 +08:00
bggRGjQaUbCoE
39778247f6 opt dyn repost
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 22:34:52 +08:00
bggRGjQaUbCoE
1d91b183fd opt custom widget
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 22:01:53 +08:00
bggRGjQaUbCoE
b2a4875ba7 opt topic scroll
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 21:35:17 +08:00
bggRGjQaUbCoE
077b31e4c9 opt topic scroll
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 21:22:48 +08:00
bggRGjQaUbCoE
dbcc19cac1 opt search topic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 21:11:07 +08:00
bggRGjQaUbCoE
83de915e54 fix topic initialScrollOffset
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 18:44:53 +08:00
bggRGjQaUbCoE
8ce33736a0 opt select topic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 18:41:42 +08:00
bggRGjQaUbCoE
3edac65ae8 opt pub panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-26 17:15:21 +08:00
My-Responsitories
db3b74e33f Revert "feat: cross row select (#867)" (#868)
This reverts commit 89a077be5c.
2025-05-25 13:02:44 +00:00
My-Responsitories
89a077be5c feat: cross row select (#867) 2025-05-25 11:59:56 +00:00
bggRGjQaUbCoE
76a5b6221d opt msg
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-25 16:16:17 +08:00
bggRGjQaUbCoE
18f8831b7e count format
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-25 15:54:08 +08:00
bggRGjQaUbCoE
b674d102e3 pgc review sort
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-25 15:38:22 +08:00
bggRGjQaUbCoE
86e52eec4c opt slider
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-25 15:38:22 +08:00
bggRGjQaUbCoE
fd55383778 opt handle res
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-25 11:45:20 +08:00
My-Responsitories
f29385ccef mod: isRedirect (#866) 2025-05-24 16:19:36 +00:00
bggRGjQaUbCoE
3993ff8a8e feat: slide dismiss tabbarview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 22:30:53 +08:00
bggRGjQaUbCoE
a130b5db98 opt pgc review
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 20:14:16 +08:00
bggRGjQaUbCoE
2d22501d08 opt pgc review input
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 18:51:36 +08:00
bggRGjQaUbCoE
b478427522 opt live count
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 18:43:19 +08:00
bggRGjQaUbCoE
70164fa3f7 feat: pgc review
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 18:39:04 +08:00
bggRGjQaUbCoE
8e1b2be073 opt repost dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 14:14:36 +08:00
bggRGjQaUbCoE
b6b67884f4 opt dyn auth
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 13:23:00 +08:00
bggRGjQaUbCoE
fe97a485c7 opt dyn panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 12:42:54 +08:00
bggRGjQaUbCoE
86c64fdd05 opt dyn panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 12:24:04 +08:00
bggRGjQaUbCoE
da56c66168 opt msg item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 11:10:57 +08:00
bggRGjQaUbCoE
5bd6b38908 opt img preview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 11:10:57 +08:00
bggRGjQaUbCoE
81cfe3efe1 opt anim to top
opt refresh

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-24 11:09:48 +08:00
bggRGjQaUbCoE
0a9897f6a4 opt pub img panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-23 18:09:55 +08:00
bggRGjQaUbCoE
0b495f100f upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-23 17:57:35 +08:00
bggRGjQaUbCoE
70b55e5fdd opt article page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-23 15:30:07 +08:00
bggRGjQaUbCoE
9c2f3d3f86 opt slider color picker
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-23 13:53:17 +08:00
bggRGjQaUbCoE
5452b3de4f opt slider color picker
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-23 12:31:50 +08:00
bggRGjQaUbCoE
b1666095a6 enable new slider/progress
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-23 12:00:14 +08:00
bggRGjQaUbCoE
7fa6d81dc8 opt search trending header
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-22 21:07:25 +08:00
bggRGjQaUbCoE
04a10e62d6 change msg badge pos
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-22 20:31:40 +08:00
bggRGjQaUbCoE
ecce23589a dyn sub card
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-22 20:21:27 +08:00
bggRGjQaUbCoE
b6aa6aebb9 dyn addcard
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-22 17:01:15 +08:00
bggRGjQaUbCoE
4bd4178cbf revert navbar temply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-22 11:47:19 +08:00
bggRGjQaUbCoE
04a157c64a bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-21 16:55:50 +08:00
bggRGjQaUbCoE
ac60ac417b tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-21 16:55:44 +08:00
My-Responsitories
1efd62803a opt: replace SizedBox with spacing (#863) 2025-05-20 18:16:01 +00:00
My-Responsitories
218e829fd4 opt: bar set (#862)
* opt: bar set

* opt: navbar

* fix: type
2025-05-20 18:14:08 +00:00
My-Responsitories
acb3784071 opt: up panel (#861) 2025-05-20 18:11:31 +00:00
bggRGjQaUbCoE
f87957b170 opt dyn author panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 23:36:35 +08:00
bggRGjQaUbCoE
043310ca00 opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 23:25:27 +08:00
bggRGjQaUbCoE
43d71bb368 opt dyn detail
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 22:44:40 +08:00
bggRGjQaUbCoE
12eb430d8c deprecate replytype
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 21:54:22 +08:00
bggRGjQaUbCoE
cfb42075dc fix push dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 21:31:43 +08:00
bggRGjQaUbCoE
9b5457ffc0 opt icon
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 17:15:45 +08:00
bggRGjQaUbCoE
3099bd6ca1 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 16:33:08 +08:00
bggRGjQaUbCoE
ea32f705f5 fix check reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 13:20:02 +08:00
bggRGjQaUbCoE
66b7d27dc4 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-20 12:47:22 +08:00
bggRGjQaUbCoE
05b512e8cc opt icon
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 22:24:48 +08:00
bggRGjQaUbCoE
a2da381f1a opt icon
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 20:42:20 +08:00
bggRGjQaUbCoE
e4654d63c3 fix vote
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 20:40:32 +08:00
bggRGjQaUbCoE
38b1af2696 opt member video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 15:38:56 +08:00
bggRGjQaUbCoE
81c6abb879 opt member video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 15:09:25 +08:00
bggRGjQaUbCoE
d4ad738888 opt live area
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 14:37:10 +08:00
bggRGjQaUbCoE
a62670eecf opt follow btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 14:23:55 +08:00
bggRGjQaUbCoE
25adc4face opt opus
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 13:44:59 +08:00
bggRGjQaUbCoE
8fd62cf2f3 opt opus rich text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 11:59:37 +08:00
My-Responsitories
a360212dc7 feat: filter dyn (#860) 2025-05-19 01:31:41 +00:00
bggRGjQaUbCoE
d7dec1bc4d opt slide dismiss
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-19 00:06:26 +08:00
bggRGjQaUbCoE
8be86a2d95 opt slide dismiss
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 23:39:08 +08:00
bggRGjQaUbCoE
34949b8a7f opt to top
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 22:53:33 +08:00
bggRGjQaUbCoE
40502e3bff opt dyn topic
opt member opus

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 21:54:38 +08:00
bggRGjQaUbCoE
0de2603e30 opt search panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 17:29:36 +08:00
bggRGjQaUbCoE
e330359192 opt member search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 17:14:33 +08:00
bggRGjQaUbCoE
ab80b2a5af opt list
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 17:01:55 +08:00
bggRGjQaUbCoE
f642bfcf48 opt get settings
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 15:15:23 +08:00
bggRGjQaUbCoE
805a63cf59 opt get horizontalScreen
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 15:01:49 +08:00
bggRGjQaUbCoE
4d430ba42c opt get pgc follow status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 14:21:19 +08:00
bggRGjQaUbCoE
5f734758b4 opt playlist jump
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 13:32:58 +08:00
bggRGjQaUbCoE
8157dbc530 fix member opus jump
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 12:52:20 +08:00
bggRGjQaUbCoE
391d862b17 opt member info
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 12:28:07 +08:00
bggRGjQaUbCoE
271856ca89 opt member info
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 11:19:47 +08:00
bggRGjQaUbCoE
d7eb734aaf feat: fav topic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-18 10:46:05 +08:00
bggRGjQaUbCoE
1d4eabb770 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-17 19:01:46 +08:00
bggRGjQaUbCoE
906c21e252 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-17 17:26:01 +08:00
dom
7ae92970ef bump flutter (#859)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-16 23:05:02 +08:00
My-Responsitories
cf0bf1e587 opt: vote (#858) 2025-05-16 13:44:14 +00:00
bggRGjQaUbCoE
616c129ffd opt top up panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-16 18:16:47 +08:00
bggRGjQaUbCoE
1326cc4966 opt rank type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-16 18:16:32 +08:00
bggRGjQaUbCoE
35bc4a6ece top up panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-15 18:48:31 +08:00
bggRGjQaUbCoE
e54a0f127f auto fill
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-15 18:16:26 +08:00
bggRGjQaUbCoE
070ecad54b dyn addition jump
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-15 18:05:30 +08:00
bggRGjQaUbCoE
205ae2bf55 reserve btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-15 13:24:43 +08:00
bggRGjQaUbCoE
d35c85f389 dyn detail
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-15 13:15:17 +08:00
bggRGjQaUbCoE
026e40855c auto fill
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-14 21:12:33 +08:00
bggRGjQaUbCoE
553be52260 reserve btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-14 17:58:43 +08:00
bggRGjQaUbCoE
69f9fb398f emoji setting
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-14 17:46:23 +08:00
bggRGjQaUbCoE
98985a7fa4 episode badge
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-14 17:46:15 +08:00
bggRGjQaUbCoE
3f71e79809 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 19:01:28 +08:00
bggRGjQaUbCoE
55138957b7 fix: zanbtn state
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 18:58:31 +08:00
bggRGjQaUbCoE
901e8d9cb8 fix: msg type 16
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 18:45:13 +08:00
bggRGjQaUbCoE
f140fc53ad opt: pgc url
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 18:30:30 +08:00
bggRGjQaUbCoE
9b8b699ace fix: whisper mid
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 18:16:51 +08:00
bggRGjQaUbCoE
39a355ab4c fix: multi vote
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 18:08:11 +08:00
bggRGjQaUbCoE
22f9285627 fix: #848
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 15:47:29 +08:00
bggRGjQaUbCoE
152eaf2627 feat: dyn reserve
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 14:57:27 +08:00
bggRGjQaUbCoE
d15b8091bc opt: readlist url, note item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 12:15:23 +08:00
bggRGjQaUbCoE
de9eb2292e mod: member video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-13 00:11:10 +08:00
徽忆.
9b86e24513 feat: decoration color (#856) 2025-05-13 00:03:35 +08:00
bggRGjQaUbCoE
9a97a5d110 feat: msg link setting
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-12 18:08:39 +08:00
bggRGjQaUbCoE
964668c982 feat: setMsgDnd
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-12 14:39:14 +08:00
bggRGjQaUbCoE
0514c0d999 opt: livelist
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-12 12:21:13 +08:00
bggRGjQaUbCoE
4a782332d3 mod: err string
fix: typo

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 23:20:29 +08:00
My-Responsitories
72734d4b4e opt: unread & zan grpc & readlist open with browser (#852)
* opt: unread

* opt: zan grpc

* feat: readlist open with browser
2025-05-11 10:58:00 +00:00
My-Responsitories
8d34e6f340 opt: model (#851)
* opt: readlist model

* opt: video item model
2025-05-11 09:00:24 +00:00
My-Responsitories
c899ea95e1 opt: reply type (#850) 2025-05-11 08:38:15 +00:00
bggRGjQaUbCoE
0b57cd3555 opt: delete reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 14:54:31 +08:00
bggRGjQaUbCoE
f9b4f587c2 opt: reply footer
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 14:48:47 +08:00
bggRGjQaUbCoE
279f586a90 opt: pgc coin
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 14:42:23 +08:00
bggRGjQaUbCoE
2f3f712256 refa: pgc intro
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 14:34:45 +08:00
bggRGjQaUbCoE
6748a20ddb fix: update filter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 14:12:39 +08:00
bggRGjQaUbCoE
90ccb86a6f opt: space tab
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 13:38:58 +08:00
bggRGjQaUbCoE
574bf861f0 opt: common ctr
opt: state

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-11 12:22:47 +08:00
My-Responsitories
5bff1747e6 opt: IdUtils (#849) 2025-05-11 04:11:10 +00:00
My-Responsitories
17ea416c98 opt: buvid3 2025-05-11 00:29:03 +08:00
My-Responsitories
ab57aee8c1 opt: account (#846) 2025-05-10 16:19:19 +00:00
bggRGjQaUbCoE
8c80fc3578 fix: #844
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 23:47:37 +08:00
bggRGjQaUbCoE
85ab250551 opt: msg item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 17:57:26 +08:00
bggRGjQaUbCoE
3f3a1a6d7f opt: msg item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 17:49:50 +08:00
bggRGjQaUbCoE
68fe3bbd4b revert: dyn font size
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 16:57:31 +08:00
bggRGjQaUbCoE
a8054be82e mod: handle readlist url
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 16:12:18 +08:00
bggRGjQaUbCoE
3b6fd8019b opt: article list page
opt: fav/sub detail

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 15:54:43 +08:00
bggRGjQaUbCoE
91af974bd4 feat: article list
Closes #841

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 15:12:13 +08:00
bggRGjQaUbCoE
024a249e6b refa: whisper detail
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 15:08:19 +08:00
My-Responsitories
024e74115e opt: type & grpc message (#842)
* opt: grpc type

* opt: grpc message

* opt: http type
2025-05-10 04:40:27 +00:00
bggRGjQaUbCoE
7b4f08bb05 opt: msg btn action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 00:53:03 +08:00
bggRGjQaUbCoE
f75036cb8e opt: msg ctr
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 00:40:41 +08:00
bggRGjQaUbCoE
0510fbb65a opt: msg btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-10 00:28:09 +08:00
bggRGjQaUbCoE
9e4bc24365 fix: msg secondary type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 23:29:41 +08:00
bggRGjQaUbCoE
0f41d5b2f8 feat: im settings
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 22:17:31 +08:00
bggRGjQaUbCoE
a282baf5a2 feat: session secondary
Closes #837

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 21:55:34 +08:00
bggRGjQaUbCoE
dea29054e6 opt: member opus
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 14:36:07 +08:00
bggRGjQaUbCoE
efaff0ae79 opt: replyReplyPanel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 13:24:14 +08:00
bggRGjQaUbCoE
2d75d89825 feat: space opus
Closes #833

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 12:32:09 +08:00
bggRGjQaUbCoE
bcd0d63db7 opt: dyn panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-09 00:28:35 +08:00
bggRGjQaUbCoE
26f921b7e4 fix: vote
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-08 15:29:47 +08:00
bggRGjQaUbCoE
4d1a9517e1 opt: dyn block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-08 14:48:21 +08:00
bggRGjQaUbCoE
222070feba fix: dyn: temp ban
Closes #829

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-08 12:01:38 +08:00
bggRGjQaUbCoE
b28882cff5 opt: dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 22:55:29 +08:00
bggRGjQaUbCoE
fb22e5ab66 opt: live area
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 21:06:35 +08:00
bggRGjQaUbCoE
11a0f2faca feat: dyn topic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 18:09:14 +08:00
bggRGjQaUbCoE
dd6ff101d1 opt: func
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 15:19:27 +08:00
bggRGjQaUbCoE
286193f08f opt: func
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 14:32:07 +08:00
bggRGjQaUbCoE
6353ecc13e feat: pm: clear unread
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 12:16:41 +08:00
bggRGjQaUbCoE
767e93615c mod: msg item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 11:59:57 +08:00
bggRGjQaUbCoE
76998e7761 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 00:27:14 +08:00
bggRGjQaUbCoE
df205f2b9d mod: remove refresh fav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-07 00:25:17 +08:00
bggRGjQaUbCoE
3e63875659 mod: try-catch get dyn ctr
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 22:49:46 +08:00
bggRGjQaUbCoE
fcb7330970 mod: update whisper badge
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 22:44:34 +08:00
bggRGjQaUbCoE
b19c718a2a refa: whisper page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 22:31:04 +08:00
bggRGjQaUbCoE
661e7bfa78 feat: live search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 20:34:07 +08:00
bggRGjQaUbCoE
867efecc54 refa: member search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 20:31:20 +08:00
bggRGjQaUbCoE
bd31ab5d07 feat: live area page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 16:58:30 +08:00
My-Responsitories
bd1ffb0f24 fix: dynamics pendant 2025-05-06 12:34:04 +08:00
bggRGjQaUbCoE
a8fa4d72f3 feat: msg: set notice
Closes #821

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-06 00:27:08 +08:00
My-Responsitories
2d1697064d fix: card vip (#825) 2025-05-05 16:20:12 +00:00
My-Responsitories
a915650bb6 opt: enum (#824)
* opt: enum

* opt: member page type
2025-05-05 16:18:30 +00:00
bggRGjQaUbCoE
1da30d5d8f fix: reply cast
Closes #822

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 22:33:29 +08:00
bggRGjQaUbCoE
a2f72ee3f3 feat: live area
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 22:15:55 +08:00
bggRGjQaUbCoE
2e4c24393d mod: article: show top
Closes #819

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 20:16:45 +08:00
bggRGjQaUbCoE
e7b229a60f mod: refresh live data
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 18:02:39 +08:00
bggRGjQaUbCoE
562f9035e8 refa: live page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 17:50:02 +08:00
bggRGjQaUbCoE
7689fe8aa4 chore: rename tabsConfig
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 15:51:17 +08:00
bggRGjQaUbCoE
ceca78368d mod: update video tags api
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 15:36:32 +08:00
bggRGjQaUbCoE
3fa6d9820f fix: #817
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 15:25:12 +08:00
bggRGjQaUbCoE
2f4c739f0b opt: enum
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 15:13:17 +08:00
bggRGjQaUbCoE
4e68c765c5 opt: vote panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 14:00:39 +08:00
My-Responsitories
0dfc4e15bd refix: #779 (#816)
* Revert "fix: #779"

This reverts commit ddf7d82656.

* refix #779
2025-05-05 04:36:06 +00:00
dom
e8147680e6 Update 功能请求.yml 2025-05-05 12:07:45 +08:00
dom
2b3d326c41 Update bug-反馈.yml 2025-05-05 12:07:01 +08:00
bggRGjQaUbCoE
6414b377da revert: mainlist req
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 11:49:57 +08:00
bggRGjQaUbCoE
ea80d9a39c mod: update block page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 01:18:59 +08:00
bggRGjQaUbCoE
ef671f6503 fix: update grpc headers
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 01:06:53 +08:00
bggRGjQaUbCoE
cfc66e4364 fix: share selectedindex
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 01:06:53 +08:00
bggRGjQaUbCoE
1477a9058a mod: reply: remove unused val
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-05 01:06:53 +08:00
My-Responsitories
cdeb843a84 opt: avatar model (#814) 2025-05-04 16:45:24 +00:00
My-Responsitories
07d2b3b464 opt: merge danmaku in loop (#813) 2025-05-04 16:38:05 +00:00
bggRGjQaUbCoE
a49caa871d mod: update proto
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-04 23:42:08 +08:00
bggRGjQaUbCoE
fb004a0bb9 fix: get subtitles
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-04 23:40:22 +08:00
My-Responsitories
6f69a45195 opt: use cascade (#812) 2025-05-04 15:08:06 +00:00
bggRGjQaUbCoE
877732e1e7 chore: organize imports
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-04 16:27:52 +08:00
bggRGjQaUbCoE
caa58e9d7d mod: lint
mod: tweaks

opt: publish page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-04 14:56:56 +08:00
My-Responsitories
2cfad80214 feat: vote pabel (#807) 2025-05-04 05:53:00 +00:00
bggRGjQaUbCoE
9b3c3efb09 chore: rename
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 15:51:56 +08:00
bggRGjQaUbCoE
c491b5283b refa: dir
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 15:39:54 +08:00
bggRGjQaUbCoE
7f70ee5045 refa: dir
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 15:26:06 +08:00
bggRGjQaUbCoE
57fa8b4f3e opt: video title
Closes #799

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 13:38:32 +08:00
bggRGjQaUbCoE
974a74a3c7 mod: opus
Closes #802

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 13:07:49 +08:00
bggRGjQaUbCoE
478b71d6b3 mod: check istablet
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 12:49:15 +08:00
bggRGjQaUbCoE
5940c4f032 opt: get blockserver
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 12:49:15 +08:00
bggRGjQaUbCoE
9e50a195a4 opt: search settings
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-05-03 12:49:09 +08:00
My-Responsitories
b7b3460248 mod: scheme (#804) 2025-05-03 01:56:21 +00:00
徽忆.
36bf6f4ceb opt: webdav classification (#794)
* 优化设置备份[#739](https://github.com/bggRGjQaUbCoE/PiliPlus/issues/739)
2025-05-02 10:13:07 +08:00
My-Responsitories
56491591ab fix: three point (#792) 2025-05-01 15:16:47 +00:00
My-Responsitories
0b05edd6ff mod: quote color (#789) 2025-05-01 03:46:42 +00:00
My-Responsitories
c090cae1a1 opt: post redirect (#788)
* opt: cookie

* opt: post redirect
2025-05-01 02:08:48 +00:00
My-Responsitories
a46bde68f5 opt: parseRedirect use head (#787) 2025-05-01 02:04:38 +00:00
bggRGjQaUbCoE
ddf7d82656 fix: #779
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-30 18:52:25 +08:00
1614 changed files with 278269 additions and 157792 deletions

View File

@@ -9,15 +9,7 @@ body:
attributes:
label: 检查清单
options:
- label: 之前没有人提交过类似或相同的 bug report。1
required: true
- label: 之前没有人提交过类似或相同的 bug report。2
required: true
- label: 之前没有人提交过类似或相同的 bug report。3
required: true
- label: 之前没有人提交过类似或相同的 bug report。4
required: true
- label: 之前没有人提交过类似或相同的 bug report。5
- label: 之前没有人提交过类似或相同的 bug report。
required: true
- label: 正在使用最新版本。
required: true
@@ -29,14 +21,6 @@ body:
validations:
required: true
- type: textarea
id: bug
attributes:
label: 问题描述
description: 请提供一个清晰而简明的问题描述。
validations:
required: true
- type: textarea
id: steps
attributes:
@@ -53,6 +37,14 @@ body:
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为
description: 请描述实际的行为或结果。
validations:
required: true
- type: textarea
id: log
attributes:

View File

@@ -9,15 +9,7 @@ body:
attributes:
label: 检查清单
options:
- label: 之前没有人提交过类似或相同的功能请求。1
required: true
- label: 之前没有人提交过类似或相同的功能请求。2
required: true
- label: 之前没有人提交过类似或相同的功能请求。3
required: true
- label: 之前没有人提交过类似或相同的功能请求。4
required: true
- label: 之前没有人提交过类似或相同的功能请求。5
- label: 之前没有人提交过类似或相同的功能请求。
required: true
- label: 正在使用最新版本。
required: true
@@ -30,14 +22,6 @@ body:
validations:
required: true
- type: textarea
id: propose
attributes:
label: 目标
description: 请描述你希望通过这个功能实现的目标。
validations:
required: true
- type: textarea
id: solution
attributes:

View File

@@ -1,6 +1,14 @@
name: Android Release
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
@@ -33,19 +41,6 @@ jobs:
channel: stable
flutter-version-file: pubspec.yaml
- name: 修复3.24的stable显示中文不正确问题 // from orz12
run: |
version=$(grep -m 1 'flutter:' pubspec.yaml | awk '{print $2}')
if [ "$(echo "$version < 3.27.0" | awk '{print ($1 < $2)}')" -eq 1 ]; then
cd $FLUTTER_ROOT
git config --global user.name "orz12"
git config --global user.email "orz12@test.com"
git cherry-pick d4124bd --strategy-option theirs
# flutter precache
flutter --version
cd -
fi
- name: 下载项目依赖
run: flutter pub get
@@ -55,6 +50,7 @@ jobs:
sed -i "s/version: .*/version: $version_name-$(git rev-parse --short HEAD)+$(git rev-list --count HEAD)/g" pubspec.yaml
- 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
@@ -89,4 +85,4 @@ jobs:
with:
name: app-x86_64
path: |
build/app/outputs/flutter-apk/app-x86_64-release.apk
build/app/outputs/flutter-apk/app-x86_64-release.apk

View File

@@ -1,6 +1,14 @@
name: Build for iOS
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths-ignore:
- '**.md'
workflow_dispatch:
inputs:
branch:
@@ -10,7 +18,7 @@ on:
jobs:
build-macos-app:
name: Release IOS
runs-on: macos-latest
runs-on: macos-14
steps:
- name: Checkout code
uses: actions/checkout@v4

4
.gitignore vendored
View File

@@ -135,4 +135,6 @@ app.*.symbols
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json
/lib/build_config.dart
/lib/build_config.dart
devtools_options.yaml

View File

@@ -2,5 +2,9 @@
"editor.formatOnSave": true,
"[dart]": {
"editor.formatOnType": true
},
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
// "source.fixAll": "explicit",
}
}

View File

@@ -47,12 +47,21 @@
## feat
- [x] 分享视频至消息
- [x] 播放课堂视频
- [x] 发起投票
- [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户`
- [x] 修改消息设置
- [x] 修改聊天设置
- [x] 展示折叠消息
- [x] 查看用户图文
- [x] 动态话题
- [x] 直播分区
- [x] 分享`视频`/`番剧`/`动态`/`专栏`/`直播`至消息
- [x] 创建/修改/删除关注分组
- [x] 移除粉丝
- [x] 直播弹幕发送表情
- [x] 收藏夹排序
- [x] 稍后再看`未看`/`未看完`/`已看完`分类
- [x] 稍后再看 ~~`未看`~~ / `未看完` / ~~`已看完`~~ 分类
- [x] WebDAV 备份/恢复设置
- [x] 保存评论/动态
- [x] 高级弹幕 by [@My-Responsitories](https://github.com/My-Responsitories)

View File

@@ -9,6 +9,14 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/grpc/bilibili/**
- lib/grpc/google/**
formatter:
trailing_commas: preserve
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
@@ -21,9 +29,39 @@ linter:
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
# https://dart.dev/tools/linter-rules
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# - always_specify_types
# - avoid_positional_boolean_parameters
- always_declare_return_types
- always_use_package_imports
- avoid_empty_else
- avoid_field_initializers_in_const_classes
- avoid_print
- avoid_relative_lib_imports
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_slow_async_io
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_unnecessary_containers
- avoid_void_async
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- cascade_invocations
- prefer_const_constructors
- prefer_const_declarations
- sized_box_for_whitespace
- unnecessary_late
- use_colored_box
- use_decorated_box
- use_named_constants
- use_null_aware_elements
- unnecessary_lambdas
- use_is_even_rather_than_modulo
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

2
android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/.cxx
/build

View File

@@ -1,106 +0,0 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def keystorePropertiesFile = rootProject.file('key.properties')
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
def _filePath = System.getenv("KEYSTORE") ?: keystoreProperties["storeFile"]
def _storeFile = _filePath != null ? file(_filePath) : null
def _storePassword = System.getenv("KEYSTORE_PASSWORD") ?: keystoreProperties["storePassword"]
def _keyAlias = System.getenv("KEY_ALIAS") ?: keystoreProperties["keyAlias"]
def _keyPassword = System.getenv("KEY_PASSWORD") ?: keystoreProperties["keyPassword"]
android {
compileSdkVersion flutter.compileSdkVersion
namespace 'com.example.piliplus'
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.piliplus"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
minSdkVersion flutter.minSdkVersion
multiDexEnabled true
}
signingConfigs {
// 添加签名配置
if(_storeFile != null) {
release {
// 配置密钥库文件的位置、别名、密码等信息
storeFile _storeFile
storePassword _storePassword
keyAlias _keyAlias
keyPassword _keyPassword
v1SigningEnabled true
v2SigningEnabled true
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig _storeFile != null ? signingConfigs.release : signingConfigs.debug
}
debug {
applicationIdSuffix ".debug"
}
}
project.android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.versionCodeOverride = variant.versionCode
}
}
}
flutter {
source '../..'
}
dependencies {
}

View File

@@ -0,0 +1,78 @@
import com.android.build.gradle.internal.api.ApkVariantOutputImpl
import org.jetbrains.kotlin.konan.properties.Properties
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.piliplus"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.example.piliplus"
// minSdk = flutter.minSdkVersion
minSdkVersion(23)
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
packagingOptions.jniLibs.useLegacyPackaging = true
val keyProperties = Properties().also {
val properties = rootProject.file("key.properties")
if (properties.exists())
it.load(properties.inputStream())
}
val config = keyProperties.getProperty("storeFile")?.let {
signingConfigs.create("release") {
storeFile = file(it)
storePassword = keyProperties.getProperty("storePassword")
keyAlias = keyProperties.getProperty("keyAlias")
keyPassword = keyProperties.getProperty("keyPassword")
enableV1Signing = true
enableV2Signing = true
}
}
buildTypes {
all {
signingConfig = config ?: signingConfigs["debug"]
}
// release {
// proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro"
// )
// }
debug {
applicationIdSuffix = ".debug"
}
}
applicationVariants.all {
val variant = this
variant.outputs.forEach { output ->
(output as ApkVariantOutputImpl).versionCodeOverride = flutter.versionCode
}
}
}
flutter {
source = "../.."
}

1
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1 @@
-keep class com.yalantis.ucrop.util.RectUtils { *; }

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">PiliPlus debug</string>
</resources>

View File

@@ -36,14 +36,17 @@
</queries>
<application
android:label="PiliPlus"
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
xmlns:tools="http://schemas.android.com/tools"
android:enableOnBackInvokedCallback="true"
android:enableOnBackInvokedCallback="false"
android:allowBackup="false"
android:fullBackupContent="false"
tools:replace="android:allowBackup">
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity
android:name=".MainActivity"
android:exported="true"
@@ -55,6 +58,9 @@
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
>
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@@ -103,11 +109,13 @@
<data android:host="uper" />
<data android:host="article"
android:pathPattern="/readlist" />
<data android:host="opus" />
<data android:host="advertise" android:path="/home" />
<data android:host="clip" />
<data android:host="search" />
<data android:host="search" android:pathPattern=".*" />
<data android:host="stardust-search" />
<data android:host="music" />
<data android:host="cheese" />
<data android:host="bangumi"
android:pathPattern="/season.*" />
<data android:host="bangumi" android:pathPattern="/.*" />
@@ -139,7 +147,6 @@
<data android:host="video" />
<data android:host="story" />
<data android:host="podcast" />
<data android:host="search" />
<data android:host="main" android:path="/favorite" />
<data android:host="pgc" android:path="/theater/match" />
<data android:host="pgc" android:path="/theater/square" />
@@ -154,7 +161,6 @@
<data android:host="history" />
<data android:host="charge" android:path="/rank" />
<data android:host="assistant" />
<data android:host="assistant" />
<data android:host="feedback" />
<data android:host="auth" android:path="/launch" />
</intent-filter>
@@ -172,7 +178,7 @@
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
android:theme="@style/Ucrop.CropTheme"/>
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:fitsSystemWindows">true</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">PiliPlus</string>
</resources>

View File

@@ -21,4 +21,6 @@
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
<style name="Ucrop.CropTheme" parent="Theme.AppCompat.Light.NoActionBar"/>
</resources>

View File

@@ -1,60 +0,0 @@
allprojects {
repositories {
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/central" }
maven { url "https://maven.aliyun.com/repository/jcenter" }
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "http://download.flutter.io"
allowInsecureProtocol = true
}
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
rootProject.buildDir = '../build'
subprojects {
afterEvaluate { project ->
if (project.extensions.findByName("android") != null) {
Integer pluginCompileSdk = project.android.compileSdk
if (pluginCompileSdk != null) {
if (pluginCompileSdk < 31) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 31 (to work around https://issuetracker.google.com/issues/199180389)."
+ "\nIf there is not a new version of " + project.name + ", consider filing an issue against "
+ project.name
+ " to increase their compileSdk to the latest (otherwise try updating to the latest version)."
)
project.android {
compileSdk 31
}
}
if (pluginCompileSdk > 34) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 34"
)
project.android {
compileSdk 34
}
}
}
}
}
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

67
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,67 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
afterEvaluate {
if (project.extensions.findByName("android") != null) {
val androidExtension =
project.extensions.getByName("android") as com.android.build.gradle.BaseExtension
if (androidExtension.namespace == null) {
androidExtension.namespace = project.group.toString()
}
androidExtension.compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
project.tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
val pluginCompileSdkStr = androidExtension.compileSdkVersion
val pluginCompileSdk = pluginCompileSdkStr
?.removePrefix("android-")
?.toIntOrNull()
if (pluginCompileSdk != null && pluginCompileSdk < 31) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: ${project.name} " +
"from $pluginCompileSdk to 31 (to work around https://issuetracker.google.com/issues/199180389).\n" +
"If there is not a new version of ${project.name}, consider filing an issue against ${project.name} " +
"to increase their compileSdk to the latest (otherwise try updating to the latest version)."
)
androidExtension.setCompileSdkVersion(31)
}
}
project.buildDir = File(rootProject.buildDir, project.name)
}
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@@ -1,32 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/central" }
maven { url "https://maven.aliyun.com/repository/jcenter" }
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "http://download.flutter.io"
allowInsecureProtocol = true
}
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.2.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
include ":app"

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/images/loading.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FlutterDeepLinkingEnabled</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/http/constants.dart';
import 'package:flutter/material.dart';
class StyleString {
@@ -6,6 +7,10 @@ class StyleString {
static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
static const Radius imgRadius = Radius.circular(10);
static const double aspectRatio = 16 / 10;
static const bottomSheetRadius = BorderRadius.only(
topLeft: Radius.circular(18),
topRight: Radius.circular(18),
);
}
class Constants {
@@ -21,253 +26,270 @@ class Constants {
static const String traceId =
'11111111111111111111111111111111:1111111111111111:0:0';
static const String userAgent =
'Mozilla/5.0 BiliDroid/1.46.2 (bbcallen@gmail.com) os/android model/vivo mobi_app/android_hd build/2001100 channel/yingyongbao innerVer/2001100 osVer/14 network/2';
'Mozilla/5.0 BiliDroid/2.0.1 (bbcallen@gmail.com) os/android model/android_hd mobi_app/android_hd build/2001100 channel/master innerVer/2001100 osVer/15 network/2';
static const String statistics =
'{"appId":5,"platform":3,"version":"1.46.2","abtest":""}';
'{"appId":5,"platform":3,"version":"2.0.1","abtest":""}';
// 请求时会自动encodeComponent
static const urlPattern =
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]';
// app
static const String userAgentApp =
'Mozilla/5.0 BiliDroid/8.43.0 (bbcallen@gmail.com) os/android model/android mobi_app/android build/8430300 channel/master innerVer/8430300 osVer/15 network/2';
static get goodsUrlPrefix => "https://gaoneng.bilibili.com/tetris";
static const String statisticsApp =
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
static const baseHeaders = {
'connection': 'keep-alive',
'accept-encoding': 'br,gzip',
'referer': HttpString.baseUrl,
'env': 'prod',
'app-key': 'android64',
'x-bili-aurora-zone': 'sh001',
};
static final urlRegex = RegExp(
r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]',
);
static const goodsUrlPrefix = "https://gaoneng.bilibili.com/tetris";
// 超分辨率滤镜
static List<String> get mpvAnime4KShaders => [
'Anime4K_Clamp_Highlights.glsl',
'Anime4K_Restore_CNN_VL.glsl',
'Anime4K_Upscale_CNN_x2_VL.glsl',
'Anime4K_AutoDownscalePre_x2.glsl',
'Anime4K_AutoDownscalePre_x4.glsl',
'Anime4K_Upscale_CNN_x2_M.glsl'
];
static const List<String> mpvAnime4KShaders = [
'Anime4K_Clamp_Highlights.glsl',
'Anime4K_Restore_CNN_VL.glsl',
'Anime4K_Upscale_CNN_x2_VL.glsl',
'Anime4K_AutoDownscalePre_x2.glsl',
'Anime4K_AutoDownscalePre_x4.glsl',
'Anime4K_Upscale_CNN_x2_M.glsl',
];
// 超分辨率滤镜 (轻量)
static List<String> get mpvAnime4KShadersLite => [
'Anime4K_Clamp_Highlights.glsl',
'Anime4K_Restore_CNN_M.glsl',
'Anime4K_Restore_CNN_S.glsl',
'Anime4K_Upscale_CNN_x2_M.glsl',
'Anime4K_AutoDownscalePre_x2.glsl',
'Anime4K_AutoDownscalePre_x4.glsl',
'Anime4K_Upscale_CNN_x2_S.glsl'
];
static const mpvAnime4KShadersLite = [
'Anime4K_Clamp_Highlights.glsl',
'Anime4K_Restore_CNN_M.glsl',
'Anime4K_Restore_CNN_S.glsl',
'Anime4K_Upscale_CNN_x2_M.glsl',
'Anime4K_AutoDownscalePre_x2.glsl',
'Anime4K_AutoDownscalePre_x4.glsl',
'Anime4K_Upscale_CNN_x2_S.glsl',
];
//内容来自 https://passport.bilibili.com/web/generic/country/list
static List<Map<String, dynamic>> get internationalDialingPrefix => [
{"id": 1, "cname": "中国大陆", "country_id": "86"},
{"id": 5, "cname": "中国香港特别行政区", "country_id": "852"},
{"id": 2, "cname": "中国澳门特别行政区", "country_id": "853"},
{"id": 3, "cname": "中国台湾", "country_id": "886"},
{"id": 4, "cname": "美国", "country_id": "1"},
{"id": 6, "cname": "比利时", "country_id": "32"},
{"id": 7, "cname": "澳大利亚", "country_id": "61"},
{"id": 8, "cname": "法国", "country_id": "33"},
{"id": 9, "cname": "加拿大", "country_id": "1"},
{"id": 10, "cname": "日本", "country_id": "81"},
{"id": 11, "cname": "新加坡", "country_id": "65"},
{"id": 12, "cname": "韩国", "country_id": "82"},
{"id": 13, "cname": "马来西亚", "country_id": "60"},
{"id": 14, "cname": "英国", "country_id": "44"},
{"id": 15, "cname": "意大利", "country_id": "39"},
{"id": 16, "cname": "德国", "country_id": "49"},
{"id": 18, "cname": "俄罗斯", "country_id": "7"},
{"id": 19, "cname": "新西兰", "country_id": "64"}, //common:1-19
{"id": 153, "cname": "瓦利斯群岛和富图纳群岛", "country_id": "1681"},
{"id": 152, "cname": "葡萄牙", "country_id": "351"},
{"id": 151, "cname": "帕劳", "country_id": "680"},
{"id": 150, "cname": "诺福克岛", "country_id": "672"},
{"id": 149, "cname": "挪威", "country_id": "47"},
{"id": 148, "cname": "纽埃岛", "country_id": "683"},
{"id": 147, "cname": "尼日利亚", "country_id": "234"},
{"id": 146, "cname": "尼日尔", "country_id": "227"},
{"id": 145, "cname": "尼加拉瓜", "country_id": "505"},
{"id": 144, "cname": "尼泊尔", "country_id": "977"},
{"id": 143, "cname": "瑙鲁", "country_id": "674"},
{"id": 154, "cname": "格鲁吉亚", "country_id": "995"},
{"id": 155, "cname": "瑞典", "country_id": "46"},
{"id": 165, "cname": "沙特阿拉伯", "country_id": "966"},
{"id": 164, "cname": "桑给巴尔岛", "country_id": "259"},
{"id": 163, "cname": "塞舌尔共和国", "country_id": "248"},
{"id": 162, "cname": "塞浦路斯", "country_id": "357"},
{"id": 161, "cname": "塞内加尔", "country_id": "221"},
{"id": 160, "cname": "塞拉利昂", "country_id": "232"},
{"id": 159, "cname": "萨摩亚,东部", "country_id": "684"},
{"id": 158, "cname": "萨摩亚,西部", "country_id": "685"},
{"id": 157, "cname": "萨尔瓦多", "country_id": "503"},
{"id": 156, "cname": "瑞士", "country_id": "41"},
{"id": 166, "cname": "圣多美和普林西比", "country_id": "239"},
{"id": 142, "cname": "塞尔维亚", "country_id": "381"},
{"id": 141, "cname": "南非", "country_id": "27"},
{"id": 128, "cname": "毛里塔尼亚", "country_id": "222"},
{"id": 127, "cname": "毛里求斯", "country_id": "230"},
{"id": 126, "cname": "马歇尔岛", "country_id": "692"},
{"id": 125, "cname": "马提尼克岛", "country_id": "596"},
{"id": 124, "cname": "马其顿", "country_id": "389"},
{"id": 123, "cname": "马里亚纳岛", "country_id": "1670"},
{"id": 122, "cname": "马里", "country_id": "223"},
{"id": 121, "cname": "马拉维", "country_id": "265"},
{"id": 120, "cname": "马耳他", "country_id": "356"},
{"id": 119, "cname": "马尔代夫", "country_id": "960"},
{"id": 129, "cname": "蒙古", "country_id": "976"},
{"id": 130, "cname": "蒙特塞拉特岛", "country_id": "1664"},
{"id": 140, "cname": "纳米比亚", "country_id": "264"},
{"id": 139, "cname": "墨西哥", "country_id": "52"},
{"id": 138, "cname": "莫桑比克", "country_id": "258"},
{"id": 137, "cname": "摩纳哥", "country_id": "377"},
{"id": 136, "cname": "摩洛哥", "country_id": "212"},
{"id": 135, "cname": "摩尔多瓦", "country_id": "373"},
{"id": 134, "cname": "缅甸", "country_id": "95"},
{"id": 133, "cname": "密克罗尼西亚", "country_id": "691"},
{"id": 132, "cname": "秘鲁", "country_id": "51"},
{"id": 131, "cname": "孟加拉国", "country_id": "880"},
{"id": 118, "cname": "马达加斯加", "country_id": "261"},
{"id": 167, "cname": "圣卢西亚", "country_id": "1784"},
{"id": 216, "cname": "智利", "country_id": "56"},
{"id": 203, "cname": "牙买加", "country_id": "1876"},
{"id": 202, "cname": "叙利亚", "country_id": "963"},
{"id": 201, "cname": "匈牙利", "country_id": "36"},
{"id": 200, "cname": "科特迪瓦", "country_id": "225"},
{"id": 199, "cname": "希腊", "country_id": "30"},
{"id": 198, "cname": "西班牙", "country_id": "34"},
{"id": 197, "cname": "乌兹别克斯坦", "country_id": "998"},
{"id": 196, "cname": "乌拉圭", "country_id": "598"},
{"id": 195, "cname": "乌克兰", "country_id": "380"},
{"id": 194, "cname": "乌干达", "country_id": "256"},
{"id": 204, "cname": "亚美尼亚", "country_id": "374"},
{"id": 205, "cname": "也门", "country_id": "967"},
{"id": 215, "cname": "直布罗陀", "country_id": "350"},
{"id": 214, "cname": "乍得", "country_id": "235"},
{"id": 213, "cname": "赞比亚", "country_id": "260"},
{"id": 212, "cname": "越南", "country_id": "84"},
{"id": 211, "cname": "约旦", "country_id": "962"},
{"id": 210, "cname": "印尼", "country_id": "62"},
{"id": 209, "cname": "印度", "country_id": "91"},
{"id": 208, "cname": "以色列", "country_id": "972"},
{"id": 207, "cname": "伊朗", "country_id": "98"},
{"id": 206, "cname": "伊拉克", "country_id": "964"},
{"id": 193, "cname": "文莱", "country_id": "673"},
{"id": 192, "cname": "委内瑞拉", "country_id": "58"},
{"id": 191, "cname": "维珍群岛(英属)", "country_id": "1284"},
{"id": 178, "cname": "泰国", "country_id": "66"},
{"id": 177, "cname": "索马里", "country_id": "252"},
{"id": 176, "cname": "所罗门群岛", "country_id": "677"},
{"id": 175, "cname": "苏里南", "country_id": "597"},
{"id": 174, "cname": "苏丹", "country_id": "249"},
{"id": 173, "cname": "斯威士兰", "country_id": "268"},
{"id": 172, "cname": "斯洛文尼亚", "country_id": "386"},
{"id": 171, "cname": "斯洛伐克", "country_id": "421"},
{"id": 170, "cname": "斯里兰卡", "country_id": "94"},
{"id": 169, "cname": "圣皮埃尔和密克隆群岛", "country_id": "508"},
{"id": 179, "cname": "坦桑尼亚", "country_id": "255"},
{"id": 180, "cname": "汤加", "country_id": "676"},
{"id": 190, "cname": "维珍群岛(美属)", "country_id": "1340"},
{"id": 189, "cname": "瓦努阿图", "country_id": "678"},
{"id": 188, "cname": "托克劳岛", "country_id": "690"},
{"id": 187, "cname": "土库曼斯坦", "country_id": "993"},
{"id": 186, "cname": "土耳其", "country_id": "90"},
{"id": 185, "cname": "图瓦卢", "country_id": "688"},
{"id": 184, "cname": "突尼斯", "country_id": "216"},
{"id": 183, "cname": "阿森松岛", "country_id": "247"},
{"id": 182, "cname": "特立尼达和多巴哥", "country_id": "1868"},
{"id": 181, "cname": "特克斯和凯科斯", "country_id": "1649"},
{"id": 168, "cname": "圣马力诺", "country_id": "378"},
{"id": 67, "cname": "法属圭亚那", "country_id": "594"},
{"id": 54, "cname": "不丹", "country_id": "975"},
{"id": 53, "cname": "博茨瓦纳", "country_id": "267"},
{"id": 52, "cname": "伯利兹", "country_id": "501"},
{"id": 51, "cname": "玻利维亚", "country_id": "591"},
{"id": 50, "cname": "波兰", "country_id": "48"},
{"id": 49, "cname": "波黑", "country_id": "387"},
{"id": 48, "cname": "波多黎各", "country_id": "1787"},
{"id": 47, "cname": "冰岛", "country_id": "354"},
{"id": 46, "cname": "贝宁", "country_id": "229"},
{"id": 45, "cname": "保加利亚", "country_id": "359"},
{"id": 55, "cname": "布基纳法索", "country_id": "226"},
{"id": 56, "cname": "布隆迪", "country_id": "257"},
{"id": 66, "cname": "法属波利尼西亚", "country_id": "689"},
{"id": 65, "cname": "法罗岛", "country_id": "298"},
{"id": 64, "cname": "厄立特里亚", "country_id": "291"},
{"id": 63, "cname": "厄瓜多尔", "country_id": "593"},
{"id": 62, "cname": "多米尼加代表", "country_id": "1809"},
{"id": 61, "cname": "多米尼加", "country_id": "1767"},
{"id": 60, "cname": "多哥", "country_id": "228"},
{"id": 59, "cname": "迪戈加西亚岛", "country_id": "246"},
{"id": 58, "cname": "丹麦", "country_id": "45"},
{"id": 57, "cname": "赤道几内亚", "country_id": "240"},
{"id": 44, "cname": "百慕大群岛", "country_id": "1441"},
{"id": 43, "cname": "白俄罗斯", "country_id": "375"},
{"id": 42, "cname": "巴西", "country_id": "55"},
{"id": 29, "cname": "爱尔兰", "country_id": "353"},
{"id": 28, "cname": "埃塞俄比亚", "country_id": "251"},
{"id": 27, "cname": "埃及", "country_id": "20"},
{"id": 26, "cname": "阿塞拜疆", "country_id": "994"},
{"id": 25, "cname": "阿曼", "country_id": "968"},
{"id": 24, "cname": "阿联酋", "country_id": "971"},
{"id": 23, "cname": "阿根廷", "country_id": "54"},
{"id": 22, "cname": "阿富汗", "country_id": "93"},
{"id": 21, "cname": "阿尔及利亚", "country_id": "213"},
{"id": 20, "cname": "阿尔巴尼亚", "country_id": "355"},
{"id": 30, "cname": "爱沙尼亚", "country_id": "372"},
{"id": 31, "cname": "安道尔", "country_id": "376"},
{"id": 41, "cname": "巴拿马", "country_id": "507"},
{"id": 40, "cname": "巴林", "country_id": "973"},
{"id": 39, "cname": "巴拉圭", "country_id": "595"},
{"id": 38, "cname": "巴基斯坦", "country_id": "92"},
{"id": 37, "cname": "巴哈马群岛", "country_id": "1242"},
{"id": 36, "cname": "巴布亚新几内亚", "country_id": "675"},
{"id": 35, "cname": "巴巴多斯", "country_id": "1246"},
{"id": 34, "cname": "奥地利", "country_id": "43"},
{"id": 33, "cname": "安提瓜岛和巴布达", "country_id": "1268"},
{"id": 32, "cname": "安哥拉", "country_id": "244"},
{"id": 68, "cname": "非洲中部", "country_id": "236"},
{"id": 117, "cname": "罗马尼亚", "country_id": "40"},
{"id": 104, "cname": "科威特", "country_id": "965"},
{"id": 103, "cname": "科摩罗", "country_id": "269"},
{"id": 102, "cname": "开曼群岛", "country_id": "1345"},
{"id": 101, "cname": "卡塔尔", "country_id": "974"},
{"id": 100, "cname": "喀麦隆", "country_id": "237"},
{"id": 99, "cname": "聚会岛", "country_id": "262"},
{"id": 98, "cname": "津巴布韦", "country_id": "263"},
{"id": 97, "cname": "捷克", "country_id": "420"},
{"id": 96, "cname": "柬埔寨", "country_id": "855"},
{"id": 95, "cname": "加蓬", "country_id": "241"},
{"id": 105, "cname": "克罗地亚", "country_id": "385"},
{"id": 106, "cname": "肯尼亚", "country_id": "254"},
{"id": 116, "cname": "卢旺达", "country_id": "250"},
{"id": 115, "cname": "卢森堡", "country_id": "352"},
{"id": 114, "cname": "利比亚", "country_id": "218"},
{"id": 113, "cname": "利比里亚", "country_id": "231"},
{"id": 112, "cname": "立陶宛", "country_id": "370"},
{"id": 111, "cname": "黎巴嫩", "country_id": "961"},
{"id": 110, "cname": "老挝", "country_id": "856"},
{"id": 109, "cname": "莱索托", "country_id": "266"},
{"id": 108, "cname": "拉脱维亚", "country_id": "371"},
{"id": 107, "cname": "库克岛", "country_id": "682"},
{"id": 94, "cname": "加纳", "country_id": "233"},
{"id": 93, "cname": "几内亚比绍", "country_id": "245"},
{"id": 92, "cname": "几内亚", "country_id": "224"},
{"id": 79, "cname": "格林纳达", "country_id": "1473"},
{"id": 78, "cname": "哥斯达黎加", "country_id": "506"},
{"id": 77, "cname": "哥伦比亚", "country_id": "57"},
{"id": 76, "cname": "刚果(金)", "country_id": "243"},
{"id": 75, "cname": "刚果", "country_id": "242"},
{"id": 74, "cname": "冈比亚", "country_id": "220"},
{"id": 73, "cname": "福克兰岛", "country_id": "500"},
{"id": 72, "cname": "佛得角", "country_id": "238"},
{"id": 71, "cname": "芬兰", "country_id": "358"},
{"id": 70, "cname": "斐济", "country_id": "679"},
{"id": 80, "cname": "格陵兰岛", "country_id": "299"},
{"id": 81, "cname": "古巴", "country_id": "53"},
{"id": 91, "cname": "吉尔吉斯斯坦", "country_id": "996"},
{"id": 90, "cname": "吉布提", "country_id": "253"},
{"id": 89, "cname": "基里巴斯", "country_id": "686"},
{"id": 88, "cname": "维克岛", "country_id": "1808"},
{"id": 87, "cname": "洪都拉斯", "country_id": "504"},
{"id": 86, "cname": "荷兰", "country_id": "31"},
{"id": 85, "cname": "朝鲜", "country_id": "850"},
{"id": 84, "cname": "海地", "country_id": "509"},
{"id": 83, "cname": "关岛", "country_id": "1671"},
{"id": 82, "cname": "瓜德罗普岛", "country_id": "590"},
{"id": 69, "cname": "菲律宾", "country_id": "63"}
];
{"id": 1, "cname": "中国大陆", "country_id": "86"},
{"id": 5, "cname": "中国香港特别行政区", "country_id": "852"},
{"id": 2, "cname": "中国澳门特别行政区", "country_id": "853"},
{"id": 3, "cname": "中国台湾", "country_id": "886"},
{"id": 4, "cname": "美国", "country_id": "1"},
{"id": 6, "cname": "比利时", "country_id": "32"},
{"id": 7, "cname": "澳大利亚", "country_id": "61"},
{"id": 8, "cname": "法国", "country_id": "33"},
{"id": 9, "cname": "加拿大", "country_id": "1"},
{"id": 10, "cname": "日本", "country_id": "81"},
{"id": 11, "cname": "新加坡", "country_id": "65"},
{"id": 12, "cname": "韩国", "country_id": "82"},
{"id": 13, "cname": "马来西亚", "country_id": "60"},
{"id": 14, "cname": "英国", "country_id": "44"},
{"id": 15, "cname": "意大利", "country_id": "39"},
{"id": 16, "cname": "德国", "country_id": "49"},
{"id": 18, "cname": "俄罗斯", "country_id": "7"},
{"id": 19, "cname": "新西兰", "country_id": "64"}, //common:1-19
{"id": 153, "cname": "瓦利斯群岛和富图纳群岛", "country_id": "1681"},
{"id": 152, "cname": "葡萄牙", "country_id": "351"},
{"id": 151, "cname": "帕劳", "country_id": "680"},
{"id": 150, "cname": "诺福克岛", "country_id": "672"},
{"id": 149, "cname": "挪威", "country_id": "47"},
{"id": 148, "cname": "纽埃岛", "country_id": "683"},
{"id": 147, "cname": "尼日利亚", "country_id": "234"},
{"id": 146, "cname": "尼日尔", "country_id": "227"},
{"id": 145, "cname": "尼加拉瓜", "country_id": "505"},
{"id": 144, "cname": "尼泊尔", "country_id": "977"},
{"id": 143, "cname": "瑙鲁", "country_id": "674"},
{"id": 154, "cname": "格鲁吉亚", "country_id": "995"},
{"id": 155, "cname": "瑞典", "country_id": "46"},
{"id": 165, "cname": "沙特阿拉伯", "country_id": "966"},
{"id": 164, "cname": "桑给巴尔岛", "country_id": "259"},
{"id": 163, "cname": "塞舌尔共和国", "country_id": "248"},
{"id": 162, "cname": "塞浦路斯", "country_id": "357"},
{"id": 161, "cname": "塞内加尔", "country_id": "221"},
{"id": 160, "cname": "塞拉利昂", "country_id": "232"},
{"id": 159, "cname": "萨摩亚,东部", "country_id": "684"},
{"id": 158, "cname": "萨摩亚,西部", "country_id": "685"},
{"id": 157, "cname": "萨尔瓦多", "country_id": "503"},
{"id": 156, "cname": "瑞士", "country_id": "41"},
{"id": 166, "cname": "圣多美和普林西比", "country_id": "239"},
{"id": 142, "cname": "塞尔维亚", "country_id": "381"},
{"id": 141, "cname": "南非", "country_id": "27"},
{"id": 128, "cname": "毛里塔尼亚", "country_id": "222"},
{"id": 127, "cname": "毛里求斯", "country_id": "230"},
{"id": 126, "cname": "马歇尔岛", "country_id": "692"},
{"id": 125, "cname": "马提尼克岛", "country_id": "596"},
{"id": 124, "cname": "马其顿", "country_id": "389"},
{"id": 123, "cname": "马里亚纳岛", "country_id": "1670"},
{"id": 122, "cname": "马里", "country_id": "223"},
{"id": 121, "cname": "马拉维", "country_id": "265"},
{"id": 120, "cname": "马耳他", "country_id": "356"},
{"id": 119, "cname": "马尔代夫", "country_id": "960"},
{"id": 129, "cname": "蒙古", "country_id": "976"},
{"id": 130, "cname": "蒙特塞拉特岛", "country_id": "1664"},
{"id": 140, "cname": "纳米比亚", "country_id": "264"},
{"id": 139, "cname": "墨西哥", "country_id": "52"},
{"id": 138, "cname": "莫桑比克", "country_id": "258"},
{"id": 137, "cname": "摩纳哥", "country_id": "377"},
{"id": 136, "cname": "摩洛哥", "country_id": "212"},
{"id": 135, "cname": "摩尔多瓦", "country_id": "373"},
{"id": 134, "cname": "缅甸", "country_id": "95"},
{"id": 133, "cname": "密克罗尼西亚", "country_id": "691"},
{"id": 132, "cname": "秘鲁", "country_id": "51"},
{"id": 131, "cname": "孟加拉国", "country_id": "880"},
{"id": 118, "cname": "马达加斯加", "country_id": "261"},
{"id": 167, "cname": "圣卢西亚", "country_id": "1784"},
{"id": 216, "cname": "智利", "country_id": "56"},
{"id": 203, "cname": "牙买加", "country_id": "1876"},
{"id": 202, "cname": "叙利亚", "country_id": "963"},
{"id": 201, "cname": "匈牙利", "country_id": "36"},
{"id": 200, "cname": "科特迪瓦", "country_id": "225"},
{"id": 199, "cname": "希腊", "country_id": "30"},
{"id": 198, "cname": "西班牙", "country_id": "34"},
{"id": 197, "cname": "乌兹别克斯坦", "country_id": "998"},
{"id": 196, "cname": "乌拉圭", "country_id": "598"},
{"id": 195, "cname": "乌克兰", "country_id": "380"},
{"id": 194, "cname": "乌干达", "country_id": "256"},
{"id": 204, "cname": "亚美尼亚", "country_id": "374"},
{"id": 205, "cname": "也门", "country_id": "967"},
{"id": 215, "cname": "直布罗陀", "country_id": "350"},
{"id": 214, "cname": "乍得", "country_id": "235"},
{"id": 213, "cname": "赞比亚", "country_id": "260"},
{"id": 212, "cname": "越南", "country_id": "84"},
{"id": 211, "cname": "约旦", "country_id": "962"},
{"id": 210, "cname": "印尼", "country_id": "62"},
{"id": 209, "cname": "印度", "country_id": "91"},
{"id": 208, "cname": "以色列", "country_id": "972"},
{"id": 207, "cname": "伊朗", "country_id": "98"},
{"id": 206, "cname": "伊拉克", "country_id": "964"},
{"id": 193, "cname": "文莱", "country_id": "673"},
{"id": 192, "cname": "委内瑞拉", "country_id": "58"},
{"id": 191, "cname": "维珍群岛(英属)", "country_id": "1284"},
{"id": 178, "cname": "泰国", "country_id": "66"},
{"id": 177, "cname": "索马里", "country_id": "252"},
{"id": 176, "cname": "所罗门群岛", "country_id": "677"},
{"id": 175, "cname": "苏里南", "country_id": "597"},
{"id": 174, "cname": "苏丹", "country_id": "249"},
{"id": 173, "cname": "斯威士兰", "country_id": "268"},
{"id": 172, "cname": "斯洛文尼亚", "country_id": "386"},
{"id": 171, "cname": "斯洛伐克", "country_id": "421"},
{"id": 170, "cname": "斯里兰卡", "country_id": "94"},
{"id": 169, "cname": "圣皮埃尔和密克隆群岛", "country_id": "508"},
{"id": 179, "cname": "坦桑尼亚", "country_id": "255"},
{"id": 180, "cname": "汤加", "country_id": "676"},
{"id": 190, "cname": "维珍群岛(美属)", "country_id": "1340"},
{"id": 189, "cname": "瓦努阿图", "country_id": "678"},
{"id": 188, "cname": "托克劳岛", "country_id": "690"},
{"id": 187, "cname": "土库曼斯坦", "country_id": "993"},
{"id": 186, "cname": "土耳其", "country_id": "90"},
{"id": 185, "cname": "图瓦卢", "country_id": "688"},
{"id": 184, "cname": "突尼斯", "country_id": "216"},
{"id": 183, "cname": "阿森松岛", "country_id": "247"},
{"id": 182, "cname": "特立尼达和多巴哥", "country_id": "1868"},
{"id": 181, "cname": "特克斯和凯科斯", "country_id": "1649"},
{"id": 168, "cname": "圣马力诺", "country_id": "378"},
{"id": 67, "cname": "法属圭亚那", "country_id": "594"},
{"id": 54, "cname": "不丹", "country_id": "975"},
{"id": 53, "cname": "博茨瓦纳", "country_id": "267"},
{"id": 52, "cname": "伯利兹", "country_id": "501"},
{"id": 51, "cname": "玻利维亚", "country_id": "591"},
{"id": 50, "cname": "波兰", "country_id": "48"},
{"id": 49, "cname": "波黑", "country_id": "387"},
{"id": 48, "cname": "波多黎各", "country_id": "1787"},
{"id": 47, "cname": "冰岛", "country_id": "354"},
{"id": 46, "cname": "贝宁", "country_id": "229"},
{"id": 45, "cname": "保加利亚", "country_id": "359"},
{"id": 55, "cname": "布基纳法索", "country_id": "226"},
{"id": 56, "cname": "布隆迪", "country_id": "257"},
{"id": 66, "cname": "法属波利尼西亚", "country_id": "689"},
{"id": 65, "cname": "法罗岛", "country_id": "298"},
{"id": 64, "cname": "厄立特里亚", "country_id": "291"},
{"id": 63, "cname": "厄瓜多尔", "country_id": "593"},
{"id": 62, "cname": "多米尼加代表", "country_id": "1809"},
{"id": 61, "cname": "多米尼加", "country_id": "1767"},
{"id": 60, "cname": "多哥", "country_id": "228"},
{"id": 59, "cname": "迪戈加西亚岛", "country_id": "246"},
{"id": 58, "cname": "丹麦", "country_id": "45"},
{"id": 57, "cname": "赤道几内亚", "country_id": "240"},
{"id": 44, "cname": "百慕大群岛", "country_id": "1441"},
{"id": 43, "cname": "白俄罗斯", "country_id": "375"},
{"id": 42, "cname": "巴西", "country_id": "55"},
{"id": 29, "cname": "爱尔兰", "country_id": "353"},
{"id": 28, "cname": "埃塞俄比亚", "country_id": "251"},
{"id": 27, "cname": "埃及", "country_id": "20"},
{"id": 26, "cname": "阿塞拜疆", "country_id": "994"},
{"id": 25, "cname": "阿曼", "country_id": "968"},
{"id": 24, "cname": "阿联酋", "country_id": "971"},
{"id": 23, "cname": "阿根廷", "country_id": "54"},
{"id": 22, "cname": "阿富汗", "country_id": "93"},
{"id": 21, "cname": "阿尔及利亚", "country_id": "213"},
{"id": 20, "cname": "阿尔巴尼亚", "country_id": "355"},
{"id": 30, "cname": "爱沙尼亚", "country_id": "372"},
{"id": 31, "cname": "安道尔", "country_id": "376"},
{"id": 41, "cname": "巴拿马", "country_id": "507"},
{"id": 40, "cname": "巴林", "country_id": "973"},
{"id": 39, "cname": "巴拉圭", "country_id": "595"},
{"id": 38, "cname": "巴基斯坦", "country_id": "92"},
{"id": 37, "cname": "巴哈马群岛", "country_id": "1242"},
{"id": 36, "cname": "巴布亚新几内亚", "country_id": "675"},
{"id": 35, "cname": "巴巴多斯", "country_id": "1246"},
{"id": 34, "cname": "奥地利", "country_id": "43"},
{"id": 33, "cname": "安提瓜岛和巴布达", "country_id": "1268"},
{"id": 32, "cname": "安哥拉", "country_id": "244"},
{"id": 68, "cname": "非洲中部", "country_id": "236"},
{"id": 117, "cname": "罗马尼亚", "country_id": "40"},
{"id": 104, "cname": "科威特", "country_id": "965"},
{"id": 103, "cname": "科摩罗", "country_id": "269"},
{"id": 102, "cname": "开曼群岛", "country_id": "1345"},
{"id": 101, "cname": "卡塔尔", "country_id": "974"},
{"id": 100, "cname": "喀麦隆", "country_id": "237"},
{"id": 99, "cname": "聚会岛", "country_id": "262"},
{"id": 98, "cname": "津巴布韦", "country_id": "263"},
{"id": 97, "cname": "捷克", "country_id": "420"},
{"id": 96, "cname": "柬埔寨", "country_id": "855"},
{"id": 95, "cname": "加蓬", "country_id": "241"},
{"id": 105, "cname": "克罗地亚", "country_id": "385"},
{"id": 106, "cname": "肯尼亚", "country_id": "254"},
{"id": 116, "cname": "卢旺达", "country_id": "250"},
{"id": 115, "cname": "卢森堡", "country_id": "352"},
{"id": 114, "cname": "利比亚", "country_id": "218"},
{"id": 113, "cname": "利比里亚", "country_id": "231"},
{"id": 112, "cname": "立陶宛", "country_id": "370"},
{"id": 111, "cname": "黎巴嫩", "country_id": "961"},
{"id": 110, "cname": "老挝", "country_id": "856"},
{"id": 109, "cname": "莱索托", "country_id": "266"},
{"id": 108, "cname": "拉脱维亚", "country_id": "371"},
{"id": 107, "cname": "库克岛", "country_id": "682"},
{"id": 94, "cname": "加纳", "country_id": "233"},
{"id": 93, "cname": "几内亚比绍", "country_id": "245"},
{"id": 92, "cname": "几内亚", "country_id": "224"},
{"id": 79, "cname": "格林纳达", "country_id": "1473"},
{"id": 78, "cname": "哥斯达黎加", "country_id": "506"},
{"id": 77, "cname": "哥伦比亚", "country_id": "57"},
{"id": 76, "cname": "刚果(金)", "country_id": "243"},
{"id": 75, "cname": "刚果", "country_id": "242"},
{"id": 74, "cname": "冈比亚", "country_id": "220"},
{"id": 73, "cname": "福克兰岛", "country_id": "500"},
{"id": 72, "cname": "佛得角", "country_id": "238"},
{"id": 71, "cname": "芬兰", "country_id": "358"},
{"id": 70, "cname": "斐济", "country_id": "679"},
{"id": 80, "cname": "格陵兰岛", "country_id": "299"},
{"id": 81, "cname": "古巴", "country_id": "53"},
{"id": 91, "cname": "吉尔吉斯斯坦", "country_id": "996"},
{"id": 90, "cname": "吉布提", "country_id": "253"},
{"id": 89, "cname": "基里巴斯", "country_id": "686"},
{"id": 88, "cname": "维克岛", "country_id": "1808"},
{"id": 87, "cname": "洪都拉斯", "country_id": "504"},
{"id": 86, "cname": "荷兰", "country_id": "31"},
{"id": 85, "cname": "朝鲜", "country_id": "850"},
{"id": 84, "cname": "海地", "country_id": "509"},
{"id": 83, "cname": "关岛", "country_id": "1671"},
{"id": 82, "cname": "瓜德罗普岛", "country_id": "590"},
{"id": 69, "cname": "菲律宾", "country_id": "63"},
];
}

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class DynamicCardSkeleton extends StatelessWidget {
const DynamicCardSkeleton({super.key});
@@ -15,7 +16,7 @@ class DynamicCardSkeleton extends StatelessWidget {
border: Border(
bottom: BorderSide(
width: 8,
color: theme.dividerColor.withOpacity(0.05),
color: theme.dividerColor.withValues(alpha: 0.05),
),
),
),
@@ -28,7 +29,7 @@ class DynamicCardSkeleton extends StatelessWidget {
height: 40,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(20),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
),
const SizedBox(width: 10),
@@ -47,7 +48,7 @@ class DynamicCardSkeleton extends StatelessWidget {
height: 11,
),
],
)
),
],
),
Container(
@@ -89,7 +90,7 @@ class DynamicCardSkeleton extends StatelessWidget {
],
),
),
const Spacer(),
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -102,19 +103,20 @@ class DynamicCardSkeleton extends StatelessWidget {
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor:
theme.colorScheme.outline.withOpacity(0.2),
foregroundColor: theme.colorScheme.outline.withValues(
alpha: 0.2,
),
),
label: Text(
i == 0
? '转发'
: i == 1
? '评论'
: '点赞',
? '评论'
: '点赞',
),
)
),
],
)
),
],
),
),

View File

@@ -1,6 +1,6 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class FavPgcItemSkeleton extends StatelessWidget {
const FavPgcItemSkeleton({super.key});
@@ -15,7 +15,6 @@ class FavPgcItemSkeleton extends StatelessWidget {
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
@@ -25,7 +24,7 @@ class FavPgcItemSkeleton extends StatelessWidget {
return Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,

View File

@@ -1,31 +1,35 @@
import 'package:flutter/material.dart';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class MediaBangumiSkeleton extends StatefulWidget {
const MediaBangumiSkeleton({super.key});
class MediaPgcSkeleton extends StatefulWidget {
const MediaPgcSkeleton({super.key});
@override
State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();
State<MediaPgcSkeleton> createState() => _MediaPgcSkeletonState();
}
class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
class _MediaPgcSkeletonState extends State<MediaPgcSkeleton> {
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
StyleString.safeSpace,
7,
StyleString.safeSpace,
7,
),
child: Row(
children: [
Container(
width: 111,
height: 148,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: bgColor),
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: bgColor,
),
),
const SizedBox(width: 10),
Expanded(
@@ -62,8 +66,9 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
width: 90,
height: 35,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(20)),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
color: bgColor,
),
),

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class MsgFeedSysMsgSkeleton extends StatelessWidget {
const MsgFeedSysMsgSkeleton({super.key});

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class MsgFeedTopSkeleton extends StatelessWidget {
const MsgFeedTopSkeleton({super.key});

View File

@@ -74,14 +74,14 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
}
LinearGradient get gradient => LinearGradient(
colors: widget.linearGradient.colors,
stops: widget.linearGradient.stops,
begin: widget.linearGradient.begin,
end: widget.linearGradient.end,
transform: _SlidingGradientTransform(
slidePercent: _shimmerController.value,
),
);
colors: widget.linearGradient.colors,
stops: widget.linearGradient.stops,
begin: widget.linearGradient.begin,
end: widget.linearGradient.end,
transform: _SlidingGradientTransform(
slidePercent: _shimmerController.value,
),
);
bool get isSized =>
(context.findRenderObject() as RenderBox?)?.hasSize ?? false;

View File

@@ -0,0 +1,51 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
class SpaceOpusSkeleton extends StatelessWidget {
const SpaceOpusSkeleton({super.key});
@override
Widget build(BuildContext context) {
final surface = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Card(
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: LayoutBuilder(
builder: (context, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height:
(0.68 + 0.82 * Utils.random.nextDouble()) *
constraints.maxWidth,
color: surface,
),
Container(
height: 10,
color: surface,
margin: const EdgeInsets.all(10),
width: constraints.maxWidth * 0.7,
),
Container(
height: 10,
color: surface,
margin: const EdgeInsets.only(
left: 10,
right: 10,
bottom: 10,
),
width: constraints.maxWidth,
),
],
);
},
),
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class VideoCardHSkeleton extends StatelessWidget {
const VideoCardHSkeleton({super.key});
@@ -15,20 +15,15 @@ class VideoCardHSkeleton extends StatelessWidget {
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Container(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
),
),
Expanded(
@@ -69,7 +64,7 @@ class VideoCardHSkeleton extends StatelessWidget {
height: 13,
),
],
)
),
],
),
),

View File

@@ -1,6 +1,6 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class VideoCardVSkeleton extends StatelessWidget {
const VideoCardVSkeleton({super.key});
@@ -13,15 +13,11 @@ class VideoCardVSkeleton extends StatelessWidget {
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Container(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
),
),
Padding(

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class VideoReplySkeleton extends StatelessWidget {
const VideoReplySkeleton({super.key});
@@ -26,17 +26,20 @@ class VideoReplySkeleton extends StatelessWidget {
width: 80,
height: 13,
color: bgColor,
)
),
],
),
),
Container(
width: double.infinity,
margin:
const EdgeInsets.only(top: 4, left: 57, right: 6, bottom: 6),
margin: const EdgeInsets.only(
top: 4,
left: 57,
right: 6,
bottom: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: 300,
@@ -72,9 +75,9 @@ class VideoReplySkeleton extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 4),
color: bgColor,
),
const SizedBox(width: 8)
const SizedBox(width: 8),
],
)
),
],
),
),

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:flutter/material.dart';
import 'skeleton.dart';
class WhisperItemSkeleton extends StatelessWidget {
const WhisperItemSkeleton({super.key});

View File

@@ -0,0 +1,59 @@
import 'package:PiliPlus/pages/common/multi_select/base.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MultiSelectAppBarWidget extends StatelessWidget
implements PreferredSizeWidget {
final MultiSelectBase ctr;
final bool? visible;
final AppBar child;
final List<Widget>? children;
const MultiSelectAppBarWidget({
super.key,
required this.ctr,
this.visible,
this.children,
required this.child,
});
@override
Widget build(BuildContext context) {
if (visible ?? ctr.enableMultiSelect.value) {
return AppBar(
bottom: child.bottom,
leading: IconButton(
tooltip: '取消',
onPressed: ctr.handleSelect,
icon: const Icon(Icons.close_outlined),
),
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
actions: [
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => ctr.handleSelect(checked: true),
child: const Text('全选'),
),
...?children,
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: ctr.onRemove,
child: Text(
'移除',
style: TextStyle(color: Get.theme.colorScheme.error),
),
),
const SizedBox(width: 6),
],
);
}
return child;
}
@override
Size get preferredSize => child.preferredSize;
}

View File

@@ -1,21 +1,25 @@
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PBadge extends StatelessWidget {
final String? text;
final bool isStack;
final double? top;
final double? right;
final double? bottom;
final double? left;
final String? type;
final String? size;
final String? stack;
final double? fs;
final String? semanticsLabel;
final bool bold;
final double? textScaleFactor;
final EdgeInsets? padding;
final PBadgeType type;
final PBadgeSize size;
final double fontSize;
final bool isBold;
final double? textScaleFactor;
const PBadge({
super.key,
required this.text,
@@ -23,12 +27,11 @@ class PBadge extends StatelessWidget {
this.right,
this.bottom,
this.left,
this.type = 'primary',
this.size = 'medium',
this.stack = 'position',
this.fs = 11,
this.semanticsLabel,
this.bold = true,
this.type = PBadgeType.primary,
this.size = PBadgeSize.medium,
this.isStack = true,
this.fontSize = 11,
this.isBold = true,
this.textScaleFactor,
this.padding,
});
@@ -40,37 +43,49 @@ class PBadge extends StatelessWidget {
}
ColorScheme theme = Theme.of(context).colorScheme;
// 背景色
Color bgColor = theme.primary;
// 前景色
Color color = theme.onPrimary;
// 边框色
Color bgColor;
Color color;
Color borderColor = Colors.transparent;
if (type == 'gray') {
bgColor = Colors.black45;
color = Colors.white;
} else if (type == 'color') {
bgColor = theme.secondaryContainer.withOpacity(0.5);
color = theme.onSecondaryContainer;
} else if (type == 'line') {
bgColor = Colors.transparent;
color = theme.primary;
borderColor = theme.primary;
} else if (type == 'error') {
bgColor = theme.error;
color = theme.onError;
switch (type) {
case PBadgeType.primary:
bgColor = theme.primary;
color = theme.onPrimary;
case PBadgeType.secondary:
bgColor = theme.secondaryContainer.withValues(alpha: 0.5);
color = theme.onSecondaryContainer;
case PBadgeType.gray:
bgColor = Colors.black45;
color = Colors.white;
case PBadgeType.error:
if (Get.isDarkMode) {
bgColor = theme.errorContainer;
color = theme.onErrorContainer;
} else {
bgColor = theme.error;
color = theme.onError;
}
case PBadgeType.line_primary:
color = theme.primary;
bgColor = Colors.transparent;
borderColor = theme.primary;
case PBadgeType.line_secondary:
color = theme.secondary;
bgColor = Colors.transparent;
borderColor = theme.secondary;
case PBadgeType.free:
bgColor = theme.freeColor;
color = Colors.white;
}
late EdgeInsets paddingStyle =
const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
double fontSize = 11;
BorderRadius br = BorderRadius.circular(4);
if (size == 'small') {
paddingStyle = const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
fontSize = 11;
br = BorderRadius.circular(3);
}
late EdgeInsets paddingStyle = const EdgeInsets.symmetric(
vertical: 2,
horizontal: 3,
);
BorderRadius br = size == PBadgeSize.small
? const BorderRadius.all(Radius.circular(3))
: const BorderRadius.all(Radius.circular(4));
Widget content = Container(
padding: padding ?? paddingStyle,
@@ -86,20 +101,19 @@ class PBadge extends StatelessWidget {
: null,
style: TextStyle(
height: 1,
fontSize: fs ?? fontSize,
fontSize: fontSize,
color: color,
fontWeight: bold ? FontWeight.bold : null,
fontWeight: isBold ? FontWeight.bold : null,
),
strutStyle: StrutStyle(
leading: 0,
height: 1,
fontSize: fs ?? fontSize,
fontWeight: bold ? FontWeight.bold : null,
fontSize: fontSize,
fontWeight: isBold ? FontWeight.bold : null,
),
semanticsLabel: semanticsLabel,
),
);
if (stack == 'position') {
if (isStack) {
return Positioned(
top: top,
left: left,

View File

@@ -41,8 +41,8 @@ Widget mediumButton({
child: IconButton(
tooltip: tooltip,
icon: Icon(icon),
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
style: const ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.zero),
),
onPressed: onPressed,
),

View File

@@ -29,10 +29,10 @@ class ToolbarIconButton extends StatelessWidget {
? theme.colorScheme.onSecondaryContainer
: theme.colorScheme.outline,
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
backgroundColor: WidgetStateProperty.resolveWith((states) {
return selected ? theme.colorScheme.secondaryContainer : null;
}),
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
backgroundColor: WidgetStatePropertyAll(
selected ? theme.colorScheme.secondaryContainer : null,
),
),
),
);

View File

@@ -0,0 +1,77 @@
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 bool selected;
const ColorPalette({
super.key,
required this.color,
required this.selected,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final Hct hct = Hct.fromInt(color.value);
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());
final checkbox = Color(Hct.from(hct.hue, 30.0, 40.0).toInt());
Widget coloredBox(Color color) => Expanded(
child: ColoredBox(
color: color,
child: const SizedBox.expand(),
),
);
Widget child = ClipOval(
child: Column(
children: [
coloredBox(primary),
Expanded(
child: Row(
children: [
coloredBox(tertiary),
coloredBox(primaryContainer),
],
),
),
],
),
);
if (selected) {
child = Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
child,
Container(
width: 23,
height: 23,
decoration: BoxDecoration(
color: checkbox,
shape: BoxShape.circle,
),
child: Icon(
Icons.check_rounded,
color: primary,
size: 12,
),
),
],
);
}
return Container(
width: 50,
height: 50,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
borderRadius: StyleString.mdRadius,
),
child: child,
);
}
}

View File

@@ -0,0 +1,30 @@
// ignore_for_file: constant_identifier_names
import 'package:flutter/widgets.dart';
class CustomIcon {
static const IconData coin = _CustomIconData(0xe800);
static const IconData dm_off = _CustomIconData(0xe801);
static const IconData dm_on = _CustomIconData(0xe802);
static const IconData dm_settings = _CustomIconData(0xe803);
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);
}
class _CustomIconData extends IconData {
const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon');
}

View File

@@ -6,31 +6,38 @@ class CustomSliverPersistentHeaderDelegate
required this.child,
required this.bgColor,
double extent = 45,
}) : _minExtent = extent,
_maxExtent = extent;
this.needRebuild,
}) : _minExtent = extent,
_maxExtent = extent;
final double _minExtent;
final double _maxExtent;
final Widget child;
final Color bgColor;
final Color? bgColor;
final bool? needRebuild;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
//创建child子组件
//shrinkOffsetchild偏移值minExtent~maxExtent
//overlapsContentSliverPersistentHeader覆盖其他子组件返回true否则返回false
return DecoratedBox(
decoration: BoxDecoration(
color: bgColor,
boxShadow: [
BoxShadow(
color: bgColor,
offset: const Offset(0, -2),
),
],
),
child: child,
);
return bgColor != null
? DecoratedBox(
decoration: BoxDecoration(
color: bgColor,
boxShadow: [
BoxShadow(
color: bgColor!,
offset: const Offset(0, -2),
),
],
),
child: child,
)
: child;
}
//SliverPersistentHeader最大高度
@@ -42,8 +49,8 @@ class CustomSliverPersistentHeaderDelegate
double get minExtent => _minExtent;
@override
bool shouldRebuild(
covariant CustomSliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate.bgColor != bgColor;
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate.bgColor != bgColor ||
(needRebuild == true && oldDelegate.child != child);
}
}

View File

@@ -1,23 +1,26 @@
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/storage.dart';
class CustomToast extends StatelessWidget {
const CustomToast({super.key, required this.msg});
final String msg;
static double toastOpacity = Pref.defaultToastOp;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final double toastOpacity = GStorage.setting
.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0) as double;
return Container(
margin:
EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 30),
margin: EdgeInsets.only(
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
),
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(toastOpacity),
borderRadius: BorderRadius.circular(20),
color: theme.colorScheme.primaryContainer.withValues(
alpha: toastOpacity,
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: Text(
msg,
@@ -44,21 +47,24 @@ class LoadingWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
decoration: BoxDecoration(
color: theme.dialogBackgroundColor,
borderRadius: BorderRadius.circular(15),
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
//loading animation
CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//loading animation
CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
),
//msg
Container(
margin: const EdgeInsets.only(top: 20),
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
),
]),
//msg
Container(
margin: const EdgeInsets.only(top: 20),
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
),
],
),
);
}
}

View File

@@ -0,0 +1,390 @@
import 'dart:math' as math;
import 'dart:ui' show clampDouble;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
enum TooltipType { top, right }
class CustomTooltip extends StatefulWidget {
const CustomTooltip({
super.key,
this.type = TooltipType.top,
required this.overlayWidget,
required this.child,
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;
}
@override
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);
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;
}
void _scheduleShowTooltip() {
_controller.forward();
}
void _scheduleDismissTooltip() {
_controller.reverse();
}
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);
}
Widget _buildCustomTooltipOverlay(BuildContext context) {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
);
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);
}
@protected
@override
void dispose() {
CustomTooltip._openedTooltips.remove(this);
_longPressRecognizer?.onLongPressCancel = null;
_longPressRecognizer?.dispose();
_backingController?.dispose();
_backingOverlayAnimation?.dispose();
super.dispose();
}
@protected
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque,
child: widget.child,
);
return OverlayPortal(
controller: _overlayController,
overlayChildBuilder: _buildCustomTooltipOverlay,
child: result,
);
}
}
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,
});
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;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDismiss,
child: CustomMultiChildLayout(
delegate: _CustomMultiTooltipPositionDelegate(
type: type,
target: target,
verticalOffset: verticalOffset,
horizontslOffset: horizontslOffset,
preferBelow: false,
),
children: [
LayoutId(
id: 'overlay',
child: overlayWidget(),
),
if (indicator != null)
LayoutId(
id: 'indicator',
child: indicator!(),
),
],
),
);
}
}
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate {
_CustomMultiTooltipPositionDelegate({
required this.type,
required this.target,
required this.verticalOffset,
required this.horizontslOffset,
required this.preferBelow,
});
final TooltipType type;
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));
}
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));
}
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);
}
}
}
@override
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) {
return target != oldDelegate.target ||
verticalOffset != oldDelegate.verticalOffset ||
preferBelow != oldDelegate.preferBelow;
}
}
class TrianglePainter extends CustomPainter {
TrianglePainter(this.color, {this.type = TooltipType.top});
final TooltipType type;
final Color color;
@override
void paint(Canvas canvas, Size 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();
}
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);
}
}

View File

@@ -15,8 +15,8 @@ void showConfirmDialog({
content: content is String
? Text(content)
: content is Widget
? content
: null,
? content
: null,
actions: [
TextButton(
onPressed: Get.back,
@@ -30,7 +30,7 @@ void showConfirmDialog({
Get.back();
onConfirm();
},
child: Text('确认'),
child: const Text('确认'),
),
],
);
@@ -65,42 +65,43 @@ void showPgcFollowDialog({
}
showDialog(
context: context,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
...[
{'followStatus': 3, 'title': '看过'},
{'followStatus': 2, 'title': '在看'},
{'followStatus': 1, 'title': '想看'},
].map(
(Map item) => statusItem(
enabled: followStatus != item['followStatus'],
text: item['title'],
onTap: () {
Get.back();
onUpdateStatus(item['followStatus']);
},
),
),
ListTile(
dense: true,
title: Padding(
padding: EdgeInsets.only(left: 10),
child: Text(
'取消$type',
style: TextStyle(fontSize: 14),
),
),
onTap: () {
Get.back();
onUpdateStatus(-1);
},
)
],
context: context,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
...const [
(followStatus: 3, title: '看过'),
(followStatus: 2, title: '在看'),
(followStatus: 1, title: '想看'),
].map(
(item) => statusItem(
enabled: followStatus != item.followStatus,
text: item.title,
onTap: () {
Get.back();
onUpdateStatus(item.followStatus);
},
),
));
),
ListTile(
dense: true,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
'取消$type',
style: const TextStyle(fontSize: 14),
),
),
onTap: () {
Get.back();
onUpdateStatus(-1);
},
),
],
),
),
);
}

View File

@@ -7,7 +7,8 @@ import 'package:get/get.dart';
void autoWrapReportDialog(
BuildContext context,
Map<String, Map<int, String>> options,
Future<Map> Function(int, String?, bool) onSuccess,
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
onSuccess,
) {
int? reasonType;
String? reasonDesc;
@@ -21,8 +22,11 @@ void autoWrapReportDialog(
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),
actionsPadding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 10,
),
content: Form(
key: key,
child: Column(
@@ -55,13 +59,20 @@ void autoWrapReportDialog(
),
if (reasonType == 0)
ReasonField(
onChanged: (value) => reasonDesc = value),
onChanged: (value) => reasonDesc = value,
),
],
),
),
),
),
BanUserCheckbox(onChanged: (value) => banUid = value),
Padding(
padding: const EdgeInsets.only(left: 14, top: 6),
child: CheckBoxText(
text: '拉黑该用户',
onChanged: (value) => banUid = value,
),
),
],
),
),
@@ -136,53 +147,65 @@ class _ReasonFieldState extends State<ReasonField> {
border: OutlineInputBorder(),
contentPadding: EdgeInsets.all(10),
),
onChanged: (value) {
widget.onChanged(value);
},
onChanged: widget.onChanged,
validator: widget._validator,
),
);
}
}
class BanUserCheckbox extends StatefulWidget {
class CheckBoxText extends StatefulWidget {
final String text;
final ValueChanged<bool> onChanged;
final bool selected;
const BanUserCheckbox({super.key, required this.onChanged});
const CheckBoxText({
super.key,
required this.text,
required this.onChanged,
this.selected = false,
});
@override
State<BanUserCheckbox> createState() => _BanUserCheckboxState();
State<CheckBoxText> createState() => _CheckBoxTextState();
}
class _BanUserCheckboxState extends State<BanUserCheckbox> {
bool _banUid = false;
class _CheckBoxTextState extends State<CheckBoxText> {
late bool _selected;
@override
void initState() {
super.initState();
_selected = widget.selected;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: () {
setState(() => _banUid = !_banUid);
widget.onChanged(_banUid);
setState(() {
_selected = !_selected;
});
widget.onChanged(_selected);
},
child: Padding(
padding: const EdgeInsets.only(left: 18, top: 10),
padding: const EdgeInsets.all(4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 22,
_banUid
_selected
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: _banUid
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant,
color: _selected
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
Text(
' 拉黑该用户',
style: TextStyle(
color: _banUid ? theme.colorScheme.primary : null,
),
' ${widget.text}',
style: TextStyle(color: _selected ? colorScheme.primary : null),
),
],
),
@@ -193,34 +216,34 @@ class _BanUserCheckboxState extends State<BanUserCheckbox> {
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 => {
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
'谣言类不实信息': {19: '涉政谣言', 22: '虚假不实信息', 20: '涉社会事件谣言'},
'侵犯个人权益': {7: '人身攻击', 15: '侵犯隐私'},
'有害社区环境': {
1: '垃圾广告',
4: '引战',
5: '剧透',
3: '刷屏',
8: '视频不相关',
18: '违规抽奖',
17: '青少年不良信息',
},
'其他': {0: '其他'},
};
static Map<String, Map<int, String>> get commentReport => const {
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
'谣言类不实信息': {19: '涉政谣言', 22: '虚假不实信息', 20: '涉社会事件谣言'},
'侵犯个人权益': {7: '人身攻击', 15: '侵犯隐私'},
'有害社区环境': {
1: '垃圾广告',
4: '引战',
5: '剧透',
3: '刷屏',
8: '视频不相关',
18: '违规抽奖',
17: '青少年不良信息',
},
'其他': {0: '其他'},
};
static Map<String, Map<int, String>> get dynamicReport => {
'': {
4: '垃圾广告',
8: '引战',
1: '色情',
5: '人身攻击',
3: '违法信息',
9: '涉政谣言',
10: '涉社会事件谣言',
12: '虚假不实信息',
13: '违法信息外链',
0: '其他',
},
};
static Map<String, Map<int, String>> get dynamicReport => const {
'': {
4: '垃圾广告',
8: '引战',
1: '色情',
5: '人身攻击',
3: '违法信息',
9: '涉政谣言',
10: '涉社会事件谣言',
12: '虚假不实信息',
13: '违法信息外链',
0: '其他',
},
};
}

View File

@@ -0,0 +1,126 @@
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class MemberReportPanel extends StatefulWidget {
const MemberReportPanel({
super.key,
required this.name,
required this.mid,
});
final dynamic name;
final dynamic mid;
@override
State<MemberReportPanel> createState() => _MemberReportPanelState();
}
class _MemberReportPanelState extends State<MemberReportPanel> {
final List<bool> _reasonList = List.generate(3, (_) => false);
final Set<int> _reason = {};
int? _reasonV2;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'举报: ${widget.name}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 4),
Text('uid: ${widget.mid}'),
const SizedBox(height: 10),
const Text('举报内容(必选,可多选)'),
...List.generate(
3,
(index) => _checkBoxWidget(
_reasonList[index],
(value) {
setState(() => _reasonList[index] = value);
if (value) {
_reason.add(index + 1);
} else {
_reason.remove(index + 1);
}
},
const ['头像违规', '昵称违规', '签名违规'][index],
),
),
const Text('举报理由(单选,非必选)'),
...List.generate(
5,
(index) => RadioWidget<int>(
value: index,
groupValue: _reasonV2,
onChanged: (value) {
setState(() => _reasonV2 = value);
},
title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
),
),
TextButton(
onPressed: () async {
if (_reason.isEmpty) {
SmartDialog.showToast('至少选择一项作为举报内容');
} else {
Get.back();
var result = await MemberHttp.reportMember(
widget.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('确定'),
),
],
),
],
),
);
}
}
Widget _checkBoxWidget(
bool defValue,
ValueChanged onChanged,
String title,
) {
return InkWell(
onTap: () => onChanged(!defValue),
child: Row(
children: [
Checkbox(
value: defValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
),
);
}

View File

@@ -14,9 +14,9 @@ class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
this.color,
double? lineLengthScale,
StrokeCap? strokeCap,
}) : lineLengthScale = lineLengthScale ?? 0.9,
strokeCap = strokeCap ?? StrokeCap.butt,
super(child: child);
}) : lineLengthScale = lineLengthScale ?? 0.9,
strokeCap = strokeCap ?? StrokeCap.butt,
super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
@@ -60,20 +60,21 @@ class RenderMaskedIcon extends RenderProxyBox {
// );
final path = Path.combine(
PathOperation.union,
Path() // bottom
..moveTo(rect.left, rect.bottom)
..lineTo(rect.left, rect.top + sqrt2Width)
..lineTo(rect.right - sqrt2Width, rect.bottom)
..close(),
Path() // top
..moveTo(rect.right, rect.top)
..lineTo(rect.right, rect.bottom - sqrt2Width)
..lineTo(rect.left + sqrt2Width, rect.top));
PathOperation.union,
Path() // bottom
..moveTo(rect.left, rect.bottom)
..lineTo(rect.left, rect.top + sqrt2Width)
..lineTo(rect.right - sqrt2Width, rect.bottom)
..close(),
Path() // top
..moveTo(rect.right, rect.top)
..lineTo(rect.right, rect.bottom - sqrt2Width)
..lineTo(rect.left + sqrt2Width, rect.top),
);
canvas.save();
canvas.clipPath(path, doAntiAlias: false);
canvas
..save()
..clipPath(path, doAntiAlias: false);
super.paint(context, offset);
context.canvas.restore();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,789 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: uri_does_not_exist_in_doc_import
/// @docImport 'elevated_button_theme.dart';
/// @docImport 'menu_anchor.dart';
/// @docImport 'text_button_theme.dart';
/// @docImport 'text_theme.dart';
/// @docImport 'theme.dart';
library;
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/dyn/ink_well.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide InkWell;
import 'package:flutter/rendering.dart';
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
///
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
///
/// See also:
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * [TextButton], a button with no outline or fill color.
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
/// the Material Design button types and how they should be used in designs.
abstract class ButtonStyleButton extends StatefulWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ButtonStyleButton({
super.key,
required this.onPressed,
required this.onLongPress,
required this.onHover,
required this.onFocusChange,
required this.style,
required this.focusNode,
required this.autofocus,
required this.clipBehavior,
this.statesController,
this.isSemanticButton = true,
@Deprecated(
'Remove this parameter as it is now ignored. '
'Use ButtonStyle.iconAlignment instead. '
'This feature was deprecated after v3.28.0-1.0.pre.',
)
this.iconAlignment,
this.tooltip,
required this.child,
});
/// Called when the button is tapped or otherwise activated.
///
/// If this callback and [onLongPress] are null, then the button will be disabled.
///
/// See also:
///
/// * [enabled], which is true if the button is enabled.
final VoidCallback? onPressed;
/// Called when the button is long-pressed.
///
/// If this callback and [onPressed] are null, then the button will be disabled.
///
/// See also:
///
/// * [enabled], which is true if the button is enabled.
final VoidCallback? onLongPress;
/// Called when a pointer enters or exits the button response area.
///
/// The value passed to the callback is true if a pointer has entered this
/// part of the material and false if a pointer has exited this part of the
/// material.
final ValueChanged<bool>? onHover;
/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses
/// focus.
final ValueChanged<bool>? onFocusChange;
/// Customizes this button's appearance.
///
/// Non-null properties of this style override the corresponding
/// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s
/// that resolve to non-null values will similarly override the corresponding
/// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf].
///
/// Null by default.
final ButtonStyle? style;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
/// [ButtonStyle.foregroundBuilder] is specified. In those
/// cases the default is [Clip.antiAlias].
final Clip? clipBehavior;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// {@macro flutter.material.inkwell.statesController}
final WidgetStatesController? statesController;
/// Determine whether this subtree represents a button.
///
/// If this is null, the screen reader will not announce "button" when this
/// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we
/// traverse the menu system.
///
/// Defaults to true.
final bool? isSemanticButton;
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
@Deprecated(
'Remove this parameter as it is now ignored. '
'Use ButtonStyle.iconAlignment instead. '
'This feature was deprecated after v3.28.0-1.0.pre.',
)
final IconAlignment? iconAlignment;
/// Text that describes the action that will occur when the button is pressed or
/// hovered over.
///
/// This text is displayed when the user long-presses or hovers over the button
/// in a tooltip. This string is also used for accessibility.
///
/// If null, the button will not display a tooltip.
final String? tooltip;
/// Typically the button's label.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// Returns a [ButtonStyle] that's based primarily on the [Theme]'s
/// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values
/// filled out (non-null).
///
/// The returned style can be overridden by the [style] parameter and by the
/// style returned by [themeStyleOf] that some button-specific themes like
/// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the
/// default style of the [TextButton] subclass can be overridden with its
/// [TextButton.style] constructor parameter, or with a [TextButtonTheme].
///
/// Concrete button subclasses should return a [ButtonStyle] with as many
/// non-null properties as possible, where all of the non-null
/// [WidgetStateProperty] properties resolve to non-null values.
///
/// ## Properties that can be null
///
/// Some properties, like [ButtonStyle.fixedSize] would override other values
/// in the same [ButtonStyle] if set, so they are allowed to be null. Here is
/// a summary of properties that are allowed to be null when returned in the
/// [ButtonStyle] returned by this function, an why:
///
/// - [ButtonStyle.fixedSize] because it would override other values in the
/// same [ButtonStyle], like [ButtonStyle.maximumSize].
/// - [ButtonStyle.side] because null is a valid value for a button that has
/// no side. [OutlinedButton] returns a non-null default for this, however.
/// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]
/// because they would override the [ButtonStyle.foregroundColor] and
/// [ButtonStyle.backgroundColor] of the same [ButtonStyle].
///
/// See also:
///
/// * [themeStyleOf], returns the ButtonStyle of this button's component
/// theme.
@protected
ButtonStyle defaultStyleOf(BuildContext context);
/// Returns the ButtonStyle that belongs to the button's component theme.
///
/// The returned style can be overridden by the [style] parameter.
///
/// Concrete button subclasses should return the ButtonStyle for the
/// nearest subclass-specific inherited theme, and if no such theme
/// exists, then the same value from the overall [Theme].
///
/// See also:
///
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
@protected
ButtonStyle? themeStyleOf(BuildContext context);
/// Whether the button is enabled or disabled.
///
/// Buttons are disabled by default. To enable a button, set its [onPressed]
/// or [onLongPress] properties to a non-null value.
bool get enabled => onPressed != null || onLongPress != null;
@override
State<ButtonStyleButton> createState() => _ButtonStyleState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(
FlagProperty('enabled', value: enabled, ifFalse: 'disabled'),
)
..add(
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null),
)
..add(
DiagnosticsProperty<FocusNode>(
'focusNode',
focusNode,
defaultValue: null,
),
);
}
/// Returns null if [value] is null, otherwise `WidgetStatePropertyAll<T>(value)`.
///
/// A convenience method for subclasses.
static WidgetStateProperty<T>? allOrNull<T>(T? value) =>
value == null ? null : WidgetStatePropertyAll<T>(value);
/// Returns null if [enabled] and [disabled] are null.
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
///
/// A convenience method for subclasses.
static WidgetStateProperty<Color?>? defaultColor(
Color? enabled,
Color? disabled,
) {
if ((enabled ?? disabled) == null) {
return null;
}
return WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
WidgetState.disabled: disabled,
WidgetState.any: enabled,
});
}
/// A convenience method used by subclasses in the framework, that returns an
/// interpolated value based on the [fontSizeMultiplier] parameter:
///
/// * 0 - 1 [geometry1x]
/// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1)
/// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2)
/// * otherwise [geometry3x]
///
/// This method is used by the framework for estimating the default paddings to
/// use on a button with a text label, when the system text scaling setting
/// changes. It's usually supplied with empirical [geometry1x], [geometry2x],
/// [geometry3x] values adjusted for different system text scaling values, when
/// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge]
/// value).
///
/// The `fontSizeMultiplier` argument, for historical reasons, is the default
/// font size specified in the [ButtonStyle], scaled by the ambient font
/// scaler, then divided by 14.0 (the default font size used in buttons).
static EdgeInsetsGeometry scaledPadding(
EdgeInsetsGeometry geometry1x,
EdgeInsetsGeometry geometry2x,
EdgeInsetsGeometry geometry3x,
double fontSizeMultiplier,
) {
return switch (fontSizeMultiplier) {
<= 1 => geometry1x,
< 2 => EdgeInsetsGeometry.lerp(
geometry1x,
geometry2x,
fontSizeMultiplier - 1,
)!,
< 3 => EdgeInsetsGeometry.lerp(
geometry2x,
geometry3x,
fontSizeMultiplier - 2,
)!,
_ => geometry3x,
};
}
}
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
///
/// See also:
///
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
/// * [OutlinedButton], similar to [TextButton], but with an outline.
/// * [TextButton], a simple button without a shadow.
class _ButtonStyleState extends State<ButtonStyleButton>
with TickerProviderStateMixin {
AnimationController? controller;
double? elevation;
Color? backgroundColor;
WidgetStatesController? internalStatesController;
void handleStatesControllerChange() {
// Force a rebuild to resolve WidgetStateProperty properties
setState(() {});
}
WidgetStatesController get statesController =>
widget.statesController ?? internalStatesController!;
void initStatesController() {
if (widget.statesController == null) {
internalStatesController = WidgetStatesController();
}
statesController
..update(WidgetState.disabled, !widget.enabled)
..addListener(handleStatesControllerChange);
}
@override
void initState() {
super.initState();
initStatesController();
}
@override
void didUpdateWidget(ButtonStyleButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.statesController != oldWidget.statesController) {
oldWidget.statesController?.removeListener(handleStatesControllerChange);
if (widget.statesController != null) {
internalStatesController?.dispose();
internalStatesController = null;
}
initStatesController();
}
if (widget.enabled != oldWidget.enabled) {
statesController.update(WidgetState.disabled, !widget.enabled);
if (!widget.enabled) {
// The button may have been disabled while a press gesture is currently underway.
statesController.update(WidgetState.pressed, false);
}
}
}
@override
void dispose() {
statesController.removeListener(handleStatesControllerChange);
internalStatesController?.dispose();
controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final IconThemeData iconTheme = IconTheme.of(context);
final ButtonStyle? widgetStyle = widget.style;
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
final T? widgetValue = getProperty(widgetStyle);
final T? themeValue = getProperty(themeStyle);
final T? defaultValue = getProperty(defaultStyle);
return widgetValue ?? themeValue ?? defaultValue;
}
T? resolve<T>(
WidgetStateProperty<T>? Function(ButtonStyle? style) getProperty,
) {
return effectiveValue((ButtonStyle? style) {
return getProperty(style)?.resolve(statesController.value);
});
}
Color? effectiveIconColor() {
return widgetStyle?.iconColor?.resolve(statesController.value) ??
themeStyle?.iconColor?.resolve(statesController.value) ??
widgetStyle?.foregroundColor?.resolve(statesController.value) ??
themeStyle?.foregroundColor?.resolve(statesController.value) ??
defaultStyle.iconColor?.resolve(statesController.value) ??
// Fallback to foregroundColor if iconColor is null.
defaultStyle.foregroundColor?.resolve(statesController.value);
}
final double? resolvedElevation = resolve<double?>(
(ButtonStyle? style) => style?.elevation,
);
final TextStyle? resolvedTextStyle = resolve<TextStyle?>(
(ButtonStyle? style) => style?.textStyle,
);
Color? resolvedBackgroundColor = resolve<Color?>(
(ButtonStyle? style) => style?.backgroundColor,
);
final Color? resolvedForegroundColor = resolve<Color?>(
(ButtonStyle? style) => style?.foregroundColor,
);
final Color? resolvedShadowColor = resolve<Color?>(
(ButtonStyle? style) => style?.shadowColor,
);
final Color? resolvedSurfaceTintColor = resolve<Color?>(
(ButtonStyle? style) => style?.surfaceTintColor,
);
final EdgeInsetsGeometry? resolvedPadding = resolve<EdgeInsetsGeometry?>(
(ButtonStyle? style) => style?.padding,
);
final Size? resolvedMinimumSize = resolve<Size?>(
(ButtonStyle? style) => style?.minimumSize,
);
final Size? resolvedFixedSize = resolve<Size?>(
(ButtonStyle? style) => style?.fixedSize,
);
final Size? resolvedMaximumSize = resolve<Size?>(
(ButtonStyle? style) => style?.maximumSize,
);
final Color? resolvedIconColor = effectiveIconColor();
final double? resolvedIconSize = resolve<double?>(
(ButtonStyle? style) => style?.iconSize,
);
final BorderSide? resolvedSide = resolve<BorderSide?>(
(ButtonStyle? style) => style?.side,
);
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>(
(ButtonStyle? style) => style?.shape,
);
final WidgetStateMouseCursor mouseCursor = _MouseCursor(
(Set<WidgetState> states) => effectiveValue(
(ButtonStyle? style) => style?.mouseCursor?.resolve(states),
),
);
final WidgetStateProperty<Color?> overlayColor =
WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) => effectiveValue(
(ButtonStyle? style) => style?.overlayColor?.resolve(states),
),
);
final VisualDensity? resolvedVisualDensity = effectiveValue(
(ButtonStyle? style) => style?.visualDensity,
);
final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue(
(ButtonStyle? style) => style?.tapTargetSize,
);
final Duration? resolvedAnimationDuration = effectiveValue(
(ButtonStyle? style) => style?.animationDuration,
);
final bool resolvedEnableFeedback =
effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true;
final AlignmentGeometry? resolvedAlignment = effectiveValue(
(ButtonStyle? style) => style?.alignment,
);
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue(
(ButtonStyle? style) => style?.splashFactory,
);
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue(
(ButtonStyle? style) => style?.backgroundBuilder,
);
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue(
(ButtonStyle? style) => style?.foregroundBuilder,
);
final Clip effectiveClipBehavior =
widget.clipBehavior ??
((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null
? Clip.antiAlias
: Clip.none);
BoxConstraints effectiveConstraints = resolvedVisualDensity
.effectiveConstraints(
BoxConstraints(
minWidth: resolvedMinimumSize!.width,
minHeight: resolvedMinimumSize.height,
maxWidth: resolvedMaximumSize!.width,
maxHeight: resolvedMaximumSize.height,
),
);
if (resolvedFixedSize != null) {
final Size size = effectiveConstraints.constrain(resolvedFixedSize);
if (size.width.isFinite) {
effectiveConstraints = effectiveConstraints.copyWith(
minWidth: size.width,
maxWidth: size.width,
);
}
if (size.height.isFinite) {
effectiveConstraints = effectiveConstraints.copyWith(
minHeight: size.height,
maxHeight: size.height,
);
}
}
// Per the Material Design team: don't allow the VisualDensity
// adjustment to reduce the width of the left/right padding. If we
// did, VisualDensity.compact, the default for desktop/web, would
// reduce the horizontal padding to zero.
final double dy = densityAdjustment.dy;
final double dx = math.max(0, densityAdjustment.dx);
final EdgeInsetsGeometry padding = resolvedPadding!
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
// If an opaque button's background is becoming translucent while its
// elevation is changing, change the elevation first. Material implicitly
// animates its elevation but not its color. SKIA renders non-zero
// elevations as a shadow colored fill behind the Material's background.
if (resolvedAnimationDuration! > Duration.zero &&
elevation != null &&
backgroundColor != null &&
elevation != resolvedElevation &&
backgroundColor!.value != resolvedBackgroundColor!.value &&
backgroundColor!.opacity == 1 &&
resolvedBackgroundColor.opacity < 1 &&
resolvedElevation == 0) {
if (controller?.duration != resolvedAnimationDuration) {
controller?.dispose();
controller =
AnimationController(
duration: resolvedAnimationDuration,
vsync: this,
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {}); // Rebuild with the final background color.
}
});
}
resolvedBackgroundColor =
backgroundColor; // Defer changing the background color.
controller!.value = 0;
controller!.forward();
}
elevation = resolvedElevation;
backgroundColor = resolvedBackgroundColor;
Widget result = Padding(
padding: padding,
child: Align(
alignment: resolvedAlignment!,
widthFactor: 1.0,
heightFactor: 1.0,
child: resolvedForegroundBuilder != null
? resolvedForegroundBuilder(
context,
statesController.value,
widget.child,
)
: widget.child,
),
);
if (resolvedBackgroundBuilder != null) {
result = resolvedBackgroundBuilder(
context,
statesController.value,
result,
);
}
result = AnimatedTheme(
duration: resolvedAnimationDuration,
data: theme.copyWith(
iconTheme: iconTheme.merge(
IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
),
),
child: InkWell(
onTap: widget.onPressed,
onLongPress: widget.onLongPress,
onHover: widget.onHover,
mouseCursor: mouseCursor,
enableFeedback: resolvedEnableFeedback,
focusNode: widget.focusNode,
canRequestFocus: widget.enabled,
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
splashFactory: resolvedSplashFactory,
overlayColor: overlayColor,
highlightColor: Colors.transparent,
customBorder: resolvedShape!.copyWith(side: resolvedSide),
statesController: statesController,
child: result,
),
);
if (widget.tooltip != null) {
result = Tooltip(message: widget.tooltip, child: result);
}
final Size minSize;
switch (resolvedTapTargetSize!) {
case MaterialTapTargetSize.padded:
minSize = Size(
kMinInteractiveDimension + densityAdjustment.dx,
kMinInteractiveDimension + densityAdjustment.dy,
);
assert(minSize.width >= 0.0);
assert(minSize.height >= 0.0);
case MaterialTapTargetSize.shrinkWrap:
minSize = Size.zero;
}
return Semantics(
container: true,
button: widget.isSemanticButton,
enabled: widget.enabled,
child: _InputPadding(
minSize: minSize,
child: ConstrainedBox(
constraints: effectiveConstraints,
child: Material(
elevation: resolvedElevation!,
textStyle: resolvedTextStyle?.copyWith(
color: resolvedForegroundColor,
),
shape: resolvedShape.copyWith(side: resolvedSide),
color: resolvedBackgroundColor,
shadowColor: resolvedShadowColor,
surfaceTintColor: resolvedSurfaceTintColor,
type: resolvedBackgroundColor == null
? MaterialType.transparency
: MaterialType.button,
animationDuration: resolvedAnimationDuration,
clipBehavior: effectiveClipBehavior,
borderOnForeground: false,
child: result,
),
),
),
);
}
}
class _MouseCursor extends WidgetStateMouseCursor {
const _MouseCursor(this.resolveCallback);
final WidgetPropertyResolver<MouseCursor?> resolveCallback;
@override
MouseCursor resolve(Set<WidgetState> states) => resolveCallback(states)!;
@override
String get debugDescription => 'ButtonStyleButton_MouseCursor';
}
/// A widget to pad the area around a [ButtonStyleButton]'s inner [Material].
///
/// Redirect taps that occur in the padded area around the child to the center
/// of the child. This increases the size of the button and the button's
/// "tap target", but not its material or its ink splashes.
class _InputPadding extends SingleChildRenderObjectWidget {
const _InputPadding({super.child, required this.minSize});
final Size minSize;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderInputPadding(minSize);
}
@override
void updateRenderObject(
BuildContext context,
covariant _RenderInputPadding renderObject,
) {
renderObject.minSize = minSize;
}
}
class _RenderInputPadding extends RenderShiftedBox {
_RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
Size get minSize => _minSize;
Size _minSize;
set minSize(Size value) {
if (_minSize == value) {
return;
}
_minSize = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null) {
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null) {
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null) {
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null) {
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
}
return 0.0;
}
Size _computeSize({
required BoxConstraints constraints,
required ChildLayouter layoutChild,
}) {
if (child != null) {
final Size childSize = layoutChild(child!, constraints);
final double height = math.max(childSize.width, minSize.width);
final double width = math.max(childSize.height, minSize.height);
return constraints.constrain(Size(height, width));
}
return Size.zero;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.dryLayoutChild,
);
}
@override
double? computeDryBaseline(
covariant BoxConstraints constraints,
TextBaseline baseline,
) {
final RenderBox? child = this.child;
if (child == null) {
return null;
}
final double? result = child.getDryBaseline(constraints, baseline);
if (result == null) {
return null;
}
final Size childSize = child.getDryLayout(constraints);
return result +
Alignment.center
.alongOffset(getDryLayout(constraints) - childSize as Offset)
.dy;
}
@override
void performLayout() {
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild,
);
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Alignment.center.alongOffset(
size - child!.size as Offset,
);
}
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (super.hitTest(result, position: position)) {
return true;
}
final Offset center = child!.size.center(Offset.zero);
return result.addWithRawTransform(
transform: MatrixUtils.forceToPoint(center),
position: center,
hitTest: (BoxHitTestResult result, Offset position) {
assert(position == center);
return child!.hitTest(result, position: center);
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,676 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: uri_does_not_exist_in_doc_import
/// @docImport 'elevated_button.dart';
/// @docImport 'filled_button.dart';
/// @docImport 'material.dart';
/// @docImport 'outlined_button.dart';
library;
import 'dart:ui' show lerpDouble;
import 'package:PiliPlus/common/widgets/dyn/button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
/// A Material Design "Text Button".
///
/// Use text buttons on toolbars, in dialogs, or inline with other
/// content but offset from that content with padding so that the
/// button's presence is obvious. Text buttons do not have visible
/// borders and must therefore rely on their position relative to
/// other content for context. In dialogs and cards, they should be
/// grouped together in one of the bottom corners. Avoid using text
/// buttons where they would blend in with other content, for example
/// in the middle of lists.
///
/// A text button is a label [child] displayed on a (zero elevation)
/// [Material] widget. The label's [Text] and [Icon] widgets are
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
/// button reacts to touches by filling with the [style]'s
/// [ButtonStyle.backgroundColor].
///
/// The text button's default style is defined by [defaultStyleOf].
/// The style of this text button can be overridden with its [style]
/// parameter. The style of all text buttons in a subtree can be
/// overridden with the [TextButtonTheme] and the style of all of the
/// text buttons in an app can be overridden with the [Theme]'s
/// [ThemeData.textButtonTheme] property.
///
/// The static [styleFrom] method is a convenient way to create a
/// text button [ButtonStyle] from simple values.
///
/// If the [onPressed] and [onLongPress] callbacks are null, then this
/// button will be disabled, it will not react to touch.
///
/// {@tool dartpad}
/// This sample shows various ways to configure TextButtons, from the
/// simplest default appearance to versions that don't resemble
/// Material Design at all.
///
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample demonstrates using the [statesController] parameter to create a button
/// that adds support for [WidgetState.selected].
///
/// ** See code in examples/api/lib/material/text_button/text_button.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ElevatedButton], a filled button whose material elevates when pressed.
/// * [FilledButton], a filled button that doesn't elevate when pressed.
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
/// * [OutlinedButton], a button with an outlined border and no fill color.
/// * <https://material.io/design/components/buttons.html>
/// * <https://m3.material.io/components/buttons>
class TextButton extends ButtonStyleButton {
/// Create a [TextButton].
const TextButton({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
super.autofocus = false,
super.clipBehavior,
super.statesController,
super.isSemanticButton,
required Widget super.child,
});
/// Create a text button from a pair of widgets that serve as the button's
/// [icon] and [label].
///
/// The icon and label are arranged in a row and padded by 8 logical pixels
/// at the ends, with an 8 pixel gap in between.
///
/// If [icon] is null, will create a [TextButton] instead.
///
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
///
factory TextButton.icon({
Key? key,
required VoidCallback? onPressed,
VoidCallback? onLongPress,
ValueChanged<bool>? onHover,
ValueChanged<bool>? onFocusChange,
ButtonStyle? style,
FocusNode? focusNode,
bool? autofocus,
Clip? clipBehavior,
WidgetStatesController? statesController,
Widget? icon,
required Widget label,
IconAlignment? iconAlignment,
}) {
if (icon == null) {
return TextButton(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
statesController: statesController,
child: label,
);
}
return _TextButtonWithIcon(
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
onHover: onHover,
onFocusChange: onFocusChange,
style: style,
focusNode: focusNode,
autofocus: autofocus ?? false,
clipBehavior: clipBehavior ?? Clip.none,
statesController: statesController,
icon: icon,
label: label,
iconAlignment: iconAlignment,
);
}
/// A static convenience method that constructs a text button
/// [ButtonStyle] given simple values.
///
/// The [foregroundColor] and [disabledForegroundColor] colors are used
/// to create a [WidgetStateProperty] [ButtonStyle.foregroundColor], and
/// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
///
/// The [backgroundColor] and [disabledBackgroundColor] colors are
/// used to create a [WidgetStateProperty] [ButtonStyle.backgroundColor].
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle.mouseCursor].
///
/// The [iconColor], [disabledIconColor] are used to construct
/// [ButtonStyle.iconColor] and [iconSize] is used to construct
/// [ButtonStyle.iconSize].
///
/// If [iconColor] is null, the button icon will use [foregroundColor]. If [foregroundColor] is also
/// null, the button icon will use the default icon color.
///
/// If [overlayColor] is specified and its value is [Colors.transparent]
/// then the pressed/focused/hovered highlights are effectively defeated.
/// Otherwise a [WidgetStateProperty] with the same opacities as the
/// default is created.
///
/// All of the other parameters are either used directly or used to
/// create a [WidgetStateProperty] with a single value for all
/// states.
///
/// All parameters default to null. By default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default text and icon colors for a
/// [TextButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// TextButton(
/// style: TextButton.styleFrom(foregroundColor: Colors.green),
/// child: const Text('Give Kate a mix tape'),
/// onPressed: () {
/// // ...
/// },
/// ),
/// ```
static ButtonStyle styleFrom({
Color? foregroundColor,
Color? backgroundColor,
Color? disabledForegroundColor,
Color? disabledBackgroundColor,
Color? shadowColor,
Color? surfaceTintColor,
Color? iconColor,
double? iconSize,
IconAlignment? iconAlignment,
Color? disabledIconColor,
Color? overlayColor,
double? elevation,
TextStyle? textStyle,
EdgeInsetsGeometry? padding,
Size? minimumSize,
Size? fixedSize,
Size? maximumSize,
BorderSide? side,
OutlinedBorder? shape,
MouseCursor? enabledMouseCursor,
MouseCursor? disabledMouseCursor,
VisualDensity? visualDensity,
MaterialTapTargetSize? tapTargetSize,
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
ButtonLayerBuilder? backgroundBuilder,
ButtonLayerBuilder? foregroundBuilder,
}) {
final WidgetStateProperty<Color?>? backgroundColorProp = switch ((
backgroundColor,
disabledBackgroundColor,
)) {
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
(_, _) => ButtonStyleButton.defaultColor(
backgroundColor,
disabledBackgroundColor,
),
};
final WidgetStateProperty<Color?>? iconColorProp = switch ((
iconColor,
disabledIconColor,
)) {
(_?, null) => WidgetStatePropertyAll<Color?>(iconColor),
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
};
final WidgetStateProperty<Color?>? overlayColorProp = switch ((
foregroundColor,
overlayColor,
)) {
(null, null) => null,
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
(_, final Color color) || (final Color color, _) =>
WidgetStateProperty<Color?>.fromMap(<WidgetState, Color?>{
WidgetState.pressed: color.withValues(alpha: 0.1),
WidgetState.hovered: color.withValues(alpha: 0.08),
WidgetState.focused: color.withValues(alpha: 0.1),
}),
};
return ButtonStyle(
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
foregroundColor: ButtonStyleButton.defaultColor(
foregroundColor,
disabledForegroundColor,
),
backgroundColor: backgroundColorProp,
overlayColor: overlayColorProp,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
iconColor: iconColorProp,
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
iconAlignment: iconAlignment,
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
<WidgetStatesConstraint, MouseCursor?>{
WidgetState.disabled: disabledMouseCursor,
WidgetState.any: enabledMouseCursor,
},
),
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
backgroundBuilder: backgroundBuilder,
foregroundBuilder: foregroundBuilder,
);
}
/// Defines the button's default appearance.
///
/// {@template flutter.material.text_button.default_style_of}
/// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// color and is transparent by default.
///
/// All of the [ButtonStyle]'s defaults appear below.
///
/// In this list "Theme.foo" is shorthand for
/// `Theme.of(context).foo`. Color scheme values like
/// "onSurface(0.38)" are shorthand for
/// `onSurface.withValues(alpha: 0.38)`. [WidgetStateProperty] valued
/// properties that are not followed by a sublist have the same
/// value for all states, otherwise the values are as specified for
/// each state and "others" means all other states.
///
/// The "default font size" below refers to the font size specified in the
/// [defaultStyleOf] method (or 14.0 if unspecified), scaled by the
/// `MediaQuery.textScalerOf(context).scale` method. And the names of the
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been abbreviated
/// for readability.
///
/// The color of the [ButtonStyle.textStyle] is not used, the
/// [ButtonStyle.foregroundColor] color is used instead.
/// {@endtemplate}
///
/// ## Material 2 defaults
///
/// * `textStyle` - Theme.textTheme.button
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * `shadowColor` - Theme.shadowColor
/// * `elevation` - 0
/// * `padding`
/// * `default font size <= 14` - (horizontal(12), vertical(8))
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
/// * `36 < default font size` - horizontal(4)
/// * `minimumSize` - Size(64, 36)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `side` - null
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.basic
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - InkRipple.splashFactory
///
/// The default padding values for the [TextButton.icon] factory are slightly different:
///
/// * `padding`
/// * `default font size <= 14` - all(8)
/// * `14 < default font size <= 28 `- lerp(all(8), horizontal(4))
/// * `28 < default font size` - horizontal(4)
///
/// The default value for `side`, which defines the appearance of the button's
/// outline, is null. That means that the outline is defined by the button
/// shape's [OutlinedBorder.side]. Typically the default value of an
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
///
/// ## Material 3 defaults
///
/// If [ThemeData.useMaterial3] is set to true the following defaults will
/// be used:
///
/// {@template flutter.material.text_button.material3_defaults}
/// * `textStyle` - Theme.textTheme.labelLarge
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.primary
/// * `overlayColor`
/// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.1)
/// * others - null
/// * `shadowColor` - Colors.transparent,
/// * `surfaceTintColor` - null
/// * `elevation` - 0
/// * `padding`
/// * `default font size <= 14` - lerp(horizontal(12), horizontal(4))
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
/// * `36 < default font size` - horizontal(4)
/// * `minimumSize` - Size(64, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `side` - null
/// * `shape` - StadiumBorder()
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.basic
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
///
/// For the [TextButton.icon] factory, the end (generally the right) value of
/// `padding` is increased from 12 to 16.
/// {@endtemplate}
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
return Theme.of(context).useMaterial3
? _TextButtonDefaultsM3(context)
: styleFrom(
foregroundColor: colorScheme.primary,
disabledForegroundColor: colorScheme.onSurface.withValues(
alpha: 0.38,
),
backgroundColor: Colors.transparent,
disabledBackgroundColor: Colors.transparent,
shadowColor: theme.shadowColor,
elevation: 0,
textStyle: theme.textTheme.labelLarge,
padding: _scaledPadding(context),
minimumSize: const Size(64, 36),
maximumSize: Size.infinite,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
enabledMouseCursor: SystemMouseCursors.click,
disabledMouseCursor: SystemMouseCursors.basic,
visualDensity: theme.visualDensity,
tapTargetSize: theme.materialTapTargetSize,
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
splashFactory: InkRipple.splashFactory,
);
}
/// Returns the [TextButtonThemeData.style] of the closest
/// [TextButtonTheme] ancestor.
@override
ButtonStyle? themeStyleOf(BuildContext context) {
return TextButtonTheme.of(context).style;
}
}
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
final ThemeData theme = Theme.of(context);
final double defaultFontSize = theme.textTheme.labelLarge?.fontSize ?? 14.0;
final double effectiveTextScale =
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
return ButtonStyleButton.scaledPadding(
theme.useMaterial3
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
: const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
effectiveTextScale,
);
}
class _TextButtonWithIcon extends TextButton {
_TextButtonWithIcon({
super.key,
required super.onPressed,
super.onLongPress,
super.onHover,
super.onFocusChange,
super.style,
super.focusNode,
bool? autofocus,
super.clipBehavior,
super.statesController,
required Widget icon,
required Widget label,
IconAlignment? iconAlignment,
}) : super(
autofocus: autofocus ?? false,
child: _TextButtonWithIconChild(
icon: icon,
label: label,
buttonStyle: style,
iconAlignment: iconAlignment,
),
);
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final bool useMaterial3 = Theme.of(context).useMaterial3;
final ButtonStyle buttonStyle = super.defaultStyleOf(context);
final double defaultFontSize =
buttonStyle.textStyle?.resolve(const <WidgetState>{})?.fontSize ?? 14.0;
final double effectiveTextScale =
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
useMaterial3
? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8)
: const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 4),
const EdgeInsets.symmetric(horizontal: 4),
effectiveTextScale,
);
return buttonStyle.copyWith(
padding: WidgetStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
);
}
}
class _TextButtonWithIconChild extends StatelessWidget {
const _TextButtonWithIconChild({
required this.label,
required this.icon,
required this.buttonStyle,
required this.iconAlignment,
});
final Widget label;
final Widget icon;
final ButtonStyle? buttonStyle;
final IconAlignment? iconAlignment;
@override
Widget build(BuildContext context) {
final double defaultFontSize =
buttonStyle?.textStyle?.resolve(const <WidgetState>{})?.fontSize ??
14.0;
final double scale =
clampDouble(
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0,
1.0,
2.0,
) -
1.0;
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
final IconAlignment effectiveIconAlignment =
iconAlignment ??
textButtonTheme.style?.iconAlignment ??
buttonStyle?.iconAlignment ??
IconAlignment.start;
return Row(
mainAxisSize: MainAxisSize.min,
spacing: lerpDouble(8, 4, scale)!,
children: effectiveIconAlignment == IconAlignment.start
? <Widget>[icon, Flexible(child: label)]
: <Widget>[Flexible(child: label), icon],
);
}
}
// BEGIN GENERATED TOKEN PROPERTIES - TextButton
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
class _TextButtonDefaultsM3 extends ButtonStyle {
_TextButtonDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
@override
WidgetStateProperty<TextStyle?> get textStyle =>
WidgetStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
@override
WidgetStateProperty<Color?>? get backgroundColor =>
const WidgetStatePropertyAll<Color>(Colors.transparent);
@override
WidgetStateProperty<Color?>? get foregroundColor =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return _colors.onSurface.withValues(alpha: 0.38);
}
return _colors.primary;
});
@override
WidgetStateProperty<Color?>? get overlayColor =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return _colors.primary.withValues(alpha: 0.1);
}
if (states.contains(WidgetState.hovered)) {
return _colors.primary.withValues(alpha: 0.08);
}
if (states.contains(WidgetState.focused)) {
return _colors.primary.withValues(alpha: 0.1);
}
return null;
});
@override
WidgetStateProperty<Color>? get shadowColor =>
const WidgetStatePropertyAll<Color>(Colors.transparent);
@override
WidgetStateProperty<Color>? get surfaceTintColor =>
const WidgetStatePropertyAll<Color>(Colors.transparent);
@override
WidgetStateProperty<double>? get elevation =>
const WidgetStatePropertyAll<double>(0.0);
@override
WidgetStateProperty<EdgeInsetsGeometry>? get padding =>
WidgetStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context));
@override
WidgetStateProperty<Size>? get minimumSize =>
const WidgetStatePropertyAll<Size>(Size(64.0, 40.0));
// No default fixedSize
@override
WidgetStateProperty<double>? get iconSize =>
const WidgetStatePropertyAll<double>(18.0);
@override
WidgetStateProperty<Color>? get iconColor {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return _colors.onSurface.withValues(alpha: 0.38);
}
if (states.contains(WidgetState.pressed)) {
return _colors.primary;
}
if (states.contains(WidgetState.hovered)) {
return _colors.primary;
}
if (states.contains(WidgetState.focused)) {
return _colors.primary;
}
return _colors.primary;
});
}
@override
WidgetStateProperty<Size>? get maximumSize =>
const WidgetStatePropertyAll<Size>(Size.infinite);
// No default side
@override
WidgetStateProperty<OutlinedBorder>? get shape =>
const WidgetStatePropertyAll<OutlinedBorder>(StadiumBorder());
@override
WidgetStateProperty<MouseCursor?>? get mouseCursor =>
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
});
@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// dart format on
// END GENERATED TOKEN PROPERTIES - TextButton

View File

@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
class DynamicSliverAppBar extends StatefulWidget {
const DynamicSliverAppBar({
class DynamicSliverAppBarMedium extends StatefulWidget {
const DynamicSliverAppBarMedium({
this.flexibleSpace,
super.key,
this.leading,
@@ -88,22 +88,17 @@ class DynamicSliverAppBar extends StatefulWidget {
final CustomClipper<Path>? appBarClipper;
@override
State<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
State<DynamicSliverAppBarMedium> createState() =>
_DynamicSliverAppBarMediumState();
}
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
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;
@override
void initState() {
super.initState();
_updateHeight();
}
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
@@ -118,11 +113,21 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
});
}
Orientation? _orientation;
late Size size;
@override
void didChangeDependencies() {
_height = 0;
_updateHeight();
super.didChangeDependencies();
size = MediaQuery.sizeOf(context);
final orientation = size.width > size.height
? Orientation.landscape
: Orientation.portrait;
if (orientation != _orientation) {
_orientation = orientation;
_height = 0;
_updateHeight();
}
}
@override
@@ -130,16 +135,19 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
if (_height == 0) {
return SliverToBoxAdapter(
child: SizedBox(
key: _childKey,
child: widget.flexibleSpace ?? SizedBox(height: kToolbarHeight),
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: SizedBox(
key: _childKey,
width: size.width,
child: widget.flexibleSpace,
),
),
);
}
MediaQuery.orientationOf(context);
return SliverAppBar(
final padding = MediaQuery.viewPaddingOf(context).top;
return SliverAppBar.medium(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
@@ -158,7 +166,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
collapsedHeight: widget.collapsedHeight,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
@@ -166,8 +173,9 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: widget.toolbarHeight,
expandedHeight: _height,
toolbarHeight: kToolbarHeight,
collapsedHeight: kToolbarHeight + padding + 1,
expandedHeight: _height - padding,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,

View File

@@ -1,656 +0,0 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/bangumi/info.dart' as bangumi;
import 'package:PiliPlus/models/video_detail_res.dart' as video;
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/video/detail/controller.dart';
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/storage.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';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
enum EpisodeType { part, season, bangumi }
extension EpisodeTypeExt on EpisodeType {
String get title => ['分P', '合集', '番剧'][index];
}
class EpisodePanel extends CommonSlidePage {
const EpisodePanel({
super.key,
super.enableSlide,
required this.videoIntroController,
required this.heroTag,
required this.type,
// required this.count,
// required this.name,
required this.aid,
required this.bvid,
required this.cid,
required this.cover,
this.showTitle,
required this.list,
this.seasonId,
this.initialTabIndex = 0,
this.isSupportReverse,
this.isReversed,
this.onReverse,
required this.changeFucCall,
this.onClose,
});
final VideoIntroController videoIntroController;
final String heroTag;
final EpisodeType type;
// final int count;
// final String name;
final int? aid;
final String bvid;
final int cid;
final String? cover;
final bool? showTitle;
final List list;
final int? seasonId;
final int initialTabIndex;
final bool? isSupportReverse;
final bool? isReversed;
final Function changeFucCall;
final VoidCallback? onReverse;
final VoidCallback? onClose;
@override
State<EpisodePanel> createState() => _EpisodePanelState();
}
class _EpisodePanelState extends CommonSlidePageState<EpisodePanel>
with SingleTickerProviderStateMixin {
// tab
late final TabController _tabController = TabController(
initialIndex: widget.initialTabIndex,
length: widget.list.length,
vsync: this,
)..addListener(listener);
late final RxInt _currentTabIndex = _tabController.index.obs;
List get _getCurrEpisodes => widget.type == EpisodeType.season
? widget.list[_currentTabIndex.value].episodes
: widget.list[_currentTabIndex.value];
// item
late RxInt _currentItemIndex;
int get _findCurrentItemIndex => max(
0,
_getCurrEpisodes.indexWhere((item) => item.cid == widget.cid),
);
late final List<bool> _isReversed;
late final List<ItemScrollController> _itemScrollController;
// fav
Rx<LoadingState>? _favState;
late bool _isInit = true;
void listener() {
_currentTabIndex.value = _tabController.index;
}
@override
void didUpdateWidget(EpisodePanel oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.showTitle != false) {
return;
}
void jumpToCurrent() {
int newItemIndex = _findCurrentItemIndex;
if (_currentItemIndex.value != _findCurrentItemIndex) {
_currentItemIndex.value = newItemIndex;
try {
_itemScrollController[_currentTabIndex.value].jumpTo(
index: newItemIndex,
);
} catch (_) {}
}
}
// jump to current
if (_currentTabIndex.value != widget.initialTabIndex) {
_tabController.animateTo(
widget.initialTabIndex,
duration: const Duration(milliseconds: 200),
);
Future.delayed(const Duration(milliseconds: 300)).then((_) {
jumpToCurrent();
});
} else {
jumpToCurrent();
}
}
@override
void initState() {
super.initState();
_itemScrollController =
List.generate(widget.list.length, (_) => ItemScrollController());
_isReversed = List.generate(widget.list.length, (_) => false);
if (widget.type == EpisodeType.season && Accounts.main.isLogin) {
_favState = LoadingState.loading().obs;
VideoHttp.videoRelation(bvid: widget.bvid).then((result) {
if (result['status']) {
if (result['data']?['season_fav'] is bool) {
_favState!.value =
LoadingState.success(result['data']['season_fav']);
}
}
});
}
_currentItemIndex = _findCurrentItemIndex.obs;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isInit = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
_itemScrollController[widget.initialTabIndex]
.jumpTo(index: _currentItemIndex.value);
} catch (_) {}
});
}
});
}
@override
void dispose() {
_tabController.removeListener(listener);
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_isInit) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
);
}
return super.build(context);
}
@override
Widget buildPage(ThemeData theme) {
return Material(
color: widget.showTitle == false
? Colors.transparent
: theme.colorScheme.surface,
child: Column(
children: [
_buildToolbar(theme),
if (widget.type == EpisodeType.season && widget.list.length > 1) ...[
TabBar(
controller: _tabController,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
tabs: widget.list.map((item) => Tab(text: item.title)).toList(),
dividerHeight: 1,
dividerColor: theme.dividerColor.withOpacity(0.1),
),
Expanded(
child: Material(
color: Colors.transparent,
child: tabBarView(
controller: _tabController,
children: List.generate(
widget.list.length,
(index) => _buildBody(
theme,
index,
widget.list[index].episodes,
),
),
),
),
),
] else
Expanded(
child: enableSlide ? slideList(theme) : buildList(theme),
),
],
),
);
}
@override
Widget buildList(ThemeData theme) {
return Material(
color: Colors.transparent,
child: _buildBody(theme, 0, _getCurrEpisodes),
);
}
Widget _buildBody(ThemeData theme, int index, episodes) {
return KeepAliveWrapper(
builder: (context) => ScrollablePositionedList.separated(
padding: EdgeInsets.only(
top: 7,
bottom: MediaQuery.of(context).padding.bottom + 80,
),
reverse: _isReversed[index],
itemCount: episodes.length,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final episode = episodes[index];
return widget.type == EpisodeType.season &&
widget.showTitle != false &&
episode.pages.length > 1
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(
() => _buildEpisodeItem(
theme: theme,
episode: episode,
index: index,
length: episodes.length,
isCurrentIndex:
_currentTabIndex.value == widget.initialTabIndex
? _currentItemIndex.value == index
: false,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 5),
child: PagesPanel(
list:
widget.initialTabIndex == _currentTabIndex.value &&
index == _currentItemIndex.value
? null
: episode.pages,
cover: episode.arc?.pic,
heroTag: widget.heroTag,
videoIntroController: widget.videoIntroController,
bvid: IdUtils.av2bv(episode.aid),
),
),
],
)
: Obx(
() => _buildEpisodeItem(
theme: theme,
episode: episode,
index: index,
length: episodes.length,
isCurrentIndex:
_currentTabIndex.value == widget.initialTabIndex
? _currentItemIndex.value == index
: false,
),
);
},
itemScrollController: _itemScrollController[index],
separatorBuilder: (context, index) => const SizedBox(height: 2),
),
);
}
Widget _buildEpisodeItem({
required ThemeData theme,
required dynamic episode,
required int index,
required int length,
required bool isCurrentIndex,
}) {
late String title;
String? cover;
num? duration;
int? pubdate;
int? view;
int? danmaku;
switch (episode) {
case video.Part():
cover = episode.firstFrame ?? widget.cover;
title = episode.pagePart!;
duration = episode.duration;
pubdate = episode.ctime;
break;
case video.EpisodeItem():
title = episode.title!;
cover = episode.arc?.pic;
duration = episode.arc?.duration;
pubdate = episode.arc?.pubdate;
view = episode.arc?.stat?.view;
danmaku = episode.arc?.stat?.danmaku;
break;
case bangumi.EpisodeItem():
if (episode.longTitle != null && episode.longTitle != "") {
dynamic leading = episode.title ?? index + 1;
title =
"${Utils.isStringNumeric(leading) ? '$leading话' : leading} ${episode.longTitle!}";
} else {
title = episode.title!;
}
cover = episode.cover;
duration = episode.duration == null ? null : episode.duration! ~/ 1000;
pubdate = episode.pubTime;
break;
}
late final Color primary = theme.colorScheme.primary;
return Material(
color: Colors.transparent,
child: SizedBox(
height: 98,
child: InkWell(
onTap: () {
if (episode.badge != null && episode.badge == "会员") {
dynamic userInfo = GStorage.userInfo.get('userInfoCache');
int vipStatus = 0;
if (userInfo != null) {
vipStatus = userInfo.vipStatus;
}
if (vipStatus != 1) {
SmartDialog.showToast('需要大会员');
// return;
}
}
SmartDialog.showToast('切换到:$title');
widget.onClose?.call();
if (widget.showTitle == false) {
_currentItemIndex.value = index;
}
widget.changeFucCall(
episode is bangumi.EpisodeItem ? episode.epId : null,
episode.runtimeType.toString() == "EpisodeItem"
? episode.bvid
: widget.bvid,
episode.cid,
episode.runtimeType.toString() == "EpisodeItem"
? episode.aid
: widget.aid,
cover,
);
if (widget.type == EpisodeType.season) {
try {
Get.find<VideoDetailController>(
tag: widget.videoIntroController.heroTag)
.seasonCid = episode.cid;
} catch (_) {}
}
},
onLongPress: () {
if (cover?.isNotEmpty == true) {
imageSaveDialog(title: title, cover: cover);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (cover?.isNotEmpty == true)
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
),
if (duration != null && duration > 0)
PBadge(
text: Utils.timeFormat(duration),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
),
)
else if (isCurrentIndex)
Image.asset(
'assets/images/live.png',
color: primary,
height: 12,
semanticLabel: "正在播放:",
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
title,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
fontWeight: isCurrentIndex ? FontWeight.bold : null,
color: isCurrentIndex ? primary : null,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (pubdate != null)
Text(
Utils.dateFormat(pubdate),
maxLines: 1,
style: TextStyle(
fontSize: 12,
height: 1,
color: theme.colorScheme.outline,
overflow: TextOverflow.clip,
),
),
if (view != null) ...[
const SizedBox(height: 2),
Row(
children: [
StatView(
context: context,
theme: 'gray',
value: view,
),
if (danmaku != null) ...[
const SizedBox(width: 8),
StatDanMu(
context: context,
theme: 'gray',
value: danmaku,
),
],
],
),
],
],
),
),
if (episode.badge != null) ...[
if (episode.badge == '会员')
Image.asset(
'assets/images/big-vip.png',
height: 20,
semanticLabel: "大会员",
)
else
Text(episode.badge),
const SizedBox(width: 10),
],
],
),
),
),
),
);
}
Widget _buildFavBtn(LoadingState loadingState) {
return switch (loadingState) {
Success() => mediumButton(
tooltip: loadingState.response ? '取消订阅' : '订阅',
icon: loadingState.response
? Icons.notifications_off_outlined
: Icons.notifications_active_outlined,
onPressed: () async {
dynamic result = await VideoHttp.seasonFav(
isFav: loadingState.response,
seasonId: widget.seasonId,
);
if (result['status']) {
SmartDialog.showToast('${loadingState.response ? '取消' : ''}订阅成功');
_favState!.value = LoadingState.success(!loadingState.response);
} else {
SmartDialog.showToast(result['msg']);
}
},
),
_ => const SizedBox.shrink(),
};
}
Widget get _buildReverseBtn => mediumButton(
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',
icon: widget.isReversed == true
? MdiIcons.sortDescending
: MdiIcons.sortAscending,
onPressed: () {
widget.onReverse?.call();
},
);
Widget _buildToolbar(ThemeData theme) => Container(
height: 45,
padding: EdgeInsets.symmetric(
horizontal: widget.showTitle != false ? 14 : 6),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.1),
),
),
),
child: Row(
children: [
if (widget.showTitle != false)
Text(
widget.type.title,
style: theme.textTheme.titleMedium,
),
if (_favState != null) Obx(() => _buildFavBtn(_favState!.value)),
mediumButton(
tooltip: '跳至顶部',
icon: Icons.vertical_align_top,
onPressed: () {
try {
_itemScrollController[_currentTabIndex.value].scrollTo(
index: !_isReversed[_currentTabIndex.value]
? 0
: _getCurrEpisodes.length - 1,
duration: const Duration(milliseconds: 200),
);
} catch (e) {
debugPrint('to top: $e');
}
},
),
mediumButton(
tooltip: '跳至底部',
icon: Icons.vertical_align_bottom,
onPressed: () {
try {
_itemScrollController[_currentTabIndex.value].scrollTo(
index: !_isReversed[_currentTabIndex.value]
? _getCurrEpisodes.length - 1
: 0,
duration: const Duration(milliseconds: 200),
);
} catch (e) {
debugPrint('to bottom: $e');
}
},
),
mediumButton(
tooltip: '跳至当前',
icon: Icons.my_location,
onPressed: () async {
try {
if (_currentTabIndex.value != widget.initialTabIndex) {
_tabController.animateTo(widget.initialTabIndex);
await Future.delayed(const Duration(milliseconds: 225));
}
_itemScrollController[_currentTabIndex.value].scrollTo(
index: _currentItemIndex.value,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
if (widget.isSupportReverse == true)
Obx(
() {
return _currentTabIndex.value == widget.initialTabIndex
? _buildReverseBtn
: const SizedBox.shrink();
},
),
const Spacer(),
Obx(
() => mediumButton(
tooltip: _isReversed[_currentTabIndex.value] ? '顺序' : '倒序',
icon: !_isReversed[_currentTabIndex.value]
? MdiIcons.sortNumericAscending
: MdiIcons.sortNumericDescending,
onPressed: () {
setState(() {
_isReversed[_currentTabIndex.value] =
!_isReversed[_currentTabIndex.value];
});
},
),
),
if (widget.onClose != null)
mediumButton(
tooltip: '关闭',
icon: Icons.close,
onPressed: widget.onClose,
),
],
),
);
}

View File

@@ -1,9 +1,8 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/utils/download.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -11,18 +10,37 @@ import 'package:get/get.dart';
void imageSaveDialog({
required String? title,
required String? cover,
dynamic aid,
String? bvid,
}) {
final double imgWidth = min(Get.width, Get.height) - 8 * 2;
final double imgWidth = Get.mediaQuery.size.shortestSide - 8 * 2;
SmartDialog.show(
animationType: SmartAnimationType.centerScale_otherSlide,
builder: (context) {
final theme = Theme.of(context);
late final iconColor = theme.colorScheme.onSurfaceVariant;
Widget iconBtn({
String? tooltip,
required IconData icon,
required VoidCallback? onPressed,
}) {
return iconButton(
context: context,
onPressed: onPressed,
iconSize: 20,
icon: icon,
bgColor: Colors.transparent,
iconColor: iconColor,
);
}
return Container(
width: imgWidth,
margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(10.0),
borderRadius: StyleString.mdRadius,
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -46,15 +64,15 @@ void imageSaveDialog({
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
color: Colors.black.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: IconButton(
child: const IconButton(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
padding: WidgetStatePropertyAll(EdgeInsets.zero),
),
onPressed: SmartDialog.dismiss,
icon: const Icon(
icon: Icon(
Icons.close,
size: 18,
color: Colors.white,
@@ -68,31 +86,39 @@ void imageSaveDialog({
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
children: [
Expanded(
child: SelectableText(
title ?? '',
style: theme.textTheme.titleSmall,
if (title != null)
Expanded(
child: SelectableText(
title,
style: theme.textTheme.titleSmall,
),
)
else
const Spacer(),
if (aid != null || bvid != null)
iconBtn(
tooltip: '稍后再看',
onPressed: () => {
SmartDialog.dismiss(),
UserHttp.toViewLater(aid: aid, bvid: bvid).then(
(res) => SmartDialog.showToast(res['msg']),
),
},
icon: Icons.watch_later_outlined,
),
),
if (cover?.isNotEmpty == true) ...[
const SizedBox(width: 4),
iconButton(
context: context,
iconBtn(
tooltip: '分享',
onPressed: () {
SmartDialog.dismiss();
DownloadUtils.onShareImg(cover!);
ImageUtil.onShareImg(cover!);
},
iconSize: 20,
icon: Icons.share,
bgColor: Colors.transparent,
iconColor: theme.colorScheme.onSurfaceVariant,
),
iconButton(
context: context,
iconBtn(
tooltip: '保存封面图',
onPressed: () async {
bool saveStatus = await DownloadUtils.downloadImg(
bool saveStatus = await ImageUtil.downloadImg(
context,
[cover!],
);
@@ -100,10 +126,7 @@ void imageSaveDialog({
SmartDialog.dismiss();
}
},
iconSize: 20,
icon: Icons.download,
bgColor: Colors.transparent,
iconColor: theme.colorScheme.onSurfaceVariant,
),
],
],

View File

@@ -0,0 +1,200 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/image/nine_grid_view.dart';
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart';
class ImageModel {
ImageModel({
required num? width,
required num? height,
required this.url,
this.liveUrl,
}) {
this.width = width == null || width == 0 ? 1 : width;
this.height = height == null || height == 0 ? 1 : height;
}
late num width;
late num height;
String url;
String? liveUrl;
bool? _isLongPic;
bool? _isLivePhoto;
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
bool get isLivePhoto =>
_isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
static bool enableLivePhoto = Pref.enableLivePhoto;
}
const double _maxRatio = 22 / 9;
Widget imageView(
double maxWidth,
List<ImageModel> picArr, {
VoidCallback? onViewImage,
ValueChanged<int>? onDismissed,
Function(List<String>, int)? callback,
}) {
double imageWidth = (maxWidth - 10) / 3;
double imageHeight = imageWidth;
if (picArr.length == 1) {
num width = picArr[0].width;
num height = picArr[0].height;
double ratioWH = width / height;
double ratioHW = height / width;
imageWidth = ratioWH > 1.5
? maxWidth
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
? 2 * imageWidth
: 1.5 * imageWidth;
if (width != 1) {
imageWidth = min(imageWidth, width.toDouble());
}
imageHeight = imageWidth * min(ratioHW, _maxRatio);
} else if (picArr.length == 2) {
imageWidth = imageHeight = 2 * imageWidth;
}
late final int row = picArr.length == 4 ? 2 : 3;
BorderRadius borderRadius(index) {
if (picArr.length == 1) {
return StyleString.mdRadius;
}
return BorderRadius.only(
topLeft:
index - row >= 0 ||
((index - 1) >= 0 && (index - 1) % row < index % row)
? Radius.zero
: StyleString.imgRadius,
topRight:
index - row >= 0 ||
((index + 1) < picArr.length && (index + 1) % row > index % row)
? Radius.zero
: StyleString.imgRadius,
bottomLeft:
index + row < picArr.length ||
((index - 1) >= 0 && (index - 1) % row < index % row)
? Radius.zero
: StyleString.imgRadius,
bottomRight:
index + row < picArr.length ||
((index + 1) < picArr.length && (index + 1) % row > index % row)
? Radius.zero
: StyleString.imgRadius,
);
}
int parseSize(size) {
return switch (size) {
int() => size,
double() => size.round(),
String() => int.tryParse(size) ?? 1,
_ => 1,
};
}
void onTap(int index) {
if (callback != null) {
callback(picArr.map((item) => item.url).toList(), index);
} else {
onViewImage?.call();
PageUtils.imageView(
initialPage: index,
imgList: picArr.map(
(item) {
bool isLive = item.isLivePhoto;
return SourceModel(
sourceType: isLive
? SourceType.livePhoto
: SourceType.networkImage,
url: item.url,
liveUrl: isLive ? item.liveUrl : null,
width: isLive ? parseSize(item.width) : null,
height: isLive ? parseSize(item.height) : null,
);
},
).toList(),
onDismissed: onDismissed,
);
}
}
return NineGridView(
type: NineGridType.weiBo,
margin: const EdgeInsets.only(top: 6),
bigImageWidth: imageWidth,
bigImageHeight: imageHeight,
space: 5,
height: picArr.length == 1 ? imageHeight : null,
width: picArr.length == 1 ? imageWidth : maxWidth,
itemCount: picArr.length,
itemBuilder: (context, index) {
final item = picArr[index];
return Hero(
tag: item.url,
child: GestureDetector(
onTap: () => onTap(index),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: borderRadius(index),
child: NetworkImgLayer(
radius: 0,
src: item.url,
width: imageWidth,
height: imageHeight,
isLongPic: item.isLongPic,
forceUseCacheWidth: item.width <= item.height,
getPlaceHolder: () {
return Container(
width: imageWidth,
height: imageHeight,
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
borderRadius: borderRadius(index),
),
child: Center(
child: Image.asset(
'assets/images/loading.png',
width: imageWidth,
height: imageHeight,
cacheWidth: imageWidth.cacheSize(context),
),
),
);
},
),
),
if (item.isLivePhoto)
const PBadge(
text: 'Live',
right: 8,
bottom: 8,
type: PBadgeType.gray,
)
else if (item.isLongPic)
const PBadge(
text: '长图',
right: 8,
bottom: 8,
),
],
),
),
);
},
);
}

View File

@@ -0,0 +1,134 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class NetworkImgLayer extends StatelessWidget {
const NetworkImgLayer({
super.key,
required this.src,
required this.width,
this.height,
this.type = ImageType.def,
this.fadeOutDuration,
this.fadeInDuration,
// 图片质量 默认1%
this.quality,
this.semanticsLabel,
this.radius,
this.imageBuilder,
this.isLongPic = false,
this.forceUseCacheWidth = false,
this.getPlaceHolder,
this.boxFit,
});
final String? src;
final double width;
final double? height;
final ImageType type;
final Duration? fadeOutDuration;
final Duration? fadeInDuration;
final int? quality;
final String? semanticsLabel;
final double? radius;
final ImageWidgetBuilder? imageBuilder;
final bool isLongPic;
final bool forceUseCacheWidth;
final Widget Function()? getPlaceHolder;
final BoxFit? boxFit;
static Color? reduceLuxColor = Pref.reduceLuxColor;
static bool reduce = false;
@override
Widget build(BuildContext context) {
final noRadius = type == ImageType.emote || radius == 0;
final Widget child;
if (src?.isNotEmpty == true) {
child = noRadius
? _buildImage(context, noRadius)
: type == ImageType.avatar
? ClipOval(child: _buildImage(context, noRadius))
: ClipRRect(
borderRadius: radius != null
? BorderRadius.circular(radius!)
: StyleString.mdRadius,
child: _buildImage(context, noRadius),
);
} else {
child = getPlaceHolder?.call() ?? _placeholder(context, noRadius);
}
return semanticsLabel?.isNotEmpty == true
? Semantics(
container: true,
image: true,
excludeSemantics: true,
label: semanticsLabel,
child: child,
)
: child;
}
Widget _buildImage(BuildContext context, bool noRadius) {
int? memCacheWidth, memCacheHeight;
if (height == null || forceUseCacheWidth || width <= height!) {
memCacheWidth = width.cacheSize(context);
} else {
memCacheHeight = height.cacheSize(context);
}
return CachedNetworkImage(
imageUrl: ImageUtil.thumbnailUrl(src, quality),
width: width,
height: height,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: boxFit ?? BoxFit.cover,
alignment: isLongPic ? Alignment.topCenter : Alignment.center,
fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120),
fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120),
filterQuality: FilterQuality.low,
placeholder: (BuildContext context, String url) =>
getPlaceHolder?.call() ?? _placeholder(context, noRadius),
imageBuilder: imageBuilder,
errorWidget: (context, url, error) => _placeholder(context, noRadius),
colorBlendMode: reduce ? BlendMode.modulate : null,
color: reduce ? reduceLuxColor : null,
);
}
Widget _placeholder(BuildContext context, bool noRadius) {
final isAvatar = type == ImageType.avatar;
return Container(
width: width,
height: height,
clipBehavior: noRadius ? Clip.none : Clip.antiAlias,
decoration: BoxDecoration(
shape: isAvatar ? BoxShape.circle : BoxShape.rectangle,
color: Theme.of(
context,
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
borderRadius: noRadius || isAvatar
? null
: radius != null
? BorderRadius.circular(radius!)
: StyleString.mdRadius,
),
child: Center(
child: Image.asset(
isAvatar ? 'assets/images/noface.jpeg' : 'assets/images/loading.png',
width: width,
height: height,
cacheWidth: width.cacheSize(context),
colorBlendMode: reduce ? BlendMode.modulate : null,
color: reduce ? reduceLuxColor : null,
),
),
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:collection';
import 'dart:math' as math;
import 'package:flutter/material.dart';
/**
* @Author: Sky24n
* @GitHub: https://github.com/Sky24n
@@ -133,8 +133,9 @@ class _NineGridViewState extends State<NineGridView> {
if (widget.itemCount == 0) {
return Rect.fromLTRB(0, 0, padding.horizontal, padding.vertical);
}
double width = widget.width ??
(MediaQuery.of(context).size.width - widget.margin.horizontal);
double width =
widget.width ??
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
width = width - padding.horizontal;
double space = widget.space;
double itemW;
@@ -158,18 +159,22 @@ class _NineGridViewState extends State<NineGridView> {
/// build nine grid view.
Widget _buildChild(BuildContext context, double itemW) {
double space = widget.space;
int column =
(widget.itemCount == 4 && widget.type != NineGridType.normal) ? 2 : 3;
int column = (widget.itemCount == 4 && widget.type != NineGridType.normal)
? 2
: 3;
List<Widget> list = [];
for (int i = 0; i < widget.itemCount; i++) {
list.add(Positioned(
list.add(
Positioned(
top: (space + itemW) * (i ~/ column),
left: (space + itemW) * (i % column),
child: SizedBox(
width: itemW,
height: itemW,
child: widget.itemBuilder(context, i),
)));
),
),
);
}
return Stack(
clipBehavior: Clip.none,
@@ -191,11 +196,14 @@ class _NineGridViewState extends State<NineGridView> {
if (!_isZero(bigImgWidth) && !_isZero(bigImgHeight)) {
return _getOneChild(context, bigImgWidth!, bigImgHeight!);
} else {
_ImageUtil().getImageSize(widget.bigImage)?.then((rect) {
ngvBigImageSizeMap[bigImageUrl] = rect;
if (!mounted) return;
setState(() {});
}).catchError((e) {});
_ImageUtil()
.getImageSize(widget.bigImage)
?.then((rect) {
ngvBigImageSizeMap[bigImageUrl] = rect;
if (!mounted) return;
setState(() {});
})
.catchError((e) {});
}
}
return null;
@@ -238,7 +246,8 @@ class _NineGridViewState extends State<NineGridView> {
for (int i = 0; i < widget.itemCount; i++) {
double left;
if (first > 0 && i < first) {
left = (width - itemW * first - space * (first - 1)) / 2 +
left =
(width - itemW * first - space * (first - 1)) / 2 +
(itemW + space) * i;
} else {
left = (space + itemW) * ((i - first) % column);
@@ -248,17 +257,21 @@ class _NineGridViewState extends State<NineGridView> {
? 0
: (first > 0 ? (i + column - first) : i) ~/ column;
double top = (width - itemW * row - space * (row - 1)) / 2 +
double top =
(width - itemW * row - space * (row - 1)) / 2 +
(space + itemW) * itemIndex;
list.add(Positioned(
list.add(
Positioned(
top: top,
left: left,
child: SizedBox(
width: itemW,
height: itemW,
child: widget.itemBuilder(context, i),
)));
),
),
);
}
return Stack(
clipBehavior: Clip.none,
@@ -273,18 +286,22 @@ class _NineGridViewState extends State<NineGridView> {
double itemW = (width - widget.space) / 2;
List<Widget> children = [];
for (int i = 0; i < itemCount; i++) {
children.add(Positioned(
children.add(
Positioned(
top: (widget.space + itemW) * (i ~/ 2),
left: (widget.space + itemW) *
left:
(widget.space + itemW) *
(((itemCount == 3 && i == 2) ? i + 1 : i) % 2),
child: SizedBox(
width: itemCount == 1 ? width : itemW,
height:
(itemCount == 1 || itemCount == 2 || (itemCount == 3 && i == 0))
? width
: itemW,
? width
: itemW,
child: widget.itemBuilder(context, i),
)));
),
),
);
}
return ClipOval(
child: Stack(
@@ -300,11 +317,12 @@ class _NineGridViewState extends State<NineGridView> {
int itemCount = math.min(5, widget.itemCount);
if (itemCount == 1) {
return ClipOval(
child: SizedBox(
width: width,
height: width,
child: widget.itemBuilder(context, 0),
));
child: SizedBox(
width: width,
height: width,
child: widget.itemBuilder(context, 0),
),
);
}
List<Widget> children = [];
@@ -323,7 +341,8 @@ class _NineGridViewState extends State<NineGridView> {
startDegree = 210;
r = width / (2 + 4 * math.sin(math.pi * (3 - 2) / (2 * 3)));
r1 = r / math.cos(math.pi * (3 - 2) / (2 * 3));
double R = r *
double R =
r *
(1 + math.sin(math.pi / itemCount)) /
math.sin(math.pi / itemCount);
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 3)));
@@ -338,7 +357,8 @@ class _NineGridViewState extends State<NineGridView> {
startDegree = 126;
r = width / (2 + 4 * math.sin(math.pi * (5 - 2) / (2 * 5)));
r1 = r / math.cos(math.pi * (5 - 2) / (2 * 5));
double R = r *
double R =
r *
(1 + math.sin(math.pi / itemCount)) /
math.sin(math.pi / itemCount);
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 5)));
@@ -388,12 +408,13 @@ class _NineGridViewState extends State<NineGridView> {
/// get big image size.
Rect _getBigImgSize(double originalWidth, double originalHeight) {
double width = widget.width ??
(MediaQuery.of(context).size.width - widget.margin.horizontal);
double width =
widget.width ??
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
width = width - widget.padding.horizontal;
double itemW = (width - widget.space * 2) / 3;
//double devicePixelRatio = MediaQuery.of(context)?.devicePixelRatio ?? 3;
// double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
double devicePixelRatio = 1.0;
double tempWidth = originalWidth / devicePixelRatio;
double tempHeight = originalHeight / devicePixelRatio;
@@ -475,8 +496,14 @@ class _ImageUtil {
(ImageInfo info, bool synchronousCall) {
imageStream.removeListener(listener);
if (!completer.isCompleted) {
completer.complete(Rect.fromLTWH(
0, 0, info.image.width.toDouble(), info.image.height.toDouble()));
completer.complete(
Rect.fromLTWH(
0,
0,
info.image.width.toDouble(),
info.image.height.toDouble(),
),
);
}
},
onError: (dynamic exception, StackTrace? stackTrace) {
@@ -486,7 +513,7 @@ class _ImageUtil {
}
},
);
imageStream = image.image.resolve(const ImageConfiguration());
imageStream = image.image.resolve(ImageConfiguration.empty);
imageStream.addListener(listener);
return completer.future;
}
@@ -535,8 +562,10 @@ class QQClipper extends CustomClipper<Path> {
points.add(Offset(x1, y1));
}
double spaceB = math.atan(
r * math.sin(d2r(spaceA)) / (2 * r - r * math.cos(d2r(spaceA)))) /
double spaceB =
math.atan(
r * math.sin(d2r(spaceA)) / (2 * r - r * math.cos(d2r(spaceA))),
) /
math.pi *
180;
double r1 = (2 * r - r * math.cos(d2r(spaceA))) / math.cos(d2r(spaceB));

View File

@@ -1,195 +0,0 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
show SourceModel, SourceType;
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/nine_grid_view.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
class ImageModel {
ImageModel({
required this.width,
required this.height,
required this.url,
this.liveUrl,
});
dynamic width;
dynamic height;
String url;
String? liveUrl;
bool? _isLongPic;
bool? _isLivePhoto;
dynamic get safeWidth => width ?? 1;
dynamic get safeHeight => height ?? 1;
bool get isLongPic => _isLongPic ??= (safeHeight / safeWidth) > (22 / 9);
bool get isLivePhoto => _isLivePhoto ??= liveUrl?.isNotEmpty == true;
}
Widget imageView(
double maxWidth,
List<ImageModel> picArr, {
VoidCallback? onViewImage,
ValueChanged<int>? onDismissed,
Function(List<String>, int)? callback,
}) {
double imageWidth = (maxWidth - 2 * 5) / 3;
double imageHeight = imageWidth;
if (picArr.length == 1) {
dynamic width = picArr[0].safeWidth;
dynamic height = picArr[0].safeHeight;
double ratioWH = width / height;
double ratioHW = height / width;
double maxRatio = 22 / 9;
imageWidth = ratioWH > 1.5
? maxWidth
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
? 2 * imageWidth
: 1.5 * imageWidth;
imageHeight = imageWidth * min(ratioHW, maxRatio);
} else if (picArr.length == 2) {
imageWidth = imageHeight = 2 * imageWidth;
}
BorderRadius borderRadius(index) {
if (picArr.length == 1) {
return StyleString.mdRadius;
}
final int row = picArr.length == 4 ? 2 : 3;
return BorderRadius.only(
topLeft: Radius.circular(
(index - row >= 0 ||
((index - 1) >= 0 && (index - 1) % row < index % row))
? 0
: 10,
),
topRight: Radius.circular(
(index - row >= 0 ||
((index + 1) < picArr.length &&
(index + 1) % row > index % row))
? 0
: 10,
),
bottomLeft: Radius.circular(
(index + row < picArr.length ||
((index - 1) >= 0 && (index - 1) % row < index % row))
? 0
: 10,
),
bottomRight: Radius.circular(
(index + row < picArr.length ||
((index + 1) < picArr.length &&
(index + 1) % row > index % row))
? 0
: 10,
),
);
}
late final enableLivePhoto = GStorage.enableLivePhoto;
int parseSize(size) {
return switch (size) {
int() => size,
double() => size.round(),
String() => int.tryParse(size) ?? 1,
_ => 1,
};
}
return NineGridView(
type: NineGridType.weiBo,
margin: const EdgeInsets.only(top: 6),
bigImageWidth: imageWidth,
bigImageHeight: imageHeight,
space: 5,
height: picArr.length == 1 ? imageHeight : null,
width: picArr.length == 1 ? imageWidth : maxWidth,
itemCount: picArr.length,
itemBuilder: (context, index) => Hero(
tag: picArr[index].url,
child: GestureDetector(
onTap: () {
if (callback != null) {
callback(picArr.map((item) => item.url).toList(), index);
} else {
onViewImage?.call();
context.imageView(
initialPage: index,
imgList: picArr.map(
(item) {
bool isLive = item.isLivePhoto && enableLivePhoto;
return SourceModel(
sourceType:
isLive ? SourceType.livePhoto : SourceType.networkImage,
url: item.url,
liveUrl: isLive ? item.liveUrl : null,
width: isLive ? parseSize(item.width) : null,
height: isLive ? parseSize(item.height) : null,
);
},
).toList(),
onDismissed: onDismissed,
);
}
},
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: borderRadius(index),
child: NetworkImgLayer(
radius: 0,
src: picArr[index].url,
width: imageWidth,
height: imageHeight,
isLongPic: () => picArr[index].isLongPic,
callback: () =>
picArr[index].safeWidth <= picArr[index].safeHeight,
getPlaceHolder: () {
return Container(
width: imageWidth,
height: imageHeight,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.4),
borderRadius: borderRadius(index),
),
child: Center(
child: Image.asset(
'assets/images/loading.png',
width: imageWidth,
height: imageHeight,
cacheWidth: imageWidth.cacheSize(context),
),
),
);
},
),
),
if (picArr[index].isLivePhoto)
const PBadge(
text: 'Live',
right: 8,
bottom: 8,
type: 'gray',
)
else if (picArr[index].isLongPic)
const PBadge(
text: '长图',
right: 8,
bottom: 8,
),
],
),
),
),
);
}

View File

@@ -22,8 +22,8 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3;
///
/// * [InteractiveViewer.builder], whose builder is of this type.
/// * [WidgetBuilder], which is similar, but takes no viewport.
typedef InteractiveViewerWidgetBuilder = Widget Function(
BuildContext context, Quad viewport);
typedef InteractiveViewerWidgetBuilder =
Widget Function(BuildContext context, Quad viewport);
/// A widget that enables pan and zoom interactions with its child.
///
@@ -82,23 +82,23 @@ class InteractiveViewer extends StatefulWidget {
this.onReset,
this.isAnimating,
required Widget this.child,
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
builder = null;
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
builder = null;
/// Creates an InteractiveViewer for a child that is created on demand.
///
@@ -132,24 +132,24 @@ class InteractiveViewer extends StatefulWidget {
this.onReset,
this.isAnimating,
required InteractiveViewerWidgetBuilder this.builder,
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
constrained = false,
child = null;
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
constrained = false,
child = null;
final Function? isAnimating;
final VoidCallback? onReset;
@@ -402,7 +402,8 @@ class InteractiveViewer extends StatefulWidget {
/// Returns the closest point to the given point on the given line segment.
@visibleForTesting
static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) {
final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() +
final double lengthSquared =
math.pow(l2.x - l1.x, 2.0).toDouble() +
math.pow(l2.y - l1.y, 2.0).toDouble();
// In this case, l1 == l2.
@@ -414,8 +415,11 @@ class InteractiveViewer extends StatefulWidget {
// the point.
final Vector3 l1P = point - l1;
final Vector3 l1L2 = l2 - l1;
final double fraction =
clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0);
final double fraction = clampDouble(
l1P.dot(l1L2) / lengthSquared,
0.0,
1.0,
);
return l1 + l1L2 * fraction;
}
@@ -558,8 +562,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
final RenderBox childRenderBox =
_childKey.currentContext!.findRenderObject()! as RenderBox;
final Size childSize = childRenderBox.size;
final Rect boundaryRect =
widget.boundaryMargin.inflateRect(Offset.zero & childSize);
final Rect boundaryRect = widget.boundaryMargin.inflateRect(
Offset.zero & childSize,
);
assert(
!boundaryRect.isEmpty,
"InteractiveViewer's child must have nonzero dimensions.",
@@ -631,8 +636,10 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
// If the given translation fits completely within the boundaries, allow it.
final Offset offendingDistance =
_exceedsBy(boundariesAabbQuad, nextViewport);
final Offset offendingDistance = _exceedsBy(
boundariesAabbQuad,
nextViewport,
);
if (offendingDistance == Offset.zero) {
return nextMatrix;
}
@@ -651,17 +658,23 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// complicated than this when rotated.
// https://github.com/flutter/flutter/issues/57698
final Matrix4 correctedMatrix = matrix.clone()
..setTranslation(Vector3(
correctedTotalTranslation.dx,
correctedTotalTranslation.dy,
0.0,
));
..setTranslation(
Vector3(
correctedTotalTranslation.dx,
correctedTotalTranslation.dy,
0.0,
),
);
// Double check that the corrected translation fits.
final Quad correctedViewport =
_transformViewport(correctedMatrix, _viewport);
final Offset offendingCorrectedDistance =
_exceedsBy(boundariesAabbQuad, correctedViewport);
final Quad correctedViewport = _transformViewport(
correctedMatrix,
_viewport,
);
final Offset offendingCorrectedDistance = _exceedsBy(
boundariesAabbQuad,
correctedViewport,
);
if (offendingCorrectedDistance == Offset.zero) {
return correctedMatrix;
}
@@ -680,12 +693,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
);
return matrix.clone()
..setTranslation(Vector3(
return matrix.clone()..setTranslation(
Vector3(
unidirectionalCorrectedTotalTranslation.dx,
unidirectionalCorrectedTotalTranslation.dy,
0.0,
));
),
);
}
// Return a new matrix representing the given matrix after applying the given
@@ -698,8 +712,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// Don't allow a scale that results in an overall scale beyond min/max
// scale.
final double currentScale =
_transformationController!.value.getMaxScaleOnAxis();
final double currentScale = _transformationController!.value
.getMaxScaleOnAxis();
final double totalScale = math.max(
currentScale * scale,
// Ensure that the scale cannot make the child so big that it can't fit
@@ -771,14 +785,16 @@ class _InteractiveViewerState extends State<InteractiveViewer>
widget.onInteractionStart?.call(details);
if (_controller.isAnimating) {
_controller.stop();
_controller.reset();
_controller
..stop()
..reset();
_animation?.removeListener(_onAnimate);
_animation = null;
}
if (_scaleController.isAnimating) {
_scaleController.stop();
_scaleController.reset();
_scaleController
..stop()
..reset();
_scaleAnimation?.removeListener(_onScaleAnimate);
_scaleAnimation = null;
}
@@ -931,10 +947,12 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_currentAxis = null;
return;
}
final Vector3 translationVector =
_transformationController!.value.getTranslation();
final Offset translation =
Offset(translationVector.x, translationVector.y);
final Vector3 translationVector = _transformationController!.value
.getTranslation();
final Offset translation = Offset(
translationVector.x,
translationVector.y,
);
final FrictionSimulation frictionSimulationX = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dx,
@@ -949,13 +967,19 @@ class _InteractiveViewerState extends State<InteractiveViewer>
details.velocity.pixelsPerSecond.distance,
widget.interactionEndFrictionCoefficient,
);
_animation = Tween<Offset>(
begin: translation,
end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
));
_animation =
Tween<Offset>(
begin: translation,
end: Offset(
frictionSimulationX.finalX,
frictionSimulationY.finalX,
),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
),
);
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
_animation!.addListener(_onAnimate);
_controller.forward();
@@ -964,21 +988,31 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_currentAxis = null;
return;
}
final double scale =
_transformationController!.value.getMaxScaleOnAxis();
final double scale = _transformationController!.value
.getMaxScaleOnAxis();
final FrictionSimulation frictionSimulation = FrictionSimulation(
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
details.scaleVelocity / 10);
final double tFinal = _getFinalTime(details.scaleVelocity.abs(),
widget.interactionEndFrictionCoefficient,
effectivelyMotionless: 0.1);
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
details.scaleVelocity / 10,
);
final double tFinal = _getFinalTime(
details.scaleVelocity.abs(),
widget.interactionEndFrictionCoefficient,
effectivelyMotionless: 0.1,
);
_scaleAnimation =
Tween<double>(begin: scale, end: frictionSimulation.x(tFinal))
.animate(CurvedAnimation(
parent: _scaleController, curve: Curves.decelerate));
_scaleController.duration =
Duration(milliseconds: (tFinal * 1000).round());
Tween<double>(
begin: scale,
end: frictionSimulation.x(tFinal),
).animate(
CurvedAnimation(
parent: _scaleController,
curve: Curves.decelerate,
),
);
_scaleController.duration = Duration(
milliseconds: (tFinal * 1000).round(),
);
_scaleAnimation!.addListener(_onScaleAnimate);
_scaleController.forward();
case _GestureType.rotate || null:
@@ -1007,11 +1041,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
if (!_gestureIsSupported(_GestureType.pan)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - event.scrollDelta,
focalPointDelta: -localDelta,
));
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - event.scrollDelta,
focalPointDelta: -localDelta,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
@@ -1025,13 +1061,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
newFocalPointScene - focalPointScene);
_transformationController!.value,
newFocalPointScene - focalPointScene,
);
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - localDelta,
focalPointDelta: -localDelta));
focalPointDelta: -localDelta,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
@@ -1053,11 +1093,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
@@ -1081,11 +1123,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
}
@@ -1099,8 +1143,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
return;
}
// Translate such that the resulting translation is _animation.value.
final Vector3 translationVector =
_transformationController!.value.getTranslation();
final Vector3 translationVector = _transformationController!.value
.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final Offset translationScene = _transformationController!.toScene(
translation,
@@ -1174,27 +1218,33 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// transformationControllers.
if (oldWidget.transformationController == null) {
if (widget.transformationController != null) {
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController!.dispose();
_transformationController = widget.transformationController;
_transformationController!
.addListener(_onTransformationControllerChange);
_transformationController!.addListener(
_onTransformationControllerChange,
);
}
} else {
if (widget.transformationController == null) {
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController = TransformationController();
_transformationController!
.addListener(_onTransformationControllerChange);
_transformationController!.addListener(
_onTransformationControllerChange,
);
} else if (widget.transformationController !=
oldWidget.transformationController) {
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController = widget.transformationController;
_transformationController!
.addListener(_onTransformationControllerChange);
_transformationController!.addListener(
_onTransformationControllerChange,
);
}
}
}
@@ -1203,8 +1253,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
void dispose() {
_controller.dispose();
_scaleController.dispose();
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
if (widget.transformationController == null) {
_transformationController!.dispose();
}
@@ -1327,7 +1378,7 @@ class TransformationController extends ValueNotifier<Matrix4> {
/// The [value] defaults to the identity matrix, which corresponds to no
/// transformation.
TransformationController([Matrix4? value])
: super(value ?? Matrix4.identity());
: super(value ?? Matrix4.identity());
/// Return the scene point at the given viewport point.
///
@@ -1363,11 +1414,13 @@ class TransformationController extends ValueNotifier<Matrix4> {
// On viewportPoint, perform the inverse transformation of the scene to get
// where the point would be in the scene before the transformation.
final Matrix4 inverseMatrix = Matrix4.inverted(value);
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
viewportPoint.dx,
viewportPoint.dy,
0,
));
final Vector3 untransformed = inverseMatrix.transform3(
Vector3(
viewportPoint.dx,
viewportPoint.dy,
0,
),
);
return Offset(untransformed.x, untransformed.y);
}
}
@@ -1382,8 +1435,11 @@ enum _GestureType {
// Given a velocity and drag, calculate the time at which motion will come to
// a stop, within the margin of effectivelyMotionless.
double _getFinalTime(double velocity, double drag,
{double effectivelyMotionless = 10}) {
double _getFinalTime(
double velocity,
double drag, {
double effectivelyMotionless = 10,
}) {
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
}
@@ -1400,26 +1456,34 @@ Offset _getMatrixTranslation(Matrix4 matrix) {
Quad _transformViewport(Matrix4 matrix, Rect viewport) {
final Matrix4 inverseMatrix = matrix.clone()..invert();
return Quad.points(
inverseMatrix.transform3(Vector3(
viewport.topLeft.dx,
viewport.topLeft.dy,
0.0,
)),
inverseMatrix.transform3(Vector3(
viewport.topRight.dx,
viewport.topRight.dy,
0.0,
)),
inverseMatrix.transform3(Vector3(
viewport.bottomRight.dx,
viewport.bottomRight.dy,
0.0,
)),
inverseMatrix.transform3(Vector3(
viewport.bottomLeft.dx,
viewport.bottomLeft.dy,
0.0,
)),
inverseMatrix.transform3(
Vector3(
viewport.topLeft.dx,
viewport.topLeft.dy,
0.0,
),
),
inverseMatrix.transform3(
Vector3(
viewport.topRight.dx,
viewport.topRight.dy,
0.0,
),
),
inverseMatrix.transform3(
Vector3(
viewport.bottomRight.dx,
viewport.bottomRight.dy,
0.0,
),
),
inverseMatrix.transform3(
Vector3(
viewport.bottomLeft.dx,
viewport.bottomLeft.dy,
0.0,
),
),
);
}
@@ -1451,8 +1515,10 @@ Offset _exceedsBy(Quad boundary, Quad viewport) {
];
Offset largestExcess = Offset.zero;
for (final Vector3 point in viewportPoints) {
final Vector3 pointInside =
InteractiveViewer.getNearestPointInside(point, boundary);
final Vector3 pointInside = InteractiveViewer.getNearestPointInside(
point,
boundary,
);
final Offset excess = Offset(
pointInside.x - point.x,
pointInside.y - point.y,

View File

@@ -1,4 +1,5 @@
import 'interactive_viewer.dart' as custom;
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
as custom;
import 'package:flutter/material.dart';
/// https://github.com/qq326646683/interactiveviewer_gallery
@@ -212,15 +213,15 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
}
Widget get content => DecoratedBoxTransition(
decoration: _opacityAnimation,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
),
);
decoration: _opacityAnimation,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
),
);
@override
Widget build(BuildContext context) {

View File

@@ -1,18 +1,20 @@
import 'dart:io';
import 'package:PiliPlus/utils/download.dart';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
as custom;
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'interactive_viewer_boundary.dart';
import 'interactive_viewer.dart' as custom;
/// https://github.com/qq326646683/interactiveviewer_gallery
@@ -26,30 +28,17 @@ import 'interactive_viewer.dart' as custom;
/// source is hit after zooming in to disable or enable the swiping gesture of
/// the [PageView].
///
typedef IndexedFocusedWidgetBuilder = Widget Function(
BuildContext context, int index, bool isFocus, bool enablePageView);
typedef IndexedFocusedWidgetBuilder =
Widget Function(
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
);
typedef IndexedTagStringBuilder = String Function(int index);
enum SourceType { fileImage, networkImage, livePhoto }
class SourceModel {
final SourceType sourceType;
final String url;
final String? liveUrl;
final int? width;
final int? height;
const SourceModel({
this.sourceType = SourceType.networkImage,
required this.url,
this.liveUrl,
this.width,
this.height,
});
}
class InteractiveviewerGallery<T> extends StatefulWidget {
class InteractiveviewerGallery extends StatefulWidget {
const InteractiveviewerGallery({
super.key,
required this.sources,
@@ -59,13 +48,16 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
this.minScale = 1.0,
this.onPageChanged,
this.onDismissed,
this.setStatusBar,
this.setStatusBar = true,
this.onClose,
required this.quality,
});
final ValueChanged? onClose;
final int quality;
final bool? setStatusBar;
final ValueChanged<bool>? onClose;
final bool setStatusBar;
/// The sources to show.
final List<SourceModel> sources;
@@ -107,7 +99,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
late final RxInt currentIndex = widget.initIndex.obs;
late final int _quality = GStorage.previewQ;
late final int _quality = Pref.previewQ;
@override
void initState() {
@@ -122,12 +114,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
duration: const Duration(milliseconds: 300),
)..addListener(listener);
if (widget.setStatusBar != false) {
if (widget.setStatusBar) {
setStatusBar();
}
if (widget.sources[currentIndex.value].sourceType == SourceType.livePhoto) {
_onPlay(currentIndex.value);
var item = widget.sources[currentIndex.value];
if (item.sourceType == SourceType.livePhoto) {
_onPlay(item.liveUrl!);
}
}
@@ -136,14 +129,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
}
SystemUiMode? mode;
setStatusBar() async {
Future<void> setStatusBar() async {
if (Platform.isIOS || Platform.isAndroid) {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
);
}
if (Platform.isAndroid &&
(await DeviceInfoPlugin().androidInfo).version.sdkInt < 29) {
if (Platform.isAndroid && (await Utils.sdkInt < 29)) {
mode = SystemUiMode.manual;
}
}
@@ -153,9 +145,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
widget.onClose?.call(true);
_player?.dispose();
_pageController?.dispose();
_animationController.removeListener(listener);
_animationController.dispose();
if (widget.setStatusBar != false) {
_animationController
..removeListener(listener)
..dispose();
if (widget.setStatusBar) {
if (Platform.isIOS || Platform.isAndroid) {
SystemChrome.setEnabledSystemUIMode(
mode ?? SystemUiMode.edgeToEdge,
@@ -163,9 +156,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
);
}
}
for (int index = 0; index < widget.sources.length; index++) {
if (widget.sources[index].sourceType == SourceType.networkImage) {
CachedNetworkImageProvider(_getActualUrl(index)).evict();
for (var item in widget.sources) {
if (item.sourceType == SourceType.networkImage) {
CachedNetworkImageProvider(_getActualUrl(item.url)).evict();
}
}
super.dispose();
@@ -224,10 +217,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
}
}
void _onPlay(int index) {
void _onPlay(String liveUrl) {
_player ??= Player();
_videoController ??= VideoController(_player!);
_player!.open(Media(widget.sources[index].liveUrl!));
_player!.open(Media(liveUrl));
}
/// When the page view changed its page, the source will animate back into the
@@ -237,28 +230,30 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
void _onPageChanged(int page) {
_player?.pause();
currentIndex.value = page;
if (widget.sources[page].sourceType == SourceType.livePhoto) {
_onPlay(page);
var item = widget.sources[page];
if (item.sourceType == SourceType.livePhoto) {
_onPlay(item.liveUrl!);
}
widget.onPageChanged?.call(page);
if (_transformationController!.value != Matrix4.identity()) {
// animate the reset for the transformation of the interactive viewer
_animation = Matrix4Tween(
begin: _transformationController!.value,
end: Matrix4.identity(),
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animation =
Matrix4Tween(
begin: _transformationController!.value,
end: Matrix4.identity(),
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController.forward(from: 0);
}
}
String _getActualUrl(int index) {
String _getActualUrl(String url) {
return _quality != 100
? Utils.thumbnailImgUrl(widget.sources[index].url, _quality)
: widget.sources[index].url.http2https;
? ImageUtil.thumbnailUrl(url, _quality)
: url.http2https;
}
void onClose() {
@@ -280,7 +275,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
children: [
InteractiveViewerBoundary(
controller: _transformationController,
boundaryWidth: MediaQuery.of(context).size.width,
boundaryWidth: MediaQuery.sizeOf(context).width,
onScaleChanged: _onScaleChanged,
onLeftBoundaryHit: _onLeftBoundaryHit,
onRightBoundaryHit: _onRightBoundaryHit,
@@ -298,21 +293,30 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
child: PageView.builder(
onPageChanged: _onPageChanged,
controller: _pageController,
physics:
_enablePageView ? null : const NeverScrollableScrollPhysics(),
physics: _enablePageView
? null
: const NeverScrollableScrollPhysics(),
itemCount: widget.sources.length,
itemBuilder: (BuildContext context, int index) {
final item = widget.sources[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onClose,
onTap: () => EasyThrottle.throttle(
'preview',
const Duration(milliseconds: 555),
onClose,
),
onDoubleTapDown: (TapDownDetails details) {
_doubleTapLocalPosition = details.localPosition;
},
onDoubleTap: onDoubleTap,
onLongPress:
widget.sources[index].sourceType == SourceType.fileImage
? null
: onLongPress,
onDoubleTap: () => EasyThrottle.throttle(
'preview',
const Duration(milliseconds: 555),
onDoubleTap,
),
onLongPress: item.sourceType == SourceType.fileImage
? null
: () => onLongPress(item),
child: widget.itemBuilder != null
? widget.itemBuilder!(
context,
@@ -320,7 +324,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
index == currentIndex.value,
_enablePageView,
)
: _itemBuilder(index),
: _itemBuilder(index, item),
);
},
),
@@ -330,7 +334,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
left: 0,
right: 0,
child: Container(
padding: MediaQuery.paddingOf(context) +
padding:
MediaQuery.viewPaddingOf(context) +
const EdgeInsets.fromLTRB(12, 8, 20, 8),
decoration: _enablePageView
? BoxDecoration(
@@ -339,7 +344,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.3)
Colors.black.withValues(alpha: 0.3),
],
),
)
@@ -371,53 +376,40 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
alignment: Alignment.centerRight,
child: PopupMenuButton(
itemBuilder: (context) {
final item = widget.sources[currentIndex.value];
return [
PopupMenuItem(
onTap: () => DownloadUtils.onShareImg(
widget.sources[currentIndex.value].url),
onTap: () => ImageUtil.onShareImg(item.url),
child: const Text("分享图片"),
),
PopupMenuItem(
onTap: () {
Utils.copyText(
widget.sources[currentIndex.value].url);
},
onTap: () => Utils.copyText(item.url),
child: const Text("复制链接"),
),
PopupMenuItem(
onTap: () {
DownloadUtils.downloadImg(
this.context,
[widget.sources[currentIndex.value].url],
);
},
onTap: () => ImageUtil.downloadImg(
this.context,
[item.url],
),
child: const Text("保存图片"),
),
if (widget.sources.length > 1)
PopupMenuItem(
onTap: () {
DownloadUtils.downloadImg(
this.context,
widget.sources
.map((item) => item.url)
.toList(),
);
},
child: const Text("保存全部图片"),
onTap: () => ImageUtil.downloadImg(
this.context,
widget.sources.map((item) => item.url).toList(),
),
child: const Text("保存全部"),
),
if (widget.sources[currentIndex.value].sourceType ==
SourceType.livePhoto)
if (item.sourceType == SourceType.livePhoto)
PopupMenuItem(
onTap: () {
DownloadUtils.downloadLivePhoto(
ImageUtil.downloadLivePhoto(
context: this.context,
url: widget.sources[currentIndex.value].url,
liveUrl: widget
.sources[currentIndex.value].liveUrl!,
width:
widget.sources[currentIndex.value].width!,
height: widget
.sources[currentIndex.value].height!,
url: item.url,
liveUrl: item.liveUrl!,
width: item.width!,
height: item.height!,
);
},
child: const Text("保存 Live Photo"),
@@ -435,42 +427,44 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
);
}
Widget _itemBuilder(index) {
Widget _itemBuilder(int index, SourceModel item) {
return Center(
child: Hero(
tag: widget.sources[index].url,
child: switch (widget.sources[index].sourceType) {
tag: item.url,
child: switch (item.sourceType) {
SourceType.fileImage => Image(
filterQuality: FilterQuality.low,
image: FileImage(File(widget.sources[index].url)),
),
filterQuality: FilterQuality.low,
image: FileImage(File(item.url)),
),
SourceType.networkImage => CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: _getActualUrl(index),
placeholderFadeInDuration: Duration.zero,
placeholder: (context, url) {
return CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: Utils.thumbnailImgUrl(widget.sources[index].url),
);
},
),
SourceType.livePhoto => Obx(() => currentIndex.value == index
? IgnorePointer(
child: Video(
controller: _videoController!,
fill: Colors.transparent,
),
)
: const SizedBox.shrink()),
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: _getActualUrl(item.url),
placeholderFadeInDuration: Duration.zero,
placeholder: (context, url) {
return CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: ImageUtil.thumbnailUrl(item.url, widget.quality),
);
},
),
SourceType.livePhoto => Obx(
() => currentIndex.value == index
? IgnorePointer(
child: Video(
controller: _videoController!,
fill: Colors.transparent,
),
)
: const SizedBox.shrink(),
),
},
),
);
}
onDoubleTap() {
void onDoubleTap() {
Matrix4 matrix = _transformationController!.value.clone();
double currentScale = matrix.row0.x;
@@ -503,35 +497,35 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
offSetX,
offSetY,
matrix.row2.w,
matrix.row3.w
matrix.row3.w,
]);
_animation = Matrix4Tween(
begin: _transformationController!.value,
end: matrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animation =
Matrix4Tween(
begin: _transformationController!.value,
end: matrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController
.forward(from: 0)
.whenComplete(() => _onScaleChanged(targetScale));
}
onLongPress() {
void onLongPress(SourceModel item) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
DownloadUtils.onShareImg(
widget.sources[currentIndex.value].url);
Get.back();
ImageUtil.onShareImg(item.url);
},
dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)),
@@ -539,7 +533,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
ListTile(
onTap: () {
Get.back();
Utils.copyText(widget.sources[currentIndex.value].url);
Utils.copyText(item.url);
},
dense: true,
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
@@ -547,9 +541,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
ListTile(
onTap: () {
Get.back();
DownloadUtils.downloadImg(
ImageUtil.downloadImg(
this.context,
[widget.sources[currentIndex.value].url],
[item.url],
);
},
dense: true,
@@ -559,7 +553,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
ListTile(
onTap: () {
Get.back();
DownloadUtils.downloadImg(
ImageUtil.downloadImg(
this.context,
widget.sources.map((item) => item.url).toList(),
);
@@ -567,17 +561,16 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
dense: true,
title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
),
if (widget.sources[currentIndex.value].sourceType ==
SourceType.livePhoto)
if (item.sourceType == SourceType.livePhoto)
ListTile(
onTap: () {
Get.back();
DownloadUtils.downloadLivePhoto(
ImageUtil.downloadLivePhoto(
context: this.context,
url: widget.sources[currentIndex.value].url,
liveUrl: widget.sources[currentIndex.value].liveUrl!,
width: widget.sources[currentIndex.value].width!,
height: widget.sources[currentIndex.value].height!,
url: item.url,
liveUrl: item.liveUrl!,
width: item.width!,
height: item.height!,
);
},
dense: true,

View File

@@ -1,20 +0,0 @@
import 'package:PiliPlus/common/widgets/http_error.dart';
import 'package:flutter/material.dart';
Widget get loadingWidget => Center(child: CircularProgressIndicator());
Widget errorWidget({errMsg, onReload}) => HttpError(
isSliver: false,
errMsg: errMsg,
onReload: onReload,
);
Widget scrollErrorWidget({errMsg, onReload, controller}) => CustomScrollView(
controller: controller,
slivers: [
HttpError(
errMsg: errMsg,
onReload: onReload,
)
],
);

View File

@@ -51,16 +51,16 @@ class HttpError extends StatelessWidget {
FilledButton.tonal(
onPressed: onReload,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
return theme.colorScheme.primary.withAlpha(20);
}),
backgroundColor: WidgetStatePropertyAll(
theme.colorScheme.primary.withAlpha(20),
),
),
child: Text(
btnText ?? '点击重试',
style: TextStyle(color: theme.colorScheme.primary),
),
),
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
SizedBox(height: 40 + MediaQuery.viewPaddingOf(context).bottom),
],
);
}

View File

@@ -0,0 +1,23 @@
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:flutter/material.dart';
Widget get loadingWidget => const Center(child: CircularProgressIndicator());
Widget get linearLoading =>
const SliverToBoxAdapter(child: LinearProgressIndicator());
Widget errorWidget({errMsg, onReload}) => HttpError(
isSliver: false,
errMsg: errMsg,
onReload: onReload,
);
Widget scrollErrorWidget({errMsg, onReload, controller}) => CustomScrollView(
controller: controller,
slivers: [
HttpError(
errMsg: errMsg,
onReload: onReload,
),
],
);

View File

@@ -0,0 +1,359 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class MarqueeText extends StatelessWidget {
final double maxWidth;
final String text;
final TextStyle? style;
final int? count;
final bool bounce;
final double spacing;
const MarqueeText(
this.text, {
super.key,
required this.maxWidth,
this.style,
this.count,
this.bounce = true,
this.spacing = 0,
});
@override
Widget build(BuildContext context) {
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 1,
)..layout();
final width = textPainter.width;
final child = Text(
text,
style: style,
maxLines: 1,
textDirection: TextDirection.ltr,
);
if (width > maxWidth) {
return SingleWidgetMarquee(
child,
duration: Duration(milliseconds: (width / 50 * 1000).round()),
bounce: bounce,
count: count,
spacing: spacing,
);
} else {
return child;
}
}
}
class SingleWidgetMarquee extends StatefulWidget {
final Widget child;
final Duration? duration;
final bool bounce;
final double spacing;
final int? count;
const SingleWidgetMarquee(
this.child, {
super.key,
this.duration,
this.bounce = false,
this.spacing = 0,
this.count,
});
@override
State<StatefulWidget> createState() => _SingleWidgetMarqueeState();
}
class _SingleWidgetMarqueeState extends State<SingleWidgetMarquee>
with SingleTickerProviderStateMixin {
late final _controller = AnimationController(
vsync: this,
duration: widget.duration,
reverseDuration: widget.duration,
)..repeat(reverse: widget.bounce, count: widget.count);
@override
Widget build(BuildContext context) => widget.bounce
? BounceMarquee(
animation: _controller,
spacing: widget.spacing,
child: widget.child,
)
: NormalMarquee(
animation: _controller,
spacing: widget.spacing,
child: widget.child,
);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
abstract class Marquee extends SingleChildRenderObjectWidget {
final Axis direction;
final Clip clipBehavior;
final double spacing;
final Animation<double> animation;
const Marquee({
super.key,
required this.animation,
required super.child,
this.direction = Axis.horizontal,
this.clipBehavior = Clip.hardEdge,
this.spacing = 0,
});
@override
void updateRenderObject(
BuildContext context,
covariant MarqueeRender renderObject,
) {
renderObject
..direction = direction
..clipBehavior = clipBehavior
..animation = animation
..spacing = spacing;
}
}
class NormalMarquee extends Marquee {
const NormalMarquee({
super.key,
required super.animation,
required super.child,
super.direction,
super.clipBehavior,
super.spacing,
});
@override
RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender(
direction: direction,
animation: animation,
clipBehavior: clipBehavior,
spacing: spacing,
);
}
class BounceMarquee extends Marquee {
const BounceMarquee({
super.key,
required super.animation,
required super.child,
super.direction,
super.clipBehavior,
super.spacing,
});
@override
RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender(
direction: direction,
animation: animation,
clipBehavior: clipBehavior,
spacing: spacing,
);
}
abstract class MarqueeRender extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
MarqueeRender({
required Axis direction,
required Animation<double> animation,
required this.clipBehavior,
required this.spacing,
}) : _direction = direction,
_animation = animation,
assert(spacing.isFinite && !spacing.isNaN);
Clip clipBehavior;
double spacing;
Axis _direction;
Axis get direction => _direction;
set direction(Axis value) {
if (_direction == value) return;
_direction = value;
markNeedsLayout();
}
Animation<double> _animation;
Animation<double> get animation => _animation;
set animation(Animation<double> value) {
if (_animation == value) return;
if (_listened) {
_animation.removeListener(markNeedsPaint);
value.addListener(markNeedsPaint);
}
_animation = value;
}
@override
void detach() {
_removeListener();
super.detach();
}
bool _listened = false;
void _addListener() {
if (!_listened) {
_animation.addListener(markNeedsPaint);
_listened = true;
}
}
void _removeListener() {
if (_listened) {
_animation.removeListener(markNeedsPaint);
_listened = false;
}
}
late double _distance;
@override
void performLayout() {
final child = this.child;
if (child == null) {
size = constraints.smallest;
return;
}
if (_direction == Axis.horizontal) {
child.layout(
BoxConstraints(maxHeight: constraints.maxHeight),
parentUsesSize: true,
);
size = constraints.constrain(child.size);
_distance = child.size.width - size.width;
if (spacing.isNegative) spacing *= -size.width;
} else {
child.layout(
BoxConstraints(maxWidth: constraints.maxWidth),
parentUsesSize: true,
);
size = constraints.constrain(child.size);
_distance = child.size.height - size.height;
if (spacing.isNegative) spacing *= -size.height;
}
if (_distance > 0) {
_addListener();
} else {
_removeListener();
}
}
@override
bool get isRepaintBoundary => true;
void paintCenter(PaintingContext context, Offset offset) {
if (_direction == Axis.horizontal) {
context.paintChild(child!, Offset(offset.dx - _distance / 2, offset.dy));
} else {
context.paintChild(child!, Offset(offset.dx, offset.dy - _distance / 2));
}
}
}
class _BounceMarqueeRender extends MarqueeRender {
_BounceMarqueeRender({
required super.direction,
required super.animation,
required super.clipBehavior,
required super.spacing,
});
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) return;
final tick = _animation.value;
if (_distance > 0) {
final helfSpacing = spacing / 2.0;
void paintChild() {
if (_direction == Axis.horizontal) {
context.paintChild(
child!,
Offset(
offset.dx + helfSpacing - tick * (_distance + spacing),
offset.dy,
),
);
} else {
context.paintChild(
child!,
Offset(
offset.dx,
offset.dy + helfSpacing - tick * (_distance + spacing),
),
);
}
}
if (clipBehavior == Clip.none) {
paintChild();
} else {
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
}
} else {
paintCenter(context, offset);
}
}
}
class _NormalMarqueeRender extends MarqueeRender {
_NormalMarqueeRender({
required super.direction,
required super.animation,
required super.clipBehavior,
required super.spacing,
});
@override
void paint(PaintingContext context, Offset offset) {
final child = this.child;
if (child == null) return;
final tick = _animation.value;
if (_distance > 0) {
void paintChild() {
if (_direction == Axis.horizontal) {
final w = child.size.width + spacing;
final dx = tick * w;
context.paintChild(child, Offset(offset.dx - dx, offset.dy));
if (dx > _distance) {
context.paintChild(child, Offset(offset.dx + w - dx, offset.dy));
}
} else {
final h = child.size.height + spacing;
final dy = tick * h;
context.paintChild(child, Offset(offset.dx, offset.dy - dy));
if (dy > _distance) {
context.paintChild(child, Offset(offset.dx, offset.dy + h - dy));
}
}
}
if (clipBehavior == Clip.none) {
paintChild();
} else {
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
}
} else {
paintCenter(context, offset);
}
}
}

View File

@@ -1,111 +0,0 @@
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/extension.dart';
import '../constants.dart';
class NetworkImgLayer extends StatelessWidget {
const NetworkImgLayer({
super.key,
this.src,
required this.width,
this.height,
this.type,
this.fadeOutDuration,
this.fadeInDuration,
// 图片质量 默认1%
this.quality,
this.semanticsLabel,
this.radius,
this.imageBuilder,
this.isLongPic,
this.callback,
this.getPlaceHolder,
this.boxFit,
});
final String? src;
final double width;
final double? height;
final String? type;
final Duration? fadeOutDuration;
final Duration? fadeInDuration;
final int? quality;
final String? semanticsLabel;
final double? radius;
final ImageWidgetBuilder? imageBuilder;
final Function? isLongPic;
final Function? callback;
final Function? getPlaceHolder;
final BoxFit? boxFit;
@override
Widget build(BuildContext context) {
return src.isNullOrEmpty.not
? type == 'avatar'
? ClipOval(child: _buildImage(context))
: radius == 0 || type == 'emote'
? _buildImage(context)
: ClipRRect(
borderRadius: BorderRadius.circular(
radius ?? StyleString.imgRadius.x,
),
child: _buildImage(context),
)
: getPlaceHolder?.call() ?? placeholder(context);
}
Widget _buildImage(context) {
int? memCacheWidth, memCacheHeight;
if (height == null || callback?.call() == true || width <= height!) {
memCacheWidth = width.cacheSize(context);
} else {
memCacheHeight = height.cacheSize(context);
}
return CachedNetworkImage(
imageUrl: Utils.thumbnailImgUrl(src, quality),
width: width,
height: height,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: boxFit ?? BoxFit.cover,
alignment:
isLongPic?.call() == true ? Alignment.topCenter : Alignment.center,
fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120),
fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120),
filterQuality: FilterQuality.low,
placeholder: (BuildContext context, String url) =>
getPlaceHolder?.call() ?? placeholder(context),
imageBuilder: imageBuilder,
);
}
Widget placeholder(BuildContext context) {
return Container(
width: width,
height: height,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
shape: type == 'avatar' ? BoxShape.circle : BoxShape.rectangle,
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
borderRadius: type == 'avatar' || type == 'emote' || radius == 0
? null
: BorderRadius.circular(
radius ?? StyleString.imgRadius.x,
),
),
child: type == 'bg'
? const SizedBox.shrink()
: Center(
child: Image.asset(
type == 'avatar'
? 'assets/images/noface.jpeg'
: 'assets/images/loading.png',
width: width,
height: height,
cacheWidth: width.cacheSize(context),
),
),
);
}
}

View File

@@ -0,0 +1,461 @@
// 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/material.dart';
///
/// @docImport 'single_child_scroll_view.dart';
/// @docImport 'text.dart';
library;
import 'package:PiliPlus/common/widgets/page/scrollable.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
import 'package:flutter/rendering.dart';
class _ForceImplicitScrollPhysics extends ScrollPhysics {
const _ForceImplicitScrollPhysics({
required this.allowImplicitScrolling,
super.parent,
});
@override
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
return _ForceImplicitScrollPhysics(
allowImplicitScrolling: allowImplicitScrolling,
parent: buildParent(ancestor),
);
}
@override
final bool allowImplicitScrolling;
}
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
/// A scrollable list that works page by page.
///
/// Each child of a page view is forced to be the same size as the viewport.
///
/// 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
/// 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],
/// 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
/// which scroll horizontally.
///
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
/// {@end-tool}
///
/// ## 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]
/// 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.
///
/// See also:
///
/// * [PageController], which controls which page is visible in the view.
/// * [SingleChildScrollView], when you need to make a single child scrollable.
/// * [ListView], for a scrollable list of boxes.
/// * [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 {
/// Creates a scrollable list that works page by page from an explicit [List]
/// of widgets.
///
/// This constructor is appropriate for page views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the page view, instead of just
/// those children that are actually visible.
///
/// Like other widgets in the framework, this widget expects that
/// the [children] list will not be mutated after it has been passed in here.
/// See the documentation at [SliverChildListDelegate.children] for more details.
///
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
/// If [allowImplicitScrolling] is true, the [CustomPageView] 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].
/// {@endtemplate}
CustomPageView({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
this.header,
this.bgColor = Colors.transparent,
}) : childrenDelegate = SliverChildListDelegate(children);
final Widget? header;
final Color bgColor;
/// Creates a scrollable list that works page by page using widgets that are
/// created on demand.
///
/// This constructor is appropriate for page views with a large (or infinite)
/// 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
/// scroll extent.
///
/// [itemBuilder] will be called only with indices greater than or equal to
/// zero and less than [itemCount].
///
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
///
/// {@template flutter.widgets.PageView.findChildIndexCallback}
/// The [findChildIndexCallback] corresponds to the
/// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
/// a child widget may not map to its existing [RenderObject] when the order
/// of children returned from the children builder changes.
/// This may result in state-loss. This callback needs to be implemented if
/// the order of the children may change at a later time.
/// {@endtemplate}
///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
CustomPageView.builder({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
required NullableIndexedWidgetBuilder itemBuilder,
ChildIndexGetter? findChildIndexCallback,
int? itemCount,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
this.header,
this.bgColor = Colors.transparent,
}) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
);
/// Creates a scrollable list that works page by page with a custom child
/// model.
///
/// {@tool dartpad}
/// This example shows a [CustomPageView] 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({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
this.controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
this.header,
this.bgColor = Colors.transparent,
});
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
/// scrolling.
///
/// With this flag set to false, when accessibility focus reaches the end of
/// the current page and the user attempts to move it to the next element, the
/// focus will traverse to the next widget outside of the page view.
///
/// With this flag set to true, when accessibility focus reaches the end of
/// the current page and user attempts to move it to the next element, focus
/// will traverse to the next page in the page view.
final bool allowImplicitScrolling;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// The [Axis] along which the scroll view's offset increases with each page.
///
/// For the direction in which active scrolling may be occurring, see
/// [ScrollDirection].
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Whether the page view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this page
/// view is scrolled.
final PageController? controller;
/// How the page view should respond to user input.
///
/// For example, determines how the page view continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// Defaults to matching platform conventions.
final ScrollPhysics? physics;
/// Set to false to disable page snapping, useful for custom scroll behavior.
///
/// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
/// the page will snap to the beginning of the viewport; otherwise, the page
/// will snap to the center of the viewport.
final bool pageSnapping;
/// Called whenever the page in the center of the viewport changes.
final ValueChanged<int>? onPageChanged;
/// A delegate that provides the children for the [CustomPageView].
///
/// The [PageView.custom] constructor lets you specify this delegate
/// explicitly. The [CustomPageView] and [PageView.builder] constructors create a
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
/// respectively.
final SliverChildDelegate childrenDelegate;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// {@macro flutter.widgets.scrollable.hitTestBehavior}
///
/// Defaults to [HitTestBehavior.opaque].
final HitTestBehavior hitTestBehavior;
/// {@macro flutter.widgets.scrollable.scrollBehavior}
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar].
final ScrollBehavior? scrollBehavior;
/// Whether to add padding to both ends of the list.
///
/// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
/// such that the first and last child slivers will be in the center of
/// the viewport when scrolled all the way to the start or end, respectively.
///
/// If [PageController.viewportFraction] >= 1.0, this property has no effect.
///
/// This property defaults to true.
final bool padEnds;
@override
State<CustomPageView> createState() => _CustomPageViewState();
}
class _CustomPageViewState extends State<CustomPageView> {
int _lastReportedPage = 0;
late PageController _controller;
@override
void initState() {
super.initState();
_initController();
_lastReportedPage = _controller.initialPage;
}
@override
void dispose() {
if (widget.controller == null) {
_controller.dispose();
}
super.dispose();
}
void _initController() {
_controller = widget.controller ?? PageController();
}
@override
void didUpdateWidget(CustomPageView oldWidget) {
if (oldWidget.controller != widget.controller) {
if (oldWidget.controller == null) {
_controller.dispose();
}
_initController();
}
super.didUpdateWidget(oldWidget);
}
AxisDirection _getDirection(BuildContext context) {
switch (widget.scrollDirection) {
case Axis.horizontal:
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
final AxisDirection axisDirection = textDirectionToAxisDirection(
textDirection,
);
return widget.reverse
? flipAxisDirection(axisDirection)
: axisDirection;
case Axis.vertical:
return widget.reverse ? AxisDirection.up : AxisDirection.down;
}
}
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics =
_ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(
widget.pageSnapping
? _kPagePhysics.applyTo(
widget.physics ??
widget.scrollBehavior?.getScrollPhysics(context),
)
: widget.physics ??
widget.scrollBehavior?.getScrollPhysics(context),
);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
widget.onPageChanged != null &&
notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged!(currentPage);
}
}
return false;
},
child: CustomScrollable(
header: widget.header,
bgColor: widget.bgColor,
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: _controller,
physics: physics,
restorationId: widget.restorationId,
hitTestBehavior: widget.hitTestBehavior,
scrollBehavior:
widget.scrollBehavior ??
ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
),
],
);
},
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description
..add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection))
..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'))
..add(
DiagnosticsProperty<PageController>(
'controller',
_controller,
showName: false,
),
)
..add(
DiagnosticsProperty<ScrollPhysics>(
'physics',
widget.physics,
showName: false,
),
)
..add(
FlagProperty(
'pageSnapping',
value: widget.pageSnapping,
ifFalse: 'snapping disabled',
),
)
..add(
FlagProperty(
'allowImplicitScrolling',
value: widget.allowImplicitScrolling,
ifTrue: 'allow implicit scrolling',
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,378 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsRole;
import 'package:PiliPlus/common/widgets/page/page_view.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart' hide TabBarView, PageView;
/// A page view that displays the widget which corresponds to the currently
/// selected tab.
///
/// This widget is typically used in conjunction with a [TabBar].
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
///
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
/// ancestor.
///
/// The tab controller's [TabController.length] must equal the length of the
/// [children] list and the length of the [TabBar.tabs] list.
///
/// To see a sample implementation, visit the [TabController] documentation.
class CustomTabBarView extends StatefulWidget {
/// Creates a page view with one child per tab.
///
/// The length of [children] must be the same as the [controller]'s length.
const CustomTabBarView({
super.key,
required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge,
this.scrollDirection = Axis.horizontal,
this.header,
this.bgColor = Colors.transparent,
});
final Widget? header;
final Color bgColor;
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController? controller;
/// One widget per tab.
///
/// Its length must match the length of the [TabBar.tabs]
/// list, as well as the [controller]'s [TabController.length].
final List<Widget> children;
/// How the page view should respond to user input.
///
/// For example, determines how the page view continues to animate after the
/// user stops dragging the page view.
///
/// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used.
///
/// Defaults to matching platform conventions.
final ScrollPhysics? physics;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.widgets.pageview.viewportFraction}
final double viewportFraction;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
final Axis scrollDirection;
@override
State<CustomTabBarView> createState() => _CustomTabBarViewState();
}
class _CustomTabBarViewState extends State<CustomTabBarView> {
TabController? _controller;
PageController? _pageController;
late List<Widget> _childrenWithKey;
int? _currentIndex;
int _warpUnderwayCount = 0;
int _scrollUnderwayCount = 0;
bool _debugHasScheduledValidChildrenCountCheck = false;
// If the TabBarView is rebuilt with a new tab controller, the caller should
// dispose the old one. In that case the old controller's animation will be
// null and should not be accessed.
bool get _controllerIsValid => _controller?.animation != null;
void _updateTabController() {
final TabController? newController =
widget.controller ?? DefaultTabController.maybeOf(context);
assert(() {
if (newController == null) {
throw FlutterError(
'No TabController for ${widget.runtimeType}.\n'
'When creating a ${widget.runtimeType}, you must either provide an explicit '
'TabController using the "controller" property, or you must ensure that there '
'is a DefaultTabController above the ${widget.runtimeType}.\n'
'In this case, there was neither an explicit controller nor a default controller.',
);
}
return true;
}());
if (newController == _controller) {
return;
}
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = newController;
if (_controller != null) {
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
}
}
void _jumpToPage(int page) {
_warpUnderwayCount += 1;
_pageController!.jumpToPage(page);
_warpUnderwayCount -= 1;
}
Future<void> _animateToPage(
int page, {
required Duration duration,
required Curve curve,
}) async {
_warpUnderwayCount += 1;
await _pageController!.animateToPage(
page,
duration: duration,
curve: curve,
);
_warpUnderwayCount -= 1;
}
@override
void initState() {
super.initState();
_updateChildren();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateTabController();
_currentIndex = _controller!.index;
if (_pageController == null) {
_pageController = PageController(
initialPage: _currentIndex!,
viewportFraction: widget.viewportFraction,
);
} else {
_pageController!.jumpToPage(_currentIndex!);
}
}
@override
void didUpdateWidget(CustomTabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_currentIndex = _controller!.index;
_jumpToPage(_currentIndex!);
}
if (widget.viewportFraction != oldWidget.viewportFraction) {
_pageController?.dispose();
_pageController = PageController(
initialPage: _currentIndex!,
viewportFraction: widget.viewportFraction,
);
}
// While a warp is under way, we stop updating the tab page contents.
// This is tracked in https://github.com/flutter/flutter/issues/31269.
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
_updateChildren();
}
}
@override
void dispose() {
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
}
_controller = null;
_pageController?.dispose();
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
void _updateChildren() {
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
widget.children.map<Widget>((Widget child) {
return Semantics(role: SemanticsRole.tabPanel, child: child);
}).toList(),
);
}
void _handleTabControllerAnimationTick() {
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
return;
} // This widget is driving the controller's animation.
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
_warpToCurrentIndex();
}
}
void _warpToCurrentIndex() {
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
return;
}
final bool adjacentDestination =
(_currentIndex! - _controller!.previousIndex).abs() == 1;
if (adjacentDestination) {
_warpToAdjacentTab(_controller!.animationDuration);
} else {
_warpToNonAdjacentTab(_controller!.animationDuration);
}
}
Future<void> _warpToAdjacentTab(Duration duration) async {
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(_updateChildren);
}
return Future<void>.value();
}
Future<void> _warpToNonAdjacentTab(Duration duration) async {
final int previousIndex = _controller!.previousIndex;
assert((_currentIndex! - previousIndex).abs() > 1);
// initialPage defines which page is shown when starting the animation.
// This page is adjacent to the destination page.
final int initialPage = _currentIndex! > previousIndex
? _currentIndex! - 1
: _currentIndex! + 1;
setState(() {
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
final Widget temp = _childrenWithKey[initialPage];
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
_childrenWithKey[previousIndex] = temp;
});
// Make a first jump to the adjacent page.
_jumpToPage(initialPage);
// Jump or animate to the destination page.
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(_updateChildren);
}
}
void _syncControllerOffset() {
_controller!.offset = clampDouble(
_pageController!.page! - _controller!.index,
-1.0,
1.0,
);
}
// Called when the PageView scrolls
bool _handleScrollNotification(ScrollNotification notification) {
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
return false;
}
if (notification.depth != 0) {
return false;
}
if (!_controllerIsValid) {
return false;
}
_scrollUnderwayCount += 1;
final double page = _pageController!.page!;
if (notification is ScrollUpdateNotification &&
!_controller!.indexIsChanging) {
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
if (pageChanged) {
_controller!.index = page.round();
_currentIndex = _controller!.index;
}
_syncControllerOffset();
} else if (notification is ScrollEndNotification) {
_controller!.index = page.round();
_currentIndex = _controller!.index;
if (!_controller!.indexIsChanging) {
_syncControllerOffset();
}
}
_scrollUnderwayCount -= 1;
return false;
}
bool _debugScheduleCheckHasValidChildrenCount() {
if (_debugHasScheduledValidChildrenCountCheck) {
return true;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
_debugHasScheduledValidChildrenCountCheck = false;
if (!mounted) {
return;
}
assert(() {
if (_controller!.length != widget.children.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of children (${widget.children.length}) present in TabBarView's children property.",
);
}
return true;
}());
}, debugLabel: 'TabBarView.validChildrenCountCheck');
_debugHasScheduledValidChildrenCountCheck = true;
return true;
}
@override
Widget build(BuildContext context) {
assert(_debugScheduleCheckHasValidChildrenCount());
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: CustomPageView(
scrollDirection: widget.scrollDirection,
dragStartBehavior: widget.dragStartBehavior,
clipBehavior: widget.clipBehavior,
controller: _pageController,
physics: widget.physics == null
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
: const PageScrollPhysics().applyTo(widget.physics),
header: widget.header,
bgColor: widget.bgColor,
children: _childrenWithKey,
),
);
}
}

View File

@@ -1,22 +1,23 @@
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/avatar_badge_type.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'network_img_layer.dart';
class Avatar extends StatelessWidget {
final _BadgeType _badgeType;
final String avatar;
class PendantAvatar extends StatelessWidget {
final BadgeType _badgeType;
final String? avatar;
final double size;
final double badgeSize;
final String? garbPendantImage;
final dynamic roomId;
final int? roomId;
final VoidCallback? onTap;
const Avatar({
const PendantAvatar({
super.key,
required this.avatar,
this.size = 80,
@@ -26,42 +27,44 @@ class Avatar extends StatelessWidget {
this.garbPendantImage,
this.roomId,
this.onTap,
}) : _badgeType = officialType == null || officialType < 0
? isVip == true
? _BadgeType.vip
: _BadgeType.none
: officialType == 0
? _BadgeType.person
: officialType == 1
? _BadgeType.institution
: _BadgeType.none,
badgeSize = badgeSize ?? size / 3;
}) : _badgeType = officialType == null || officialType < 0
? isVip == true
? BadgeType.vip
: BadgeType.none
: officialType == 0
? BadgeType.person
: officialType == 1
? BadgeType.institution
: BadgeType.none,
badgeSize = badgeSize ?? size / 3;
static bool showDynDecorate = GStorage.showDynDecorate;
static bool showDynDecorate = Pref.showDynDecorate;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isMemberAvatar = size == 80;
return Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
onTap == null
? _buildAvatar(colorScheme)
? _buildAvatar(colorScheme, isMemberAvatar)
: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: _buildAvatar(colorScheme),
child: _buildAvatar(colorScheme, isMemberAvatar),
),
if (showDynDecorate && !garbPendantImage.isNullOrEmpty)
Positioned(
top: -0.375 *
top:
-0.375 *
(size == 80 ? size - 4 : size), // -(size * 1.75 - size) / 2
child: IgnorePointer(
child: CachedNetworkImage(
width: size * 1.75,
height: size * 1.75,
imageUrl: Utils.thumbnailImgUrl(garbPendantImage),
imageUrl: ImageUtil.thumbnailUrl(garbPendantImage),
),
),
),
@@ -69,14 +72,12 @@ class Avatar extends StatelessWidget {
Positioned(
bottom: 0,
child: InkWell(
onTap: () {
Get.toNamed('/liveRoom?roomid=$roomId');
},
onTap: () => PageUtils.toLiveRoom(roomId),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 1),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(36),
borderRadius: const BorderRadius.all(Radius.circular(36)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -99,14 +100,15 @@ class Avatar extends StatelessWidget {
),
),
)
else if (_badgeType != _BadgeType.none)
_buildBadge(colorScheme),
else if (_badgeType != BadgeType.none)
_buildBadge(colorScheme, isMemberAvatar),
],
);
}
Widget _buildAvatar(ColorScheme colorScheme) => size == 80
? Container(
Widget _buildAvatar(ColorScheme colorScheme, bool isMemberAvatar) =>
isMemberAvatar
? DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
width: 2,
@@ -114,56 +116,50 @@ class Avatar extends StatelessWidget {
),
shape: BoxShape.circle,
),
child: NetworkImgLayer(
src: avatar,
width: size,
height: size,
type: 'avatar',
child: Padding(
padding: const EdgeInsets.all(2),
child: NetworkImgLayer(
src: avatar,
width: size,
height: size,
type: ImageType.avatar,
),
),
)
: NetworkImgLayer(
src: avatar,
width: size,
height: size,
type: 'avatar',
type: ImageType.avatar,
);
Widget _buildBadge(ColorScheme colorScheme) {
Widget _buildBadge(ColorScheme colorScheme, bool isMemberAvatar) {
final child = switch (_badgeType) {
_BadgeType.vip => Image.asset(
'assets/images/big-vip.png',
height: badgeSize,
semanticLabel: _badgeType.desc,
),
BadgeType.vip => Image.asset(
'assets/images/big-vip.png',
height: badgeSize,
semanticLabel: _badgeType.desc,
),
_ => Icon(
Icons.offline_bolt,
color: _badgeType.color,
size: badgeSize,
semanticLabel: _badgeType.desc,
),
Icons.offline_bolt,
color: _badgeType.color,
size: badgeSize,
semanticLabel: _badgeType.desc,
),
};
final offset = isMemberAvatar ? 2.0 : 0.0;
return Positioned(
right: 0,
bottom: 0,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.surface,
),
child: child),
));
right: offset,
bottom: offset,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.surface,
),
child: child,
),
),
);
}
}
enum _BadgeType { none, vip, person, institution }
extension _BadgeTypeExt on _BadgeType {
String get desc => const ['', '大会员', '认证个人', '认证机构'][index];
Color get color => const [
Colors.transparent,
Color(0xFFFF6699),
Color(0xFFFFCC00),
Colors.lightBlueAccent
][index];
}

View File

@@ -267,9 +267,10 @@ class ProgressBar extends LeafRenderObjectWidget {
onDragUpdate: onDragUpdate,
onDragEnd: onDragEnd,
barHeight: barHeight,
baseBarColor: baseBarColor ?? primaryColor.withOpacity(0.24),
baseBarColor: baseBarColor ?? primaryColor.withValues(alpha: 0.24),
progressBarColor: progressBarColor ?? primaryColor,
bufferedBarColor: bufferedBarColor ?? primaryColor.withOpacity(0.24),
bufferedBarColor:
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24),
barCapShape: barCapShape,
thumbRadius: thumbRadius,
thumbColor: thumbColor ?? primaryColor,
@@ -300,9 +301,10 @@ class ProgressBar extends LeafRenderObjectWidget {
..onDragUpdate = onDragUpdate
..onDragEnd = onDragEnd
..barHeight = barHeight
..baseBarColor = baseBarColor ?? primaryColor.withOpacity(0.24)
..baseBarColor = baseBarColor ?? primaryColor.withValues(alpha: 0.24)
..progressBarColor = progressBarColor ?? primaryColor
..bufferedBarColor = bufferedBarColor ?? primaryColor.withOpacity(0.24)
..bufferedBarColor =
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24)
..barCapShape = barCapShape
..thumbRadius = thumbRadius
..thumbColor = thumbColor ?? primaryColor
@@ -320,43 +322,60 @@ class ProgressBar extends LeafRenderObjectWidget {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('progress', progress.toString()));
properties.add(StringProperty('total', total.toString()));
properties.add(StringProperty('buffered', buffered.toString()));
properties.add(ObjectFlagProperty<ValueChanged<Duration>>('onSeek', onSeek,
ifNull: 'unimplemented'));
properties.add(ObjectFlagProperty<ThumbDragStartCallback>(
'onDragStart', onDragStart,
ifNull: 'unimplemented'));
properties.add(ObjectFlagProperty<ThumbDragUpdateCallback>(
'onDragUpdate', onDragUpdate,
ifNull: 'unimplemented'));
properties.add(ObjectFlagProperty<VoidCallback>('onDragEnd', onDragEnd,
ifNull: 'unimplemented'));
properties.add(DoubleProperty('barHeight', barHeight));
properties.add(ColorProperty('baseBarColor', baseBarColor));
properties.add(ColorProperty('progressBarColor', progressBarColor));
properties.add(ColorProperty('bufferedBarColor', bufferedBarColor));
properties.add(StringProperty('barCapShape', barCapShape.toString()));
properties.add(DoubleProperty('thumbRadius', thumbRadius));
properties.add(ColorProperty('thumbColor', thumbColor));
properties.add(ColorProperty('thumbGlowColor', thumbGlowColor));
properties.add(DoubleProperty('thumbGlowRadius', thumbGlowRadius));
properties.add(
FlagProperty(
'thumbCanPaintOutsideBar',
value: thumbCanPaintOutsideBar,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
),
);
properties
.add(StringProperty('timeLabelLocation', timeLabelLocation.toString()));
properties.add(StringProperty('timeLabelType', timeLabelType.toString()));
properties
.add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle));
properties.add(DoubleProperty('timeLabelPadding', timeLabelPadding));
..add(StringProperty('progress', progress.toString()))
..add(StringProperty('total', total.toString()))
..add(StringProperty('buffered', buffered.toString()))
..add(
ObjectFlagProperty<ValueChanged<Duration>>(
'onSeek',
onSeek,
ifNull: 'unimplemented',
),
)
..add(
ObjectFlagProperty<ThumbDragStartCallback>(
'onDragStart',
onDragStart,
ifNull: 'unimplemented',
),
)
..add(
ObjectFlagProperty<ThumbDragUpdateCallback>(
'onDragUpdate',
onDragUpdate,
ifNull: 'unimplemented',
),
)
..add(
ObjectFlagProperty<VoidCallback>(
'onDragEnd',
onDragEnd,
ifNull: 'unimplemented',
),
)
..add(DoubleProperty('barHeight', barHeight))
..add(ColorProperty('baseBarColor', baseBarColor))
..add(ColorProperty('progressBarColor', progressBarColor))
..add(ColorProperty('bufferedBarColor', bufferedBarColor))
..add(StringProperty('barCapShape', barCapShape.toString()))
..add(DoubleProperty('thumbRadius', thumbRadius))
..add(ColorProperty('thumbColor', thumbColor))
..add(ColorProperty('thumbGlowColor', thumbGlowColor))
..add(DoubleProperty('thumbGlowRadius', thumbGlowRadius))
..add(
FlagProperty(
'thumbCanPaintOutsideBar',
value: thumbCanPaintOutsideBar,
ifTrue: 'true',
ifFalse: 'false',
showName: true,
),
)
..add(StringProperty('timeLabelLocation', timeLabelLocation.toString()))
..add(StringProperty('timeLabelType', timeLabelType.toString()))
..add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle))
..add(DoubleProperty('timeLabelPadding', timeLabelPadding));
}
}
@@ -385,7 +404,8 @@ class ThumbDragDetails {
final Offset localPosition;
@override
String toString() => '${objectRuntimeType(this, 'ThumbDragDetails')}('
String toString() =>
'${objectRuntimeType(this, 'ThumbDragDetails')}('
'time: $timeStamp, '
'global: $globalPosition, '
'local: $localPosition)';
@@ -430,27 +450,27 @@ class _RenderProgressBar extends RenderBox {
TextStyle? timeLabelTextStyle,
double timeLabelPadding = 0.0,
double textScaleFactor = 1.0,
}) : _total = total,
_buffered = buffered,
_onSeek = onSeek,
_onDragStartUserCallback = onDragStart,
_onDragUpdateUserCallback = onDragUpdate,
_onDragEndUserCallback = onDragEnd,
_barHeight = barHeight,
_baseBarColor = baseBarColor,
_progressBarColor = progressBarColor,
_bufferedBarColor = bufferedBarColor,
_barCapShape = barCapShape,
_thumbRadius = thumbRadius,
_thumbColor = thumbColor,
_thumbGlowColor = thumbGlowColor,
_thumbGlowRadius = thumbGlowRadius,
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
_timeLabelLocation = timeLabelLocation,
_timeLabelType = timeLabelType,
_timeLabelTextStyle = timeLabelTextStyle,
_timeLabelPadding = timeLabelPadding,
_textScaleFactor = textScaleFactor {
}) : _total = total,
_buffered = buffered,
_onSeek = onSeek,
_onDragStartUserCallback = onDragStart,
_onDragUpdateUserCallback = onDragUpdate,
_onDragEndUserCallback = onDragEnd,
_barHeight = barHeight,
_baseBarColor = baseBarColor,
_progressBarColor = progressBarColor,
_bufferedBarColor = bufferedBarColor,
_barCapShape = barCapShape,
_thumbRadius = thumbRadius,
_thumbColor = thumbColor,
_thumbGlowColor = thumbGlowColor,
_thumbGlowRadius = thumbGlowRadius,
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
_timeLabelLocation = timeLabelLocation,
_timeLabelType = timeLabelType,
_timeLabelTextStyle = timeLabelTextStyle,
_timeLabelPadding = timeLabelPadding,
_textScaleFactor = textScaleFactor {
_drag = _EagerHorizontalDragGestureRecognizer()
..onStart = _onDragStart
..onUpdate = _onDragUpdate
@@ -489,11 +509,13 @@ class _RenderProgressBar extends RenderBox {
}
_userIsDraggingThumb = true;
_updateThumbPosition(details.localPosition);
onDragStart?.call(ThumbDragDetails(
timeStamp: _currentThumbDuration(),
globalPosition: details.globalPosition,
localPosition: details.localPosition,
));
onDragStart?.call(
ThumbDragDetails(
timeStamp: _currentThumbDuration(),
globalPosition: details.globalPosition,
localPosition: details.localPosition,
),
);
}
void _onDragUpdate(DragUpdateDetails details) {
@@ -501,11 +523,13 @@ class _RenderProgressBar extends RenderBox {
return;
}
_updateThumbPosition(details.localPosition);
onDragUpdate?.call(ThumbDragDetails(
timeStamp: _currentThumbDuration(),
globalPosition: details.globalPosition,
localPosition: details.localPosition,
));
onDragUpdate?.call(
ThumbDragDetails(
timeStamp: _currentThumbDuration(),
globalPosition: details.globalPosition,
localPosition: details.localPosition,
),
);
}
void _onDragEnd(DragEndDetails details) {
@@ -621,9 +645,8 @@ class _RenderProgressBar extends RenderBox {
TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: _timeLabelTextStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
);
textPainter.layout(minWidth: 0, maxWidth: double.infinity);
textScaler: TextScaler.linear(textScaleFactor),
)..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter;
}
@@ -919,9 +942,9 @@ class _RenderProgressBar extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
final canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
final canvas = context.canvas
..save()
..translate(offset.dx, offset.dy);
switch (_timeLabelLocation) {
case TimeLabelLocation.above:
@@ -966,8 +989,9 @@ class _RenderProgressBar extends RenderBox {
_rightTimeLabel().paint(canvas, rightLabelOffset);
// progress bar
final barDy =
(isLabelBelow) ? 0.0 : _leftLabelSize.height + _timeLabelPadding;
final barDy = (isLabelBelow)
? 0.0
: _leftLabelSize.height + _timeLabelPadding;
_drawProgressBar(canvas, Offset(0, barDy), Size(barWidth, barHeight));
}
@@ -992,7 +1016,8 @@ class _RenderProgressBar extends RenderBox {
// progress bar
final leftLabelWidth = leftLabelSize.width;
final barHeight = _heightWhenNoLabels();
final barWidth = size.width -
final barWidth =
size.width -
2 * _defaultSidePadding -
2 * _timeLabelPadding -
leftLabelWidth -
@@ -1013,8 +1038,9 @@ class _RenderProgressBar extends RenderBox {
}
void _drawProgressBar(Canvas canvas, Offset offset, Size localSize) {
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas
..save()
..translate(offset.dx, offset.dy);
_drawBaseBar(canvas, localSize);
_drawBufferedBar(canvas, localSize);
_drawCurrentProgressBar(canvas, localSize);
@@ -1049,11 +1075,12 @@ class _RenderProgressBar extends RenderBox {
);
}
void _drawBar(
{required Canvas canvas,
required Size availableSize,
required double widthProportion,
required Color color}) {
void _drawBar({
required Canvas canvas,
required Size availableSize,
required double widthProportion,
required Color color,
}) {
final strokeCap = (_barCapShape == BarCapShape.round)
? StrokeCap.round
: StrokeCap.square;
@@ -1093,8 +1120,9 @@ class _RenderProgressBar extends RenderBox {
}
String _getTimeString(Duration time) {
final minutes =
time.inMinutes.remainder(Duration.minutesPerHour).toString();
final minutes = time.inMinutes
.remainder(Duration.minutesPerHour)
.toString();
final seconds = time.inSeconds
.remainder(Duration.secondsPerMinute)
.toString()
@@ -1109,17 +1137,18 @@ class _RenderProgressBar extends RenderBox {
super.describeSemanticsConfiguration(config);
// description
config.textDirection = TextDirection.ltr;
config.label = '进度条'; //'Progress bar';
config.value = '${(_thumbValue * 100).round()}%';
// increase action
config.onIncrease = increaseAction;
config
..textDirection = TextDirection.ltr
..label =
'进度条' //'Progress bar';
..value = '${(_thumbValue * 100).round()}%'
// increase action
..onIncrease = increaseAction;
final increased = _thumbValue + _semanticActionUnit;
config.increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%';
// decrease action
config.onDecrease = decreaseAction;
config
..increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%'
// decrease action
..onDecrease = decreaseAction;
final decreased = _thumbValue - _semanticActionUnit;
config.decreasedValue = '${((decreased).clamp(0.0, 1.0) * 100).round()}%';
}

View File

@@ -33,40 +33,40 @@ class SegmentProgressBar extends CustomPainter {
final paint = Paint()..style = PaintingStyle.fill;
for (int i = 0; i < segmentColors.length; i++) {
paint.color = segmentColors[i].color;
final segmentStart = segmentColors[i].start * size.width;
final segmentEnd = segmentColors[i].end * size.width;
final item = segmentColors[i];
paint.color = item.color;
final segmentStart = item.start * size.width;
final segmentEnd = item.end * size.width;
if (segmentEnd > segmentStart ||
(segmentEnd == segmentStart && segmentStart > 0)) {
if (segmentColors[i].title != null) {
if (item.title != null) {
double fontSize = 10;
_defHeight ??= (TextPainter(
_defHeight ??=
(TextPainter(
text: TextSpan(
text: segmentColors[i].title,
text: item.title,
style: TextStyle(
fontSize: fontSize,
),
),
textDirection: TextDirection.ltr,
)..layout())
.height +
)..layout()).height +
2;
TextPainter getTextPainter() => TextPainter(
text: TextSpan(
text: segmentColors[i].title,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
height: 1,
),
),
strutStyle:
StrutStyle(leading: 0, height: 1, fontSize: fontSize),
textDirection: TextDirection.ltr,
)..layout();
text: TextSpan(
text: item.title,
style: TextStyle(
color: Colors.white,
fontSize: fontSize,
height: 1,
),
),
strutStyle: StrutStyle(leading: 0, height: 1, fontSize: fontSize),
textDirection: TextDirection.ltr,
)..layout();
TextPainter textPainter = getTextPainter();
@@ -89,7 +89,7 @@ class SegmentProgressBar extends CustomPainter {
size.width,
0,
),
Paint()..color = Colors.grey[600]!.withOpacity(0.45),
Paint()..color = Colors.grey[600]!.withValues(alpha: 0.45),
);
}
@@ -106,8 +106,8 @@ class SegmentProgressBar extends CustomPainter {
double textX = i == 0
? (segmentStart - textPainter.width) / 2
: (segmentStart - prevStart - textPainter.width) / 2 +
prevStart +
1;
prevStart +
1;
double textY = (-_defHeight! - textPainter.height) / 2;
textPainter.paint(canvas, Offset(textX, textY));
} else {

View File

@@ -2,18 +2,19 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:flutter/material.dart';
Widget videoProgressIndicator(double progress) => ClipRect(
clipper: ProgressClipper(),
child: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: LinearProgressIndicator(
minHeight: 10,
value: progress,
),
),
);
clipper: ProgressClipper(),
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: LinearProgressIndicator(
minHeight: 10,
value: progress,
stopIndicatorColor: Colors.transparent,
),
),
);
class ProgressClipper extends CustomClipper<Rect> {
@override

View File

@@ -17,16 +17,16 @@ class RadioWidget<T> extends StatelessWidget {
});
Widget _child() => Row(
children: [
Radio<T>(
value: value,
groupValue: groupValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
);
children: [
Radio<T>(
value: value,
groupValue: groupValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
);
@override
Widget build(BuildContext context) {

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart' hide RefreshIndicator;
@@ -23,8 +24,8 @@ Widget refreshIndicator({
// The over-scroll distance that moves the indicator to its maximum
// displacement, as a percentage of the scrollable's container extent.
double displacement = 20;
double kDragContainerExtentPercentage = 0.25;
double displacement = Pref.refreshDisplacement;
double kDragContainerExtentPercentage = Pref.refreshDragPercentage;
// How much the scroll's drag gesture can overshoot the RefreshIndicator's
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
@@ -46,15 +47,25 @@ const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
/// Used by [RefreshIndicator.onRefresh].
typedef RefreshCallback = Future<void> Function();
// The state machine moves through these modes only when the scrollable
// identified by scrollableKey has been scrolled to its min or max limit.
enum _RefreshIndicatorMode {
drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the onRefresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
done, // Animating the indicator's fade-out after refreshing.
canceled, // Animating the indicator's fade-out after not arming.
/// Indicates current status of Material `RefreshIndicator`.
enum RefreshIndicatorStatus {
/// Pointer is down.
drag,
/// Dragged far enough that an up event will run the onRefresh callback.
armed,
/// Animating to the indicator's final "displacement".
snap,
/// Running the refresh callback.
refresh,
/// Animating the indicator's fade-out after refreshing.
done,
/// Animating the indicator's fade-out after not arming.
canceled,
}
/// Used to configure how [RefreshIndicator] can be triggered.
@@ -68,7 +79,7 @@ enum RefreshIndicatorTriggerMode {
onEdge,
}
enum _IndicatorType { material, adaptive }
enum _IndicatorType { material, adaptive, noSpinner }
/// A widget that supports the Material "swipe to refresh" idiom.
///
@@ -96,6 +107,12 @@ enum _IndicatorType { material, adaptive }
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to use [RefreshIndicator] without the spinner.
///
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart **
/// {@end-tool}
///
/// ## Troubleshooting
///
/// ### Refresh indicator does not show up
@@ -149,7 +166,10 @@ class RefreshIndicator extends StatefulWidget {
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.material;
this.elevation = 2.0,
}) : _indicatorType = _IndicatorType.material,
onStatusChange = null,
assert(elevation >= 0.0);
/// Creates an adaptive [RefreshIndicator] based on whether the target
/// platform is iOS or macOS, following Material design's
@@ -180,7 +200,35 @@ class RefreshIndicator extends StatefulWidget {
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.adaptive;
this.elevation = 2.0,
}) : _indicatorType = _IndicatorType.adaptive,
onStatusChange = null,
assert(elevation >= 0.0);
/// Creates a [RefreshIndicator] with no spinner and calls `onRefresh` when
/// successfully armed by a drag event.
///
/// Events can be optionally listened by using the `onStatusChange` callback.
const RefreshIndicator.noSpinner({
super.key,
required this.child,
required this.onRefresh,
this.onStatusChange,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
this.elevation = 2.0,
}) : _indicatorType = _IndicatorType.noSpinner,
// The following parameters aren't used because [_IndicatorType.noSpinner] is being used,
// which involves showing no spinner, hence the following parameters are useless since
// their only use is to change the spinner's appearance.
displacement = 0.0,
edgeOffset = 0.0,
color = null,
backgroundColor = null,
strokeWidth = 0.0,
assert(elevation >= 0.0);
/// The widget below this widget in the tree.
///
@@ -220,6 +268,10 @@ class RefreshIndicator extends StatefulWidget {
/// [Future] must complete when the refresh operation is finished.
final RefreshCallback onRefresh;
/// Called to get the current status of the [RefreshIndicator] to update the UI as needed.
/// This is an optional parameter, used to fine tune app cases.
final ValueChanged<RefreshIndicatorStatus?>? onStatusChange;
/// The progress indicator's foreground color. The current theme's
/// [ColorScheme.primary] by default.
final Color? color;
@@ -266,6 +318,11 @@ class RefreshIndicator extends StatefulWidget {
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
final RefreshIndicatorTriggerMode triggerMode;
/// Defines the elevation of the underlying [RefreshIndicator].
///
/// Defaults to 2.0.
final double elevation;
@override
RefreshIndicatorState createState() => RefreshIndicatorState();
}
@@ -281,38 +338,50 @@ class RefreshIndicatorState extends State<RefreshIndicator>
late Animation<double> _value;
late Animation<Color?> _valueColor;
_RefreshIndicatorMode? _mode;
RefreshIndicatorStatus? _status;
late Future<void> _pendingRefreshFuture;
bool? _isIndicatorAtTop;
double? _dragOffset;
late Color _effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
static final Animatable<double> _threeQuarterTween =
Tween<double>(begin: 0.0, end: 0.75);
static final Animatable<double> _kDragSizeFactorLimitTween =
Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit);
static final Animatable<double> _oneToZeroTween =
Tween<double>(begin: 1.0, end: 0.0);
static final Animatable<double> _threeQuarterTween = Tween<double>(
begin: 0.0,
end: 0.75,
);
static final Animatable<double> _kDragSizeFactorLimitTween = Tween<double>(
begin: 0.0,
end: _kDragSizeFactorLimit,
);
static final Animatable<double> _oneToZeroTween = Tween<double>(
begin: 1.0,
end: 0.0,
);
@protected
@override
void initState() {
super.initState();
_positionController = AnimationController(vsync: this);
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
_value = _positionController.drive(
_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
// The "value" of the circular progress indicator during a drag.
_value = _positionController.drive(_threeQuarterTween);
_scaleController = AnimationController(vsync: this);
_scaleFactor = _scaleController.drive(_oneToZeroTween);
}
@protected
@override
void didChangeDependencies() {
_setupColorTween();
super.didChangeDependencies();
}
@protected
@override
void didUpdateWidget(covariant RefreshIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -321,6 +390,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
}
@protected
@override
void dispose() {
_positionController.dispose();
@@ -343,9 +413,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
begin: color.withAlpha(0),
end: color.withAlpha(color.alpha),
).chain(
CurveTween(
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit),
),
CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)),
),
);
}
@@ -364,7 +432,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
notification.metrics.extentAfter == 0.0) ||
(notification.metrics.axisDirection == AxisDirection.down &&
notification.metrics.extentBefore == 0.0)) &&
_mode == null &&
_status == null &&
_start(notification.metrics.axisDirection);
}
@@ -374,23 +442,24 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
if (_shouldStart(notification)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
_status = RefreshIndicatorStatus.drag;
widget.onStatusChange?.call(_status);
});
return false;
}
final bool? indicatorAtTopNow =
switch (notification.metrics.axisDirection) {
AxisDirection.down || AxisDirection.up => true,
AxisDirection.left || AxisDirection.right => null,
};
AxisDirection.down || AxisDirection.up => true,
AxisDirection.left || AxisDirection.right => null,
};
if (indicatorAtTopNow != _isIndicatorAtTop) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
_dismiss(_RefreshIndicatorMode.canceled);
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
_dismiss(RefreshIndicatorStatus.canceled);
}
} else if (notification is ScrollUpdateNotification) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.scrollDelta!;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
@@ -398,7 +467,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
_checkDragOffset(notification.metrics.viewportDimension);
}
if (_mode == _RefreshIndicatorMode.armed &&
if (_status == RefreshIndicatorStatus.armed &&
notification.dragDetails == null) {
// On iOS start the refresh when the Scrollable bounces back from the
// overscroll (ScrollNotification indicating this don't have dragDetails
@@ -406,8 +475,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
_show();
}
} else if (notification is OverscrollNotification) {
if (_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.overscroll;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
@@ -416,19 +485,19 @@ class RefreshIndicatorState extends State<RefreshIndicator>
_checkDragOffset(notification.metrics.viewportDimension);
}
} else if (notification is ScrollEndNotification) {
switch (_mode) {
case _RefreshIndicatorMode.armed:
switch (_status) {
case RefreshIndicatorStatus.armed:
if (_positionController.value < 1.0) {
_dismiss(_RefreshIndicatorMode.canceled);
_dismiss(RefreshIndicatorStatus.canceled);
} else {
_show();
}
case _RefreshIndicatorMode.drag:
_dismiss(_RefreshIndicatorMode.canceled);
case _RefreshIndicatorMode.canceled:
case _RefreshIndicatorMode.done:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
case RefreshIndicatorStatus.drag:
_dismiss(RefreshIndicatorStatus.canceled);
case RefreshIndicatorStatus.canceled:
case RefreshIndicatorStatus.done:
case RefreshIndicatorStatus.refresh:
case RefreshIndicatorStatus.snap:
case null:
// do nothing
break;
@@ -438,11 +507,12 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
bool _handleIndicatorNotification(
OverscrollIndicatorNotification notification) {
OverscrollIndicatorNotification notification,
) {
if (notification.depth != 0 || !notification.leading) {
return false;
}
if (_mode == _RefreshIndicatorMode.drag) {
if (_status == RefreshIndicatorStatus.drag) {
notification.disallowIndicator();
return true;
}
@@ -450,7 +520,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
bool _start(AxisDirection direction) {
assert(_mode == null);
assert(_status == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
switch (direction) {
@@ -470,79 +540,94 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
void _checkDragOffset(double containerExtent) {
assert(_mode == _RefreshIndicatorMode.drag ||
_mode == _RefreshIndicatorMode.armed);
assert(
_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed,
);
double newValue =
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
if (_mode == _RefreshIndicatorMode.armed) {
if (_status == RefreshIndicatorStatus.armed) {
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
}
_positionController.value =
clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds
if (_mode == _RefreshIndicatorMode.drag &&
_positionController.value = clampDouble(
newValue,
0.0,
1.0,
); // This triggers various rebuilds.
if (_status == RefreshIndicatorStatus.drag &&
_valueColor.value!.alpha == _effectiveValueColor.alpha) {
_mode = _RefreshIndicatorMode.armed;
_status = RefreshIndicatorStatus.armed;
widget.onStatusChange?.call(_status);
}
}
// Stop showing the refresh indicator.
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
Future<void> _dismiss(RefreshIndicatorStatus newMode) async {
await Future<void>.value();
// This can only be called from _show() when refreshing and
// _handleScrollNotification in response to a ScrollEndNotification or
// direction change.
assert(newMode == _RefreshIndicatorMode.canceled ||
newMode == _RefreshIndicatorMode.done);
assert(
newMode == RefreshIndicatorStatus.canceled ||
newMode == RefreshIndicatorStatus.done,
);
setState(() {
_mode = newMode;
_status = newMode;
widget.onStatusChange?.call(_status);
});
switch (_mode!) {
case _RefreshIndicatorMode.done:
await _scaleController.animateTo(1.0,
duration: _kIndicatorScaleDuration);
case _RefreshIndicatorMode.canceled:
await _positionController.animateTo(0.0,
duration: _kIndicatorScaleDuration);
case _RefreshIndicatorMode.armed:
case _RefreshIndicatorMode.drag:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
switch (_status!) {
case RefreshIndicatorStatus.done:
await _scaleController.animateTo(
1.0,
duration: _kIndicatorScaleDuration,
);
case RefreshIndicatorStatus.canceled:
await _positionController.animateTo(
0.0,
duration: _kIndicatorScaleDuration,
);
case RefreshIndicatorStatus.armed:
case RefreshIndicatorStatus.drag:
case RefreshIndicatorStatus.refresh:
case RefreshIndicatorStatus.snap:
assert(false);
}
if (mounted && _mode == newMode) {
if (mounted && _status == newMode) {
_dragOffset = null;
_isIndicatorAtTop = null;
setState(() {
_mode = null;
_status = null;
});
}
}
void _show() {
assert(_mode != _RefreshIndicatorMode.refresh);
assert(_mode != _RefreshIndicatorMode.snap);
assert(_status != RefreshIndicatorStatus.refresh);
assert(_status != RefreshIndicatorStatus.snap);
final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap;
_status = RefreshIndicatorStatus.snap;
widget.onStatusChange?.call(_status);
_positionController
.animateTo(1.0 / _kDragSizeFactorLimit,
duration: _kIndicatorSnapDuration)
.then<void>((void value) {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
});
.animateTo(
1.0 / _kDragSizeFactorLimit,
duration: _kIndicatorSnapDuration,
)
.whenComplete(() {
if (mounted && _status == RefreshIndicatorStatus.snap) {
setState(() {
// Show the indeterminate progress indicator.
_status = RefreshIndicatorStatus.refresh;
});
final Future<void> refreshResult = widget.onRefresh();
refreshResult.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete();
_dismiss(_RefreshIndicatorMode.done);
widget.onRefresh().whenComplete(() {
if (mounted && _status == RefreshIndicatorStatus.refresh) {
completer.complete();
_dismiss(RefreshIndicatorStatus.done);
}
});
}
});
}
});
}
/// Show the refresh indicator and run the refresh callback as if it had
@@ -562,9 +647,9 @@ class RefreshIndicatorState extends State<RefreshIndicator>
/// actual scroll view. It defaults to showing the indicator at the top. To
/// show it at the bottom, set `atTop` to false.
Future<void> show({bool atTop = true}) {
if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
if (_mode == null) {
if (_status != RefreshIndicatorStatus.refresh &&
_status != RefreshIndicatorStatus.snap) {
if (_status == null) {
_start(atTop ? AxisDirection.down : AxisDirection.up);
}
_show();
@@ -572,6 +657,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
return _pendingRefreshFuture;
}
@protected
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
@@ -583,7 +669,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
),
);
assert(() {
if (_mode == null) {
if (_status == null) {
assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
} else {
@@ -594,14 +680,14 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}());
final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh ||
_mode == _RefreshIndicatorMode.done;
_status == RefreshIndicatorStatus.refresh ||
_status == RefreshIndicatorStatus.done;
return Stack(
clipBehavior: Clip.none,
children: <Widget>[
child,
if (_mode != null)
if (_status != null)
Positioned(
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
@@ -609,41 +695,47 @@ class RefreshIndicatorState extends State<RefreshIndicator>
right: 0.0,
child: SizeTransition(
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
sizeFactor: _positionFactor, // this is what brings it down
child: Container(
sizeFactor: _positionFactor, // This is what brings it down.
child: Padding(
padding: _isIndicatorAtTop!
? EdgeInsets.only(top: widget.displacement)
: EdgeInsets.only(bottom: widget.displacement),
alignment: _isIndicatorAtTop!
? Alignment.topCenter
: Alignment.bottomCenter,
child: ScaleTransition(
scale: _scaleFactor,
child: AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
final Widget materialIndicator = RefreshProgressIndicator(
semanticsLabel: widget.semanticsLabel ??
MaterialLocalizations.of(context)
.refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator ? null : _value.value,
valueColor: _valueColor,
backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth,
);
child: Align(
alignment: _isIndicatorAtTop!
? Alignment.topCenter
: Alignment.bottomCenter,
child: ScaleTransition(
scale: _scaleFactor,
child: AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
final Widget materialIndicator =
RefreshProgressIndicator(
semanticsLabel:
widget.semanticsLabel ??
MaterialLocalizations.of(
context,
).refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator
? null
: _value.value,
valueColor: _valueColor,
backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth,
elevation: widget.elevation,
);
final Widget cupertinoIndicator =
CupertinoActivityIndicator(
color: widget.color,
);
final Widget cupertinoIndicator =
CupertinoActivityIndicator(
color: widget.color,
);
switch (widget._indicatorType) {
case _IndicatorType.material:
return materialIndicator;
switch (widget._indicatorType) {
case _IndicatorType.material:
return materialIndicator;
case _IndicatorType.adaptive:
{
case _IndicatorType.adaptive:
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
@@ -655,9 +747,12 @@ class RefreshIndicatorState extends State<RefreshIndicator>
case TargetPlatform.macOS:
return cupertinoIndicator;
}
}
}
},
case _IndicatorType.noSpinner:
return Container();
}
},
),
),
),
),

View File

@@ -1,552 +0,0 @@
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/bangumi/introduction/controller.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/download.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'package:saver_gallery/saver_gallery.dart';
import 'package:share_plus/share_plus.dart';
class SavePanel extends StatefulWidget {
const SavePanel({
required this.item,
// reply
this.upMid,
super.key,
});
final dynamic upMid;
final dynamic item;
@override
State<SavePanel> createState() => _SavePanelState();
static void toSavePanel({upMid, item}) {
Get.generalDialog(
barrierLabel: '',
barrierDismissible: true,
pageBuilder: (context, animation, secondaryAnimation) {
return SavePanel(upMid: upMid, item: item);
},
transitionDuration: const Duration(milliseconds: 255),
transitionBuilder: (context, animation, secondaryAnimation, child) {
var tween = Tween<double>(begin: 0, end: 1)
.chain(CurveTween(curve: Curves.easeInOut));
return FadeTransition(
opacity: animation.drive(tween),
child: child,
);
},
routeSettings: RouteSettings(arguments: Get.arguments),
);
}
}
class _SavePanelState extends State<SavePanel> {
final boundaryKey = GlobalKey();
bool showBottom = true;
// item
dynamic get _item => widget.item;
late String viewType = '查看';
late String itemType = '内容';
//reply
String? cover;
String? title;
int? pubdate;
String? uname;
String uri = '';
@override
void initState() {
super.initState();
if (_item is ReplyInfo) {
itemType = '评论';
final currentRoute = Get.currentRoute;
late final hasRoot = _item.hasRoot();
if (currentRoute.startsWith('/video')) {
try {
final heroTag = Get.arguments?['heroTag'];
late final ctr = Get.find<VideoIntroController>(tag: heroTag);
cover = ctr.videoDetail.value.pic;
title = ctr.videoDetail.value.title;
pubdate = ctr.videoDetail.value.pubdate;
uname = ctr.videoDetail.value.owner?.name;
} catch (_) {}
uri =
'bilibili://video/${_item.oid}?comment_root_id=${hasRoot ? _item.root : _item.id}${hasRoot ? '&comment_secondary_id=${_item.id}' : ''}';
try {
final heroTag = Get.arguments?['heroTag'];
late final ctr = Get.find<BangumiIntroController>(tag: heroTag);
final type = _item.type.toInt();
late final oid = _item.oid;
late final rootId = hasRoot ? _item.root : _item.id;
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
uri =
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=bilibili://pgc/season/ep/${ctr.epId}';
} catch (_) {}
} else if (currentRoute.startsWith('/dynamicDetail')) {
try {
DynamicItemModel dynItem = Get.arguments['item'];
uname = dynItem.modules.moduleAuthor?.name;
final type = _item.type.toInt();
late final oid = dynItem.idStr;
late final rootId = hasRoot ? _item.root : _item.id;
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
late final enterUri = parseDyn(dynItem);
viewType = '查看';
itemType = '评论';
uri = switch (type) {
1 ||
11 ||
12 =>
'bilibili://comment/detail/$type/${dynItem.basic!.ridStr}/$rootId/?${anchor}enterUri=$enterUri',
_ =>
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
};
} catch (_) {}
} else if (currentRoute.startsWith('/Scaffold')) {
try {
final type = _item.type.toInt();
late final oid = Get.arguments['oid'];
late final rootId = hasRoot ? _item.root : _item.id;
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
late final enterUri = 'bilibili://following/detail/$oid';
uri = switch (type) {
1 ||
11 ||
12 =>
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=${Get.arguments['enterUri']}',
_ =>
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
};
} catch (_) {}
} else if (currentRoute.startsWith('/articlePage')) {
try {
final type = _item.type.toInt();
late final oid = _item.oid;
late final rootId = hasRoot ? _item.root : _item.id;
late final anchor = hasRoot ? 'anchor=${_item.id}&' : '';
late final enterUri =
'bilibili://following/detail/${Get.parameters['id'] ?? Get.arguments?['id']}';
uri =
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri';
} catch (_) {}
}
debugPrint(uri);
} else if (_item is DynamicItemModel) {
uri = parseDyn(_item);
debugPrint(uri);
}
}
String parseDyn(item) {
String uri = '';
try {
switch (item.type) {
case 'DYNAMIC_TYPE_AV':
viewType = '观看';
itemType = '视频';
uri = 'bilibili://video/${item.basic.commentIdStr}';
break;
case 'DYNAMIC_TYPE_ARTICLE':
itemType = '专栏';
uri = 'bilibili://following/detail/${item.idStr}';
break;
case 'DYNAMIC_TYPE_LIVE_RCMD':
viewType = '观看';
itemType = '直播';
final roomId = item.modules.moduleDynamic.major.liveRcmd.roomId;
uri = 'bilibili://live/$roomId';
break;
case 'DYNAMIC_TYPE_UGC_SEASON':
viewType = '观看';
itemType = '合集';
int aid = item.modules.moduleDynamic.major.ugcSeason.aid;
uri = 'bilibili://video/$aid';
break;
case 'DYNAMIC_TYPE_PGC':
case 'DYNAMIC_TYPE_PGC_UNION':
viewType = '观看';
itemType =
item?.modules?.moduleDynamic?.major?.pgc?.badge?['text'] ?? '番剧';
final epid = item.modules.moduleDynamic.major.pgc.epid;
uri = 'bilibili://pgc/season/ep/$epid';
break;
// https://www.bilibili.com/medialist/detail/ml12345678
case 'DYNAMIC_TYPE_MEDIALIST':
itemType = '收藏夹';
final mediaId = item.modules.moduleDynamic.major.medialist!['id'];
uri = 'bilibili://medialist/detail/$mediaId';
break;
// 纯文字动态查看
// case 'DYNAMIC_TYPE_WORD':
// # 装扮/剧集点评/普通分享
// case 'DYNAMIC_TYPE_COMMON_SQUARE':
// 转发的动态
// case 'DYNAMIC_TYPE_FORWARD':
// 图文动态查看
// case 'DYNAMIC_TYPE_DRAW':
default:
itemType = '动态';
uri = 'bilibili://following/detail/${item.idStr}';
break;
}
} catch (_) {}
return uri;
}
void _onSaveOrSharePic([bool isShare = false]) async {
if (!isShare) {
if (mounted &&
!await DownloadUtils.checkPermissionDependOnSdkInt(context)) {
return;
}
}
SmartDialog.showLoading();
try {
RenderRepaintBoundary boundary = boundaryKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
var image = await boundary.toImage(pixelRatio: 3);
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
String picName =
"plpl_reply_${DateTime.now().toString().substring(0, 19).replaceAll(RegExp(r'[- :]'), '')}";
if (isShare) {
Get.back();
SmartDialog.dismiss();
Share.shareXFiles(
[
XFile.fromData(
pngBytes,
name: picName,
mimeType: 'image/png',
)
],
sharePositionOrigin: await Utils.isIpad()
? Rect.fromLTWH(0, 0, Get.width, Get.height / 2)
: null,
);
} else {
final result = await SaverGallery.saveImage(
pngBytes,
fileName: '$picName.png',
androidRelativePath: "Pictures/PiliPlus",
skipIfExists: false,
);
SmartDialog.dismiss();
if (result.isSuccess) {
Get.back();
SmartDialog.showToast('保存成功');
} else if (result.errorMessage?.isNotEmpty == true) {
SmartDialog.showToast(result.errorMessage!);
}
}
} catch (e) {
debugPrint('on save/share reply: $e');
SmartDialog.dismiss();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: Get.back,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
SingleChildScrollView(
padding: const EdgeInsets.only(top: 12, bottom: 80),
child: SafeArea(
child: GestureDetector(
onTap: () {},
child: Container(
width: min(Get.width, Get.height),
margin: const EdgeInsets.symmetric(horizontal: 12),
child: RepaintBoundary(
key: boundaryKey,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: AnimatedSize(
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
duration: const Duration(milliseconds: 255),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_item is ReplyInfo)
IgnorePointer(
child: ReplyItemGrpc(
replyItem: _item,
replyLevel: '',
needDivider: false,
upMid: widget.upMid,
),
)
else if (_item is DynamicItemModel)
IgnorePointer(
child: DynamicPanel(
item: _item,
source: 'detail',
isSave: true,
),
),
if (cover?.isNotEmpty == true &&
title?.isNotEmpty == true)
Container(
height: 81,
clipBehavior: Clip.hardEdge,
margin:
const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: theme.colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
NetworkImgLayer(
radius: 6,
src: cover!,
height: MediaQuery.textScalerOf(context)
.scale(65),
width: MediaQuery.textScalerOf(context)
.scale(65) *
16 /
9,
quality: 100,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'$title\n',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (pubdate != null) ...[
const Spacer(),
Text(
DateTime.fromMillisecondsSinceEpoch(
pubdate! * 1000)
.toString()
.substring(0, 19),
style: TextStyle(
color:
theme.colorScheme.outline,
),
),
],
],
),
),
],
),
),
showBottom
? Stack(
clipBehavior: Clip.none,
children: [
if (uri.isNotEmpty)
Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Expanded(
child: Column(
mainAxisSize:
MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
if (uname?.isNotEmpty ==
true) ...[
Text(
'@$uname',
maxLines: 1,
overflow: TextOverflow
.ellipsis,
style: TextStyle(
color: theme
.colorScheme
.primary,
),
),
const SizedBox(height: 4),
],
Text(
'识别二维码,$viewType$itemType',
textAlign: TextAlign.end,
style: TextStyle(
color: theme.colorScheme
.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
DateTime.now()
.toString()
.split('.')
.first,
textAlign: TextAlign.end,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme
.outline,
),
),
],
),
),
Container(
width: 100,
height: 100,
padding:
const EdgeInsets.all(12),
child: Container(
color: Get.isDarkMode
? Colors.white
: theme
.colorScheme.surface,
padding:
const EdgeInsets.all(3),
child: PrettyQrView.data(
data: uri,
decoration:
const PrettyQrDecoration(
shape:
PrettyQrRoundedSymbol(
borderRadius:
BorderRadius.zero,
),
),
),
),
),
],
),
),
Align(
alignment: Alignment.centerLeft,
child: Image.asset(
'assets/images/logo/logo_2.png',
width: 100,
color: theme
.colorScheme.onSurfaceVariant,
),
),
],
)
: const SizedBox(height: 12),
],
),
),
),
),
),
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black54,
],
),
),
padding: const EdgeInsets.only(bottom: 25, top: 10),
child: SafeArea(
top: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
iconButton(
size: 42,
tooltip: '关闭',
context: context,
icon: Icons.clear,
onPressed: Get.back,
bgColor: theme.colorScheme.onInverseSurface,
iconColor: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 40),
iconButton(
size: 42,
tooltip: showBottom ? '隐藏' : '显示',
context: context,
icon: showBottom
? Icons.visibility_off
: Icons.visibility,
onPressed: () => setState(() {
showBottom = !showBottom;
})),
const SizedBox(width: 40),
iconButton(
size: 42,
tooltip: '分享',
context: context,
icon: Icons.share,
onPressed: () => _onSaveOrSharePic(true),
),
const SizedBox(width: 40),
iconButton(
size: 42,
tooltip: '保存',
context: context,
icon: Icons.save_alt,
onPressed: _onSaveOrSharePic,
),
],
),
),
),
),
],
),
);
}
}

View File

@@ -1,25 +1,23 @@
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart';
Widget videoTabBarView({
required List<Widget> children,
TabController? controller,
}) =>
TabBarView(
physics: const CustomTabBarViewClampingScrollPhysics(),
controller: controller,
children: children,
);
}) => TabBarView(
physics: const CustomTabBarViewClampingScrollPhysics(),
controller: controller,
children: children,
);
Widget tabBarView({
required List<Widget> children,
TabController? controller,
}) =>
TabBarView(
physics: const CustomTabBarViewScrollPhysics(),
controller: controller,
children: children,
);
}) => TabBarView(
physics: const CustomTabBarViewScrollPhysics(),
controller: controller,
children: children,
);
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
const CustomTabBarViewScrollPhysics({super.parent});
@@ -45,14 +43,21 @@ class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
SpringDescription get spring => CustomSpringDescription();
}
class PositionRetainedScrollPhysics extends AlwaysScrollableScrollPhysics {
const PositionRetainedScrollPhysics({super.parent, this.shouldRetain = true});
mixin ReloadMixin {
late bool reload = false;
}
final bool shouldRetain;
class ReloadScrollPhysics extends AlwaysScrollableScrollPhysics {
const ReloadScrollPhysics({super.parent, required this.controller});
final ReloadMixin controller;
@override
PositionRetainedScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PositionRetainedScrollPhysics(parent: buildParent(ancestor));
ReloadScrollPhysics applyTo(ScrollPhysics? ancestor) {
return ReloadScrollPhysics(
parent: buildParent(ancestor),
controller: controller,
);
}
@override
@@ -62,36 +67,42 @@ class PositionRetainedScrollPhysics extends AlwaysScrollableScrollPhysics {
required bool isScrolling,
required double velocity,
}) {
final position = super.adjustPositionForNewDimensions(
if (controller.reload) {
controller.reload = false;
return 0;
}
return super.adjustPositionForNewDimensions(
oldPosition: oldPosition,
newPosition: newPosition,
isScrolling: isScrolling,
velocity: velocity,
);
late final diff = newPosition.maxScrollExtent - oldPosition.maxScrollExtent;
if (shouldRetain && oldPosition.pixels == 0 && diff > 0) {
return position + diff;
} else {
return position;
}
}
}
class CustomSpringDescription implements SpringDescription {
@override
final mass = GStorage.springDescription[0];
static final List<double> springDescription = Pref.springDescription;
@override
final stiffness = GStorage.springDescription[1];
final mass = springDescription[0];
@override
final damping = GStorage.springDescription[2];
final stiffness = springDescription[1];
@override
final damping = springDescription[2];
CustomSpringDescription._();
static final _instance = CustomSpringDescription._();
factory CustomSpringDescription() => _instance;
/// Defaults to 0.
@override
double bounce = 0.0;
/// Defaults to 0.5 seconds.
@override
Duration duration = const Duration(milliseconds: 500);
}

View File

@@ -0,0 +1,38 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:flutter/material.dart';
Widget selectMask(
ThemeData theme,
bool checked, {
BorderRadiusGeometry borderRadius = StyleString.mdRadius,
}) {
return AnimatedOpacity(
opacity: checked ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: borderRadius,
color: Colors.black.withValues(alpha: 0.6),
),
child: AnimatedScale(
scale: checked ? 1 : 0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.8),
shape: BoxShape.circle,
),
child: Icon(
Icons.done_all_outlined,
color: theme.colorScheme.primary,
semanticLabel: '取消选择',
),
),
),
),
);
}

View File

@@ -35,6 +35,14 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
bool get isInit => height == null;
// @override
// void didUpdateWidget(SelfSizedHorizontalList oldWidget) {
// super.didUpdateWidget(oldWidget);
// if (BuildConfig.isDebug) {
// prevHeight = null;
// }
// }
@override
Widget build(BuildContext context) {
if (height == null) {
@@ -42,10 +50,13 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
}
if (widget.itemCount == 0) return const SizedBox.shrink();
if (isInit) {
return Container(
key: infoKey,
padding: widget.padding,
child: widget.childBuilder(0),
return Align(
alignment: Alignment.centerLeft,
child: Padding(
key: infoKey,
padding: widget.padding ?? EdgeInsets.zero,
child: widget.childBuilder(0),
),
);
}
@@ -56,7 +67,7 @@ class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
padding: widget.padding,
scrollDirection: Axis.horizontal,
itemCount: widget.itemCount,
itemBuilder: (c, i) => widget.childBuilder.call(i),
itemBuilder: (c, i) => widget.childBuilder(i),
separatorBuilder: (c, i) => SizedBox(width: widget.gapSize),
),
);

View File

@@ -1,90 +1,41 @@
import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/models/common/stat_type.dart';
import 'package:PiliPlus/utils/num_util.dart';
import 'package:flutter/material.dart';
abstract class _StatItemBase extends StatelessWidget {
final BuildContext context;
final Object value;
final String? theme;
final Color? textColor;
class StatWidget extends StatelessWidget {
final StatType type;
final dynamic value;
final Color? color;
final double iconSize;
const _StatItemBase({
required this.context,
const StatWidget({
super.key,
required this.type,
required this.value,
this.theme,
this.textColor,
this.color,
this.iconSize = 13,
});
IconData get iconData;
String get semanticsLabel;
Color get color {
return textColor ??
switch (theme) {
'gray' => Theme.of(context).colorScheme.outline.withOpacity(0.8),
'black' => Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
_ => Colors.white,
};
}
@override
Widget build(BuildContext context) {
Color color =
this.color ??
Theme.of(context).colorScheme.outline.withValues(alpha: 0.8);
return Row(
spacing: 2,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
iconData,
type.iconData,
semanticLabel: type.label,
size: iconSize,
color: color,
),
const SizedBox(width: 2),
Text(
Utils.numFormat(value),
NumUtil.numFormat(value),
style: TextStyle(fontSize: 12, color: color),
overflow: TextOverflow.clip,
semanticsLabel: semanticsLabel,
)
),
],
);
}
}
class StatView extends _StatItemBase {
final String? goto;
const StatView({
required super.context,
required super.value,
this.goto,
super.theme,
super.textColor,
}) : super(iconSize: 13);
@override
IconData get iconData => switch (goto) {
'picture' => Icons.remove_red_eye_outlined,
'like' => Icons.thumb_up_outlined,
'reply' => Icons.comment_outlined,
'follow' => Icons.favorite_border,
_ => Icons.play_circle_outlined,
};
@override
String get semanticsLabel =>
'${Utils.numFormat(value)}${goto == "picture" ? "浏览" : "播放"}';
}
class StatDanMu extends _StatItemBase {
const StatDanMu({
required super.context,
required super.value,
super.theme,
super.textColor,
}) : super(iconSize: 14);
@override
IconData get iconData => Icons.subtitles_outlined;
@override
String get semanticsLabel => '${Utils.numFormat(value)}条弹幕';
}

View File

@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'dart:ui' show SemanticsRole;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart' hide TabBarView;
@@ -130,8 +132,11 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
required Curve curve,
}) async {
_warpUnderwayCount += 1;
await _pageController!
.animateToPage(page, duration: duration, curve: curve);
await _pageController!.animateToPage(
page,
duration: duration,
curve: curve,
);
_warpUnderwayCount -= 1;
}
@@ -190,7 +195,11 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
}
void _updateChildren() {
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
widget.children.map<Widget>((Widget child) {
return Semantics(role: SemanticsRole.tabPanel, child: child);
}).toList(),
);
}
void _handleTabControllerAnimationTick() {
@@ -222,13 +231,14 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(_currentIndex!,
duration: duration, curve: Curves.ease);
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(() {
_updateChildren();
});
setState(_updateChildren);
}
return Future<void>.value();
}
@@ -260,20 +270,24 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
if (duration == Duration.zero) {
_jumpToPage(_currentIndex!);
} else {
await _animateToPage(_currentIndex!,
duration: duration, curve: Curves.ease);
await _animateToPage(
_currentIndex!,
duration: duration,
curve: Curves.ease,
);
}
if (mounted) {
setState(() {
_updateChildren();
});
setState(_updateChildren);
}
}
void _syncControllerOffset() {
_controller!.offset =
clampDouble(_pageController!.page! - _controller!.index, -1.0, 1.0);
_controller!.offset = clampDouble(
_pageController!.page! - _controller!.index,
-1.0,
1.0,
);
}
// Called when the PageView scrolls

Some files were not shown because too many files have changed in this diff Show More