Compare commits

..

410 Commits
1.1.6 ... dev

Author SHA1 Message Date
dom
953fdbb6bc fix RichTextField selection
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-10 10:54:04 +08:00
dom
8784cb9df8 opt icon
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-10 09:54:44 +08:00
My-Responsitories
22f37074b1 feat: custom buffer size (#2370)
* feat: custom buffer size

* cache buffer params

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-06-10 09:54:32 +08:00
dom
90d79b236f custom player/max volume
Closes #2199
Closes #2358

Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-09 10:38:55 +08:00
My-Responsitories
fe15024384 opt: subtitle & video screenshot (#2364) 2026-06-09 10:06:55 +08:00
dom
46d9f8ab1c fix #2365
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-09 10:04:59 +08:00
My-Responsitories
75bbe5ab5a fix: all music achievement (#2363)
* fix: all music achievement

* remove empty space [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-06-09 10:04:09 +08:00
dom
22fc256d8a fix #2351
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-08 11:45:26 +08:00
dom
01a1cb2dce fix #2350
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-08 10:25:08 +08:00
dom
b4fd362333 fix #2342
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-08 10:06:08 +08:00
dom
ccc7cd0453 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-08 09:50:39 +08:00
My-Responsitories
b67756c152 fix: mob geetest (#2341)
* fix: mob geetest

* geetest ios patch

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-06-07 10:54:44 +08:00
dom
3b9403a329 fix #2305
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-06 12:03:12 +08:00
dom
c44e92dcc8 update patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-06 09:41:52 +08:00
dom
14d1c37df5 flutter 3.44.1
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-06 09:37:11 +08:00
dom
78b3df4019 validate cookie on login
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-06 08:55:38 +08:00
dom
9acb32aaeb opt logout main
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-06 08:54:38 +08:00
dom
50070997de fix #2308
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-05 20:36:39 +08:00
dom
88f0ab1dea opt chat item
Closes #2292

Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-05 10:45:32 +08:00
dom
038285cbe6 fix parse opus
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-05 10:44:32 +08:00
dom
295a587df5 lint
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-04 19:17:59 +08:00
dom
73d7d78080 fix #2295
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-04 19:17:59 +08:00
dom
cd367a8649 Release 2.0.8
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-04 14:19:26 +08:00
dom
f637771527 android patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-04 13:52:53 +08:00
dom
7b01c33657 more fab anim
Closes #2265

Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-04 11:06:14 +08:00
dom
af1cd30ed7 opt video bottomsheet
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-03 23:19:23 +08:00
dom
508384c016 constraint video bottomsheet
Closes #2277

Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-03 20:38:00 +08:00
My-Responsitories
3446484d97 fix: remove hive file (#2276) 2026-06-03 19:59:11 +08:00
My-Responsitories
bed533eaeb style: account switch descriptions (#2280) 2026-06-03 19:58:24 +08:00
dom
31f8435511 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-03 11:05:47 +08:00
dom
6bcd143362 update icons
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-02 22:12:15 +08:00
dom
e5f2a1b0e5 opt image grid
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-02 22:12:15 +08:00
My-Responsitories
66a191b018 refa: cache manager (#2266)
* opt: image utils

* refa: cache manager
2026-06-02 22:09:36 +08:00
My-Responsitories
e619946b9c chore: remove unused assets (#2267) 2026-06-02 20:06:46 +08:00
My-Responsitories
6d7ae5d830 opt: java 2026-06-02 12:42:40 +08:00
dom
2de59b0c9b opt set pip actions
Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-01 12:46:27 +08:00
dom
9ac37d6fb3 add pip backward/forward btns
Closes #2251

Signed-off-by: dom <githubaccount56556@proton.me>
2026-06-01 12:13:57 +08:00
My-Responsitories
fb9568a628 opt: remove volatile (#2255) 2026-05-31 21:11:24 +08:00
dom
90590faaaf refa pip
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-31 19:01:02 +08:00
dom
bee04fb2b0 refa jni
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-31 11:05:35 +08:00
My-Responsitories
b3e2dcf2c5 opt: win32 api (#2246)
* tweaks

* opt: win32 api
2026-05-31 11:03:15 +08:00
My-Responsitories
d4d9fc3405 refa: jni (#2244)
* refa: jni

* refa: jni
2026-05-30 20:10:35 +08:00
dom
6a75061dbb fix dev build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-30 16:55:51 +08:00
dom
2e7c80422c upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-30 12:47:41 +08:00
dom
60f800b671 gitignore
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-30 12:47:41 +08:00
dom
1ba6983f39 fix #2233
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-30 12:47:41 +08:00
dom
6c61ff59ff opt publish page
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-30 12:47:25 +08:00
dom
6464987437 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-28 10:14:15 +08:00
dom
41027f7019 fix #2226
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-28 10:14:15 +08:00
My-Responsitories
41c8e1a538 refa: log (#2216)
* refa: log

* upgrade deps
2026-05-26 19:44:32 +08:00
dom
56756c5c85 fix #2209
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-26 11:31:18 +08:00
dom
3b68fe467d opt rcmd
Closes #2208

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-25 19:38:28 +08:00
dom
f14aacd664 handle msg type EN_MSG_TYPE_CUSTOM_FACE
Closes #2196

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-24 20:44:54 +08:00
dom
c2ceb9ec73 improve pay coin
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-24 13:22:00 +08:00
dom
99a950b049 fix #2192
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-24 10:12:07 +08:00
dom
87914a267a fix fav favFolder toast
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-24 10:11:52 +08:00
dom
dd85aba8a9 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-24 10:11:41 +08:00
dom
8309be9ca9 fix #2171
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-21 13:07:27 +08:00
dom
5fbc86afa3 fix #2171
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-21 11:36:15 +08:00
dom
be5889ee33 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-21 11:36:09 +08:00
dom
7c342032de improve handle opus nullType
Closes #2148

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-20 12:28:59 +08:00
dom
46baad899c upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-20 12:28:05 +08:00
dom
66f15f532f add publish throttle
Closes #2152

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-19 22:00:23 +08:00
dom
89522e2059 layoutbuilder patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-19 21:24:20 +08:00
dom
0cac46ee26 fix setEnabledSystemUIMode
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-19 14:38:08 +08:00
dom
1fcc26464f flutter 3.44.0
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-19 12:37:46 +08:00
dom
ad1583706a upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-19 09:41:55 +08:00
dom
cc169db12b upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-18 15:10:51 +08:00
dom
6a1db5d26b opt handle brightness/volume
Closes #2134

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-18 15:07:32 +08:00
dom
b04c3d878d custom angle degree
Closes #2140

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-18 14:59:52 +08:00
dom
68e302b5d7 fix #2143
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-18 12:10:31 +08:00
dom
ed39624476 opt member header
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-17 11:56:32 +08:00
dom
8683658046 opt update page transition
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-17 11:56:25 +08:00
dom
0c308a7001 fix text selection patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-16 08:28:41 +08:00
dom
cd82668435 fix #2112
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-16 08:28:36 +08:00
dom
e142cb6800 add patch link
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-16 08:28:24 +08:00
dom
200329df4a remove image anim check
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-16 08:27:58 +08:00
dom
859f14423e image anim patch
Closes #2107

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-15 19:50:19 +08:00
dom
9d21be1048 improve text selection patch
Closes #2106

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-15 19:31:08 +08:00
My-Responsitories
d5293fbc72 feat: handle vote uri (#2105) 2026-05-15 18:42:38 +08:00
dom
ad6c0e0d15 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-15 10:40:00 +08:00
dom
ce706d4732 improve horizontal touch slop
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-15 10:33:21 +08:00
dom
2c511d1a2c tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-15 10:00:58 +08:00
dom
3ac21d95f9 improve player gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-15 10:00:58 +08:00
My-Responsitories
b33fdf14af tweaks (#2100)
* mod: pgc episode title

* opt: ColorScheme.of

* mod: mpv api version

* opt: log handler

* opt: ext
2026-05-15 09:59:54 +08:00
dom
24769e144f fix #2087
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-13 13:28:43 +08:00
dom
50875421a5 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-12 12:50:05 +08:00
dom
0581d8f7d4 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-12 11:51:16 +08:00
dom
be9f39d043 remove longpress threshold on main
Closes #2075

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-12 11:38:25 +08:00
uHioVK
034482362d fix(dynamics): disable shadow ban check for scheduled posts (#2059) 2026-05-11 10:09:23 +08:00
dom
1a45b5cb4a upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-10 10:52:02 +08:00
dom
fab34df973 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-09 18:19:28 +08:00
dom
dbb865d723 fix #2002
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-09 18:06:54 +08:00
dom
23db485183 improve player gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-09 13:55:26 +08:00
dom
d63116542b tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-09 13:41:09 +08:00
My-Responsitories
f5dbfcec79 tweaks (#2032)
* fix: 1080p

* opt: import export

* opt: downloader

* opt: skeleton

* opt: parseColor

* tweak

* opt: sb seek

* opt: rxn
2026-05-08 20:50:43 +08:00
dom
b7b40c557e Release 2.0.7
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-05 10:41:19 +08:00
dom
d0373da5ca fix get video part dimension
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-05 10:41:19 +08:00
dom
31aeab3516 fix ios build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-04 21:28:35 +08:00
My-Responsitories
4228f918d7 fix: html numeric character (#1972) 2026-05-04 19:16:45 +08:00
dom
0f07dccc4b upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-04 19:15:54 +08:00
dom
9b0c9e9276 navigator patch
Closes #1947

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-04 19:02:17 +08:00
dom
ac9631462a flutter 3.41.9
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-04 11:59:56 +08:00
dom
d5bf3487f8 add audio volume button & slider on desktop
Closes #1950

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-04 11:59:56 +08:00
dom
9259e84d5c upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-03 14:27:21 +08:00
dom
583092735f opt android orientation
Closes #1953

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-03 12:44:04 +08:00
dom
d41f69e95e improve gesture patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 18:03:45 +08:00
dom
c4847caa27 opt icons
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 17:45:36 +08:00
dom
cdd9289e6d opt reply cardLabels
Closes #1925

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 15:46:53 +08:00
dom
cd1fdd42d3 transparent live dm panel barrier
releted #1944

Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 15:45:22 +08:00
dom
74ed06157a opt icons
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 13:38:41 +08:00
dom
293f046b35 fix orientation
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 13:36:30 +08:00
dom
b00b9e2816 opt view opus images
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 13:36:30 +08:00
dom
68e9afd72b fixes
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 11:24:41 +08:00
dom
09c73d3c87 improve slide gesture patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 10:46:06 +08:00
dom
a91a90b7da tweak
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-02 10:46:06 +08:00
dom
d434f54ddf fix #1929
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-01 21:09:20 +08:00
dom
dbbdb61e34 fix decrease system brightness on android
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-01 21:08:02 +08:00
dom
d85dd864fe opt sc endTime
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-01 21:08:02 +08:00
dom
84bbbd19fa update gitignore
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-01 21:08:02 +08:00
My-Responsitories
3f34d1a251 fix: orientation (#1935) 2026-05-01 21:07:41 +08:00
dom
7b6d8bef99 Release 2.0.6
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:52:51 +08:00
dom
a2b54e50d0 tweak
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:52:27 +08:00
dom
887c3525a2 fix #1917
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:38:18 +08:00
dom
3a46ce2429 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:10:10 +08:00
dom
c0b5876d5b fix #1917
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:03:30 +08:00
dom
74c389be8b custom fullscreen sc width
Closes #1910

Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:03:30 +08:00
dom
5d87361693 do not exit onDestroy
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 17:03:30 +08:00
dom
9752ae2f33 refresh live msg if needed
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 10:57:50 +08:00
dom
3a9cafaf24 handle foldable screen size
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-30 10:57:50 +08:00
dom
5937d83aa9 ios bottom sheet patch
related #1906

Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-29 21:37:05 +08:00
dom
6feb1c15ee tweak
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-29 21:18:58 +08:00
dom
57c683d5d8 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-29 14:10:54 +08:00
dom
cd26cf6d98 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-29 14:08:46 +08:00
dom
c7864ff4a3 Reapply "hide systembar when preview"
This reverts commit d6bce6aa63.
2026-04-29 14:07:37 +08:00
dom
7b51b6900f improve handling android window mode
Closes #1908

Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-29 14:07:37 +08:00
dom
11b6f241b0 opt reorder
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-29 14:07:36 +08:00
dom
88c01ffca3 improve handling video pop
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 21:08:12 +08:00
My-Responsitories
9ed8b14d1b feat: add to filter (#1903) 2026-04-28 15:23:47 +08:00
dom
f7a2be1923 fix #1904
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 15:23:01 +08:00
dom
47e3a47e00 fix fav sort
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 11:50:31 +08:00
dom
524b6bca1c unify horizontal video models
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 11:50:31 +08:00
dom
80ecd35784 sort follow tag
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 11:50:31 +08:00
dom
89fdc28150 auto add to created fav/follow
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 11:50:30 +08:00
dom
b406a00c8d opt audio normalization
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-28 11:50:30 +08:00
dom
6755b7fae6 get real-time video duration in post panel
Closes #1901

Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 18:14:52 +08:00
dom
60801f9846 disable auto reset brightness on iOS
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 17:59:58 +08:00
dom
fae0cfe800 disable blank issue
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 16:44:56 +08:00
dom
dccb5d4bf5 create fav tag from fav panel
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 13:58:49 +08:00
dom
b9ce4bad67 opt follow item
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-27 13:58:49 +08:00
dom
baef1655d6 Release 2.0.5
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-25 14:32:59 +08:00
dom
8fd5f353e4 replace errorWidget with errorBuilder in CachedNetworkSVGImage
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-25 13:24:33 +08:00
dom
326291de23 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-25 13:17:25 +08:00
dom
1761a9abc9 fix image gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-24 19:31:03 +08:00
dom
d372142d62 view vote image
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-24 19:31:03 +08:00
Starfallen
91f00173bb fix(player): suppress volume indicator during audio duck events (#1899)
When using AAudio output on Android, the system notifies the app to
manually handle ducking instead of silently lowering volume itself.
This caused the in-app volume indicator to pop up on every notification.

Pass showIndicator: false for duck begin/end calls so volume is adjusted
silently, consistent with other audio output modes.
2026-04-24 12:36:50 +08:00
dom
73484f1f72 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-24 10:54:10 +08:00
dom
490a08fa79 grpc search archive
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-24 10:49:17 +08:00
dom
5b5983ed50 precheck video dimension
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-23 21:17:56 +08:00
dom
efc202c10f upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-23 21:17:56 +08:00
dom
24ab7505e6 opt onOrientationChanged
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-23 21:17:56 +08:00
dom
d6bce6aa63 Revert "hide systembar when preview"
This reverts commit 0fb394cc36.
2026-04-23 21:17:56 +08:00
Starfallen
354282930c fix(article): fix Opus article body font size too small (#1897)
New Opus format uses font_level string instead of numeric font_size,
causing font size to fall back to Flutter theme default (14px) instead
of the expected 16px. Added Word.effectiveFontSize: small→13px,
others→16px (consistent with legacy HTML article format).
2026-04-23 21:17:43 +08:00
систем
79c9849c80 Sync progress if needed before exit (#1896)
* fix: 定时关闭退出APP前同步视频进度

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-04-22 10:36:30 +08:00
dom
d7b4ceabbf improve Dyn/Topic DraggableScrollableSheet
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-21 14:17:36 +08:00
dom
dd5ccd11b8 text selection patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-21 10:49:30 +08:00
dom
26a5d00718 opt image gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-21 10:17:02 +08:00
dom
d32456fae0 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-20 14:31:26 +08:00
dom
bffdfae1f6 scroll view patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-20 12:49:41 +08:00
dom
44b7cced09 Revert "opt scroll view"
This reverts commit f9441db232.
2026-04-20 12:49:37 +08:00
dom
91936a9f44 update pref default value
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-20 12:49:25 +08:00
dom
0fb394cc36 hide systembar when preview
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-20 12:49:12 +08:00
dom
4229024adf tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-20 12:48:53 +08:00
dom
add633bc66 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-19 14:37:34 +08:00
dom
54a88fa2ff fix initial isDyAllowed
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-19 14:37:34 +08:00
dom
9543f3b457 bring back remove player safearea
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-19 11:50:31 +08:00
dom
e88769308e fix retry
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-19 09:33:30 +08:00
dom
bac0769933 add split settings model
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-18 12:28:40 +08:00
dom
24f2cfa4e9 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-18 12:28:40 +08:00
0x535A
970ee679f1 fix: reset Dio adapters on iOS network switch (#1885)
* fix reset Dio adapters on iOS network switch

* improve iOS connectivity adapter reset safeguards

* refa

---------

Co-authored-by: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com>
2026-04-18 12:26:24 +08:00
dom
db7ad269b2 fix RichTextField affinity on desktop
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-17 11:03:54 +08:00
Origuchi
2232bc009d use linux_webview_window to support linux geetest (#1889)
* use webview_all to support linux geetest

* use desktop_webview_window to support linux geetest

* remove previous change in my_application.cc

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-04-16 18:05:25 +08:00
dom
e778f2b463 correct reply field names
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-16 17:56:57 +08:00
dom
7e9618d712 fix ios build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-16 13:05:35 +08:00
dom
62880a3769 increase dm proto extra field number
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-16 12:40:49 +08:00
dom
e566a358dd tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-16 12:40:48 +08:00
dom
1b0b966e76 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-16 12:40:48 +08:00
lesetong
dd2492e04d Add translation support to reply items (#1894)
* Add translation support to reply items

- Add translateState and showTranslate fields to ReplyControl
- Add translatedText field to ReplyInfo for translation results
- Implement TranslateReplyReq message and translateReply API method
- Add translation UI with loading state and result display in reply
  items
- Show translation button when showTranslate is true and translateState
  is 2

* refa

Signed-off-by: dom <githubaccount56556@proton.me>

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-04-16 12:40:39 +08:00
dom
0b4ed25891 Release 2.0.4
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-14 13:59:59 +08:00
dom
372c677e8f opt android orientation
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-14 13:46:47 +08:00
dom
9a6f335e48 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-14 13:46:47 +08:00
dom
f9441db232 opt scroll view
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-14 13:46:47 +08:00
dom
4de43faa2e tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-14 13:46:42 +08:00
dom
ba372a101b opt avatar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-14 10:38:18 +08:00
систем
a2ff54af70 opt player progress bar behavior (#1887)
* feat: no _showControlsIfNeeded() when slipping

* Revert "feat: no _showControlsIfNeeded() when slipping"

This reverts commit 551be3dc6a.

* feat: show progress bar during drag without revealing controls overlay

* fix: show Controls progress bar during drag instead of full overlay

* Revert "fix: show Controls progress bar during drag instead of full overlay"

This reverts commit 18ecda3379.

* fix: hide thin progress bar during drag when Controls already visible

* fix: show thin progress bar during drag when progressType is alwaysHide

* Revert "fix: show thin progress bar during drag when progressType is alwaysHide"

This reverts commit 1a60c8e6df.

* fix: show thin progress bar during drag when progressType is alwaysHide

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-04-11 21:32:37 +08:00
dom
cbc4f58323 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-11 09:35:13 +08:00
dom
b553e7554d opt video statusbar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-11 09:35:12 +08:00
dom
68724c8a9e opt sc background image
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-11 09:35:12 +08:00
dom
85baf8e0e6 opt android orientation
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-11 09:35:08 +08:00
dom
222c9d01a0 bubble page
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-09 21:32:07 +08:00
dom
db30aa8041 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-09 00:09:40 +08:00
dom
6f95456d20 fix vipStatus check
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-08 20:58:19 +08:00
HCha
de6e402d97 fix return button not working (#1883)
* fix: return button won't work #1

* feat: return horizontal | isPortrait

* revert: e85ccc7 onPopInvokedWithResult isPortrait

* revert: e85ccc7

* remove unused param

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-04-08 20:40:26 +08:00
dom
6341660788 Release 2.0.3
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-08 13:05:08 +08:00
dom
a1dbcae93e upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-08 12:50:22 +08:00
dom
1526137a64 fix AppSign
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-07 17:24:44 +08:00
dom
3097b56816 refactor device orientation
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-07 12:56:29 +08:00
dom
db74eccf77 android shortcut
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-06 15:34:07 +08:00
dom
14890d342a tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-06 00:13:35 +08:00
dom
51163dd985 create user shorcut on Android
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 20:11:13 +08:00
dom
f0d9b3a9a7 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 20:11:13 +08:00
dom
8f3707fbf1 opt reserve btn
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 13:35:19 +08:00
dom
f52bbe9804 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 12:12:10 +08:00
dom
3ec54868d0 show reserve btn in space page
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 12:11:47 +08:00
dom
c0b55f9af3 show member guard
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 12:11:36 +08:00
dom
279f21857d clean up models
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-05 12:11:22 +08:00
dom
b897103af0 Upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-03 09:39:03 +08:00
dom
353664fbd4 Open offline dir on desktop
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-03 09:35:32 +08:00
dom
de3505ce07 Floating NavBar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-03 09:35:32 +08:00
dom
cdc1720358 Reformat
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-03 09:35:25 +08:00
dom
904d210ba2 build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-04-02 12:45:08 +08:00
HCha
db8dd85b63 fix: revert commit 7ee6d1e element.weight (#1879) 2026-04-02 12:44:35 +08:00
dom
8ad130567e Release 2.0.2
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-31 18:25:39 +08:00
dom
7eb21bc5a2 build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-31 18:24:02 +08:00
dom
ea4316a847 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-31 16:57:12 +08:00
dom
2bbc97a950 fix macOS build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-31 11:51:22 +08:00
dom
0178d105ba add Local Network permissions for iOS & macOS
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-31 11:17:32 +08:00
dom
771fa75f48 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-31 11:17:09 +08:00
dom
82483b33fc opt live emote
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-30 00:02:58 +08:00
My-Responsitories
886c53c7d8 opt: m3e loading (#1877)
* opt: loading

* feat: refresh m3e

* restore refreshIndicator

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-29 23:34:04 +08:00
dom
f0050dd6e6 fix pendent offset
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-29 11:47:58 +08:00
dom
e6a2f65b4e fix reply up badge
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-29 11:47:58 +08:00
dom
2fc3f9864f opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-29 10:51:52 +08:00
dom
64c05a1b06 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-29 10:02:00 +08:00
ninatan777
7c4e20f96c safely parse up list (#1876)
* fix: 兼容 mid/roomId/count 字段为字符串或数字类型

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-29 10:01:10 +08:00
dom
ace286753c flutter 3.41.6
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-27 10:33:15 +08:00
dom
f0430eba9f opt player bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-26 18:34:57 +08:00
dom
bbcceb72a7 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-26 10:27:21 +08:00
dom
be4fa6ad2c build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-25 20:25:54 +08:00
dom
50e1f77e10 log player error instead of toast
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-25 20:22:07 +08:00
dom
ba56b45038 clamp archive page
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-25 15:01:35 +08:00
dom
b4b3764e5f web archive
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-25 12:59:29 +08:00
dom
2220372e4f tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-25 12:58:49 +08:00
dom
0957dfc66e fix ios build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-23 14:50:57 +08:00
dom
9578f948b4 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-23 13:47:20 +08:00
dom
1724f0d202 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-23 11:14:31 +08:00
dom
2bebf200df show user medal
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-23 11:03:24 +08:00
dom
fc7fc18b14 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-22 16:35:08 +08:00
0x535A
8f00ca5680 fix: macOS Media Control not activated & remove Background Play switch on desktop (#1872)
* fix: macOS Media Control not activated

* fix: remove Background Play switch on desktop

asdf

Update lib/main.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-22 16:25:31 +08:00
dom
236b524445 opt image viewer gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-22 13:53:49 +08:00
dom
ae59d257c3 show medal wall
show user follow time

show top image title

Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-22 13:53:44 +08:00
dom
662ccfcf0a upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-19 20:34:36 +08:00
dom
b7ab3655c4 flutter 3.41.5
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-19 20:34:36 +08:00
Starfallen
eda04b32a4 fix(player): clamp loudnorm measured_thresh parameters to valid range (#1871)
Co-authored-by: ci <example@example.com>
2026-03-19 08:30:17 +00:00
dom
9b1ae39922 m3e loading indicator
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-18 09:11:08 +08:00
dom
d1497115da tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-18 08:50:07 +08:00
HeXis-YS
7f2682bb7b opt(download): force cache downloads to use http/1.1 (#1870)
* opt(download): force cache downloads to use http/1.1

* refactor(http): lazily initialize fallback http/1.1 client

* fix(http): keep fallback client decision consistent at startup

* opt: use clone

* fix

* fix

---------

Co-authored-by: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com>
2026-03-17 12:43:03 +00:00
dom
d6579b29ae reduce rebuild
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-17 14:38:10 +08:00
dom
8a8aa6c1e0 unique image tag
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-17 14:37:53 +08:00
dom
ed66a4655b opt fab location
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-16 10:20:59 +08:00
My-Responsitories
e04affd0fe opt: hive ce (#1868) 2026-03-15 16:51:45 +08:00
dom
e293083492 opt set shader
opt refresh player

Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-15 09:48:31 +08:00
dom
7f39f36c75 fix auto fullscreen
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-15 09:16:21 +08:00
dom
565819febe remove unused param
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-15 09:16:21 +08:00
dom
af150118a1 opt load saved reply
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-15 09:16:21 +08:00
dom
470e519a2b opt save panel
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-15 09:16:21 +08:00
HeXis-YS
d73588f1fd fix(player): disable audio normalization on missing audio input (#1865)
* fix(player): disable audio normalization on missing audio input

* fix(player): skip normalization when local audio input is missing

* fix `hasDashAudio`

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-15 09:16:10 +08:00
dom
ffbbd8e702 Release 2.0.1
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-14 17:52:15 +08:00
dom
a1815c4cc7 unify platform file name
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-14 17:44:10 +08:00
dom
b9e543f26b improve export/import
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-14 17:09:23 +08:00
dom
0788a4de2d upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-14 13:40:58 +08:00
dom
b0c6e2f5cd fix dyn folded link
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-14 13:40:58 +08:00
dom
9489d8a7ca opt handle dyn additional
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-14 12:19:30 +08:00
My-Responsitories
aee4424dbf opt: escape performReassemble (#1864) 2026-03-13 18:38:37 +08:00
dom
96f9972895 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-13 09:49:48 +08:00
dom
6ddf282555 fix header progress
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-13 09:49:48 +08:00
dom
e98b2b69bb tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-12 17:23:51 +08:00
dom
448192b635 DynamicFlexibleSpaceBar: provide height
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-12 15:45:36 +08:00
My-Responsitories
6cda3a1880 tweaks (#1862)
* opt: linter

* tweaks

* opt: TopImage

* update

* remove repaintBoundary [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-12 15:45:18 +08:00
dom
99128b2641 stateless DynamicSliverAppBar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-12 13:01:05 +08:00
dom
b8098fe067 show member collection top
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-11 17:46:09 +08:00
dom
9fef3284db opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-11 17:17:47 +08:00
dom
f2b0a3a5ed link up to launcher
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-10 17:22:53 +08:00
dom
3090cfc6f9 live first frame
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-10 13:12:33 +08:00
dom
98ce99202e decrease image max ratio
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-10 13:12:33 +08:00
dom
fddf46a90a upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-10 11:13:15 +08:00
dom
a5231a55b8 fix dyn with title only
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-09 19:40:30 +08:00
dom
b8cae015d7 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-09 17:43:50 +08:00
dom
3b09534320 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-09 15:56:18 +08:00
dom
702cf988d3 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-09 11:53:46 +08:00
dom
5586d12b1f opt live chat list
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-09 10:22:15 +08:00
dom
4683939364 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-09 10:22:02 +08:00
My-Responsitories
f825f87dc1 opt: save reply (#1860)
* opt: save reply

* opt: reply save switch

* remove unneeded sort

* clear sub replies [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-08 20:37:59 +08:00
dom
4ad422c3ea suppress build annotation
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-08 15:47:26 +08:00
My-Responsitories
c01318c066 feat: sliver wrap (#1858)
* feat: sliver wrap

* opt: list

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-08 15:25:44 +08:00
dom
01a74e191a redirect ugc to pgc
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-08 11:28:57 +08:00
dom
a1f15b5da5 feat: record reply
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-08 11:28:51 +08:00
Dxeee
1e83a23c5c feat: "Selected Comments" status displayed (#1857)
* Add support for UpSelection in reply display and controller

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-08 00:26:58 +08:00
dom
2d69c05f33 tweak
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-08 00:24:18 +08:00
dom
7a2dbe68c7 build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-08 00:24:10 +08:00
dom
db08af6ca5 correct scrollable position when hiding top bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-07 15:05:57 +08:00
dom
fefb5c837b build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-07 15:05:57 +08:00
dom
a88429d6d7 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-07 10:49:29 +08:00
dom
cbe99a32eb fix player interactive viewer recognizer
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 18:54:01 +08:00
dom
b65d10ac5f flutter 3.41.4
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 17:51:43 +08:00
dom
868f7f5055 increase image max ratio
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:43:55 +08:00
dom
e843684109 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:06:42 +08:00
dom
631197e3b9 opt perf
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:05:54 +08:00
dom
381c385726 refactor image grid
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:05:54 +08:00
dom
077255e776 opt member profile
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:05:54 +08:00
dom
0bcc1a7f12 build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:05:54 +08:00
dom
9b145b525a opt floating header
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-06 12:05:53 +08:00
dom
b61a54bf9b fix auto fullscreen
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 23:57:22 +08:00
dom
cf103a09c1 fix typo
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 16:16:41 +08:00
dom
a802bc1cdf opt auto fullscreen
opt video fit

Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 16:03:41 +08:00
dom
8d312d8cf1 build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 11:38:13 +08:00
dom
6738142ac0 opt player listener
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 11:19:11 +08:00
dom
3d99e6c761 fetch dyn type onlyfansQaCard
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 11:19:11 +08:00
dom
f9f52e918a resize image placeholder
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 11:19:11 +08:00
dom
6108290b4b remove unused property
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-04 11:19:05 +08:00
dom
8bae275120 fix trackpad not respecting enableSlideVolumeBrightness property
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-03 18:38:18 +08:00
dom
0504011ba0 Release 2.0.0
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-03 10:54:58 +08:00
dom
dc9d4f9eed upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-03 09:56:48 +08:00
dom
187c92d691 flutter 3.41.3
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-03 09:55:59 +08:00
dom
9c7b18710c refa persistent header & dynamic sliver appbar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-03 09:44:29 +08:00
dom
1dbc54f063 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-03-02 10:31:16 +08:00
My-Responsitories
348bc8b920 opt: set repeat ui (#1854) 2026-03-01 20:38:03 +08:00
dom
a375d8525f upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-28 18:12:52 +08:00
dom
e3e423f9b1 fix app scheme
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-28 18:06:01 +08:00
NLsdt
62048992be chore(ci): upgrade upload-artifact to v7 and set archive options (#1852) 2026-02-28 13:31:45 +08:00
dom
ec9498a2ca fix profile actions constraints
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-28 12:04:05 +08:00
dom
1d35abef63 fixes
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-27 18:15:34 +08:00
dom
889f6d01c2 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-27 16:03:56 +08:00
dom
d9c47be2a9 opt chat list
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-27 15:55:40 +08:00
dom
cf44036589 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-27 15:52:14 +08:00
My-Responsitories
7276cde48a refa player (#1848)
* tweak

* Reapply "opt: audio uri" (#1833)

This reverts commit 8e726f49b2.

* opt: player

* opt: player

* refa: create player

* refa: player

* opt: UaType

* fix: sb seek preview

* opt: setSub

* fix: screenshot

* opt: unnecessary final player state

* opt: player track

* opt FileSource constructor [skip ci]

* fixes

* fix: dispose player

* fix: quote

* update

* fix: multi ua & remove unused loop

* remove unneeded check [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-27 15:51:55 +08:00
My-Responsitories
6782bee11a opt: HeaderLayout with SlottedMultiChild (#1850)
* opt: HeaderLayout with SlottedMultiChild

* ordered

* update [skip ci]
2026-02-27 14:40:17 +08:00
barmxds6ch
b55e102dc3 feat(whisper): add uploader message attachment display (#1849)
* feat(whisper): add uploader message attachment display

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-27 10:56:44 +08:00
dom
65ad8a0fdc opt anim
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-26 21:26:28 +08:00
dom
fdb3bf3edc opt member profile
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-26 16:59:51 +08:00
dom
95506ad896 tweak
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-26 14:06:34 +08:00
dom
348b2533dc opt image gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-26 10:05:06 +08:00
dom
2bdab71138 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-25 18:02:36 +08:00
My-Responsitories
e707764f84 tweaks (#1846)
* opt: live extra

* opt: remove addPointer

* opt: use ssd

* opt: cache svg

* opt: localToGlobal

* opt: disabled icon

* opt: onVideoDetailChange switch

* fix

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-25 18:01:43 +08:00
dom
4a3d827f7a opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-24 21:48:42 +08:00
dom
e88cd12dfa opt refresh
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-24 12:17:18 +08:00
dom
ee04978e0c opt scrollPhysics
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-23 12:03:35 +08:00
dom
d15ad4911d upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-23 11:53:26 +08:00
dom
14b6c115b5 opt refresh
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-23 11:44:39 +08:00
dom
ee188da6b0 opt scrollPhysics
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-22 20:45:06 +08:00
dom
998b70cd87 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-22 16:47:51 +08:00
dom
7563a52bed opt refresh
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-22 15:51:57 +08:00
dom
7e81fae2bc fix jump to item
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-22 15:22:12 +08:00
dom
639dfac8af build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-21 21:13:47 +08:00
dom
d8950adb64 opt player bottom bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-21 19:50:51 +08:00
dom
9092db86ca upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-21 18:54:22 +08:00
My-Responsitories
d7d9655f81 opt: RepaintBoundary (#1840)
* opt: RepaintBoundary

* fix [skip ci]

* opt time width

* opt: video position

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-20 21:04:34 +08:00
dom
a63ca93762 flutter 3.41.2
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-20 12:00:54 +08:00
dom
243178c112 format
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-20 12:00:54 +08:00
Just_A_Pony
dcb3a02da8 allow user to configure window decorations(CSD/SSD) (#1839)
* allow user to configure window decorations(CSD/SSD)

* rename piliplus.desktop
to com.example.piliplus.desktop

* update

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-20 11:46:25 +08:00
Starfallen
b1c0eca328 fix(player): clamp loudnorm parameters to valid FFmpeg ranges (#1838)
- Define Integrated True Peak (TP) boundary constants.
- Use .clamp() in Volume.format to ensure parameters stay within [-9, 0] for 'TP'.
- Prevents FFmpeg filter errors when video metadata contains very low peak values (e.g., TP = -22).
2026-02-19 15:31:29 +08:00
dom
e3a1eb5c87 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-19 15:12:29 +08:00
dom
736478b1c5 fix patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-19 13:33:05 +08:00
dom
12919804dc mouse cursor patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-19 12:23:20 +08:00
dom
888b3d8173 remove unused val
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-19 10:45:57 +08:00
dom
1e6b0f0b53 opt hide bottom bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-19 10:33:40 +08:00
dom
aa3e5a4737 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-18 23:12:03 +08:00
dom
3f3d54fd27 add bar hide type
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-18 21:41:15 +08:00
dom
a142b15344 fix hide bottom bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-18 18:33:33 +08:00
dom
651e79ce26 opt handle scroll
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-18 18:16:22 +08:00
dom
9b93ce84ab upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-18 17:44:46 +08:00
dom
dfa258b9e6 opt hide top/bottom bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-18 17:36:11 +08:00
dom
a5efca4e1f upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-17 18:35:03 +08:00
dom
1fe84d1d34 Revert "disable windows thread merging"
This reverts commit bebf34db23.
2026-02-17 18:35:03 +08:00
dom
b978ff5649 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-17 18:34:58 +08:00
dom
fa85ae47ac gitignore
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-16 10:22:03 +08:00
dom
3209ecd0ba opt image recognizer
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-15 22:19:54 +08:00
dom
807de41ff0 opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-15 21:43:51 +08:00
dom
d273e72a44 opt image viewer
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-15 19:14:17 +08:00
dom
2c0597175d opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-15 17:16:24 +08:00
My-Responsitories
85292a3df2 opt: image viewer (#1837)
* opt: image

* opt: MatrixTransition

* update


---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-15 17:13:55 +08:00
dom
9c7c6f9e4e tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-15 10:47:20 +08:00
Gujial
511ff71f5f opt share video link (#1835)
* feat: share video with current time position preference

* simplify


---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-14 21:16:47 +08:00
систем
e104982246 handle Cmd+Q on macOS (#1834)
* feat(video): add Pref.keyboardControl check &  support in video page

- 在 player_focus 处理按键事件前增加 Pref.keyboardControl 检查,允许禁用键盘快捷键
- 修复 Cmd+Q 事件被 Q 键逻辑拦截导致无法退出的问题

* Update player_focus.dart
2026-02-14 21:01:34 +08:00
dom
e7e79eb62a opt image preview
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-14 18:26:31 +08:00
dom
352e314ee1 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-14 18:26:31 +08:00
dom
e9dafbc227 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-14 12:03:28 +08:00
dom
96727469ac build
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-14 12:03:28 +08:00
dom
c70c9829c0 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-14 12:03:18 +08:00
dom
beb7eb1aea refa image preview
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-14 11:03:02 +08:00
My-Responsitories
8e726f49b2 Revert "opt: audio uri" (#1833)
This reverts commit 78739d9c0a.
2026-02-13 20:36:51 +08:00
dom
007375371e revert new overscrollindicator
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 17:39:26 +08:00
dom
6d79551566 fix updateTicker
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 15:27:10 +08:00
dom
483953cf56 sync flutter widgets
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 13:44:55 +08:00
dom
fbf7116edf fix modal barrier patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 11:14:51 +08:00
dom
6c164d81e3 use TickerMode.getValuesNotifier
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:30:40 +08:00
dom
d0789734ec fix get icon assets
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:20:11 +08:00
dom
f3bd305337 use findItemIndexCallback
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:15:19 +08:00
dom
5ab7000716 platform assets
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:12:08 +08:00
dom
dc1c33f086 adapt RadioClient
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:12:08 +08:00
dom
920c51100a flutter 3.41.0
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:12:08 +08:00
dom
05a385d69e tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-12 10:12:08 +08:00
My-Responsitories
9411785d26 opt: PlPlayerController (#1832)
* opt: audio uri

* opt: PlPlayerController
2026-02-10 16:33:02 +08:00
My-Responsitories
ed2bd069ee opt: segment (#1831)
* opt: segment

* revert: block config
2026-02-10 16:13:38 +08:00
My-Responsitories
0460030a2b feat: tempPlayerConf add playRepeat (#1830) 2026-02-09 18:16:45 +08:00
My-Responsitories
7e570d11d8 fix: live dm 2026-02-09 16:43:21 +08:00
dom
32cd3209d0 opt init recognizer
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-09 09:55:01 +08:00
dom
0cb07aef1c audio block
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-08 23:35:58 +08:00
dom
0c65605ac0 audio sschedule shutdown
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-08 20:51:13 +08:00
My-Responsitories
8234b7ac92 opt: matrix anim (#1829) 2026-02-08 15:27:03 +08:00
1002 changed files with 33210 additions and 33532 deletions

2
.fvmrc
View File

@@ -1,3 +1,3 @@
{
"flutter": "3.38.6"
"flutter": "3.44.1"
}

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -49,7 +49,7 @@ on:
jobs:
android:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_android == 'true' }}
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_android == 'true' }}
name: Release Android
runs-on: ubuntu-latest
permissions: write-all
@@ -78,15 +78,9 @@ jobs:
flutter-version-file: pubspec.yaml
cache: true
- name: apply bottom sheet patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
continue-on-error: true
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Apply Patch
shell: pwsh
run: lib/scripts/patch.ps1 android
- name: Write key
if: github.event_name == 'workflow_dispatch'
@@ -100,13 +94,20 @@ jobs:
fi
- name: Set and Extract version
if: ${{ github.event_name == 'workflow_dispatch' }}
shell: pwsh
run: lib/scripts/build.ps1 android
- name: flutter build apk
- name: Flutter Build Release Apk
if: ${{ github.event_name == 'workflow_dispatch' }}
run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub
- name: rename
- name: Flutter Build Dev Apk
if: ${{ github.event_name == 'pull_request' }}
run: |
flutter build apk --release --split-per-abi --android-project-arg dev=1 --pub
- name: Rename
run: |
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
@@ -120,32 +121,31 @@ jobs:
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_android_*.apk
files: PiliPlus_android_*.apk
- name: 上传
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Android_arm64-v8a
path: |
PiliPlus_android_*_arm64-v8a.apk
path: PiliPlus_android_*_arm64-v8a.apk
- name: 上传
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Android_armeabi-v7a
path: |
PiliPlus_android_*_armeabi-v7a.apk
path: PiliPlus_android_*_armeabi-v7a.apk
- name: 上传
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Android_x86_64
path: |
PiliPlus_android_*_x86_64.apk
path: PiliPlus_android_*_x86_64.apk
ios:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_ios == 'true' }}
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_ios == 'true' }}
uses: ./.github/workflows/ios.yml
permissions: write-all
with:
@@ -159,7 +159,7 @@ jobs:
tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
win_x64:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
if: ${{ (github.event_name == 'pull_request' && github.repository == 'bggRGjQaUbCoE/PiliPlus') || github.event.inputs.build_win_x64 == 'true' }}
uses: ./.github/workflows/win_x64.yml
permissions: write-all
with:

View File

@@ -13,7 +13,7 @@ on:
jobs:
build-macos-app:
name: Release IOS
runs-on: macos-latest
runs-on: macos-26
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -30,10 +30,9 @@ jobs:
shell: pwsh
run: lib/scripts/build.ps1
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Apply Patch
shell: pwsh
run: lib/scripts/patch.ps1 iOS
- name: Build iOS
run: |
@@ -41,7 +40,7 @@ jobs:
ln -sf ./build/ios/iphoneos Payload
# make AltSign happy...
find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \;
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/Runner.app
- name: Release
if: ${{ github.event.inputs.tag != '' }}
@@ -53,7 +52,8 @@ jobs:
PiliPlus_ios_*.ipa
- name: Upload ios release
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: iOS-release
path: PiliPlus_ios_*.ipa

View File

@@ -51,10 +51,9 @@ jobs:
shell: pwsh
run: lib/scripts/build.ps1
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Apply Patch
shell: pwsh
run: lib/scripts/patch.ps1 Linux
#TODO: deb and rpm packages need to be build
- name: Build Linux
@@ -75,7 +74,7 @@ jobs:
printf "复制文件...\n"
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
cp -r ../assets/linux/DEBIAN .
cp ../assets/linux/piliplus.desktop usr/share/applications
cp ../assets/linux/com.example.piliplus.desktop usr/share/applications
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
printf "修改控制文件...\n"
@@ -117,7 +116,7 @@ jobs:
SRC_DIR="$PWD/piliplus-${{ env.version }}"
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
cp assets/linux/com.example.piliplus.desktop "$SRC_DIR/assets/com.example.piliplus.desktop"
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
@@ -150,7 +149,7 @@ jobs:
# 桌面集成
mkdir -p %{buildroot}/usr/share/applications
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
install -m 644 assets/com.example.piliplus.desktop %{buildroot}/usr/share/applications/com.example.piliplus.desktop
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
@@ -166,7 +165,7 @@ jobs:
%files
/opt/PiliPlus
/usr/bin/piliplus
/usr/share/applications/piliplus.desktop
/usr/share/applications/com.example.piliplus.desktop
/usr/share/icons/hicolor/512x512/apps/piliplus.png
%changelog
@@ -202,8 +201,8 @@ jobs:
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
printf "复制桌面文件和图标...\n"
cp assets/linux/piliplus.desktop "$APPDIR/piliplus.desktop"
cp assets/linux/piliplus.desktop "$APPDIR/usr/share/applications/piliplus.desktop"
cp assets/linux/com.example.piliplus.desktop "$APPDIR/com.example.piliplus.desktop"
cp assets/linux/com.example.piliplus.desktop "$APPDIR/usr/share/applications/com.example.piliplus.desktop"
cp assets/images/logo/logo.png "$APPDIR/piliplus.png"
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
@@ -219,8 +218,8 @@ jobs:
chmod +x "$APPDIR/AppRun"
printf "修改桌面文件中的 Exec 路径...\n"
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/piliplus.desktop"
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/piliplus.desktop"
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/com.example.piliplus.desktop"
printf "打包 AppImage...\n"
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
@@ -241,25 +240,29 @@ jobs:
PiliPlus_linux_*.AppImage
- name: Upload linux targz package
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Linux_targz_amd64_packege
path: PiliPlus_linux_*.tar.gz
- name: Upload linux deb package
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Linux_deb_amd64_package
path: PiliPlus_linux_*.deb
- name: Upload linux rpm package
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Linux_rpm_amd64_package
path: PiliPlus_linux_*.rpm
- name: Upload linux AppImage package
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Linux_AppImage_amd64_package
path: PiliPlus_linux_*.AppImage

View File

@@ -13,7 +13,7 @@ on:
jobs:
build-mac-app:
name: Release Mac
runs-on: macos-latest
runs-on: macos-26
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -30,10 +30,9 @@ jobs:
shell: pwsh
run: lib/scripts/build.ps1
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Apply Patch
shell: pwsh
run: lib/scripts/patch.ps1 macOS
- name: Build Mac
run: flutter build macos --release --dart-define-from-file=pili_release.json
@@ -41,7 +40,7 @@ jobs:
- name: Prepare Upload
run: |
npm install --global create-dmg
create-dmg build/macos/Build/Products/Release/PiliPlus.app
create-dmg build/macos/Build/Products/Release/PiliPlus.app || true
continue-on-error: true
- name: Rename DMG
@@ -57,7 +56,8 @@ jobs:
PiliPlus_macos_*.dmg
- name: Upload macos release
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: macOS-release
path: PiliPlus_macos_*.dmg

View File

@@ -26,11 +26,9 @@ jobs:
channel: stable
flutter-version-file: pubspec.yaml
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $env:GITHUB_WORKSPACE\lib\scripts\modal-barrier-patch.diff
- name: Apply Patch
shell: pwsh
continue-on-error: true
run: lib/scripts/patch.ps1 windows
- name: Add fastforge and Inno Setup
run: |
@@ -58,9 +56,8 @@ jobs:
mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe
- name: Compress
if: ${{ github.event.inputs.tag != '' }}
run: |
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64.zip"
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64_portable.zip"
shell: pwsh
- name: Release
@@ -74,13 +71,15 @@ jobs:
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
- name: Upload windows file release
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Windows-file-x64-release
path: Release
path: PiliPlus_windows_*.zip
- name: Upload windows setup release
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
archive: false
name: Windows-setup-x64-release
path: PiliPlus-Win-Setup
path: PiliPlus-Win-Setup/PiliPlus_windows_*.exe

5
.gitignore vendored
View File

@@ -5,10 +5,13 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
Package.resolved
# IntelliJ related
*.iml
@@ -146,4 +149,4 @@ pili_release.json
dist
test.dart
test*

View File

@@ -12,7 +12,6 @@ include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/grpc/bilibili/**
# - lib/grpc/google/**
# - lib/common/widgets/flutter/**
formatter:
@@ -72,5 +71,11 @@ linter:
- use_truncating_division
- use_string_buffers
- unnecessary_statements
- unnecessary_nullable_for_final_variable_declarations
- tighten_type_of_initializing_formals
- prefer_void_to_null
- prefer_spread_collections
- unnecessary_to_list_in_spreads
- prefer_for_elements_to_map_fromIterable
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -3,7 +3,6 @@ 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")
}
@@ -18,10 +17,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.example.piliplus"
minSdk = flutter.minSdkVersion
@@ -49,15 +44,29 @@ android {
}
}
buildFeatures {
if (project.hasProperty("dev")) {
resValues = true
}
}
buildTypes {
all {
signingConfig = config ?: signingConfigs["debug"]
}
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (project.hasProperty("dev")) {
applicationIdSuffix = ".dev"
resValue(
type = "string",
name = "app_name",
value = "PiliPlus dev",
)
}
// proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro"
// )
}
debug {
applicationIdSuffix = ".debug"
@@ -72,6 +81,12 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}

View File

@@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.piliplus">
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
@@ -16,8 +17,7 @@
</queries>
<queries>
<intent>
<action android:name=
"android.support.customtabs.action.CustomTabsService" />
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
@@ -35,56 +35,62 @@
</intent>
</queries>
<application
android:label="@string/app_name"
<application xmlns:tools="http://schemas.android.com/tools"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
xmlns:tools="http://schemas.android.com/tools"
android:enableOnBackInvokedCallback="false"
android:allowBackup="false"
android:enableOnBackInvokedCallback="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
tools:replace="android:allowBackup">
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:supportsPictureInPicture="true"
android:launchMode="singleTask"
android:resizeableActivity="true"
>
android:supportsPictureInPicture="true"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="PiliPlus">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="*.bilibili.com"/>
<data android:host="*.bilibili.cn"/>
<data android:host="*.bilibili.tv"/>
<data android:host="bilibili.com"/>
<data android:host="bilibili.cn"/>
<data android:host="bilibili.tv"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="*.bilibili.com" />
<data android:host="*.bilibili.cn" />
<data android:host="*.bilibili.tv" />
<data android:host="bilibili.com" />
<data android:host="bilibili.cn" />
<data android:host="bilibili.tv" />
<data android:host="b23.tv" />
<!--<data android:host="live.bilibili.com"/>-->
<!--<data android:host="www.bilibili.com"/>-->
@@ -100,36 +106,56 @@
<intent-filter android:label="PiliPlus">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.example.piliplus.SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bilibili"/>
<data android:scheme="bilibili" />
<data android:host="download" />
<data android:host="forward" />
<data android:host="comment"
<data
android:host="comment"
android:pathPattern="/detail/.*/.*/.*" />
<data android:host="uper" />
<data android:host="article"
<data
android:host="article"
android:pathPattern="/readlist" />
<data android:host="opus" />
<data android:host="advertise" android:path="/home" />
<data
android:host="advertise"
android:path="/home" />
<data android:host="clip" />
<data android:host="search" android:pathPattern=".*" />
<data
android:host="search"
android:pathPattern=".*" />
<data android:host="stardust-search" />
<data android:host="music" />
<data android:host="cheese" />
<data android:host="bangumi"
<data
android:host="bangumi"
android:pathPattern="/season.*" />
<data android:host="bangumi" android:pathPattern="/.*" />
<data android:host="pictureshow"
<data
android:host="bangumi"
android:pathPattern="/.*" />
<data
android:host="pictureshow"
android:pathPrefix="/creative_center" />
<data android:host="cliparea" />
<data android:host="im" />
<data android:host="im" android:path="/notifications" />
<data
android:host="im"
android:path="/notifications" />
<data android:host="following" />
<data android:host="following"
<data
android:host="following"
android:pathPattern="/detail/.*" />
<data android:host="following"
<data
android:host="following"
android:path="/publishInfo/" />
<data android:host="laser" android:pathPattern="/.*" />
<data
android:host="laser"
android:pathPattern="/.*" />
<data android:host="livearea" />
<data android:host="live" />
<data android:host="catalog" />
@@ -147,28 +173,44 @@
<data android:host="video" />
<data android:host="story" />
<data android:host="podcast" />
<data android:host="main" android:path="/favorite" />
<data android:host="pgc" android:path="/theater/match" />
<data android:host="pgc" android:path="/theater/square" />
<data android:host="m.bilibili.com"
<data
android:host="main"
android:path="/favorite" />
<data
android:host="pgc"
android:path="/theater/match" />
<data
android:host="pgc"
android:path="/theater/square" />
<data
android:host="m.bilibili.com"
android:path="/topic-detail" />
<data android:host="article" />
<data android:host="pegasus"
<data
android:host="pegasus"
android:pathPattern="/channel/v2/.*" />
<data android:host="feed" android:pathPattern="/channel" />
<data
android:host="feed"
android:pathPattern="/channel" />
<data android:host="vip" />
<data android:host="user_center" android:path="/vip" />
<data
android:host="user_center"
android:path="/vip" />
<data android:host="history" />
<data android:host="charge" android:path="/rank" />
<data
android:host="charge"
android:path="/rank" />
<data android:host="assistant" />
<data android:host="feedback" />
<data android:host="auth" android:path="/launch" />
<data
android:host="auth"
android:path="/launch" />
</intent-filter>
</activity>
<service
<service
android:name="com.ryanheise.audioservice.AudioService"
android:exported="true"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
@@ -177,32 +219,37 @@
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Ucrop.CropTheme"/>
android:theme="@style/Ucrop.CropTheme" />
<receiver
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</receiver>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!--
Media access permissions.
Android 13 or higher.
@@ -210,5 +257,5 @@
-->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
</manifest>

View File

@@ -0,0 +1,272 @@
package com.example.piliplus;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Icon;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.Rational;
import android.view.WindowManager;
import androidx.annotation.DrawableRes;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.github.dart_lang.jni_flutter.JniFlutterPlugin;
import java.util.ArrayList;
import java.util.Objects;
@Keep
public final class AndroidHelper {
public static final boolean isFoldable;
public static final boolean isPipAvailable;
public static volatile boolean isPipMode = false;
static {
PackageManager pm = getContext().getPackageManager();
isFoldable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE);
isPipAvailable = pm.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
}
private AndroidHelper() {
}
private static Context getContext() {
return JniFlutterPlugin.getApplicationContext();
}
public static int sdkInt() {
return Build.VERSION.SDK_INT;
}
public static void back() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
}
public static void biliSendCommAntifraud(
int action, long oid, int type, long rpId, long root, long parent, long ctime, @NonNull String commentText,
String pictures, @NonNull String sourceId, long uid, @NonNull String cookie
) {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(new ComponentName(
"icu.freedomIntrovert.biliSendCommAntifraud",
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
));
intent.putExtra("action", action);
intent.putExtra("oid", oid);
intent.putExtra("type", type);
intent.putExtra("rpid", rpId);
intent.putExtra("root", root);
intent.putExtra("parent", parent);
intent.putExtra("ctime", ctime);
intent.putExtra("comment_text", commentText);
if (pictures != null) {
intent.putExtra("pictures", pictures);
}
intent.putExtra("source_id", sourceId);
intent.putExtra("uid", uid);
ArrayList<String> cookiesList = new ArrayList<>(1);
cookiesList.add(cookie);
intent.putStringArrayListExtra("cookies", cookiesList);
getContext().startActivity(intent);
}
public static void openLinkVerifySettings() {
Context context = getContext();
Uri uri = Uri.parse("package:" + context.getPackageName());
try {
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri);
} else {
intent = new Intent(Intent.ACTION_MAIN, uri);
intent.setClassName(
"com.android.settings",
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
);
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} catch (Exception ignored) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
public static boolean openMusic(@NonNull String title, String artist, String album) {
Intent intent = new Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SearchManager.QUERY, title);
intent.putExtra(MediaStore.EXTRA_MEDIA_TITLE, title);
if (artist != null) {
intent.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist);
}
if (album != null) {
intent.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album);
}
intent.addCategory(Intent.CATEGORY_DEFAULT);
Context context = getContext();
PackageManager pm = context.getPackageManager();
try {
if (pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
context.startActivity(intent);
return true;
}
} catch (Exception ignored) {
}
try {
intent.setAction(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH);
if (pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
context.startActivity(intent);
return true;
}
} catch (Exception ignored) {
}
return false;
}
public static void enterPip(long engineId, int width, int height, boolean autoEnter, boolean isLive, boolean isPlaying) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Activity activity = JniFlutterPlugin.getActivity(engineId);
assert activity != null;
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(width, height));
setPipActions(activity, builder, isLive, isPlaying);
if (autoEnter) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(true);
activity.setPictureInPictureParams(builder.build());
}
} else {
activity.enterPictureInPictureMode(builder.build());
}
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void updatePipActions(long engineId, boolean isLive, boolean isPlaying) {
Activity activity = JniFlutterPlugin.getActivity(engineId);
assert activity != null;
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
setPipActions(activity, builder, isLive, isPlaying);
activity.setPictureInPictureParams(builder.build());
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static void setPipActions(Activity activity, PictureInPictureParams.Builder builder, boolean isLive, boolean isPlaying) {
ComponentName mbrComponent = MediaHelper.getMediaButtonReceiverComponent(activity);
if (mbrComponent == null) return;
ArrayList<RemoteAction> actionList = new ArrayList<>(3);
if (!isLive) {
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_rewind_10s, "ACTION_REWIND", (int) PlaybackState.ACTION_REWIND));
}
if (isPlaying) {
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_pause, "ACTION_PAUSE", (int) PlaybackState.ACTION_PAUSE));
} else {
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_play, "ACTION_PLAY", (int) PlaybackState.ACTION_PLAY));
}
if (!isLive) {
actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_fast_forward_10s, "ACTION_FAST_FORWARD", (int) PlaybackState.ACTION_FAST_FORWARD));
}
builder.setActions(actionList);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static RemoteAction getRemoteAction(@NonNull ComponentName mbrComponent, Activity activity, @DrawableRes int resId, String title, int action) {
return new RemoteAction(
Icon.createWithResource(activity, resId),
title,
title,
Objects.requireNonNull(MediaHelper.buildMediaButtonPendingIntent(activity, mbrComponent, action))
);
}
public static void disableAutoEnterPip(long engineId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Activity activity = JniFlutterPlugin.getActivity(engineId);
if (activity != null) {
activity.setPictureInPictureParams(new PictureInPictureParams.Builder()
.setAutoEnterEnabled(false)
.build()
);
}
}
}
public static int[] maxScreenSize() {
Context context = getContext();
WindowManager wm = context.getSystemService(WindowManager.class);
try {
float density = context.getResources().getDisplayMetrics().density;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Rect maxBounds = wm.getMaximumWindowMetrics().getBounds();
return new int[]{Math.round(maxBounds.width() / density), Math.round(maxBounds.height() / density)};
} else {
Point realSize = new Point();
wm.getDefaultDisplay().getRealSize(realSize);
return new int[]{Math.round(realSize.x / density), Math.round(realSize.y / density)};
}
} catch (Exception ignored) {
return null;
}
}
public static void createShortcut(@NonNull String id, @NonNull String uri, @NonNull String label, @NonNull String icon) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Context context = getContext();
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
if (shortcutManager != null && shortcutManager.isRequestPinShortcutSupported()) {
Bitmap bitmap = BitmapFactory.decodeFile(icon);
ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id)
.setShortLabel(label)
.setIcon(Icon.createWithAdaptiveBitmap(bitmap))
.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(uri)))
.build();
// TODO: WorkerThread
Intent pinIntent = shortcutManager.createShortcutResultIntent(shortcut);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context, 0, pinIntent, PendingIntent.FLAG_IMMUTABLE
);
shortcutManager.requestPinShortcut(shortcut, pendingIntent.getIntentSender());
}
}
}
@Keep
public static final class ToDart {
public static volatile Runnable onUserLeaveHint;
public static Runnable onConfigurationChanged;
private ToDart() {
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.piliplus;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.media.session.PlaybackState;
import android.os.Build;
import android.util.Log;
import android.view.KeyEvent;
import java.util.List;
class MediaHelper {
private static final String TAG = "MediaButtonReceiver";
static PendingIntent buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, int action) {
if (mbrComponent == null) {
Log.w(TAG, "The component name of media button receiver should be provided.");
return null;
}
int keyCode = PlaybackStateCompat_toKeyCode(action);
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
Log.w(TAG,
"Cannot build a media button pending intent with the given action: " + action);
return null;
}
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
intent.setComponent(mbrComponent);
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
return PendingIntent.getBroadcast(context, keyCode, intent,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);
}
private static int PlaybackStateCompat_toKeyCode(int action) {
return switch (action) {
case (int) PlaybackState.ACTION_STOP -> KeyEvent.KEYCODE_MEDIA_STOP;
case (int) PlaybackState.ACTION_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE;
case (int) PlaybackState.ACTION_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY;
case (int) PlaybackState.ACTION_REWIND -> KeyEvent.KEYCODE_MEDIA_REWIND;
case (int) PlaybackState.ACTION_SKIP_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS;
case (int) PlaybackState.ACTION_SKIP_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT;
case (int) PlaybackState.ACTION_FAST_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
case (int) PlaybackState.ACTION_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
default -> KeyEvent.KEYCODE_UNKNOWN;
};
}
static ComponentName getMediaButtonReceiverComponent(Context context) {
Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
queryIntent.setPackage(context.getPackageName());
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0);
if (resolveInfos.size() == 1) {
ResolveInfo resolveInfo = resolveInfos.get(0);
return new ComponentName(resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name);
} else if (resolveInfos.size() > 1) {
Log.w(TAG, "More than one BroadcastReceiver that handles "
+ Intent.ACTION_MEDIA_BUTTON + " was found, returning null.");
}
return null;
}
}

View File

@@ -1,151 +1,20 @@
package com.example.piliplus
import android.app.PictureInPictureParams
import android.app.SearchManager
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.provider.Settings
import android.view.WindowManager.LayoutParams
import androidx.core.net.toUri
import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlin.system.exitProcess
class MainActivity : AudioServiceActivity() {
private lateinit var methodChannel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "PiliPlus")
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"back" -> back();
"biliSendCommAntifraud" -> {
try {
val action = call.argument<Int>("action") ?: 0
val oid = call.argument<Number>("oid") ?: 0L
val type = call.argument<Int>("type") ?: 0
val rpid = call.argument<Number>("rpid") ?: 0L
val root = call.argument<Number>("root") ?: 0L
val parent = call.argument<Number>("parent") ?: 0L
val ctime = call.argument<Number>("ctime") ?: 0L
val commentText = call.argument<String>("comment_text") ?: ""
val pictures = call.argument<String?>("pictures")
val sourceId = call.argument<String>("source_id") ?: ""
val uid = call.argument<Number>("uid") ?: 0L
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
val intent = Intent().apply {
component = ComponentName(
"icu.freedomIntrovert.biliSendCommAntifraud",
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
)
putExtra("action", action)
putExtra("oid", oid.toLong())
putExtra("type", type)
putExtra("rpid", rpid.toLong())
putExtra("root", root.toLong())
putExtra("parent", parent.toLong())
putExtra("ctime", ctime.toLong())
putExtra("comment_text", commentText)
if (pictures != null)
putExtra("pictures", pictures)
putExtra("source_id", sourceId)
putExtra("uid", uid.toLong())
putStringArrayListExtra("cookies", ArrayList(cookies))
}
startActivity(intent)
} catch (_: Exception) {
}
}
"linkVerifySettings" -> {
val uri = ("package:" + context.packageName).toUri()
try {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
} else {
Intent("android.intent.action.MAIN", uri).setClassName(
"com.android.settings",
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
)
}
context.startActivity(intent)
} catch (_: Throwable) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
context.startActivity(intent)
}
}
"music" -> {
val title = call.argument<String>("title")
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
putExtra(SearchManager.QUERY, title)
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
call.argument<String?>("artist")
?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
call.argument<String?>("album")
?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
addCategory(Intent.CATEGORY_DEFAULT)
}
try {
if (packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY
) != null
) {
startActivity(intent)
result.success(true)
return@setMethodCallHandler
}
} catch (_: Throwable) {
}
try {
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
if (packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY
) != null
) {
startActivity(intent)
result.success(true)
return@setMethodCallHandler
}
} catch (_: Throwable) {
}
result.success(false)
}
"setPipAutoEnterEnabled" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val params = PictureInPictureParams.Builder()
.setAutoEnterEnabled(call.argument<Boolean>("autoEnable") ?: false)
.build()
setPictureInPictureParams(params)
}
}
else -> result.notImplemented()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (AndroidHelper.isFoldable) {
AndroidHelper.ToDart.onConfigurationChanged?.run()
}
}
private fun back() {
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -157,23 +26,15 @@ class MainActivity : AudioServiceActivity() {
override fun onDestroy() {
stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java))
super.onDestroy()
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
methodChannel.invokeMethod("onUserLeaveHint", null)
AndroidHelper.ToDart.onUserLeaveHint?.run()
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration?
) {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
MethodChannel(
flutterEngine!!.dartExecutor.binaryMessenger,
"floating"
).invokeMethod("onPipChanged", isInPictureInPictureMode)
AndroidHelper.isPipMode = isInPictureInPictureMode
}
}

View File

@@ -1,7 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
</vector>

View File

@@ -1,7 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
<path android:fillColor="@android:color/white" android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
</vector>

View File

@@ -6,7 +6,6 @@
android:viewportHeight="108.0">
<path
android:fillColor="#FF5CB67B"
android:pathData="M57.54,54L28.82,54l3.93,-19.36h24.78c5.35,0 9.68,4.33 9.68,9.68 0,5.35 -4.33,9.68 -9.68,9.68zM57.54,21.73L40.5,21.73L27.88,86.27h13.3l3.83,-19.36h12.54c12.48,0 22.59,-10.11 22.59,-22.59 0,-12.48 -10.11,-22.59 -22.59,-22.59z"
android:strokeWidth="0.252073"
android:pathData="m58.52,54h-36.71l5.03,-24.75h31.68c6.83,0 12.37,5.54 12.37,12.37 0,6.83 -5.54,12.37 -12.37,12.37zM58.52,12.76h-21.77l-16.14,82.49h17l4.89,-24.75h16.02c15.95,0 28.87,-12.93 28.87,-28.87 0,-15.95 -12.92,-28.87 -28.87,-28.87z"
android:fillType="evenOdd" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="30.0"
android:viewportHeight="31.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M23.5,11C23.423,10.894 23.325,10.803 23.213,10.734C23.101,10.666 22.976,10.62 22.846,10.599C22.716,10.579 22.583,10.584 22.455,10.615C22.327,10.646 22.206,10.702 22.1,10.78C21.89,10.929 21.745,11.154 21.697,11.408C21.648,11.661 21.699,11.924 21.84,12.14C22.788,13.456 23.296,15.038 23.29,16.66C23.278,17.713 23.059,18.754 22.644,19.722C22.229,20.69 21.627,21.567 20.873,22.302C20.118,23.037 19.226,23.615 18.248,24.004C17.269,24.394 16.223,24.586 15.17,24.57C14.112,24.593 13.06,24.405 12.075,24.018C11.09,23.631 10.191,23.053 9.431,22.316C8.671,21.579 8.065,20.699 7.648,19.727C7.231,18.754 7.01,17.708 7,16.65C7.021,14.61 7.82,12.655 9.234,11.184C10.649,9.714 12.572,8.84 14.61,8.74V10.62C14.607,10.732 14.634,10.843 14.69,10.94C14.738,11.02 14.812,11.08 14.9,11.11C14.969,11.144 15.044,11.161 15.12,11.161C15.196,11.161 15.271,11.144 15.34,11.11L20,8.3C20.102,8.244 20.187,8.161 20.247,8.061C20.307,7.961 20.339,7.847 20.34,7.73C20.342,7.613 20.311,7.497 20.251,7.397C20.191,7.296 20.104,7.214 20,7.16L15.34,4.38C15.209,4.298 15.052,4.269 14.9,4.3C14.815,4.319 14.738,4.365 14.68,4.43C14.62,4.511 14.588,4.609 14.59,4.71V6.79C12.03,6.895 9.608,7.978 7.824,9.817C6.039,11.655 5.029,14.108 5,16.67C5.037,19.33 6.127,21.868 8.032,23.725C9.936,25.583 12.5,26.61 15.16,26.58C17.82,26.61 20.384,25.583 22.288,23.725C24.193,21.868 25.283,19.33 25.32,16.67C25.316,14.637 24.68,12.656 23.5,11ZM11.77,20.77H12.67C12.71,20.771 12.749,20.765 12.786,20.75C12.823,20.736 12.857,20.713 12.885,20.685C12.913,20.657 12.935,20.624 12.95,20.587C12.965,20.55 12.971,20.51 12.97,20.47V13.14C12.971,13.1 12.965,13.061 12.95,13.024C12.935,12.987 12.913,12.953 12.885,12.925C12.857,12.897 12.823,12.875 12.786,12.86C12.749,12.846 12.71,12.839 12.67,12.84H12.08C11.963,12.838 11.847,12.862 11.74,12.91C11.495,13.029 11.049,13.285 10.63,13.526C10.52,13.59 10.412,13.652 10.31,13.71L9.81,14.01C9.764,14.031 9.725,14.066 9.7,14.11C9.681,14.189 9.681,14.271 9.7,14.35V15.3C9.693,15.33 9.693,15.361 9.7,15.39L9.76,15.44H9.82H9.92C10.158,15.327 10.348,15.229 10.525,15.139L10.525,15.139L10.525,15.139C10.659,15.07 10.786,15.005 10.92,14.94L11.33,14.72C11.349,14.71 11.369,14.705 11.39,14.705C11.411,14.705 11.432,14.71 11.45,14.72C11.459,14.737 11.463,14.756 11.463,14.775C11.463,14.794 11.459,14.813 11.45,14.83V20.47C11.45,20.511 11.458,20.552 11.475,20.59C11.491,20.628 11.515,20.662 11.545,20.69C11.575,20.718 11.61,20.74 11.649,20.753C11.688,20.767 11.729,20.773 11.77,20.77ZM15.952,12.977C16.339,12.772 16.772,12.67 17.21,12.68C17.66,12.661 18.108,12.759 18.509,12.964C18.91,13.169 19.252,13.474 19.5,13.85C20.039,14.737 20.3,15.764 20.25,16.8C20.302,17.837 20.041,18.865 19.5,19.75C19.255,20.12 18.92,20.422 18.527,20.627C18.133,20.831 17.694,20.933 17.25,20.92C16.806,20.937 16.365,20.838 15.97,20.633C15.575,20.428 15.241,20.124 15,19.75C14.437,18.872 14.158,17.842 14.2,16.8C14.16,15.758 14.439,14.729 15,13.85C15.238,13.482 15.566,13.182 15.952,12.977ZM17.94,19.4C18.176,19.215 18.353,18.965 18.45,18.68V18.69C18.654,18.081 18.752,17.442 18.74,16.8C18.763,16.163 18.675,15.527 18.48,14.92C18.38,14.637 18.208,14.385 17.98,14.19C17.76,14.04 17.501,13.96 17.235,13.96C16.969,13.96 16.709,14.04 16.49,14.19C16.259,14.381 16.089,14.634 16,14.92C15.791,15.521 15.693,16.154 15.71,16.79C15.683,17.431 15.771,18.071 15.97,18.68C16.056,18.964 16.227,19.216 16.46,19.4C16.674,19.565 16.94,19.65 17.21,19.64C17.474,19.648 17.732,19.563 17.94,19.4Z"
android:fillType="evenOdd" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="36.0"
android:viewportHeight="36.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M13.402,8.296C13.402,6.887 12.259,5.744 10.849,5.744C9.439,5.744 8.296,6.887 8.296,8.296V27.442C8.296,28.852 9.439,29.995 10.849,29.995C12.259,29.995 13.402,28.852 13.402,27.442V8.296ZM27.442,8.296C27.442,6.887 26.299,5.744 24.889,5.744C23.479,5.744 22.337,6.887 22.337,8.296V27.442C22.337,28.852 23.479,29.995 24.889,29.995C26.299,29.995 27.442,28.852 27.442,27.442V8.296Z"
android:fillType="evenOdd" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="36.0"
android:viewportHeight="36.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M29.436,21.035L13.449,31.028C12.545,31.592 11.356,31.318 10.791,30.414C10.599,30.108 10.498,29.754 10.498,29.392L10.498,8.317C10.498,7.252 11.361,6.389 12.427,6.389C12.788,6.389 13.142,6.49 13.449,6.682L29.436,16.674C30.641,17.427 31.007,19.013 30.254,20.218C30.047,20.549 29.767,20.829 29.436,21.035Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="31.0"
android:viewportHeight="31.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M16.375,6.79V4.73C16.379,4.627 16.343,4.527 16.275,4.45C16.225,4.377 16.15,4.324 16.065,4.3C15.92,4.277 15.772,4.305 15.645,4.38L10.955,7.16C10.846,7.208 10.753,7.287 10.688,7.386C10.622,7.485 10.587,7.601 10.585,7.72C10.59,7.84 10.625,7.956 10.688,8.058C10.752,8.16 10.84,8.243 10.945,8.3L15.645,11.06C15.716,11.093 15.792,11.11 15.87,11.11C15.948,11.11 16.025,11.093 16.095,11.06C16.182,11.028 16.255,10.968 16.305,10.89C16.361,10.793 16.388,10.682 16.385,10.57V8.74C18.416,8.849 20.329,9.728 21.735,11.197C23.142,12.667 23.935,14.616 23.955,16.65C23.944,17.703 23.724,18.744 23.309,19.712C22.894,20.68 22.292,21.557 21.538,22.292C20.784,23.027 19.891,23.605 18.913,23.994C17.934,24.384 16.888,24.576 15.835,24.56C14.776,24.585 13.723,24.4 12.736,24.015C11.749,23.63 10.849,23.053 10.087,22.317C9.325,21.581 8.717,20.701 8.298,19.728C7.879,18.756 7.657,17.709 7.645,16.65C7.651,15.024 8.172,13.441 9.135,12.13C9.276,11.914 9.327,11.651 9.279,11.398C9.23,11.144 9.086,10.92 8.875,10.77C8.768,10.691 8.646,10.635 8.517,10.604C8.387,10.573 8.253,10.568 8.122,10.589C7.991,10.61 7.865,10.658 7.752,10.728C7.639,10.799 7.542,10.891 7.465,11C6.283,12.655 5.646,14.637 5.645,16.67C5.682,19.33 6.772,21.868 8.677,23.725C10.581,25.583 13.145,26.61 15.805,26.58C18.465,26.61 21.029,25.583 22.934,23.725C24.838,21.868 25.928,19.33 25.965,16.67C25.937,14.108 24.926,11.655 23.141,9.817C21.357,7.978 18.935,6.895 16.375,6.79ZM15.645,19.75C15.886,20.124 16.221,20.428 16.615,20.633C17.01,20.838 17.451,20.937 17.895,20.92C18.337,20.934 18.776,20.833 19.168,20.628C19.56,20.423 19.893,20.121 20.135,19.75C20.675,18.864 20.936,17.836 20.885,16.8C20.935,15.764 20.674,14.737 20.135,13.85C19.89,13.48 19.556,13.178 19.162,12.974C18.768,12.769 18.329,12.668 17.885,12.68C17.442,12.665 17.003,12.764 16.611,12.969C16.218,13.175 15.885,13.478 15.645,13.85C15.084,14.729 14.805,15.758 14.845,16.8C14.804,17.842 15.083,18.872 15.645,19.75ZM16.655,14.88C16.739,14.592 16.91,14.337 17.145,14.15C17.36,13.985 17.625,13.9 17.895,13.91C18.159,13.902 18.417,13.987 18.625,14.15C18.859,14.342 19.036,14.594 19.135,14.88C19.33,15.487 19.418,16.123 19.395,16.76C19.417,17.397 19.329,18.033 19.135,18.64C19.036,18.926 18.859,19.178 18.625,19.37C18.417,19.533 18.159,19.618 17.895,19.61C17.625,19.62 17.36,19.535 17.145,19.37C16.917,19.181 16.747,18.932 16.655,18.65C16.453,18.041 16.362,17.401 16.385,16.76C16.361,16.136 16.449,15.513 16.645,14.92L16.655,14.88ZM10.455,15.43H10.385L10.345,15.39C10.338,15.361 10.338,15.33 10.345,15.3V14.34C10.325,14.261 10.325,14.179 10.345,14.1C10.356,14.077 10.371,14.056 10.39,14.039C10.409,14.022 10.431,14.009 10.455,14L10.955,13.71C11.445,13.42 12.075,13.05 12.385,12.91C12.491,12.859 12.607,12.832 12.725,12.83H13.285C13.365,12.83 13.441,12.862 13.497,12.918C13.554,12.974 13.585,13.051 13.585,13.13V20.46C13.587,20.5 13.58,20.54 13.565,20.577C13.551,20.614 13.529,20.647 13.5,20.675C13.472,20.704 13.439,20.726 13.402,20.74C13.365,20.755 13.325,20.762 13.285,20.76H12.385C12.346,20.76 12.307,20.752 12.27,20.737C12.234,20.722 12.201,20.7 12.173,20.672C12.145,20.645 12.123,20.611 12.108,20.575C12.093,20.539 12.085,20.5 12.085,20.46V14.82C12.094,14.803 12.099,14.784 12.099,14.765C12.099,14.746 12.094,14.727 12.085,14.71C12.067,14.701 12.046,14.696 12.025,14.696C12.004,14.696 11.984,14.701 11.965,14.71L11.555,14.93L10.555,15.43H10.455Z"
android:fillType="evenOdd" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,3 +1,5 @@
<resources>
<string name="app_name">PiliPlus</string>
<string name="search">搜索</string>
<string name="offline_video">离线视频</string>
</resources>

View File

@@ -0,0 +1,20 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:icon="@drawable/ic_shortcut_search"
android:shortcutId="search"
android:shortcutLongLabel="@string/search"
android:shortcutShortLabel="@string/search">
<intent
android:action="com.example.piliplus.SHORTCUT"
android:data="bilibili://search" />
</shortcut>
<shortcut
android:icon="@drawable/ic_shortcut_download"
android:shortcutId="offline_video"
android:shortcutLongLabel="@string/offline_video"
android:shortcutShortLabel="@string/offline_video">
<intent
android:action="com.example.piliplus.SHORTCUT"
android:data="bilibili://download" />
</shortcut>
</shortcuts>

View File

@@ -43,14 +43,14 @@ subprojects {
val pluginCompileSdk = pluginCompileSdkStr
?.removePrefix("android-")
?.toIntOrNull()
if (pluginCompileSdk != null && pluginCompileSdk < 31) {
if (pluginCompileSdk != null && pluginCompileSdk < 36) {
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" +
"from $pluginCompileSdk to 36 (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)
androidExtension.setCompileSdkVersion(36)
}
}

View File

@@ -1,3 +1,5 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
android.builtInKotlin=false
android.newDsl=false

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -19,8 +19,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.12.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")

Binary file not shown.

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 4.0625C7.85812 4.0625 5.98983 4.1725 4.67735 4.2798C3.77861 4.35327 3.08174 5.04067 3.00119 5.93221C2.90388 7.00924 2.8125 8.43727 2.8125 10C2.8125 11.5627 2.90388 12.9908 3.00119 14.0678C3.08174 14.9593 3.77861 15.6467 4.67735 15.7202C5.98983 15.8275 7.85812 15.9375 10 15.9375C12.1421 15.9375 14.0105 15.8275 15.323 15.7202C16.2216 15.6467 16.9184 14.9595 16.9989 14.0682C17.0962 12.9916 17.1875 11.5639 17.1875 10C17.1875 8.43614 17.0962 7.00837 16.9989 5.9318C16.9184 5.04049 16.2216 4.3533 15.323 4.27983C14.0105 4.17252 12.1421 4.0625 10 4.0625ZM4.5755 3.03395C5.9136 2.92456 7.81674 2.8125 10 2.8125C12.1835 2.8125 14.0868 2.92458 15.4249 3.03398C16.9228 3.15645 18.108 4.31588 18.2438 5.81931C18.3435 6.92296 18.4375 8.38948 18.4375 10C18.4375 11.6105 18.3435 13.077 18.2438 14.1807C18.108 15.6841 16.9228 16.8436 15.4249 16.966C14.0868 17.0754 12.1835 17.1875 10 17.1875C7.81674 17.1875 5.9136 17.0754 4.5755 16.966C3.07738 16.8436 1.8921 15.6838 1.75626 14.1803C1.65651 13.0762 1.5625 11.6094 1.5625 10C1.5625 8.39058 1.65651 6.92379 1.75626 5.81973C1.8921 4.31616 3.07738 3.15643 4.5755 3.03395ZM5 8.4375C5 8.09232 5.27982 7.8125 5.625 7.8125H5.9375C6.28268 7.8125 6.5625 8.09232 6.5625 8.4375C6.5625 8.78268 6.28268 9.0625 5.9375 9.0625H5.625C5.27982 9.0625 5 8.78268 5 8.4375ZM7.5 8.4375C7.5 8.09232 7.77982 7.8125 8.125 7.8125H13.125C13.4702 7.8125 13.75 8.09232 13.75 8.4375C13.75 8.78268 13.4702 9.0625 13.125 9.0625H8.125C7.77982 9.0625 7.5 8.78268 7.5 8.4375ZM6.875 10.9375C6.52982 10.9375 6.25 11.2173 6.25 11.5625C6.25 11.9077 6.52982 12.1875 6.875 12.1875H7.1875C7.53268 12.1875 7.8125 11.9077 7.8125 11.5625C7.8125 11.2173 7.53268 10.9375 7.1875 10.9375H6.875ZM9.375 10.9375C9.02982 10.9375 8.75 11.2173 8.75 11.5625C8.75 11.9077 9.02982 12.1875 9.375 12.1875H14.375C14.7202 12.1875 15 11.9077 15 11.5625C15 11.2173 14.7202 10.9375 14.375 10.9375H9.375Z" fill="#999"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="M17.982 9.275L8.06 3.27A2.013 2.013 0 005 4.994v12.011a2.017 2.017 0 003.06 1.725l9.922-6.005a2.017 2.017 0 000-3.45z"></path></svg>

Before

Width:  |  Height:  |  Size: 201 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="M16.118 3.667h.382a3.667 3.667 0 013.667 3.667v7.333a3.667 3.667 0 01-3.667 3.667h-11a3.667 3.667 0 01-3.667-3.667V7.333A3.667 3.667 0 015.5 3.666h.382L4.95 2.053a1.1 1.1 0 011.906-1.1l1.567 2.714h5.156L15.146.953a1.101 1.101 0 011.906 1.1l-.934 1.614z" fill="#333" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#c3beb7;"></path><path d="M5.561 5.194h10.878a2.2 2.2 0 012.2 2.2v7.211a2.2 2.2 0 01-2.2 2.2H5.561a2.2 2.2 0 01-2.2-2.2V7.394a2.2 2.2 0 012.2-2.2z" fill="#fff" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#e1ded9;"></path><path d="M6.967 8.556a1.1 1.1 0 011.1 1.1v2.689a1.1 1.1 0 11-2.2 0V9.656a1.1 1.1 0 011.1-1.1zM15.033 8.556a1.1 1.1 0 011.1 1.1v2.689a1.1 1.1 0 11-2.2 0V9.656a1.1 1.1 0 011.1-1.1z" fill="#333" data-darkreader-inline-fill="" style="--darkreader-inline-fill:#c3beb7;"></path></svg>

Before

Width:  |  Height:  |  Size: 916 B

View File

@@ -1 +0,0 @@
<svg width="18" height="16" viewBox="0 0 18 16" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M9 2.0625C6.85812 2.0625 4.98983 2.1725 3.67735 2.2798C2.77861 2.35327 2.08174 3.04067 2.00119 3.93221C1.90388 5.00924 1.8125 6.43727 1.8125 8C1.8125 9.56273 1.90388 10.9908 2.00119 12.0678C2.08174 12.9593 2.77861 13.6467 3.67735 13.7202C4.98983 13.8275 6.85812 13.9375 9 13.9375C11.1421 13.9375 13.0105 13.8275 14.323 13.7202C15.2216 13.6467 15.9184 12.9595 15.9989 12.0682C16.0962 10.9916 16.1875 9.56386 16.1875 8C16.1875 6.43614 16.0962 5.00837 15.9989 3.9318C15.9184 3.04049 15.2216 2.3533 14.323 2.27983C13.0105 2.17252 11.1421 2.0625 9 2.0625ZM3.5755 1.03395C4.9136 0.924562 6.81674 0.8125 9 0.8125C11.1835 0.8125 13.0868 0.924583 14.4249 1.03398C15.9228 1.15645 17.108 2.31588 17.2438 3.81931C17.3435 4.92296 17.4375 6.38948 17.4375 8C17.4375 9.61052 17.3435 11.077 17.2438 12.1807C17.108 13.6841 15.9228 14.8436 14.4249 14.966C13.0868 15.0754 11.1835 15.1875 9 15.1875C6.81674 15.1875 4.9136 15.0754 3.5755 14.966C2.07738 14.8436 0.892104 13.6838 0.756256 12.1803C0.656505 11.0762 0.5625 9.60942 0.5625 8C0.5625 6.39058 0.656505 4.92379 0.756257 3.81973C0.892104 2.31616 2.07738 1.15643 3.5755 1.03395ZM4.41663 4.93726C4.72729 4.93726 4.97913 5.1891 4.97913 5.49976V8.62476C4.97913 9.34963 5.56675 9.93726 6.29163 9.93726C7.0165 9.93726 7.60413 9.34963 7.60413 8.62476V5.49976C7.60413 5.1891 7.85597 4.93726 8.16663 4.93726C8.47729 4.93726 8.72913 5.1891 8.72913 5.49976V8.62476C8.72913 9.97095 7.63782 11.0623 6.29163 11.0623C4.94543 11.0623 3.85413 9.97095 3.85413 8.62476V5.49976C3.85413 5.1891 4.10597 4.93726 4.41663 4.93726ZM10.2501 4.93726C9.9394 4.93726 9.68756 5.1891 9.68756 5.49976V10.4998C9.68756 10.8104 9.9394 11.0623 10.2501 11.0623C10.5607 11.0623 10.8126 10.8104 10.8126 10.4998V9.60392H12.2292C13.5179 9.60392 14.5626 8.55925 14.5626 7.27059C14.5626 5.98193 13.5179 4.93726 12.2292 4.93726H10.2501ZM12.2292 8.47892H10.8126V6.06226H12.2292C12.8966 6.06226 13.4376 6.60325 13.4376 7.27059C13.4376 7.93793 12.8966 8.47892 12.2292 8.47892Z" fill="#999"></path></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24"><path fill-rule="evenodd" d="m8.085 4.891-.999-1.499a1.008 1.008 0 0 1 1.679-1.118l1.709 2.566c.54-.008 1.045-.012 1.515-.012h.13c.345 0 .707.003 1.088.007l1.862-2.59a1.008 1.008 0 0 1 1.637 1.177l-1.049 1.46c.788.02 1.631.046 2.53.078 1.958.069 3.468 1.6 3.74 3.507.088.613.13 2.158.16 3.276l.001.027c.01.333.017.63.025.856a.987.987 0 0 1-1.974.069c-.008-.23-.016-.539-.025-.881v-.002c-.028-1.103-.066-2.541-.142-3.065-.143-1.004-.895-1.78-1.854-1.813-2.444-.087-4.466-.13-6.064-.131-1.598 0-3.619.044-6.063.13a2.037 2.037 0 0 0-1.945 1.748c-.15 1.04-.225 2.341-.225 3.904 0 1.874.11 3.474.325 4.798.154.949.95 1.66 1.91 1.708a97.58 97.58 0 0 0 5.416.139.988.988 0 0 1 0 1.975c-2.196 0-3.61-.047-5.513-.141A4.012 4.012 0 0 1 2.197 17.7c-.236-1.446-.351-3.151-.351-5.116 0-1.64.08-3.035.245-4.184A4.013 4.013 0 0 1 5.92 4.96c.761-.027 1.483-.05 2.164-.069Zm4.436 4.707h-1.32v4.63h2.222v.848h-2.618v1.078h2.431a5.01 5.01 0 0 1 3.575-3.115V9.598h-1.276a8.59 8.59 0 0 0 .748-1.42l-1.089-.384a14.232 14.232 0 0 1-.814 1.804h-1.518l.693-.308a8.862 8.862 0 0 0-.814-1.408l-1.045.352c.297.396.572.847.825 1.364Zm-4.18 3.564.154-1.485h1.98V8.289h-3.2v.979h2.067v1.43H7.483l-.308 3.454h2.277c0 1.166-.044 1.925-.12 2.277-.078.352-.386.528-.936.528-.308 0-.616-.022-.902-.055l.297 1.067.062.004c.285.02.551.04.818.04 1.001-.066 1.562-.418 1.694-1.056.11-.638.176-1.903.176-3.795h-2.2Zm7.458.11v-.858h-1.254v.858H15.8Zm-2.376-.858v.858h-1.199v-.858h1.2Zm-1.199-.946h1.2v-.902h-1.2v.902Zm2.321 0v-.902H15.8v.902h-1.254Zm3.517 10.594a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-.002-1.502a2.5 2.5 0 0 1-2.217-3.657l3.326 3.398a2.49 2.49 0 0 1-1.109.259Zm2.5-2.5c0 .42-.103.815-.286 1.162l-3.328-3.401a2.5 2.5 0 0 1 3.614 2.239Z" clip-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24" id="svgcontent" overflow="visible" width="24" height="24" x="597" y="286"><g class="layer" style="pointer-events:all"><title style="pointer-events:inherit">Layer 1</title><path fill-rule="evenodd" d="M11.99,4.83C11.52,4.83 11.01,4.83 10.47,4.84L8.76,2.27A1.01,1.01 0 0 0 7.09,3.39L8.08,4.89C7.4,4.91 6.68,4.93 5.92,4.96A4.01,4.01 0 0 0 2.09,8.4C1.93,9.55 1.85,10.94 1.85,12.58C1.85,14.55 1.96,16.25 2.2,17.7A4.01,4.01 0 0 0 5.96,21.06L6.86,21.11C8.07,21.17 8.67,21.2 10.47,21.2A0.99,0.99 0 0 0 10.47,19.23C8.71,19.23 8.13,19.2 6.97,19.14L6.06,19.09A2.04,2.04 0 0 1 4.15,17.38C3.93,16.06 3.82,14.46 3.82,12.58C3.82,11.02 3.9,9.72 4.05,8.68C4.19,7.7 5.01,6.97 5.99,6.93C8.43,6.85 10.46,6.8 12.05,6.8C13.65,6.8 15.67,6.85 18.12,6.93C19.08,6.97 19.83,7.74 19.97,8.75C20.05,9.27 20.09,10.71 20.11,11.81L20.11,11.81C20.12,12.16 20.13,12.46 20.14,12.69A0.99,0.99 0 1 0 22.11,12.63C22.1,12.4 22.1,12.1 22.09,11.77L22.09,11.74C22.06,10.62 22.01,9.08 21.93,8.47C21.65,6.56 20.14,5.03 18.19,4.96C17.29,4.93 16.44,4.9 15.66,4.88L16.71,3.42A1.01,1.01 0 0 0 15.07,2.24L13.21,4.83C12.83,4.83 12.46,4.83 12.12,4.83L11.99,4.83zM12.51,9.6L11.19,9.6L11.19,14.23L13.41,14.23L13.41,15.08L10.79,15.08L10.79,16.16L13.41,16.16L13.42,16.84C13.78,16.86 14.13,17 14.43,17.24L14.54,17.24L14.54,16.16L17.23,16.16L17.23,15.08L14.53,15.08L14.53,14.23L16.8,14.23L16.8,9.6L15.52,9.6A8.59,8.59 0 0 0 16.27,8.18L15.18,7.8A14.23,14.23 0 0 1 14.37,9.6L12.85,9.6L13.54,9.3A8.86,8.86 0 0 0 12.73,7.89L11.68,8.24C11.98,8.64 12.26,9.09 12.51,9.6zM8.33,13.17L8.48,11.68L10.46,11.68L10.46,8.29L7.26,8.29L7.26,9.27L9.33,9.27L9.33,10.7L7.47,10.7L7.16,14.16L9.44,14.16C9.44,15.32 9.4,16.08 9.32,16.43C9.24,16.79 8.94,16.96 8.39,16.96C8.08,16.96 7.77,16.94 7.48,16.91L7.78,17.97L7.84,17.98C8.13,18 8.39,18.02 8.66,18.02C9.66,17.95 10.22,17.6 10.35,16.96C10.46,16.32 10.53,15.06 10.53,13.17L8.33,13.17zM15.79,13.28L15.79,12.42L14.53,12.42L14.53,13.28L15.79,13.28zM13.41,12.42L13.41,13.28L12.21,13.28L12.21,12.42L13.41,12.42zM12.21,11.47L13.41,11.47L13.41,10.57L12.21,10.57L12.21,11.47zM14.53,11.47L14.53,10.57L15.79,10.57L15.79,11.47L14.53,11.47z" clip-rule="evenodd" id="svg_1"></path><path fill="#000000" fill-rule="evenodd" d="M22.85,14.63A1,1 0 0 0 21.43,14.7L16.34,20.41L14.13,18.13L14.03,18.04L14.02,18.04A1,1 0 0 0 12.7,19.53L15.66,22.57L15.76,22.66L15.76,22.66C16.17,22.98 16.76,22.93 17.12,22.54L22.93,16.03L23.01,15.93L23.01,15.92A1,1 0 0 0 22.85,14.63z" clip-rule="evenodd" id="svg_2"></path></g></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.67735 4.2798C5.98983 4.1725 7.85812 4.0625 10 4.0625C12.1421 4.0625 14.0105 4.17252 15.323 4.27983C16.2216 4.3533 16.9184 5.04049 16.9989 5.9318C17.0962 7.00837 17.1875 8.43614 17.1875 10C17.1875 11.5639 17.0962 12.9916 16.9989 14.0682C16.9184 14.9595 16.2216 15.6467 15.323 15.7202C14.0105 15.8275 12.1421 15.9375 10 15.9375C7.85812 15.9375 5.98983 15.8275 4.67735 15.7202C3.77861 15.6467 3.08174 14.9593 3.00119 14.0678C2.90388 12.9908 2.8125 11.5627 2.8125 10C2.8125 8.43727 2.90388 7.00924 3.00119 5.93221C3.08174 5.04067 3.77861 4.35327 4.67735 4.2798ZM10 2.8125C7.81674 2.8125 5.9136 2.92456 4.5755 3.03395C3.07738 3.15643 1.8921 4.31616 1.75626 5.81973C1.65651 6.92379 1.5625 8.39058 1.5625 10C1.5625 11.6094 1.65651 13.0762 1.75626 14.1803C1.8921 15.6838 3.07738 16.8436 4.5755 16.966C5.9136 17.0754 7.81674 17.1875 10 17.1875C12.1835 17.1875 14.0868 17.0754 15.4249 16.966C16.9228 16.8436 18.108 15.6841 18.2438 14.1807C18.3435 13.077 18.4375 11.6105 18.4375 10C18.4375 8.38948 18.3435 6.92296 18.2438 5.81931C18.108 4.31588 16.9228 3.15645 15.4249 3.03398C14.0868 2.92458 12.1835 2.8125 10 2.8125ZM12.1876 10.722C12.7431 10.4013 12.7431 9.59941 12.1876 9.27866L9.06133 7.47373C8.50577 7.15298 7.81133 7.55392 7.81133 8.19542V11.8053C7.81133 12.4468 8.50577 12.8477 9.06133 12.527L12.1876 10.722Z" fill="#999"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '13.0'
platform :ios, '14.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -6,7 +6,7 @@ PODS:
- FlutterMacOS
- audio_session (0.0.1):
- Flutter
- auto_orientation (0.0.1):
- battery_plus (1.0.0):
- Flutter
- chat_bottom_container (0.0.1):
- Flutter
@@ -14,40 +14,9 @@ PODS:
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- FlutterMacOS
- Flutter (1.0.0)
- flutter_inappwebview_ios (0.0.1):
- Flutter
@@ -67,10 +36,10 @@ PODS:
- gt3_flutter_plugin (0.0.9):
- Flutter
- GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3)
- image_cropper (0.0.4):
- GT3Captcha-iOS (0.15.9)
- image_cropper (0.0.5):
- Flutter
- TOCropViewController (~> 2.8.0)
- TOCropViewController (~> 3.1.2)
- image_picker_ios (0.0.1):
- Flutter
- live_photo_maker (0.0.3):
@@ -81,21 +50,17 @@ PODS:
- Flutter
- media_kit_video (0.0.1):
- Flutter
- native_device_orientation (0.0.1):
- Flutter
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- saver_gallery (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -104,8 +69,7 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- TOCropViewController (2.8.0)
- TOCropViewController (3.1.2)
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
@@ -115,11 +79,11 @@ DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- battery_plus (from `.symlinks/plugins/battery_plus/ios`)
- chat_bottom_container (from `.symlinks/plugins/chat_bottom_container/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_picker (from `.symlinks/plugins/file_picker/darwin`)
- Flutter (from `Flutter`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
@@ -133,8 +97,8 @@ DEPENDENCIES:
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
@@ -146,12 +110,8 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- GT3Captcha-iOS
- OrderedSet
- SDWebImage
- SwiftyGif
- TOCropViewController
EXTERNAL SOURCES:
@@ -161,8 +121,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_service/darwin"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
battery_plus:
:path: ".symlinks/plugins/battery_plus/ios"
chat_bottom_container:
:path: ".symlinks/plugins/chat_bottom_container/ios"
connectivity_plus:
@@ -170,7 +130,7 @@ EXTERNAL SOURCES:
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
:path: ".symlinks/plugins/file_picker/darwin"
Flutter:
:path: Flutter
flutter_inappwebview_ios:
@@ -197,10 +157,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
native_device_orientation:
:path: ".symlinks/plugins/native_device_orientation/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
@@ -219,45 +179,41 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
chat_bottom_container: d8b077152c91b0ab90001e900748ea50353a5520
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977
chat_bottom_container: f1eb8323db77a87db50f361142c679f11e892d1b
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
image_cropper: b8ef14d3fcff4040b0f9da2ca28d98219a5cba0e
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
live_photo_maker: 7d57bfc70a120b4673c10871f354f4b1b6fde5fd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
gt3_flutter_plugin: 37090e5fa66ff2a52939eb9d208fc36fa49d36e5
GT3Captcha-iOS: aeb6fed2e8594099821430a89208679e5a55b740
image_cropper: fca51f94982730acae168c4b5d691e0f11aeb259
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
live_photo_maker: 29280ca88323bd5a33aafd00d98624d5cf522176
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_native_event_loop: 5fba1a849a6c87a34985f1e178a0de5bd444a0cf
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
native_device_orientation: e3580675687d5034770da198f6839ebf2122ef94
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
screen_brightness_ios: 6a6f7794b67f07c4f1e24f6374b2d8ad367ffb39
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
screen_brightness_ios: 9953fd7da5bd480f1a93990daeec2eb42d4f3b52
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
TOCropViewController: a916930c465b5d9445a74d95e0c0da931771b4df
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: f62db4fb414ebdecb264109948f76dfef35fdc3d
PODFILE CHECKSUM: 5e755568c318fde60f7b59d132a4ba634d53bf27
COCOAPODS: 1.16.2

View File

@@ -360,7 +360,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -438,7 +438,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -487,7 +487,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -131,5 +131,13 @@
</array>
<key>UIStatusBarHidden</key>
<false/>
<key>NSLocalNetworkUsageDescription</key>
<string>需要访问本地网络以发现和连接 DLNA 投屏设备</string>
<key>NSBonjourServices</key>
<array>
<string>_ssdp._udp</string>
<string>_upnp._tcp</string>
<string>_http._tcp</string>
</array>
</dict>
</plist>

57
lib/common/assets.dart Normal file
View File

@@ -0,0 +1,57 @@
abstract final class Assets {
static const digitalNum = 'digital_id_num';
static const logo = 'assets/images/logo/logo.png';
static const logo2 = 'assets/images/logo/logo_2.png';
static const logoIco = 'assets/images/logo/ico/app_icon.ico';
static const logoLarge = 'assets/images/logo/desktop/logo_large.png';
static const vipIcon = 'assets/images/big-vip.png';
static const avatarPlaceHolder = 'assets/images/noface.jpeg';
static const loading = 'assets/images/loading.png';
static const buffering = 'assets/images/loading.webp';
static const play = 'assets/images/play.png';
static const topicHeader = 'assets/images/topic-header-bg.png';
static const trendingBanner = 'assets/images/trending_banner.png';
static const ai = 'assets/images/ai.png';
static const error = 'assets/images/error.svg';
static const livingChart = 'assets/images/live.gif';
static const livingStatic = 'assets/images/live.png';
static const livingRect = 'assets/images/live/live.gif';
static const livingBackground = 'assets/images/live/default_bg.webp';
static const thunder1 = 'assets/images/paycoins/ic_thunder_1.png';
static const thunder2 = 'assets/images/paycoins/ic_thunder_2.png';
static const thunder3 = 'assets/images/paycoins/ic_thunder_3.png';
static const notEnough = 'assets/images/paycoins/ic_22_not_enough_pay.png';
static const mario = 'assets/images/paycoins/ic_22_mario.png';
static const gunSister = 'assets/images/paycoins/ic_22_gun_sister.png';
static const payBox = 'assets/images/paycoins/ic_pay_coins_box.png';
static const coinsOne = 'assets/images/paycoins/ic_coins_one.png';
static const coinsTwo = 'assets/images/paycoins/ic_coins_two.png';
static const left = 'assets/images/paycoins/ic_left.png';
static const leftDisable = 'assets/images/paycoins/ic_left_disable.png';
static const right = 'assets/images/paycoins/ic_right.png';
static const rightDisable = 'assets/images/paycoins/ic_right_disable.png';
static const panelClose = 'assets/images/paycoins/ic_panel_close.png';
static const List<String> mpvAnime4KShaders = [
'Anime4K_Clamp_Highlights.glsl',
'Anime4K_Restore_CNN_VL.glsl',
'Anime4K_Upscale_CNN_x2_VL.glsl',
'Anime4K_AutoDownscalePre_x2.glsl',
'Anime4K_AutoDownscalePre_x4.glsl',
'Anime4K_Upscale_CNN_x2_M.glsl',
];
static const mpvAnime4KShadersLite = [
'Anime4K_Clamp_Highlights.glsl',
'Anime4K_Restore_CNN_M.glsl',
'Anime4K_Restore_CNN_S.glsl',
'Anime4K_Upscale_CNN_x2_M.glsl',
'Anime4K_AutoDownscalePre_x2.glsl',
'Anime4K_AutoDownscalePre_x4.glsl',
'Anime4K_Upscale_CNN_x2_S.glsl',
];
}

View File

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

220
lib/common/dial_prefix.dart Normal file
View File

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

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:flutter/material.dart';
class FavPgcItemSkeleton extends StatelessWidget {
@@ -11,7 +11,7 @@ class FavPgcItemSkeleton extends StatelessWidget {
return Skeleton(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
horizontal: Style.safeSpace,
vertical: 5,
),
child: Row(

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:flutter/material.dart';
class MediaPgcSkeleton extends StatefulWidget {
@@ -15,11 +15,9 @@ class _MediaPgcSkeletonState extends State<MediaPgcSkeleton> {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace,
7,
StyleString.safeSpace,
7,
padding: const .symmetric(
horizontal: Style.safeSpace,
vertical: 7,
),
child: Row(
children: [

View File

@@ -1,188 +1,62 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class Skeleton extends StatelessWidget {
class Skeleton extends StatefulWidget {
final Widget child;
const Skeleton({
required this.child,
super.key,
});
const Skeleton({super.key, required this.child});
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.surface.withAlpha(10);
final shimmerGradient = LinearGradient(
colors: [
Colors.transparent,
color,
color,
Colors.transparent,
],
stops: const [
0.1,
0.3,
0.5,
0.7,
],
begin: const Alignment(-1.0, -0.3),
end: const Alignment(1.0, 0.9),
tileMode: TileMode.clamp,
);
return Shimmer(
linearGradient: shimmerGradient,
child: ShimmerLoading(
isLoading: true,
child: child,
),
);
}
State<Skeleton> createState() => _SkeletonState();
}
class Shimmer extends StatefulWidget {
static ShimmerState? of(BuildContext context) {
return context.findAncestorStateOfType<ShimmerState>();
}
const Shimmer({
super.key,
required this.linearGradient,
this.child,
});
final LinearGradient linearGradient;
final Widget? child;
@override
ShimmerState createState() => ShimmerState();
}
class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
late AnimationController _shimmerController;
class _SkeletonState extends State<Skeleton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Color color;
final matrix = Matrix4.identity();
@override
void initState() {
super.initState();
_shimmerController = AnimationController.unbounded(vsync: this)
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
_controller = AnimationController.unbounded(vsync: this)
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000))
..addListener(_setState);
}
@override
void dispose() {
_shimmerController.dispose();
_controller.dispose();
super.dispose();
}
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,
),
);
bool get isSized =>
(context.findRenderObject() as RenderBox?)?.hasSize ?? false;
Size get size => (context.findRenderObject() as RenderBox).size;
Offset getDescendantOffset({
required RenderBox descendant,
Offset offset = Offset.zero,
}) {
final shimmerBox = context.findRenderObject() as RenderBox;
return descendant.localToGlobal(offset, ancestor: shimmerBox);
void _setState() {
setState(() {});
}
Listenable get shimmerChanges => _shimmerController;
@override
Widget build(BuildContext context) {
return widget.child ?? const SizedBox.shrink();
}
}
class _SlidingGradientTransform extends GradientTransform {
const _SlidingGradientTransform({
required this.slidePercent,
});
final double slidePercent;
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
}
}
class ShimmerLoading extends StatefulWidget {
const ShimmerLoading({
super.key,
required this.isLoading,
required this.child,
});
final bool isLoading;
final Widget child;
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading> {
Listenable? _shimmerChanges;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_shimmerChanges != null) {
_shimmerChanges!.removeListener(_onShimmerChange);
}
_shimmerChanges = Shimmer.of(context)?.shimmerChanges;
if (_shimmerChanges != null) {
_shimmerChanges!.addListener(_onShimmerChange);
}
}
@override
void dispose() {
_shimmerChanges?.removeListener(_onShimmerChange);
super.dispose();
}
void _onShimmerChange() {
if (widget.isLoading) {
setState(() {});
}
color = ColorScheme.of(context).surface.withAlpha(10);
}
@override
Widget build(BuildContext context) {
if (!widget.isLoading) {
return widget.child;
}
final shimmer = Shimmer.of(context)!;
if (!shimmer.isSized) {
return const SizedBox.shrink();
}
final shimmerSize = shimmer.size;
final gradient = shimmer.gradient;
final offsetWithinShimmer = shimmer.getDescendantOffset(
descendant: context.findRenderObject() as RenderBox,
);
final colors = [Colors.transparent, color, color, Colors.transparent];
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
return gradient.createShader(
Rect.fromLTWH(
-offsetWithinShimmer.dx,
-offsetWithinShimmer.dy,
shimmerSize.width,
shimmerSize.height,
),
shaderCallback: (Rect bounds) {
final width = bounds.width;
final height = bounds.height;
matrix[12] = width * _controller.value;
return ui.Gradient.linear(
Offset(0, 0.35 * height),
Offset(width, 0.95 * height),
colors,
const [0.1, 0.3, 0.5, 0.7],
TileMode.clamp,
matrix.storage,
);
},
child: widget.child,

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:flutter/material.dart';
class VideoCardHSkeleton extends StatelessWidget {
@@ -10,25 +10,25 @@ class VideoCardHSkeleton extends StatelessWidget {
final color = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
padding: const .symmetric(
horizontal: Style.safeSpace,
vertical: 5,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
aspectRatio: Style.aspectRatio,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
borderRadius: Style.mdRadius,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 4, 6, 4),
padding: const .fromLTRB(10, 4, 6, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:flutter/material.dart';
class VideoCardVSkeleton extends StatelessWidget {
@@ -13,11 +13,11 @@ class VideoCardVSkeleton extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
aspectRatio: Style.aspectRatio,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
borderRadius: Style.mdRadius,
),
),
),

21
lib/common/style.dart Normal file
View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart'
show BorderRadius, Radius, BoxConstraints, ButtonStyle, VisualDensity;
abstract final class Style {
static const cardSpace = 8.0;
static const safeSpace = 12.0;
static const mdRadius = BorderRadius.all(imgRadius);
static const imgRadius = Radius.circular(10);
static const aspectRatio = 16 / 10;
static const aspectRatio16x9 = 16 / 9;
static const imgMaxRatio = 2.6;
static const bottomSheetRadius = BorderRadius.vertical(
top: Radius.circular(18),
);
static const dialogFixedConstraints = BoxConstraints.tightFor(width: 420);
static const topBarHeight = 52.0;
static const buttonStyle = ButtonStyle(
visualDensity: VisualDensity(horizontal: -2, vertical: -1.25),
tapTargetSize: .shrinkWrap,
);
}

View File

@@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
Widget avatars({
required ColorScheme colorScheme,
required Iterable<Owner> users,
double gap = 6.0,
}) {
const gap = 6.0;
const size = 22.0;
const padding = 0.8;
const offset = size - gap;
final offset = size - gap;
const imgSize = size - 2 * padding;
if (users.length == 1) {
return NetworkImgLayer(

View File

@@ -82,26 +82,21 @@ class PBadge extends StatelessWidget {
color = theme.onSurfaceVariant;
}
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));
late EdgeInsets paddingStyle = const .symmetric(vertical: 2, horizontal: 3);
final BorderRadius br = size == .small
? const .all(.circular(3))
: const .all(.circular(4));
Widget content = Container(
padding: padding ?? paddingStyle,
decoration: BoxDecoration(
borderRadius: br,
color: bgColor,
border: Border.all(color: borderColor),
border: .all(color: borderColor),
),
child: Text(
text!,
textScaler: textScaleFactor != null
? TextScaler.linear(textScaleFactor!)
: null,
textScaler: textScaleFactor != null ? .linear(textScaleFactor!) : null,
style: TextStyle(
height: 1,
fontSize: fontSize,

View File

@@ -16,7 +16,7 @@ class ToolbarIconButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final colorScheme = ColorScheme.of(context);
return SizedBox(
width: 36,
height: 36,
@@ -24,14 +24,14 @@ class ToolbarIconButton extends StatelessWidget {
tooltip: tooltip,
onPressed: onPressed,
icon: icon,
highlightColor: theme.colorScheme.secondaryContainer,
highlightColor: colorScheme.secondaryContainer,
color: selected
? theme.colorScheme.onSecondaryContainer
: theme.colorScheme.outline,
? colorScheme.onSecondaryContainer
: colorScheme.outline,
style: ButtonStyle(
padding: const WidgetStatePropertyAll(EdgeInsets.zero),
backgroundColor: WidgetStatePropertyAll(
selected ? theme.colorScheme.secondaryContainer : null,
selected ? colorScheme.secondaryContainer : null,
),
),
),

View File

@@ -1,4 +1,4 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:flutter/material.dart';
class ColorPalette extends StatelessWidget {
@@ -62,7 +62,7 @@ class ColorPalette extends StatelessWidget {
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: colorScheme.onInverseSurface,
borderRadius: StyleString.mdRadius,
borderRadius: Style.mdRadius,
),
child: child,
);

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class ColoredBoxTransition extends AnimatedWidget {
const ColoredBoxTransition({
super.key,
required this.color,
this.child,
}) : super(listenable: color);
final Animation<Color?> color;
final Widget? child;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: color.value!,
child: child,
);
}
}

View File

@@ -70,20 +70,14 @@ class CroppedImage extends LeafRenderObjectWidget {
class RenderCroppedImage extends RenderBox {
RenderCroppedImage({
required Size preferredSize,
required ui.Image image,
required Rect srcRect,
required Rect dstRect,
required RRect rrect,
required Paint imgPaint,
required Paint borderPaint,
}) : _preferredSize = preferredSize,
_image = image,
_srcRect = srcRect,
_dstRect = dstRect,
_rrect = rrect,
_imgPaint = imgPaint,
_borderPaint = borderPaint;
required this._preferredSize,
required this._image,
required this._srcRect,
required this._dstRect,
required this._rrect,
required this._imgPaint,
required this._borderPaint,
});
Size _preferredSize;
Size get preferredSize => _preferredSize;

View File

@@ -41,14 +41,11 @@ class Arc extends LeafRenderObjectWidget {
class RenderArc extends RenderBox {
RenderArc({
required double preferredSize,
required Color color,
required double progress,
required double strokeWidth,
}) : _preferredSize = preferredSize,
_color = color,
_progress = progress,
_strokeWidth = strokeWidth;
required this._preferredSize,
required this._color,
required this._progress,
required this._strokeWidth,
});
Color _color;
Color get color => _color;

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
class CustomHeightWidget extends SingleChildRenderObjectWidget {
const CustomHeightWidget({
super.key,
this.height,
this.offset = .zero,
required Widget super.child,
});
final double? height;
final Offset offset;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomHeightWidget(
height: height,
offset: offset,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderCustomHeightWidget renderObject,
) {
renderObject
..height = height
..offset = offset;
}
}
class RenderCustomHeightWidget extends RenderProxyBox {
RenderCustomHeightWidget({
this._height,
required this._offset,
});
double? _height;
double? get height => _height;
set height(double? value) {
if (_height == value) return;
_height = value;
markNeedsLayout();
}
Offset _offset;
Offset get offset => _offset;
set offset(Offset value) {
if (_offset == value) return;
_offset = value;
markNeedsPaint();
}
@override
void performLayout() {
if (height != null) {
child!.layout(constraints.copyWith(maxHeight: .infinity));
size = constraints.constrainDimensions(constraints.maxWidth, height!);
} else {
child!.layout(
constraints.copyWith(maxHeight: .infinity),
parentUsesSize: true,
);
size = constraints.constrainDimensions(
constraints.maxWidth,
child!.size.height,
);
}
}
@override
void paint(PaintingContext context, Offset offset) {
context.paintChild(child!, offset + _offset);
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
return result.addWithPaintOffset(
offset: _offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - _offset);
return child!.hitTest(result, position: transformed);
},
);
}
@override
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0);
}
}

View File

@@ -1,35 +1,46 @@
// ignore_for_file: constant_identifier_names
import 'package:flutter/widgets.dart';
import 'package:flutter/widgets.dart' show IconData;
class CustomIcons {
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 player_dm_tip_back = _CustomIconData(0xe807);
static const IconData player_dm_tip_copy = _CustomIconData(0xe808);
static const IconData player_dm_tip_like = _CustomIconData(0xe809);
static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a);
static const IconData player_dm_tip_recall = _CustomIconData(0xe80b);
static const IconData share = _CustomIconData(0xe80c);
static const IconData share_line = _CustomIconData(0xe80d);
static const IconData share_node = _CustomIconData(0xe80e);
static const IconData star_favorite_line = _CustomIconData(0xe80f);
static const IconData star_favorite_solid = _CustomIconData(0xe810);
static const IconData thumbs_down = _CustomIconData(0xe811);
static const IconData thumbs_down_outline = _CustomIconData(0xe812);
static const IconData thumbs_up = _CustomIconData(0xe813);
static const IconData thumbs_up_fill = _CustomIconData(0xe814);
static const IconData thumbs_up_line = _CustomIconData(0xe815);
static const IconData thumbs_up_outline = _CustomIconData(0xe816);
static const IconData topic_tag = _CustomIconData(0xe817);
static const IconData watch_later = _CustomIconData(0xe818);
}
// dart format off
abstract final class CustomIcons {
static const _kFontFam = 'custom_icon';
class _CustomIconData extends IconData {
const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon');
static const IconData ai_circle = IconData(0xe800, fontFamily: _kFontFam);
static const IconData coin = IconData(0xe801, fontFamily: _kFontFam);
static const IconData dm_off = IconData(0xe802, fontFamily: _kFontFam);
static const IconData dm_on = IconData(0xe803, fontFamily: _kFontFam);
static const IconData dm_settings = IconData(0xe804, fontFamily: _kFontFam);
static const IconData dyn = IconData(0xe805, fontFamily: _kFontFam);
static const IconData fav = IconData(0xe806, fontFamily: _kFontFam);
static const IconData flip_rotate_90 = IconData(0xe807, fontFamily: _kFontFam);
static const IconData identifier_circle = IconData(0xe808, fontFamily: _kFontFam);
static const IconData live_reserve = IconData(0xe809, fontFamily: _kFontFam);
static const IconData open_in_full_rotate_45 = IconData(0xe80a, fontFamily: _kFontFam);
static const IconData player_dm_tip_back = IconData(0xe80b, fontFamily: _kFontFam);
static const IconData player_dm_tip_copy = IconData(0xe80c, fontFamily: _kFontFam);
static const IconData player_dm_tip_like = IconData(0xe80d, fontFamily: _kFontFam);
static const IconData player_dm_tip_like_solid = IconData(0xe80e, fontFamily: _kFontFam);
static const IconData player_dm_tip_recall = IconData(0xe80f, fontFamily: _kFontFam);
static const IconData repeat_rounded_rotate_90 = IconData(0xe810, fontFamily: _kFontFam);
static const IconData share = IconData(0xe811, fontFamily: _kFontFam);
static const IconData share_line = IconData(0xe812, fontFamily: _kFontFam);
static const IconData share_node = IconData(0xe813, fontFamily: _kFontFam);
static const IconData shield_play_arrow = IconData(0xe814, fontFamily: _kFontFam);
static const IconData shield_published = IconData(0xe815, fontFamily: _kFontFam);
static const IconData shield_reply = IconData(0xe816, fontFamily: _kFontFam);
static const IconData shopping_bag_not_interested = IconData(0xe817, fontFamily: _kFontFam);
static const IconData splitscreen_rotate_90 = IconData(0xe818, fontFamily: _kFontFam);
static const IconData star_favorite_line = IconData(0xe819, fontFamily: _kFontFam);
static const IconData star_favorite_solid = IconData(0xe81a, fontFamily: _kFontFam);
static const IconData thumbs_down = IconData(0xe81b, fontFamily: _kFontFam);
static const IconData thumbs_down_outline = IconData(0xe81c, fontFamily: _kFontFam);
static const IconData thumbs_up = IconData(0xe81d, fontFamily: _kFontFam);
static const IconData thumbs_up_fill = IconData(0xe81e, fontFamily: _kFontFam);
static const IconData thumbs_up_line = IconData(0xe81f, fontFamily: _kFontFam);
static const IconData thumbs_up_outline = IconData(0xe820, fontFamily: _kFontFam);
static const IconData topic_tag = IconData(0xe821, fontFamily: _kFontFam);
static const IconData touch_app_rotate_270 = IconData(0xe822, fontFamily: _kFontFam);
static const IconData view_headline_rotate_90 = IconData(0xe823, fontFamily: _kFontFam);
static const IconData watch_later = IconData(0xe824, fontFamily: _kFontFam);
}

View File

@@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RenderProxyBox;
class CustomSliverPersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
const CustomSliverPersistentHeaderDelegate({
required this.child,
this.bgColor,
this.extent = 45,
this.needRebuild = false,
});
final double extent;
final Widget child;
final Color? bgColor;
final bool needRebuild;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
//创建child子组件
//shrinkOffsetchild偏移值minExtent~maxExtent
//overlapsContentSliverPersistentHeader覆盖其他子组件返回true否则返回false
return _DecoratedBox(color: bgColor, child: child);
}
//SliverPersistentHeader最大高度
@override
double get maxExtent => extent;
//SliverPersistentHeader最小高度
@override
double get minExtent => extent;
@override
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate.bgColor != bgColor ||
(needRebuild && oldDelegate.child != child);
}
}
class _DecoratedBox extends SingleChildRenderObjectWidget {
const _DecoratedBox({
this.color,
super.child,
});
final Color? color;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderDecoratedBox(color: color);
}
@override
void updateRenderObject(
BuildContext context,
_RenderDecoratedBox renderObject,
) {
renderObject.color = color;
}
}
class _RenderDecoratedBox extends RenderProxyBox {
_RenderDecoratedBox({
Color? color,
}) : _color = color;
Color? _color;
Color? get color => _color;
set color(Color? value) {
if (_color == value) return;
_color = value;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
if (_color case final color?) {
final size = this.size;
context.canvas.drawRect(
Rect.fromLTWH(
offset.dx,
offset.dy - 2,
size.width,
size.height + 2,
),
Paint()..color = color,
);
}
super.paint(context, offset);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool get isRepaintBoundary => true;
}

View File

@@ -2,7 +2,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart';
class CustomToast extends StatelessWidget {
const CustomToast({super.key, required this.msg});
const CustomToast(this.msg, {super.key});
final String msg;
@@ -10,23 +10,21 @@ class CustomToast extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final colorScheme = ColorScheme.of(context);
return Container(
margin: EdgeInsets.only(
margin: .only(
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
),
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
padding: const .symmetric(horizontal: 17, vertical: 10),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues(
alpha: toastOpacity,
),
borderRadius: const BorderRadius.all(Radius.circular(20)),
color: colorScheme.primaryContainer.withValues(alpha: toastOpacity),
borderRadius: const .all(.circular(20)),
),
child: Text(
msg,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.onPrimaryContainer,
color: colorScheme.onPrimaryContainer,
),
),
);
@@ -34,31 +32,30 @@ class CustomToast extends StatelessWidget {
}
class LoadingWidget extends StatelessWidget {
const LoadingWidget({super.key, required this.msg});
const LoadingWidget(this.msg, {super.key});
///loading msg
final String msg;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final theme = Theme.of(context);
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
padding: const .symmetric(horizontal: 30, vertical: 20),
decoration: BoxDecoration(
color: theme.dialogTheme.backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(15)),
borderRadius: const .all(.circular(15)),
),
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
mainAxisSize: .min,
children: [
//loading animation
CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
),
//msg
Text(msg, style: TextStyle(color: onSurfaceVariant)),
],
@@ -66,3 +63,34 @@ class LoadingWidget extends StatelessWidget {
);
}
}
class NotifyWarning extends StatelessWidget {
const NotifyWarning(this.msg, {super.key});
final String msg;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
return Container(
decoration: BoxDecoration(
borderRadius: const .all(.circular(8)),
color: theme.dialogTheme.backgroundColor,
),
padding: const .symmetric(horizontal: 20, vertical: 10),
child: Column(
spacing: 5,
mainAxisSize: .min,
children: [
Icon(
Icons.warning_amber_rounded,
size: 22,
color: onSurfaceVariant,
),
Text(msg, style: TextStyle(color: onSurfaceVariant)),
],
),
);
}
}

View File

@@ -162,11 +162,9 @@ class _RenderToolTip extends RenderBox
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
_RenderToolTip({
VoidCallback? onTap,
required Offset target,
required bool preferBelow,
}) : _target = target,
_preferBelow = preferBelow,
_hitTestSelf = onTap != null {
required this._target,
required this._preferBelow,
}) : _hitTestSelf = onTap != null {
if (onTap != null) {
_tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap;
}
@@ -252,16 +250,8 @@ class _RenderToolTip extends RenderBox
@override
void paint(PaintingContext context, Offset offset) {
RenderBox? child = firstChild;
while (child != null) {
final childParentData = child.parentData as MultiChildLayoutParentData;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
defaultPaint(context, offset);
}
@override
bool get isRepaintBoundary => true;
}
class Triangle extends LeafRenderObjectWidget {
@@ -295,10 +285,9 @@ class Triangle extends LeafRenderObjectWidget {
class RenderTriangle extends RenderBox {
RenderTriangle({
required Color color,
required Size preferredSize,
}) : _color = color,
_preferredSize = preferredSize;
required this._color,
required this._preferredSize,
});
Color _color;
Color get color => _color;
@@ -328,14 +317,11 @@ class RenderTriangle extends RenderBox {
..style = PaintingStyle.fill;
final path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width / 2, size.height)
..moveTo(offset.dx, offset.dy)
..lineTo(offset.dx + size.width, offset.dy)
..lineTo(offset.dx + size.width / 2, size.height + offset.dy)
..close();
context.canvas.drawPath(path, paint);
}
@override
bool get isRepaintBoundary => true;
}

View File

@@ -3,21 +3,16 @@ import 'package:get/get.dart';
Future<bool> showConfirmDialog({
required BuildContext context,
required String title,
Object? content,
required Widget title,
Widget? content,
// @Deprecated('use `bool result = await showConfirmDialog()` instead')
VoidCallback? onConfirm,
}) async {
assert(content is String? || content is Widget);
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: content is String
? Text(content)
: content is Widget
? content
: null,
title: title,
content: content,
actions: [
TextButton(
onPressed: Get.back,

View File

@@ -0,0 +1,272 @@
import 'dart:async' show FutureOr;
import 'dart:convert' show utf8, jsonDecode;
import 'package:PiliPlus/common/style.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/storage_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show Clipboard;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
import 'package:intl/intl.dart' show DateFormat;
import 'package:re_highlight/languages/json.dart';
import 'package:re_highlight/re_highlight.dart';
import 'package:re_highlight/styles/base16/github.dart';
import 'package:re_highlight/styles/github-dark.dart';
void exportToClipBoard({
required ValueGetter<String> onExport,
}) {
Utils.copyText(onExport());
}
void exportToLocalFile({
required ValueGetter<String> onExport,
required ValueGetter<String> localFileName,
}) {
final res = utf8.encode(onExport());
StorageUtils.saveBytes2File(
name:
'piliplus_${localFileName()}_'
'${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json',
bytes: res,
allowedExtensions: const ['json'],
);
}
Future<void> importFromClipBoard<T>(
BuildContext context, {
required String title,
required ValueGetter<String> onExport,
required FutureOr<void> Function(T json) onImport,
bool showConfirmDialog = true,
}) async {
final data = await Clipboard.getData('text/plain');
if (data?.text case final text? when (text.isNotEmpty)) {
if (!context.mounted) return;
final T json;
final String formatText;
try {
json = jsonDecode(text);
formatText = Utils.jsonEncoder.convert(json);
} catch (e) {
SmartDialog.showToast('解析json失败$e');
return;
}
bool? executeImport;
if (showConfirmDialog) {
final highlight = Highlight()..registerLanguage('json', langJson);
final result = highlight.highlight(
code: formatText,
language: 'json',
);
late TextSpanRenderer renderer;
bool? isDarkMode;
executeImport = await showDialog<bool>(
context: context,
builder: (context) {
final colorScheme = ColorScheme.of(context);
final isDark = colorScheme.isDark;
if (isDark != isDarkMode) {
isDarkMode = isDark;
renderer = TextSpanRenderer(
null,
isDark ? githubDarkTheme : githubTheme,
);
result.render(renderer);
}
return AlertDialog(
title: Text('是否导入如下$title'),
content: SingleChildScrollView(
child: Text.rich(renderer.span!),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text('取消', style: TextStyle(color: colorScheme.outline)),
),
TextButton(
onPressed: () => Get.back(result: true),
child: const Text('确定'),
),
],
);
},
);
} else {
executeImport = true;
}
if (executeImport ?? false) {
try {
await onImport(json);
SmartDialog.showToast('导入成功');
} catch (e) {
SmartDialog.showToast('导入失败:$e');
}
}
} else {
SmartDialog.showToast('剪贴板无数据');
return;
}
}
Future<void> importFromLocalFile<T>({
required FutureOr<void> Function(T json) onImport,
}) async {
final result = await FilePicker.pickFile(
type: .custom,
allowedExtensions: const ['json', 'txt'],
);
if (result != null) {
final data = await result.xFile.readAsString();
final T json;
try {
json = jsonDecode(data);
} catch (e) {
SmartDialog.showToast('解析json失败$e');
return;
}
try {
await onImport(json);
SmartDialog.showToast('导入成功');
} catch (e) {
SmartDialog.showToast('导入失败:$e');
}
}
}
void importFromInput<T>(
BuildContext context, {
required String title,
required FutureOr<void> Function(T json) onImport,
}) {
final key = GlobalKey<FormFieldState<String>>();
late T json;
String? forceErrorText;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('输入$title'),
constraints: Style.dialogFixedConstraints,
content: TextFormField(
key: key,
minLines: 4,
maxLines: 12,
autofocus: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
errorMaxLines: 3,
),
validator: (value) {
if (forceErrorText != null) return forceErrorText;
try {
json = jsonDecode(value!) as T;
return null;
} catch (e) {
return '解析json失败$e';
}
},
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: ColorScheme.of(context).outline,
),
),
),
TextButton(
onPressed: () async {
if (key.currentState?.validate() == true) {
try {
await onImport(json);
Get.back();
SmartDialog.showToast('导入成功');
return;
} catch (e) {
forceErrorText = '导入失败:$e';
}
key.currentState?.validate();
forceErrorText = null;
}
},
child: const Text('确定'),
),
],
),
);
}
Future<void> showImportExportDialog<T>(
BuildContext context, {
required String title,
required ValueGetter<String> onExport,
required FutureOr<void> Function(T json) onImport,
required ValueGetter<String> localFileName,
}) => showDialog(
context: context,
builder: (context) {
const style = TextStyle(fontSize: 15);
return SimpleDialog(
clipBehavior: Clip.hardEdge,
title: Text('导入/导出$title'),
children: [
ListTile(
dense: true,
title: const Text('导出至剪贴板', style: style),
onTap: () {
Get.back();
exportToClipBoard(onExport: onExport);
},
),
ListTile(
dense: true,
title: const Text('导出文件至本地', style: style),
onTap: () {
Get.back();
exportToLocalFile(onExport: onExport, localFileName: localFileName);
},
),
Divider(
height: 1,
color: ColorScheme.of(context).outline.withValues(alpha: 0.1),
),
ListTile(
dense: true,
title: const Text('输入', style: style),
onTap: () {
Get.back();
importFromInput<T>(context, title: title, onImport: onImport);
},
),
ListTile(
dense: true,
title: const Text('从剪贴板导入', style: style),
onTap: () {
Get.back();
importFromClipBoard<T>(
context,
title: title,
onExport: onExport,
onImport: onImport,
);
},
),
ListTile(
dense: true,
title: const Text('从本地文件导入', style: style),
onTap: () {
Get.back();
importFromLocalFile<T>(onImport: onImport);
},
),
],
);
},
);

View File

@@ -3,18 +3,16 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
class DisabledIcon extends SingleChildRenderObjectWidget {
const DisabledIcon({
super.key,
required T child,
required Widget super.child,
this.disable = false,
this.color,
this.iconSize,
double? lineLengthScale,
StrokeCap? strokeCap,
}) : lineLengthScale = lineLengthScale ?? 0.9,
strokeCap = strokeCap ?? StrokeCap.butt,
super(child: child);
this.lineLengthScale = 0.9,
this.strokeCap = .butt,
});
final bool disable;
final Color? color;
@@ -22,20 +20,16 @@ class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
final StrokeCap strokeCap;
final double lineLengthScale;
Icon? get _icon => child is Icon ? child as Icon : null;
@override
RenderObject createRenderObject(BuildContext context) {
late final iconTheme = IconTheme.of(context);
final icon = _icon;
return RenderMaskedIcon(
disable: disable,
iconSize:
iconSize ??
(child is Icon ? (child as Icon).size : null) ??
iconTheme.size ??
24.0,
color:
color ??
(child is Icon ? (child as Icon).color : null) ??
iconTheme.color!,
iconSize: iconSize ?? icon?.size ?? iconTheme.size ?? 24.0,
color: color ?? icon?.color ?? iconTheme.color!,
strokeCap: strokeCap,
lineLengthScale: lineLengthScale,
);
@@ -44,17 +38,11 @@ class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
@override
void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) {
late final iconTheme = IconTheme.of(context);
final icon = _icon;
renderObject
..disable = disable
..iconSize =
iconSize ??
(child is Icon ? (child as Icon?)?.size : null) ??
iconTheme.size ??
24.0
..color =
color ??
(child is Icon ? (child as Icon?)?.color : null) ??
iconTheme.color!
..iconSize = iconSize ?? icon?.size ?? iconTheme.size ?? 24.0
..color = color ?? icon?.color ?? iconTheme.color!
..strokeCap = strokeCap
..lineLengthScale = lineLengthScale;
}
@@ -62,16 +50,12 @@ class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
class RenderMaskedIcon extends RenderProxyBox {
RenderMaskedIcon({
required bool disable,
required double iconSize,
required Color color,
required StrokeCap strokeCap,
required double lineLengthScale,
}) : _disable = disable,
_iconSize = iconSize,
_color = color,
_strokeCap = strokeCap,
_lineLengthScale = lineLengthScale;
required this._disable,
required this._iconSize,
required this._color,
required this._strokeCap,
required this._lineLengthScale,
});
bool _disable;
bool get disable => _disable;
@@ -121,6 +105,7 @@ class RenderMaskedIcon extends RenderProxyBox {
final canvas = context.canvas;
var rectOffset = offset;
Size size = this.size;
final exceedWidth = size.width > _iconSize;
final exceedHeight = size.height > _iconSize;
@@ -128,14 +113,14 @@ class RenderMaskedIcon extends RenderProxyBox {
final dx = exceedWidth ? (size.width - _iconSize) / 2.0 : 0.0;
final dy = exceedHeight ? (size.height - _iconSize) / 2.0 : 0.0;
size = Size.square(_iconSize);
offset = Offset(dx, dy);
rectOffset += Offset(dx, dy);
} else if (size.width < _iconSize && size.height < _iconSize) {
size = Size.square(_iconSize);
}
final strokeWidth = size.width / 12;
var rect = offset & size;
var rect = rectOffset & size;
final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4
@@ -155,7 +140,7 @@ class RenderMaskedIcon extends RenderProxyBox {
canvas
..save()
..clipPath(path, doAntiAlias: false);
super.paint(context, .zero);
super.paint(context, offset);
canvas.restore();
@@ -174,7 +159,4 @@ class RenderMaskedIcon extends RenderProxyBox {
linePaint,
);
}
@override
bool get isRepaintBoundary => true;
}

View File

@@ -0,0 +1,91 @@
part of 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart';
class DynDraggableScrollableSheet extends DraggableScrollableSheet {
const DynDraggableScrollableSheet({
super.key,
super.initialChildSize,
super.minChildSize,
super.maxChildSize,
super.expand,
super.snap,
super.snapSizes,
super.snapAnimationDuration,
super.controller,
super.shouldCloseOnMinExtent,
required super.builder,
});
@override
State<DraggableScrollableSheet> createState() =>
_DynDraggableScrollableSheetState();
}
class _DynDraggableScrollableSheetState extends _DraggableScrollableSheetState {
@override
void initState() {
super.initState();
_extent = _DraggableSheetExtent(
minSize: widget.minChildSize,
maxSize: widget.maxChildSize,
snap: widget.snap,
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
);
_scrollController = _DynDraggableScrollableSheetScrollController(
extent: _extent,
);
widget.controller?._attach(_scrollController);
}
}
class _DynDraggableScrollableSheetScrollController
extends _DraggableScrollableSheetScrollController {
_DynDraggableScrollableSheetScrollController({
required super.extent,
});
@override
_DraggableScrollableSheetScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return _DynDraggableScrollableSheetScrollPosition(
physics: physics.applyTo(const AlwaysScrollableScrollPhysics()),
context: context,
oldPosition: oldPosition,
getExtent: () => extent,
);
}
}
class _DynDraggableScrollableSheetScrollPosition
extends _DraggableScrollableSheetScrollPosition {
_DynDraggableScrollableSheetScrollPosition({
required super.physics,
required super.context,
super.oldPosition,
required super.getExtent,
});
bool _isAtTop = true;
@override
bool get listShouldScroll => !_isAtTop || super.listShouldScroll;
@override
void applyUserOffset(double delta) {
if (_isAtTop && pixels > 0) {
_isAtTop = false;
}
super.applyUserOffset(delta);
}
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
_isAtTop = pixels == 0;
return super.drag(details, dragCancelCallback);
}
}

View File

@@ -0,0 +1,74 @@
part of 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart';
class TopicDraggableScrollableSheet extends DraggableScrollableSheet {
const TopicDraggableScrollableSheet({
super.key,
super.initialChildSize,
super.minChildSize,
super.maxChildSize,
super.expand,
super.snap,
super.snapSizes,
super.snapAnimationDuration,
super.controller,
super.shouldCloseOnMinExtent,
required super.builder,
this.initialScrollOffset = 0.0,
});
final double initialScrollOffset;
@override
State<DraggableScrollableSheet> createState() =>
_TopicDraggableScrollableSheetState();
}
class _TopicDraggableScrollableSheetState
extends _DraggableScrollableSheetState {
@override
void initState() {
super.initState();
_extent = _DraggableSheetExtent(
minSize: widget.minChildSize,
maxSize: widget.maxChildSize,
snap: widget.snap,
snapSizes: _impliedSnapSizes(),
snapAnimationDuration: widget.snapAnimationDuration,
initialSize: widget.initialChildSize,
shouldCloseOnMinExtent: widget.shouldCloseOnMinExtent,
);
_scrollController = _TopicDraggableScrollableSheetScrollController(
extent: _extent,
initialScrollOffset:
(widget as TopicDraggableScrollableSheet).initialScrollOffset,
);
widget.controller?._attach(_scrollController);
}
}
class _TopicDraggableScrollableSheetScrollController
extends _DraggableScrollableSheetScrollController {
_TopicDraggableScrollableSheetScrollController({
required super.extent,
this._initialScrollOffset = 0.0,
});
@override
double get initialScrollOffset => _initialScrollOffset;
final double _initialScrollOffset;
@override
_DraggableScrollableSheetScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return _DraggableScrollableSheetScrollPosition(
physics: physics.applyTo(const AlwaysScrollableScrollPhysics()),
context: context,
oldPosition: oldPosition,
getExtent: () => extent,
initialPixels: _initialScrollOffset,
);
}
}

View File

@@ -0,0 +1,492 @@
/*
* This file is part of PiliPlus
*
* PiliPlus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PiliPlus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/custom_height_widget.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
show LayoutCallback;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
import 'package:flutter/rendering.dart' show RenderOpacity, OpacityLayer;
import 'package:flutter/services.dart';
/// ref [SliverAppBar]
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
required this.leading,
required this.automaticallyImplyLeading,
required this.title,
required this.actions,
required this.automaticallyImplyActions,
required this.flexibleSpace,
required this.bottom,
required this.elevation,
required this.scrolledUnderElevation,
required this.shadowColor,
required this.surfaceTintColor,
required this.forceElevated,
required this.backgroundColor,
required this.foregroundColor,
required this.iconTheme,
required this.actionsIconTheme,
required this.primary,
required this.centerTitle,
required this.excludeHeaderSemantics,
required this.titleSpacing,
required this.collapsedHeight,
required this.topPadding,
required this.shape,
required this.toolbarHeight,
required this.leadingWidth,
required this.toolbarTextStyle,
required this.titleTextStyle,
required this.systemOverlayStyle,
required this.forceMaterialTransparency,
required this.useDefaultSemanticsOrder,
required this.clipBehavior,
required this.actionsPadding,
}) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget title;
final List<Widget>? actions;
final bool automaticallyImplyActions;
final Widget flexibleSpace;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final double collapsedHeight;
final double topPadding;
final ShapeBorder? shape;
final double? toolbarHeight;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final double _bottomHeight;
final bool forceMaterialTransparency;
final bool useDefaultSemanticsOrder;
final Clip? clipBehavior;
final EdgeInsetsGeometry? actionsPadding;
@override
double get minExtent => collapsedHeight;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
) {
maxExtent ??= double.infinity;
final bool isScrolledUnder =
overlapsContent ||
forceElevated ||
(shrinkOffset > maxExtent - minExtent);
final effectiveTitle = AnimatedOpacity(
opacity: isScrolledUnder ? 1 : 0,
duration: const Duration(milliseconds: 500),
curve: const Cubic(0.2, 0.0, 0.0, 1.0),
child: title,
);
return FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
isScrolledUnder: isScrolledUnder,
hasLeading: leading != null || automaticallyImplyLeading,
child: AppBar(
clipBehavior: clipBehavior,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
title: effectiveTitle,
actions: actions,
automaticallyImplyActions: automaticallyImplyActions,
flexibleSpace: IgnorePointer(
ignoring: isScrolledUnder,
child: DynamicFlexibleSpaceBar(background: flexibleSpace),
),
bottom: bottom,
elevation: isScrolledUnder ? elevation : 0.0,
scrolledUnderElevation: scrolledUnderElevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
primary: primary,
centerTitle: centerTitle,
excludeHeaderSemantics: excludeHeaderSemantics,
titleSpacing: titleSpacing,
shape: shape,
toolbarHeight: toolbarHeight,
leadingWidth: leadingWidth,
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
forceMaterialTransparency: forceMaterialTransparency,
useDefaultSemanticsOrder: useDefaultSemanticsOrder,
actionsPadding: actionsPadding,
),
);
}
@override
bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
return leading != oldDelegate.leading ||
automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading ||
title != oldDelegate.title ||
actions != oldDelegate.actions ||
automaticallyImplyActions != oldDelegate.automaticallyImplyActions ||
flexibleSpace != oldDelegate.flexibleSpace ||
bottom != oldDelegate.bottom ||
_bottomHeight != oldDelegate._bottomHeight ||
elevation != oldDelegate.elevation ||
shadowColor != oldDelegate.shadowColor ||
backgroundColor != oldDelegate.backgroundColor ||
foregroundColor != oldDelegate.foregroundColor ||
iconTheme != oldDelegate.iconTheme ||
actionsIconTheme != oldDelegate.actionsIconTheme ||
primary != oldDelegate.primary ||
centerTitle != oldDelegate.centerTitle ||
titleSpacing != oldDelegate.titleSpacing ||
topPadding != oldDelegate.topPadding ||
forceElevated != oldDelegate.forceElevated ||
toolbarHeight != oldDelegate.toolbarHeight ||
leadingWidth != oldDelegate.leadingWidth ||
toolbarTextStyle != oldDelegate.toolbarTextStyle ||
titleTextStyle != oldDelegate.titleTextStyle ||
systemOverlayStyle != oldDelegate.systemOverlayStyle ||
forceMaterialTransparency != oldDelegate.forceMaterialTransparency ||
useDefaultSemanticsOrder != oldDelegate.useDefaultSemanticsOrder ||
actionsPadding != oldDelegate.actionsPadding;
}
@override
String toString() {
return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
}
}
class DynamicSliverAppBar extends StatelessWidget {
const DynamicSliverAppBar.medium({
super.key,
this.leading,
this.automaticallyImplyLeading = true,
required this.title,
this.actions,
this.automaticallyImplyActions = true,
required this.flexibleSpace,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.shape,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.useDefaultSemanticsOrder = true,
this.clipBehavior,
this.actionsPadding,
this.onPerformLayout,
});
final LayoutCallback? onPerformLayout;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget title;
final List<Widget>? actions;
final bool automaticallyImplyActions;
final Widget flexibleSpace;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final ShapeBorder? shape;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final bool forceMaterialTransparency;
final bool useDefaultSemanticsOrder;
final Clip? clipBehavior;
final EdgeInsetsGeometry? actionsPadding;
@override
Widget build(BuildContext context) {
final double bottomHeight = bottom?.preferredSize.height ?? 0.0;
final double topPadding = primary
? MediaQuery.viewPaddingOf(context).top
: 0.0;
final double effectiveCollapsedHeight =
topPadding + kToolbarHeight + bottomHeight + 1;
return SliverPinnedHeader(
onPerformLayout: onPerformLayout,
delegate: _SliverAppBarDelegate(
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
title: title,
actions: actions,
automaticallyImplyActions: automaticallyImplyActions,
flexibleSpace: flexibleSpace,
bottom: bottom,
elevation: elevation,
scrolledUnderElevation: scrolledUnderElevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
forceElevated: forceElevated,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
primary: primary,
centerTitle: centerTitle,
excludeHeaderSemantics: excludeHeaderSemantics,
titleSpacing: titleSpacing,
collapsedHeight: effectiveCollapsedHeight,
topPadding: topPadding,
shape: shape,
toolbarHeight: kToolbarHeight,
leadingWidth: leadingWidth,
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
forceMaterialTransparency: forceMaterialTransparency,
useDefaultSemanticsOrder: useDefaultSemanticsOrder,
clipBehavior: clipBehavior,
actionsPadding: actionsPadding,
),
);
}
}
/// ref [FlexibleSpaceBar]
class DynamicFlexibleSpaceBar extends StatelessWidget {
const DynamicFlexibleSpaceBar({
super.key,
required this.background,
this.collapseMode = CollapseMode.parallax,
});
final Widget background;
final CollapseMode collapseMode;
static double _getCollapsePadding(
CollapseMode collapseMode,
double t,
FlexibleSpaceBarSettings settings,
) {
switch (collapseMode) {
case CollapseMode.pin:
return -(settings.maxExtent - settings.currentExtent);
case CollapseMode.none:
return 0.0;
case CollapseMode.parallax:
final double deltaExtent = settings.maxExtent - settings.minExtent;
return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
}
}
@override
Widget build(BuildContext context) {
final FlexibleSpaceBarSettings settings = context
.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
double? height;
final double opacity;
final double topPadding;
if (settings.maxExtent == .infinity) {
opacity = 1.0;
topPadding = 0.0;
} else {
height = settings.maxExtent;
final double deltaExtent = settings.maxExtent - settings.minExtent;
// 0.0 -> Expanded
// 1.0 -> Collapsed to toolbar
final double t = clampDouble(
1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent,
0.0,
1.0,
);
final double fadeStart = math.max(
0.0,
1.0 - kToolbarHeight / deltaExtent,
);
const fadeEnd = 1.0;
assert(fadeStart <= fadeEnd);
// If the min and max extent are the same, the app bar cannot collapse
// and the content should be visible, so opacity = 1.
opacity = settings.maxExtent == settings.minExtent
? 1.0
: 1.0 - Interval(fadeStart, fadeEnd).transform(t);
topPadding = _getCollapsePadding(collapseMode, t, settings);
}
return ClipRect(
child: CustomHeightWidget(
height: height,
offset: Offset(0.0, topPadding),
child: _FlexibleSpaceHeaderOpacity(
// IOS is relying on this semantics node to correctly traverse
// through the app bar when it is collapsed.
alwaysIncludeSemantics: true,
opacity: opacity,
child: background,
),
),
);
}
}
/// [_FlexibleSpaceHeaderOpacity]
class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget {
const _FlexibleSpaceHeaderOpacity({
required this.opacity,
required super.child,
required this.alwaysIncludeSemantics,
});
final double opacity;
final bool alwaysIncludeSemantics;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderFlexibleSpaceHeaderOpacity(
opacity: opacity,
alwaysIncludeSemantics: alwaysIncludeSemantics,
);
}
@override
void updateRenderObject(
BuildContext context,
covariant _RenderFlexibleSpaceHeaderOpacity renderObject,
) {
renderObject
..alwaysIncludeSemantics = alwaysIncludeSemantics
..opacity = opacity;
}
}
class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity {
_RenderFlexibleSpaceHeaderOpacity({
super.opacity,
super.alwaysIncludeSemantics,
});
@override
bool get isRepaintBoundary => false;
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) {
return;
}
if ((opacity * 255).roundToDouble() <= 0) {
layer = null;
return;
}
assert(needsCompositing);
layer = context.pushOpacity(
offset,
(opacity * 255).round(),
super.paint,
oldLayer: layer as OpacityLayer?,
);
assert(() {
layer!.debugCreator = debugCreator;
return true;
}());
}
}

View File

@@ -0,0 +1,285 @@
/*
* This file is part of PiliPlus
*
* PiliPlus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PiliPlus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart' hide LayoutCallback;
import 'package:flutter/widgets.dart'
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
/// ref [SliverPersistentHeader]
Rect? _trim(
Rect? original, {
double top = -double.infinity,
double right = double.infinity,
double bottom = double.infinity,
double left = -double.infinity,
}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
abstract class RenderSliverPersistentHeader extends RenderSliver
with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
RenderSliverPersistentHeader({RenderBox? child}) {
this.child = child;
}
SliverPersistentHeaderElement? element;
double get minExtent =>
(element!.widget as SliverPinnedHeader).delegate.minExtent;
bool _needsUpdateChild = true;
double get lastShrinkOffset => _lastShrinkOffset;
double _lastShrinkOffset = 0.0;
bool get lastOverlapsContent => _lastOverlapsContent;
bool _lastOverlapsContent = false;
@protected
void updateChild(
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
) {
assert(element != null);
element!.build(shrinkOffset, overlapsContent, maxExtent);
}
@override
void markNeedsLayout() {
_needsUpdateChild = true;
super.markNeedsLayout();
}
@protected
void updateChildIfNeeded(
double scrollOffset,
double? maxExtent, {
bool overlapsContent = false,
}) {
final double shrinkOffset = maxExtent == null
? scrollOffset
: math.min(scrollOffset, maxExtent);
if (_needsUpdateChild ||
_lastShrinkOffset != shrinkOffset ||
_lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
updateChild(shrinkOffset, overlapsContent, maxExtent);
});
_lastShrinkOffset = shrinkOffset;
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
}
}
@override
double childMainAxisPosition(covariant RenderObject child) =>
super.childMainAxisPosition(child);
@override
bool hitTestChildren(
SliverHitTestResult result, {
required double mainAxisPosition,
required double crossAxisPosition,
}) {
assert(geometry!.hitTestExtent > 0.0);
if (child != null) {
return hitTestBoxChild(
BoxHitTestResult.wrap(result),
child!,
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
);
}
return false;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child == this.child);
applyPaintTransformForBoxChild(child as RenderBox, transform);
}
void triggerRebuild() {
markNeedsLayout();
}
}
class SliverPinnedHeader extends RenderObjectWidget {
const SliverPinnedHeader({
super.key,
required this.delegate,
this.onPerformLayout,
});
final SliverPersistentHeaderDelegate delegate;
final LayoutCallback? onPerformLayout;
@override
SliverPersistentHeaderElement createElement() =>
SliverPersistentHeaderElement(this);
@override
RenderSliverPinnedHeader createRenderObject(BuildContext context) {
return RenderSliverPinnedHeader(onPerformLayout: onPerformLayout);
}
@override
void updateRenderObject(
BuildContext context,
RenderSliverPinnedHeader renderObject,
) {
renderObject.onPerformLayout = onPerformLayout;
}
}
class RenderSliverPinnedHeader extends RenderSliverPersistentHeader {
RenderSliverPinnedHeader({
super.child,
this.onPerformLayout,
});
LayoutCallback? onPerformLayout;
({double crossAxisExtent, double maxExtent})? _maxExtent;
double? get maxExtent => _maxExtent?.maxExtent;
void _rawLayout() {
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
_maxExtent = (
crossAxisExtent: constraints.crossAxisExtent,
maxExtent: child!.size.height,
);
onPerformLayout?.call(child!.size);
}
void _layout() {
final double shrinkOffset = math.min(
constraints.scrollOffset,
_maxExtent!.maxExtent,
);
child!.layout(
constraints.asBoxConstraints(
maxExtent: math.max(minExtent, _maxExtent!.maxExtent - shrinkOffset),
),
parentUsesSize: true,
);
}
@override
void performLayout() {
final constraints = this.constraints;
final bool overlapsContent = constraints.overlap > 0.0;
if (_maxExtent == null) {
updateChildIfNeeded(
constraints.scrollOffset,
_maxExtent?.maxExtent,
overlapsContent: overlapsContent,
);
_rawLayout();
} else {
if (_maxExtent!.crossAxisExtent == constraints.crossAxisExtent) {
updateChildIfNeeded(
constraints.scrollOffset,
_maxExtent?.maxExtent,
overlapsContent: overlapsContent,
);
_layout();
} else {
_needsUpdateChild = true;
updateChildIfNeeded(
constraints.scrollOffset,
null,
overlapsContent: overlapsContent,
);
_rawLayout();
if (constraints.scrollOffset > 0.0) {
_needsUpdateChild = true;
updateChildIfNeeded(
constraints.scrollOffset,
_maxExtent?.maxExtent,
overlapsContent: overlapsContent,
);
_layout();
}
}
}
final childExtent = child!.size.height;
final maxExtent = _maxExtent!.maxExtent;
final double effectiveRemainingPaintExtent = math.max(
0,
constraints.remainingPaintExtent - constraints.overlap,
);
final double layoutExtent = clampDouble(
maxExtent - constraints.scrollOffset,
0.0,
effectiveRemainingPaintExtent,
);
geometry = SliverGeometry(
scrollExtent: maxExtent,
paintOrigin: constraints.overlap,
paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
layoutExtent: layoutExtent,
maxPaintExtent: maxExtent,
maxScrollObstructionExtent: minExtent,
cacheExtent: layoutExtent > 0.0
? -constraints.cacheOrigin + layoutExtent
: layoutExtent,
hasVisualOverflow: false,
);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
context.paintChild(child!, offset);
}
}
@override
double childMainAxisPosition(RenderBox child) => 0.0;
@override
void showOnScreen({
RenderObject? descendant,
Rect? rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
final Rect? localBounds = descendant != null
? MatrixUtils.transformRect(
descendant.getTransformTo(this),
rect ?? descendant.paintBounds,
)
: rect;
final Rect? newRect = _trim(localBounds, top: 0);
super.showOnScreen(
descendant: this,
rect: newRect,
duration: duration,
curve: curve,
);
}
}

View File

@@ -0,0 +1,148 @@
/*
* This file is part of PiliPlus
*
* PiliPlus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PiliPlus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
*/
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart';
import 'package:flutter/widgets.dart';
/// ref [SliverPersistentHeader]
abstract class SliverPersistentHeaderDelegate {
const SliverPersistentHeaderDelegate();
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
);
double get minExtent;
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
}
class SliverPersistentHeaderElement extends RenderObjectElement {
SliverPersistentHeaderElement(
SliverPinnedHeader super.widget,
);
@override
RenderSliverPinnedHeader get renderObject =>
super.renderObject as RenderSliverPinnedHeader;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
renderObject.element = this;
}
@override
void unmount() {
renderObject.element = null;
super.unmount();
}
@override
void update(SliverPinnedHeader newWidget) {
final oldWidget = widget as SliverPinnedHeader;
super.update(newWidget);
final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate;
final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate;
if (newDelegate != oldDelegate &&
(newDelegate.runtimeType != oldDelegate.runtimeType ||
newDelegate.shouldRebuild(oldDelegate))) {
final RenderSliverPinnedHeader renderObject = this.renderObject;
_updateChild(
newDelegate,
renderObject.lastShrinkOffset,
renderObject.lastOverlapsContent,
renderObject.maxExtent,
);
renderObject.triggerRebuild();
}
}
@override
void performRebuild() {
super.performRebuild();
renderObject.triggerRebuild();
}
Element? child;
void _updateChild(
SliverPersistentHeaderDelegate delegate,
double shrinkOffset,
bool overlapsContent,
double? maxExtent,
) {
final Widget newWidget = delegate.build(
this,
shrinkOffset,
overlapsContent,
maxExtent,
);
child = updateChild(child, newWidget, null);
}
void build(double shrinkOffset, bool overlapsContent, double? maxExtent) {
owner!.buildScope(this, () {
final sliverPersistentHeaderRenderObjectWidget =
widget as SliverPinnedHeader;
_updateChild(
sliverPersistentHeaderRenderObjectWidget.delegate,
shrinkOffset,
overlapsContent,
maxExtent,
);
});
}
@override
void forgetChild(Element child) {
assert(child == this.child);
this.child = null;
super.forgetChild(child);
}
@override
void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
assert(renderObject.debugValidateChild(child));
renderObject.child = child;
}
@override
void moveRenderObjectChild(
covariant RenderObject child,
Object? oldSlot,
Object? newSlot,
) {
assert(false);
}
@override
void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
renderObject.child = null;
}
@override
void visitChildren(ElementVisitor visitor) {
if (child != null) {
visitor(child!);
}
}
}

View File

@@ -1,171 +0,0 @@
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DynamicSliverAppBarMedium extends StatefulWidget {
const DynamicSliverAppBarMedium({
this.flexibleSpace,
super.key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.backgroundGradient,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.collapsedHeight,
this.expandedHeight,
this.floating = false,
this.pinned = false,
this.snap = false,
this.stretch = false,
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
this.toolbarHeight = kToolbarHeight,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
this.appBarClipper,
this.onPerformLayout,
});
final ValueChanged<double>? onPerformLayout;
final Widget? flexibleSpace;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget? title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
/// If backgroundGradient is non null, backgroundColor will be ignored
final LinearGradient? backgroundGradient;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final double? expandedHeight;
final double? collapsedHeight;
final bool floating;
final bool pinned;
final ShapeBorder? shape;
final double toolbarHeight;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final bool forceMaterialTransparency;
final Clip? clipBehavior;
final bool snap;
final bool stretch;
final double stretchTriggerOffset;
final AsyncCallback? onStretchTrigger;
final CustomClipper<Path>? appBarClipper;
@override
State<DynamicSliverAppBarMedium> createState() =>
_DynamicSliverAppBarMediumState();
}
class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
double? _height;
double? _width;
late double _topPadding;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_topPadding = MediaQuery.viewPaddingOf(context).top;
final width = MediaQuery.widthOf(context);
if (_width != width) {
_width = width;
_height = null;
}
}
@override
Widget build(BuildContext context) {
if (_height == null) {
return SliverToBoxAdapter(
child: OnlyLayoutWidget(
onPerformLayout: (Size size) {
if (!mounted) return;
_height = size.height;
widget.onPerformLayout?.call(_height!);
setState(() {});
},
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: SizedBox(
width: _width,
child: widget.flexibleSpace,
),
),
),
);
}
return SliverAppBar.medium(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
actions: widget.actions,
bottom: widget.bottom,
elevation: widget.elevation,
scrolledUnderElevation: widget.scrolledUnderElevation,
shadowColor: widget.shadowColor,
surfaceTintColor: widget.surfaceTintColor,
forceElevated: widget.forceElevated,
backgroundColor: widget.backgroundColor,
foregroundColor: widget.foregroundColor,
iconTheme: widget.iconTheme,
actionsIconTheme: widget.actionsIconTheme,
primary: widget.primary,
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
stretch: widget.stretch,
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: kToolbarHeight,
collapsedHeight: kToolbarHeight + _topPadding + 1,
expandedHeight: _height! - _topPadding,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior,
flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
import 'package:flutter/widgets.dart';
class ExtraHitTestWidget extends SingleChildRenderObjectWidget {
const ExtraHitTestWidget({
super.key,
required this.width,
required Widget super.child,
});
final double width;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderExtraHitTestWidget(width: width);
}
}
class RenderExtraHitTestWidget extends RenderProxyBox {
RenderExtraHitTestWidget({
required this._width,
});
final double _width;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return super.hitTestChildren(result, position: position) ||
position.dx <= _width;
}
}

View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
show RenderStack, BoxHitTestResult, BoxHitTestEntry;
class ExtraHitTestStack extends Stack {
const ExtraHitTestStack({
super.key,
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
super.children,
});
@override
RenderExtraHitTestStack createRenderObject(BuildContext context) {
return RenderExtraHitTestStack(
alignment: alignment,
textDirection: textDirection ?? Directionality.maybeOf(context),
fit: fit,
clipBehavior: clipBehavior,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderExtraHitTestStack renderObject,
) {
renderObject
..alignment = alignment
..textDirection = textDirection ?? Directionality.maybeOf(context)
..fit = fit
..clipBehavior = clipBehavior;
}
}
class RenderExtraHitTestStack extends RenderStack {
RenderExtraHitTestStack({
super.children,
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
});
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
assert(() {
if (!hasSize) {
if (debugNeedsLayout) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Cannot hit test a render box that has never been laid out.',
),
describeForError(
'The hitTest() method was called on this RenderBox',
),
ErrorDescription(
"Unfortunately, this object's geometry is not known at this time, "
'probably because it has never been laid out. '
'This means it cannot be accurately hit-tested.',
),
ErrorHint(
'If you are trying '
'to perform a hit test during the layout phase itself, make sure '
"you only hit test nodes that have completed layout (e.g. the node's "
'children, after their layout() method has been called).',
),
]);
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot hit test a render box with no size.'),
describeForError('The hitTest() method was called on this RenderBox'),
ErrorDescription(
'Although this node is not marked as needing layout, '
'its size is not set.',
),
ErrorHint(
'A RenderBox object must have an '
'explicit size before it can be hit-tested. Make sure '
'that the RenderBox in question sets its size during layout.',
),
]);
}
return true;
}());
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
}

View File

@@ -0,0 +1,777 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:flutter/material.dart';
const double _kMaxLabelTextScaleFactor = 1.3;
const _kNavigationHeight = 64.0;
const _kIndicatorHeight = _kNavigationHeight - 2 * _kIndicatorPaddingInt;
const _kIndicatorWidth = 86.0;
const _kIndicatorPaddingInt = 4.0;
const _kIndicatorPadding = EdgeInsets.all(_kIndicatorPaddingInt);
const _kBorderRadius = BorderRadius.all(.circular(_kNavigationHeight / 2));
const _kNavigationShape = RoundedSuperellipseBorder(
borderRadius: _kBorderRadius,
);
/// ref [NavigationBar]
class FloatingNavigationBar extends StatelessWidget {
// ignore: prefer_const_constructors_in_immutables
FloatingNavigationBar({
super.key,
this.animationDuration = const Duration(milliseconds: 500),
this.selectedIndex = 0,
required this.destinations,
this.onDestinationSelected,
this.backgroundColor,
this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.indicatorColor,
this.indicatorShape,
this.labelBehavior,
this.overlayColor,
this.labelTextStyle,
this.labelPadding,
this.bottomPadding = 8.0,
}) : assert(destinations.length >= 2),
assert(0 <= selectedIndex && selectedIndex < destinations.length);
final Duration animationDuration;
final int selectedIndex;
final List<Widget> destinations;
final ValueChanged<int>? onDestinationSelected;
final Color? backgroundColor;
final double? elevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final Color? indicatorColor;
final ShapeBorder? indicatorShape;
final NavigationDestinationLabelBehavior? labelBehavior;
final WidgetStateProperty<Color?>? overlayColor;
final WidgetStateProperty<TextStyle?>? labelTextStyle;
final EdgeInsetsGeometry? labelPadding;
final double bottomPadding;
VoidCallback _handleTap(int index) {
return onDestinationSelected != null
? () => onDestinationSelected!(index)
: () {};
}
@override
Widget build(BuildContext context) {
final defaults = _NavigationBarDefaultsM3(context);
final navigationBarTheme = NavigationBarTheme.of(context);
final effectiveLabelBehavior =
labelBehavior ??
navigationBarTheme.labelBehavior ??
defaults.labelBehavior!;
final padding = MediaQuery.viewPaddingOf(context);
return UnconstrainedBox(
child: Padding(
padding: .fromLTRB(
padding.left,
0,
padding.right,
bottomPadding + padding.bottom,
),
child: SizedBox(
height: _kNavigationHeight,
width: destinations.length * _kIndicatorWidth,
child: DecoratedBox(
decoration: ShapeDecoration(
color: ElevationOverlay.applySurfaceTint(
backgroundColor ??
navigationBarTheme.backgroundColor ??
defaults.backgroundColor!,
surfaceTintColor ??
navigationBarTheme.surfaceTintColor ??
defaults.surfaceTintColor,
elevation ??
navigationBarTheme.elevation ??
defaults.elevation!,
),
shape: RoundedSuperellipseBorder(
side: defaults.borderSide,
borderRadius: _kBorderRadius,
),
),
child: Padding(
padding: _kIndicatorPadding,
child: Row(
crossAxisAlignment: .stretch,
children: <Widget>[
for (int i = 0; i < destinations.length; i++)
Expanded(
child: _SelectableAnimatedBuilder(
duration: animationDuration,
isSelected: i == selectedIndex,
builder: (context, animation) {
return _NavigationDestinationInfo(
index: i,
selectedIndex: selectedIndex,
totalNumberOfDestinations: destinations.length,
selectedAnimation: animation,
labelBehavior: effectiveLabelBehavior,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
overlayColor: overlayColor,
onTap: _handleTap(i),
labelTextStyle: labelTextStyle,
labelPadding: labelPadding,
child: destinations[i],
);
},
),
),
],
),
),
),
),
),
);
}
}
class FloatingNavigationDestination extends StatelessWidget {
const FloatingNavigationDestination({
super.key,
required this.icon,
this.selectedIcon,
required this.label,
this.tooltip,
this.enabled = true,
});
final Widget icon;
final Widget? selectedIcon;
final String label;
final String? tooltip;
final bool enabled;
@override
Widget build(BuildContext context) {
final info = _NavigationDestinationInfo.of(context);
const selectedState = <WidgetState>{WidgetState.selected};
const unselectedState = <WidgetState>{};
const disabledState = <WidgetState>{WidgetState.disabled};
final navigationBarTheme = NavigationBarTheme.of(context);
final defaults = _NavigationBarDefaultsM3(context);
final animation = info.selectedAnimation;
return Stack(
alignment: .center,
clipBehavior: .none,
children: [
NavigationIndicator(
animation: animation,
color:
info.indicatorColor ??
navigationBarTheme.indicatorColor ??
defaults.indicatorColor!,
),
_NavigationDestinationBuilder(
label: label,
tooltip: tooltip,
enabled: enabled,
buildIcon: (context) {
final IconThemeData selectedIconTheme =
navigationBarTheme.iconTheme?.resolve(selectedState) ??
defaults.iconTheme!.resolve(selectedState)!;
final IconThemeData unselectedIconTheme =
navigationBarTheme.iconTheme?.resolve(unselectedState) ??
defaults.iconTheme!.resolve(unselectedState)!;
final IconThemeData disabledIconTheme =
navigationBarTheme.iconTheme?.resolve(disabledState) ??
defaults.iconTheme!.resolve(disabledState)!;
final Widget selectedIconWidget = IconTheme.merge(
data: enabled ? selectedIconTheme : disabledIconTheme,
child: selectedIcon ?? icon,
);
final Widget unselectedIconWidget = IconTheme.merge(
data: enabled ? unselectedIconTheme : disabledIconTheme,
child: icon,
);
return _StatusTransitionWidgetBuilder(
animation: animation,
builder: (context, child) {
return animation.isForwardOrCompleted
? selectedIconWidget
: unselectedIconWidget;
},
);
},
buildLabel: (context) {
final TextStyle? effectiveSelectedLabelTextStyle =
info.labelTextStyle?.resolve(selectedState) ??
navigationBarTheme.labelTextStyle?.resolve(selectedState) ??
defaults.labelTextStyle!.resolve(selectedState);
final TextStyle? effectiveUnselectedLabelTextStyle =
info.labelTextStyle?.resolve(unselectedState) ??
navigationBarTheme.labelTextStyle?.resolve(unselectedState) ??
defaults.labelTextStyle!.resolve(unselectedState);
final TextStyle? effectiveDisabledLabelTextStyle =
info.labelTextStyle?.resolve(disabledState) ??
navigationBarTheme.labelTextStyle?.resolve(disabledState) ??
defaults.labelTextStyle!.resolve(disabledState);
final EdgeInsetsGeometry labelPadding =
info.labelPadding ??
navigationBarTheme.labelPadding ??
defaults.labelPadding!;
final textStyle = enabled
? animation.isForwardOrCompleted
? effectiveSelectedLabelTextStyle
: effectiveUnselectedLabelTextStyle
: effectiveDisabledLabelTextStyle;
return Padding(
padding: labelPadding,
child: MediaQuery.withClampedTextScaling(
maxScaleFactor: _kMaxLabelTextScaleFactor,
child: Text(label, style: textStyle),
),
);
},
),
],
);
}
}
class _NavigationDestinationBuilder extends StatefulWidget {
const _NavigationDestinationBuilder({
required this.buildIcon,
required this.buildLabel,
required this.label,
this.tooltip,
this.enabled = true,
});
final WidgetBuilder buildIcon;
final WidgetBuilder buildLabel;
final String label;
final String? tooltip;
final bool enabled;
@override
State<_NavigationDestinationBuilder> createState() =>
_NavigationDestinationBuilderState();
}
class _NavigationDestinationBuilderState
extends State<_NavigationDestinationBuilder> {
final GlobalKey iconKey = GlobalKey();
@override
Widget build(BuildContext context) {
final info = _NavigationDestinationInfo.of(context);
final child = GestureDetector(
behavior: .opaque,
onTap: widget.enabled ? info.onTap : null,
child: _NavigationBarDestinationLayout(
icon: widget.buildIcon(context),
iconKey: iconKey,
label: widget.buildLabel(context),
),
);
if (info.labelBehavior == .alwaysShow) {
return child;
}
return _NavigationBarDestinationTooltip(
message: widget.tooltip ?? widget.label,
child: child,
);
}
}
class _NavigationDestinationInfo extends InheritedWidget {
const _NavigationDestinationInfo({
required this.index,
required this.selectedIndex,
required this.totalNumberOfDestinations,
required this.selectedAnimation,
required this.labelBehavior,
required this.indicatorColor,
required this.indicatorShape,
required this.overlayColor,
required this.onTap,
this.labelTextStyle,
this.labelPadding,
required super.child,
});
final int index;
final int selectedIndex;
final int totalNumberOfDestinations;
final Animation<double> selectedAnimation;
final NavigationDestinationLabelBehavior labelBehavior;
final Color? indicatorColor;
final ShapeBorder? indicatorShape;
final WidgetStateProperty<Color?>? overlayColor;
final VoidCallback onTap;
final WidgetStateProperty<TextStyle?>? labelTextStyle;
final EdgeInsetsGeometry? labelPadding;
static _NavigationDestinationInfo of(BuildContext context) {
final _NavigationDestinationInfo? result = context
.dependOnInheritedWidgetOfExactType<_NavigationDestinationInfo>();
assert(
result != null,
'Navigation destinations need a _NavigationDestinationInfo parent, '
'which is usually provided by NavigationBar.',
);
return result!;
}
@override
bool updateShouldNotify(_NavigationDestinationInfo oldWidget) {
return index != oldWidget.index ||
totalNumberOfDestinations != oldWidget.totalNumberOfDestinations ||
selectedAnimation != oldWidget.selectedAnimation ||
labelBehavior != oldWidget.labelBehavior ||
onTap != oldWidget.onTap;
}
}
class NavigationIndicator extends StatelessWidget {
const NavigationIndicator({
super.key,
required this.animation,
this.color,
this.width = _kIndicatorWidth,
this.height = _kIndicatorHeight,
});
final Animation<double> animation;
final Color? color;
final double width;
final double height;
static final _anim = Tween<double>(
begin: .5,
end: 1.0,
).chain(CurveTween(curve: Curves.easeInOutCubicEmphasized));
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
final double scale = animation.isDismissed
? 0.0
: _anim.evaluate(animation);
return Transform(
alignment: Alignment.center,
transform: Matrix4.diagonal3Values(scale, 1.0, 1.0),
child: child,
);
},
child: _StatusTransitionWidgetBuilder(
animation: animation,
builder: (context, child) {
return _SelectableAnimatedBuilder(
isSelected: animation.isForwardOrCompleted,
duration: const Duration(milliseconds: 100),
alwaysDoFullAnimation: true,
builder: (context, fadeAnimation) {
return FadeTransition(
opacity: fadeAnimation,
child: DecoratedBox(
decoration: ShapeDecoration(
shape: _kNavigationShape,
color: color ?? Theme.of(context).colorScheme.secondary,
),
child: const SizedBox(
width: _kIndicatorWidth,
height: _kIndicatorHeight,
),
),
);
},
);
},
),
);
}
}
class _NavigationBarDestinationLayout extends StatelessWidget {
const _NavigationBarDestinationLayout({
required this.icon,
required this.iconKey,
required this.label,
});
final Widget icon;
final GlobalKey iconKey;
final Widget label;
@override
Widget build(BuildContext context) {
return _DestinationLayoutAnimationBuilder(
builder: (context, animation) {
return CustomMultiChildLayout(
delegate: _NavigationDestinationLayoutDelegate(animation: animation),
children: <Widget>[
LayoutId(
id: _NavigationDestinationLayoutDelegate.iconId,
child: KeyedSubtree(key: iconKey, child: icon),
),
LayoutId(
id: _NavigationDestinationLayoutDelegate.labelId,
child: FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: label,
),
),
],
);
},
);
}
}
class _DestinationLayoutAnimationBuilder extends StatelessWidget {
const _DestinationLayoutAnimationBuilder({required this.builder});
final Widget Function(BuildContext, Animation<double>) builder;
@override
Widget build(BuildContext context) {
final info = _NavigationDestinationInfo.of(context);
switch (info.labelBehavior) {
case NavigationDestinationLabelBehavior.alwaysShow:
return builder(context, kAlwaysCompleteAnimation);
case NavigationDestinationLabelBehavior.alwaysHide:
return builder(context, kAlwaysDismissedAnimation);
case NavigationDestinationLabelBehavior.onlyShowSelected:
return _CurvedAnimationBuilder(
animation: info.selectedAnimation,
curve: Curves.easeInOutCubicEmphasized,
reverseCurve: Curves.easeInOutCubicEmphasized.flipped,
builder: builder,
);
}
}
}
class _NavigationBarDestinationTooltip extends StatelessWidget {
const _NavigationBarDestinationTooltip({
required this.message,
required this.child,
});
final String message;
final Widget child;
@override
Widget build(BuildContext context) {
return Tooltip(
message: message,
verticalOffset: 34,
excludeFromSemantics: true,
preferBelow: false,
child: child,
);
}
}
class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate {
_NavigationDestinationLayoutDelegate({required this.animation})
: super(relayout: animation);
final Animation<double> animation;
static const int iconId = 1;
static const int labelId = 2;
@override
void performLayout(Size size) {
double halfWidth(Size size) => size.width / 2;
double halfHeight(Size size) => size.height / 2;
final Size iconSize = layoutChild(iconId, BoxConstraints.loose(size));
final Size labelSize = layoutChild(labelId, BoxConstraints.loose(size));
final double yPositionOffset = Tween<double>(
begin: halfHeight(iconSize),
end: halfHeight(iconSize) + halfHeight(labelSize),
).transform(animation.value);
final double iconYPosition = halfHeight(size) - yPositionOffset;
positionChild(
iconId,
Offset(
halfWidth(size) - halfWidth(iconSize),
iconYPosition,
),
);
positionChild(
labelId,
Offset(
halfWidth(size) - halfWidth(labelSize),
iconYPosition + iconSize.height,
),
);
}
@override
bool shouldRelayout(_NavigationDestinationLayoutDelegate oldDelegate) {
return oldDelegate.animation != animation;
}
}
class _StatusTransitionWidgetBuilder extends StatusTransitionWidget {
const _StatusTransitionWidgetBuilder({
required super.animation,
required this.builder,
// ignore: unused_element_parameter
this.child,
});
final TransitionBuilder builder;
final Widget? child;
@override
Widget build(BuildContext context) => builder(context, child);
}
class _SelectableAnimatedBuilder extends StatefulWidget {
const _SelectableAnimatedBuilder({
required this.isSelected,
this.duration = const Duration(milliseconds: 200),
this.alwaysDoFullAnimation = false,
required this.builder,
});
final bool isSelected;
final Duration duration;
final bool alwaysDoFullAnimation;
final Widget Function(BuildContext, Animation<double>) builder;
@override
_SelectableAnimatedBuilderState createState() =>
_SelectableAnimatedBuilderState();
}
class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.duration = widget.duration;
_controller.value = widget.isSelected ? 1.0 : 0.0;
}
@override
void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller.duration = widget.duration;
}
if (oldWidget.isSelected != widget.isSelected) {
if (widget.isSelected) {
_controller.forward(from: widget.alwaysDoFullAnimation ? 0 : null);
} else {
_controller.reverse(from: widget.alwaysDoFullAnimation ? 1 : null);
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(context, _controller);
}
}
class _CurvedAnimationBuilder extends StatefulWidget {
const _CurvedAnimationBuilder({
required this.animation,
required this.curve,
required this.reverseCurve,
required this.builder,
});
final Animation<double> animation;
final Curve curve;
final Curve reverseCurve;
final Widget Function(BuildContext, Animation<double>) builder;
@override
_CurvedAnimationBuilderState createState() => _CurvedAnimationBuilderState();
}
class _CurvedAnimationBuilderState extends State<_CurvedAnimationBuilder> {
late AnimationStatus _animationDirection;
AnimationStatus? _preservedDirection;
@override
void initState() {
super.initState();
_animationDirection = widget.animation.status;
_updateStatus(widget.animation.status);
widget.animation.addStatusListener(_updateStatus);
}
@override
void dispose() {
widget.animation.removeStatusListener(_updateStatus);
super.dispose();
}
void _updateStatus(AnimationStatus status) {
if (_animationDirection != status) {
setState(() {
_animationDirection = status;
});
}
switch (status) {
case AnimationStatus.forward || AnimationStatus.reverse
when _preservedDirection != null:
break;
case AnimationStatus.forward || AnimationStatus.reverse:
setState(() {
_preservedDirection = status;
});
case AnimationStatus.completed || AnimationStatus.dismissed:
setState(() {
_preservedDirection = null;
});
}
}
@override
Widget build(BuildContext context) {
final shouldUseForwardCurve =
(_preservedDirection ?? _animationDirection) != AnimationStatus.reverse;
final Animation<double> curvedAnimation = CurveTween(
curve: shouldUseForwardCurve ? widget.curve : widget.reverseCurve,
).animate(widget.animation);
return widget.builder(context, curvedAnimation);
}
}
const _indicatorDark = Color(0x15FFFFFF);
const _indicatorLight = Color(0x10000000);
class _NavigationBarDefaultsM3 extends NavigationBarThemeData {
_NavigationBarDefaultsM3(this.context)
: super(
height: _kNavigationHeight,
elevation: 3.0,
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
);
final BuildContext context;
late final _colors = Theme.of(context).colorScheme;
late final _textTheme = Theme.of(context).textTheme;
BorderSide get borderSide => _colors.isDark
? const BorderSide(color: Color(0x08FFFFFF))
: const BorderSide(color: Color(0x08000000));
@override
Color? get backgroundColor => _colors.surfaceContainer;
@override
Color? get shadowColor => Colors.transparent;
@override
Color? get surfaceTintColor => Colors.transparent;
@override
WidgetStateProperty<IconThemeData?>? get iconTheme {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
return IconThemeData(
size: 24.0,
color: states.contains(WidgetState.disabled)
? _colors.onSurfaceVariant.withValues(alpha: 0.38)
: states.contains(WidgetState.selected)
? _colors.onSecondaryContainer
: _colors.onSurfaceVariant,
);
});
}
@override
Color? get indicatorColor =>
_colors.isDark ? _indicatorDark : _indicatorLight;
@override
ShapeBorder? get indicatorShape => const StadiumBorder();
@override
WidgetStateProperty<TextStyle?>? get labelTextStyle {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
final TextStyle style = _textTheme.labelMedium!;
return style.apply(
color: states.contains(WidgetState.disabled)
? _colors.onSurfaceVariant.withValues(alpha: 0.38)
: states.contains(WidgetState.selected)
? _colors.onSurface
: _colors.onSurfaceVariant,
);
});
}
@override
EdgeInsetsGeometry? get labelPadding => const EdgeInsets.only(top: 2);
}

View File

@@ -0,0 +1,392 @@
// 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: prefer_initializing_formals
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide ListView;
import 'package:flutter/rendering.dart' hide RenderSliverList;
class ChatListView extends BoxScrollView {
ChatListView.separated({
super.key,
super.scrollDirection,
super.controller,
super.primary,
super.physics,
super.padding,
required NullableIndexedWidgetBuilder itemBuilder,
@Deprecated(
'Use findItemIndexCallback instead. '
'findChildIndexCallback returns child indices (which include separators), '
'while findItemIndexCallback returns item indices (which do not). '
'If you were multiplying results by 2 to account for separators, '
'you can remove that workaround when migrating to findItemIndexCallback. '
'This feature was deprecated after v3.37.0-1.0.pre.',
)
ChildIndexGetter? findChildIndexCallback,
ChildIndexGetter? findItemIndexCallback,
required IndexedWidgetBuilder separatorBuilder,
required int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
@Deprecated(
'Use scrollCacheExtent instead. '
'This feature was deprecated after v3.41.0-0.0.pre.',
)
super.cacheExtent,
super.scrollCacheExtent,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
super.hitTestBehavior,
}) : assert(itemCount >= 0),
assert(
findItemIndexCallback == null || findChildIndexCallback == null,
'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
),
childrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final int itemIndex = index ~/ 2;
if (index.isEven) {
return itemBuilder(context, itemIndex);
}
return separatorBuilder(context, itemIndex);
},
findChildIndexCallback: findItemIndexCallback != null
? (Key key) {
final int? itemIndex = findItemIndexCallback(key);
return itemIndex == null ? null : itemIndex * 2;
}
: findChildIndexCallback,
childCount: _computeActualChildCount(itemCount),
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
semanticIndexCallback: (Widget widget, int index) {
return index.isEven ? index ~/ 2 : null;
},
),
super(semanticChildCount: itemCount, reverse: true);
final SliverChildDelegate childrenDelegate;
@override
Widget buildChildLayout(BuildContext context) {
return SliverChatList(delegate: childrenDelegate);
}
static int _computeActualChildCount(int itemCount) {
return math.max(0, itemCount * 2 - 1);
}
}
class SliverChatList extends SliverMultiBoxAdaptorWidget {
const SliverChatList({super.key, required super.delegate});
@override
SliverMultiBoxAdaptorElement createElement() =>
SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true);
@override
RenderSliverChatList createRenderObject(BuildContext context) {
final element = context as SliverMultiBoxAdaptorElement;
return RenderSliverChatList(childManager: element);
}
}
class RenderSliverChatList extends RenderSliverMultiBoxAdaptor
with ExtendedRenderObjectMixin {
RenderSliverChatList({required super.childManager});
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
childManager
..didStartLayout()
..setDidUnderflow(false);
final double scrollOffset =
constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
var leadingGarbage = 0;
var trailingGarbage = 0;
var reachedEnd = false;
if (firstChild == null) {
if (!addInitialChild()) {
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
///
handleCloseToTrailingBegin();
RenderBox? leadingChildWithLayout, trailingChildWithLayout;
RenderBox? earliestUsefulChild = firstChild;
if (childScrollOffset(firstChild!) == null) {
var leadingChildrenWithoutLayoutOffset = 0;
while (earliestUsefulChild != null &&
childScrollOffset(earliestUsefulChild) == null) {
earliestUsefulChild = childAfter(earliestUsefulChild);
leadingChildrenWithoutLayoutOffset += 1;
}
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
if (firstChild == null) {
if (!addInitialChild()) {
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
}
earliestUsefulChild = firstChild;
for (
double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)!
) {
earliestUsefulChild = insertAndLayoutLeadingChild(
childConstraints,
parentUsesSize: true,
);
if (earliestUsefulChild == null) {
final childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
firstChild!.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
geometry = SliverGeometry(scrollOffsetCorrection: -scrollOffset);
return;
}
}
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
);
final childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
final childParentData =
earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
if (scrollOffset < precisionErrorTolerance) {
while (indexOf(firstChild!) > 0) {
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
earliestUsefulChild = insertAndLayoutLeadingChild(
childConstraints,
parentUsesSize: true,
);
assert(earliestUsefulChild != null);
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
final childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
);
return;
}
}
}
assert(earliestUsefulChild == firstChild);
assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
if (leadingChildWithLayout == null) {
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}
var inLayoutRange = true;
var child = earliestUsefulChild;
int index = indexOf(child!);
double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
bool advance() {
assert(child != null);
if (child == trailingChildWithLayout) {
inLayoutRange = false;
}
child = childAfter(child!);
if (child == null) {
inLayoutRange = false;
}
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child!) != index) {
child = insertAndLayoutChild(
childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
return false;
}
} else {
child!.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final childParentData =
child!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent =
childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
geometry = SliverGeometry(scrollExtent: extent, maxPaintExtent: extent);
return;
}
}
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
if (child != null) {
child = childAfter(child!);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child!);
}
}
collectGarbage(leadingGarbage, trailingGarbage);
assert(debugAssertChildListIsNonEmptyAndContiguous());
final double estimatedMaxScrollOffset;
///
endScrollOffset = handleCloseToTrailingEnd(endScrollOffset);
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild!),
lastIndex: indexOf(lastChild!),
leadingScrollOffset: childScrollOffset(firstChild!),
trailingScrollOffset: endScrollOffset,
);
assert(
estimatedMaxScrollOffset >=
endScrollOffset - childScrollOffset(firstChild!)!,
);
}
final double firstChildScrollOffset = childScrollOffset(firstChild!)!;
double paintExtent = calculatePaintOffset(
constraints,
from: firstChildScrollOffset,
to: endScrollOffset,
);
final double cacheExtent = calculateCacheOffset(
constraints,
from: firstChildScrollOffset,
to: endScrollOffset,
);
final double targetEndScrollOffsetForPaint =
constraints.scrollOffset + constraints.remainingPaintExtent;
///
paintExtent += _closeToTrailingDistance;
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
hasVisualOverflow:
endScrollOffset > targetEndScrollOffsetForPaint ||
constraints.scrollOffset > 0.0,
);
if (estimatedMaxScrollOffset == endScrollOffset) {
childManager.setDidUnderflow(true);
}
childManager.didFinishLayout();
}
}
const double kChatListPadding = 14.0;
/// from https://github.com/fluttercandies/extended_list
mixin ExtendedRenderObjectMixin on RenderSliverMultiBoxAdaptor {
void handleCloseToTrailingBegin() {
_closeToTrailingDistance = 0.0;
}
double handleCloseToTrailingEnd(double endScrollOffset) {
final extent = constraints.remainingPaintExtent - kChatListPadding;
if (endScrollOffset < extent) {
_closeToTrailingDistance = extent - endScrollOffset;
return extent;
}
return endScrollOffset;
}
double _closeToTrailingDistance = 0.0;
@override
double? childScrollOffset(RenderObject child) {
return (super.childScrollOffset(child) ?? 0.0) + _closeToTrailingDistance;
}
}

View File

@@ -2,25 +2,17 @@
// 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, depend_on_referenced_packages
/// @docImport 'package:flutter/material.dart';
/// @docImport 'package:flutter_test/flutter_test.dart';
///
/// @docImport 'primary_scroll_controller.dart';
/// @docImport 'scroll_configuration.dart';
/// @docImport 'scroll_view.dart';
/// @docImport 'scrollable.dart';
/// @docImport 'single_child_scroll_view.dart';
/// @docImport 'viewport.dart';
library;
// ignore_for_file: prefer_initializing_formals
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide DraggableScrollableSheet;
part 'package:PiliPlus/common/widgets/draggable_sheet/dyn.dart';
part 'package:PiliPlus/common/widgets/draggable_sheet/topic.dart';
/// Controls a [DraggableScrollableSheet].
///
@@ -112,11 +104,10 @@ class DraggableScrollableController extends ChangeNotifier {
_assertAttached();
assert(size >= 0 && size <= 1);
assert(duration != Duration.zero);
final AnimationController animationController =
AnimationController.unbounded(
vsync: _attachedController!.position.context.vsync,
value: _attachedController!.extent.currentSize,
);
final animationController = AnimationController.unbounded(
vsync: _attachedController!.position.context.vsync,
value: _attachedController!.extent.currentSize,
);
_animationControllers.add(animationController);
_attachedController!.position.goIdle();
// This disables any snapping until the next user interaction with the sheet.
@@ -583,7 +574,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
}
List<double> _impliedSnapSizes() {
for (int index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
for (var index = 0; index < (widget.snapSizes?.length ?? 0); index += 1) {
final double snapSize = widget.snapSizes![index];
assert(
snapSize >= widget.minChildSize && snapSize <= widget.maxChildSize,
@@ -684,11 +675,11 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
// have changed when the widget was updated.
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
for (
int index = 0;
var index = 0;
index < _scrollController.positions.length;
index++
) {
final _DraggableScrollableSheetScrollPosition position =
final position =
_scrollController.positions.elementAt(index)
as _DraggableScrollableSheetScrollPosition;
position.goBallistic(0);
@@ -702,7 +693,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
.asMap()
.keys
.map((int index) {
final String snapSizeString = widget.snapSizes![index].toString();
final snapSizeString = widget.snapSizes![index].toString();
if (index == invalidIndex) {
return '>>> $snapSizeString <<<';
}
@@ -729,9 +720,7 @@ class _DraggableScrollableSheetState extends State<DraggableScrollableSheet> {
/// [_DraggableScrollableSheetScrollController] as the primary controller for
/// descendants.
class _DraggableScrollableSheetScrollController extends ScrollController {
_DraggableScrollableSheetScrollController({
required this.extent,
});
_DraggableScrollableSheetScrollController({required this.extent});
_DraggableSheetExtent extent;
VoidCallback? onPositionDetached;
@@ -806,6 +795,7 @@ class _DraggableScrollableSheetScrollPosition
required super.context,
super.oldPosition,
required this.getExtent,
super.initialPixels,
});
VoidCallback? _dragCancelCallback;
@@ -816,8 +806,6 @@ class _DraggableScrollableSheetScrollPosition
_DraggableSheetExtent get extent => getExtent();
bool _isAtTop = true;
@override
void absorb(ScrollPosition other) {
super.absorb(other);
@@ -845,9 +833,7 @@ class _DraggableScrollableSheetScrollPosition
@override
void applyUserOffset(double delta) {
if (!_isAtTop) {
super.applyUserOffset(delta);
} else if (!listShouldScroll &&
if (!listShouldScroll &&
(!(extent.isAtMin || extent.isAtMax) ||
(extent.isAtMin && delta < 0) ||
(extent.isAtMax && delta > 0))) {
@@ -882,10 +868,6 @@ class _DraggableScrollableSheetScrollPosition
@override
void goBallistic(double velocity) {
if (!_isAtTop) {
super.goBallistic(velocity);
return;
}
if ((velocity == 0.0 && !_shouldSnap()) ||
(velocity < 0.0 && listShouldScroll) ||
(velocity > 0.0 && extent.isAtMax)) {
@@ -917,14 +899,10 @@ class _DraggableScrollableSheetScrollPosition
);
}
final AnimationController ballisticController =
AnimationController.unbounded(
debugLabel: objectRuntimeType(
this,
'_DraggableScrollableSheetPosition',
),
vsync: context.vsync,
);
final ballisticController = AnimationController.unbounded(
debugLabel: objectRuntimeType(this, '_DraggableScrollableSheetPosition'),
vsync: context.vsync,
);
_ballisticControllers.add(ballisticController);
double lastPosition = extent.currentPixels;
@@ -967,71 +945,12 @@ class _DraggableScrollableSheetScrollPosition
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
_isAtTop = pixels == 0;
// Save this so we can call it later if we have to [goBallistic] on our own.
_dragCancelCallback = dragCancelCallback;
return super.drag(details, dragCancelCallback);
}
}
/// A widget that can notify a descendent [DraggableScrollableSheet] that it
/// should reset its position to the initial state.
///
/// The [Scaffold] uses this widget to notify a persistent bottom sheet that
/// the user has tapped back if the sheet has started to cover more of the body
/// than when at its initial position. This is important for users of assistive
/// technology, where dragging may be difficult to communicate.
///
/// This is just a wrapper on top of [DraggableScrollableController]. It is
/// primarily useful for controlling a sheet in a part of the widget tree that
/// the current code does not control (e.g. library code trying to affect a sheet
/// in library users' code). Generally, it's easier to control the sheet
/// directly by creating a controller and passing the controller to the sheet in
/// its constructor (see [DraggableScrollableSheet.controller]).
class DraggableScrollableActuator extends StatefulWidget {
/// Creates a widget that can notify descendent [DraggableScrollableSheet]s
/// to reset to their initial position.
///
/// The [child] parameter is required.
const DraggableScrollableActuator({super.key, required this.child});
/// This child's [DraggableScrollableSheet] descendant will be reset when the
/// [reset] method is applied to a context that includes it.
final Widget child;
/// Notifies any descendant [DraggableScrollableSheet] that it should reset
/// to its initial position.
///
/// Returns `true` if a [DraggableScrollableActuator] is available and
/// some [DraggableScrollableSheet] is listening for updates, `false`
/// otherwise.
static bool reset(BuildContext context) {
final _InheritedResetNotifier? notifier = context
.dependOnInheritedWidgetOfExactType<_InheritedResetNotifier>();
return notifier?._sendReset() ?? false;
}
@override
State<DraggableScrollableActuator> createState() =>
_DraggableScrollableActuatorState();
}
class _DraggableScrollableActuatorState
extends State<DraggableScrollableActuator> {
final _ResetNotifier _notifier = _ResetNotifier();
@override
Widget build(BuildContext context) {
return _InheritedResetNotifier(notifier: _notifier, child: widget.child);
}
@override
void dispose() {
_notifier.dispose();
super.dispose();
}
}
/// A [ChangeNotifier] to use with [_InheritedResetNotifier] to notify
/// descendants that they should reset to initial state.
class _ResetNotifier extends ChangeNotifier {
@@ -1067,6 +986,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
required _ResetNotifier super.notifier,
});
// ignore: unused_element
bool _sendReset() => notifier!.sendReset();
/// Specifies whether the [DraggableScrollableSheet] should reset to its
@@ -1080,8 +1000,7 @@ class _InheritedResetNotifier extends InheritedNotifier<_ResetNotifier> {
return false;
}
assert(widget is _InheritedResetNotifier);
final _InheritedResetNotifier inheritedNotifier =
widget as _InheritedResetNotifier;
final inheritedNotifier = widget as _InheritedResetNotifier;
final bool wasCalled = inheritedNotifier.notifier!._wasCalled;
inheritedNotifier.notifier!._wasCalled = false;
return wasCalled;
@@ -1158,6 +1077,10 @@ class _SnappingSimulation extends Simulation {
return pixelSnapSizes.first;
}
final double nextSize = pixelSnapSizes[indexOfNextSize];
// If already snapped - keep this as target size
if (nextSize == position) {
return nextSize;
}
final double previousSize = pixelSnapSizes[indexOfNextSize - 1];
if (initialVelocity.abs() <= tolerance.velocity) {
// If velocity is zero, snap to the nearest snap size with the minimum velocity.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,7 +2,7 @@
// 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
// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import
/// @docImport 'card.dart';
/// @docImport 'checkbox.dart';
@@ -20,7 +20,7 @@ library;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide ListTile;
import 'package:flutter/rendering.dart';
// Examples can assume:
@@ -742,19 +742,6 @@ class ListTile extends StatelessWidget {
return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false;
}
Color _tileBackgroundColor(
ThemeData theme,
ListTileThemeData tileTheme,
ListTileThemeData defaults,
) {
final Color? color = selected
? selectedTileColor ??
tileTheme.selectedTileColor ??
theme.listTileTheme.selectedTileColor
: tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor;
return color ?? defaults.tileColor!;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
@@ -769,6 +756,25 @@ class ListTile extends StatelessWidget {
final ListTileThemeData defaults = theme.useMaterial3
? _LisTileDefaultsM3(context)
: _LisTileDefaultsM2(context, listTileStyle);
final Color backgroundColor =
tileColor ??
tileTheme.tileColor ??
theme.listTileTheme.tileColor ??
defaults.tileColor!;
final Color selectedBackgroundColor =
selectedTileColor ??
tileTheme.selectedTileColor ??
theme.listTileTheme.selectedTileColor ??
defaults.tileColor!;
final effectiveTileColor = selected
? selectedBackgroundColor
: backgroundColor;
final bool hasOpaqueBackground =
backgroundColor.alpha > 0 || selectedBackgroundColor.alpha > 0;
if (onTap != null || onLongPress != null || hasOpaqueBackground) {
assert(_debugCheckBackgroundIsHidden(context));
}
final Set<WidgetState> states = <WidgetState>{
if (!enabled) WidgetState.disabled,
if (selected) WidgetState.selected,
@@ -1015,7 +1021,7 @@ class ListTile extends StatelessWidget {
child: Ink(
decoration: ShapeDecoration(
shape: shape ?? tileTheme.shape ?? const Border(),
color: _tileBackgroundColor(theme, tileTheme, defaults),
color: effectiveTileColor,
),
child: child,
),
@@ -1189,6 +1195,68 @@ class ListTile extends StatelessWidget {
),
);
}
bool _debugCheckBackgroundIsHidden(BuildContext context) {
assert(() {
final Widget? intermediateWidget = _findIntermediateWidget(context);
if (intermediateWidget != null) {
FlutterError.reportError(
FlutterErrorDetails(
exception: FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'ListTile background color or ink splashes may be invisible.',
),
ErrorDescription(
'The ListTile is wrapped in a ${intermediateWidget.runtimeType} that has a background color. '
'Because ListTile paints its background and ink splashes on the nearest Material ancestor, '
'this ${intermediateWidget.runtimeType} will hide those effects.',
),
ErrorHint(
'To fix this, wrap the ListTile in its own Material widget, '
'or remove the background color from the intermediate ${intermediateWidget.runtimeType}.',
),
]),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ListTile>(
'ListTile',
this,
expandableValue: true,
),
DiagnosticsProperty<Widget>(
'${intermediateWidget.runtimeType}',
intermediateWidget,
expandableValue: true,
),
],
),
);
}
return true;
}());
return true;
}
Widget? _findIntermediateWidget(BuildContext context) {
Widget? intermediateWidget;
(context as Element).visitAncestorElements((Element ancestor) {
if (ancestor.widget is Material) {
return false;
}
final Widget widget = ancestor.widget;
final Color? color = switch (widget) {
ColoredBox(:final Color color) => color,
DecoratedBox(decoration: BoxDecoration(:final Color? color)) => color,
DecoratedBox(decoration: ShapeDecoration(:final Color? color)) => color,
_ => null,
};
if (color != null && color.a > 0) {
intermediateWidget = widget;
return false;
}
return true;
});
return intermediateWidget;
}
}
class _IndividualOverrides extends WidgetStateProperty<Color?> {
@@ -1506,11 +1574,16 @@ class _RenderListTile extends RenderBox
@override
double computeMinIntrinsicHeight(double width) {
return math.max(
_targetTileHeight,
title.getMinIntrinsicHeight(width) +
(subtitle?.getMinIntrinsicHeight(width) ?? 0.0),
);
final double titleMinHeight = title.getMinIntrinsicHeight(width);
final double? subtitleMinHeight = subtitle?.getMinIntrinsicHeight(width);
const topAndBottomPaddingMultiplier = 2;
final double contentHeight =
titleMinHeight +
(subtitleMinHeight ?? 0.0) +
topAndBottomPaddingMultiplier * _minVerticalPadding;
return math.max(_targetTileHeight, contentHeight);
}
@override

View File

@@ -2,18 +2,13 @@
// 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;
// ignore_for_file: prefer_initializing_formals
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
import 'package:flutter/gestures.dart'
show DragStartBehavior, HorizontalDragGestureRecognizer;
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
import 'package:flutter/material.dart'
hide PageView, Scrollable, ScrollableState;
import 'package:flutter/rendering.dart';
class _ForceImplicitScrollPhysics extends ScrollPhysics {
@@ -106,15 +101,26 @@ class PageView<T extends HorizontalDragGestureRecognizer>
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
ScrollCacheExtent? scrollCacheExtent,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
required this.horizontalDragGestureRecognizer,
}) : childrenDelegate = SliverChildListDelegate(children);
}) : assert(
scrollCacheExtent == null ||
(scrollCacheExtent.value > 0.0) == allowImplicitScrolling,
'scrollCacheExtent and allowImplicitScrolling must be consistent: '
'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, '
'and must be 0.0 when allowImplicitScrolling is false.',
),
scrollCacheExtent =
scrollCacheExtent ??
ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0),
childrenDelegate = SliverChildListDelegate(children);
final T horizontalDragGestureRecognizer;
final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;
/// Creates a scrollable list that works page by page using widgets that are
/// created on demand.
@@ -154,13 +160,24 @@ class PageView<T extends HorizontalDragGestureRecognizer>
int? itemCount,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
ScrollCacheExtent? scrollCacheExtent,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
required this.horizontalDragGestureRecognizer,
}) : childrenDelegate = SliverChildBuilderDelegate(
}) : assert(
scrollCacheExtent == null ||
(scrollCacheExtent.value > 0.0) == allowImplicitScrolling,
'scrollCacheExtent and allowImplicitScrolling must be consistent: '
'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, '
'and must be 0.0 when allowImplicitScrolling is false.',
),
scrollCacheExtent =
scrollCacheExtent ??
ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
@@ -177,7 +194,7 @@ class PageView<T extends HorizontalDragGestureRecognizer>
/// {@end-tool}
///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
const PageView.custom({
PageView.custom({
super.key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
@@ -188,14 +205,25 @@ class PageView<T extends HorizontalDragGestureRecognizer>
required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
ScrollCacheExtent? scrollCacheExtent,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior,
this.padEnds = true,
required this.horizontalDragGestureRecognizer,
});
}) : assert(
scrollCacheExtent == null ||
(scrollCacheExtent.value > 0.0) == allowImplicitScrolling,
'scrollCacheExtent and allowImplicitScrolling must be consistent: '
'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, '
'and must be 0.0 when allowImplicitScrolling is false.',
),
scrollCacheExtent =
scrollCacheExtent ??
ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0);
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
/// Controls whether the widget's pages will respond to
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
/// scrolling.
@@ -207,8 +235,37 @@ class PageView<T extends HorizontalDragGestureRecognizer>
/// 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.
/// {@endtemplate}
final bool allowImplicitScrolling;
/// {@macro flutter.rendering.RenderViewportBase.scrollCacheExtent}
///
/// In [PageView], the default [scrollCacheExtent] uses
/// [ScrollCacheExtent.viewport], where the value represents the number of
/// viewport lengths to cache beyond the visible area.
///
/// When [PageController.viewportFraction] is 1.0 (the default), this is
/// equivalent to the number of pages. For example,
/// `ScrollCacheExtent.viewport(2.0)` caches 2 pages before and after the
/// visible page.
///
/// When [PageController.viewportFraction] is less than 1.0, multiple pages
/// may be visible in a single viewport, so `ScrollCacheExtent.viewport(1.0)`
/// may cache more than one additional page in each direction.
///
/// [ScrollCacheExtent.pixels] can also be used to specify the cache extent
/// in logical pixels instead of viewport sizes.
///
/// If [scrollCacheExtent] is specified, its value must be consistent with
/// [allowImplicitScrolling]: the value must be greater than 0.0 when
/// [allowImplicitScrolling] is true, and must be 0.0 when
/// [allowImplicitScrolling] is false.
///
/// Defaults to `ScrollCacheExtent.viewport(1.0)` if
/// [allowImplicitScrolling] is true, and `ScrollCacheExtent.viewport(0.0)` if
/// [allowImplicitScrolling] is false.
final ScrollCacheExtent scrollCacheExtent;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
@@ -378,7 +435,7 @@ class _PageViewState<T extends HorizontalDragGestureRecognizer>
if (notification.depth == 0 &&
widget.onPageChanged != null &&
notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page!.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
@@ -399,11 +456,7 @@ class _PageViewState<T extends HorizontalDragGestureRecognizer>
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,
scrollCacheExtent: widget.scrollCacheExtent,
axisDirection: axisDirection,
offset: position,
clipBehavior: widget.clipBehavior,
@@ -412,6 +465,7 @@ class _PageViewState<T extends HorizontalDragGestureRecognizer>
viewportFraction: _controller.viewportFraction,
delegate: widget.childrenDelegate,
padEnds: widget.padEnds,
allowImplicitScrolling: widget.allowImplicitScrolling,
),
],
);
@@ -454,6 +508,15 @@ class _PageViewState<T extends HorizontalDragGestureRecognizer>
value: widget.allowImplicitScrolling,
ifTrue: 'allow implicit scrolling',
),
)
..add(
DiagnosticsProperty<ScrollCacheExtent>(
'scrollCacheExtent',
widget.scrollCacheExtent,
defaultValue: ScrollCacheExtent.viewport(
widget.allowImplicitScrolling ? 1.0 : 0.0,
),
),
);
}
}

View File

@@ -2,19 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: uri_does_not_exist_in_doc_import
/// @docImport 'package:flutter/material.dart';
///
/// @docImport 'page_storage.dart';
/// @docImport 'page_view.dart';
/// @docImport 'scroll_metrics.dart';
/// @docImport 'scroll_notification.dart';
/// @docImport 'scroll_view.dart';
/// @docImport 'single_child_scroll_view.dart';
/// @docImport 'two_dimensional_scroll_view.dart';
/// @docImport 'two_dimensional_viewport.dart';
library;
// ignore_for_file: prefer_initializing_formals
import 'dart:async';
import 'dart:math' as math;
@@ -105,7 +93,7 @@ class Scrollable<T extends HorizontalDragGestureRecognizer>
required this.horizontalDragGestureRecognizer,
}) : assert(semanticChildCount == null || semanticChildCount >= 0);
final T horizontalDragGestureRecognizer;
final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;
/// {@template flutter.widgets.Scrollable.axisDirection}
/// The direction in which this widget scrolls.
@@ -350,7 +338,7 @@ class Scrollable<T extends HorizontalDragGestureRecognizer>
/// if no [Scrollable] ancestor is found.
static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) {
// This is the context that will need to establish the dependency.
final BuildContext originalContext = context;
final originalContext = context;
InheritedElement? element = context
.getElementForInheritedWidgetOfExactType<_ScrollableScope>();
while (element != null) {
@@ -477,7 +465,7 @@ class Scrollable<T extends HorizontalDragGestureRecognizer>
ScrollPositionAlignmentPolicy alignmentPolicy =
ScrollPositionAlignmentPolicy.explicit,
}) {
final List<Future<void>> futures = <Future<void>>[];
final futures = <Future<void>>[];
// The targetRenderObject is used to record the first target renderObject.
// If there are multiple scrollable widgets nested, the targetRenderObject
@@ -815,7 +803,7 @@ class ScrollableState<T extends HorizontalDragGestureRecognizer>
case Axis.horizontal:
_gestureRecognizers = <Type, GestureRecognizerFactory>{
T: GestureRecognizerFactoryWithHandlers<T>(
() => widget.horizontalDragGestureRecognizer,
widget.horizontalDragGestureRecognizer,
(T instance) {
instance
..onDown = _handleDragDown
@@ -855,7 +843,7 @@ class ScrollableState<T extends HorizontalDragGestureRecognizer>
}
_shouldIgnorePointer = value;
if (_ignorePointerKey.currentContext != null) {
final RenderIgnorePointer renderBox =
final renderBox =
_ignorePointerKey.currentContext!.findRenderObject()!
as RenderIgnorePointer;
renderBox.ignoring = _shouldIgnorePointer;
@@ -962,9 +950,6 @@ class ScrollableState<T extends HorizontalDragGestureRecognizer>
void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent && _position != null) {
if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) {
// The handler won't use the `event`, so allow the platform to trigger
// any default native actions.
event.respond(allowPlatformDefault: true);
return;
}
final double delta = _pointerSignalEventDelta(event);
@@ -979,9 +964,6 @@ class ScrollableState<T extends HorizontalDragGestureRecognizer>
);
return;
}
// The `event` won't result in a scroll, so allow the platform to trigger
// any default native actions.
event.respond(allowPlatformDefault: true);
} else if (event is PointerScrollInertiaCancelEvent) {
position.pointerScroll(0);
// Don't use the pointer signal resolver, all hit-tested scrollables should stop.
@@ -990,12 +972,16 @@ class ScrollableState<T extends HorizontalDragGestureRecognizer>
void _handlePointerScroll(PointerEvent event) {
assert(event is PointerScrollEvent);
final double delta = _pointerSignalEventDelta(event as PointerScrollEvent);
final scrollEvent = event as PointerScrollEvent;
final double delta = _pointerSignalEventDelta(scrollEvent);
final double targetScrollOffset = _targetScrollOffsetForPointerScroll(
delta,
);
if (delta != 0.0 && targetScrollOffset != position.pixels) {
position.pointerScroll(delta);
// Tell engine this scrollable handled the event.
// This prevents parent page from scrolling when nested scrollables exist.
scrollEvent.respond(allowPlatformDefault: false);
}
}
@@ -1014,7 +1000,7 @@ class ScrollableState<T extends HorizontalDragGestureRecognizer>
}
Widget _buildChrome(BuildContext context, Widget child) {
final ScrollableDetails details = ScrollableDetails(
final details = ScrollableDetails(
direction: widget.axisDirection,
controller: _effectiveScrollController,
decorationClipBehavior: widget.clipBehavior,
@@ -1344,7 +1330,7 @@ class _ScrollableSelectionContainerDelegate
}
Offset _inferPositionRelatedToOrigin(Offset globalPosition) {
final RenderBox box = state.context.findRenderObject()! as RenderBox;
final box = state.context.findRenderObject()! as RenderBox;
final Offset localPosition = box.globalToLocal(globalPosition);
if (!_selectionStartsInScrollable) {
// If the selection starts outside of the scrollable, selecting across the
@@ -1377,7 +1363,7 @@ class _ScrollableSelectionContainerDelegate
bool forceUpdateEnd = true,
}) {
final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
final RenderBox box = state.context.findRenderObject()! as RenderBox;
final box = state.context.findRenderObject()! as RenderBox;
final Matrix4 transform = box.getTransformTo(null);
if (currentSelectionStartIndex != -1 &&
(_currentDragStartRelatedToOrigin == null || forceUpdateStart)) {
@@ -1492,14 +1478,13 @@ class _ScrollableSelectionContainerDelegate
if (lineHeight == null || edge == null) {
return;
}
final RenderBox scrollableBox =
state.context.findRenderObject()! as RenderBox;
final scrollableBox = state.context.findRenderObject()! as RenderBox;
final Matrix4 transform = selectable.getTransformTo(scrollableBox);
final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint(
transform,
edge.localPosition,
);
final Rect scrollableRect = Rect.fromLTRB(
final scrollableRect = Rect.fromLTRB(
0,
0,
scrollableBox.size.width,
@@ -1568,9 +1553,9 @@ class _ScrollableSelectionContainerDelegate
}
bool _globalPositionInScrollable(Offset globalPosition) {
final RenderBox box = state.context.findRenderObject()! as RenderBox;
final box = state.context.findRenderObject()! as RenderBox;
final Offset localPosition = box.globalToLocal(globalPosition);
final Rect rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height);
final rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height);
return rect.contains(localPosition);
}
@@ -1818,9 +1803,9 @@ class _RenderScrollSemantics extends RenderProxyBox {
(_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect;
int? firstVisibleIndex;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode!];
final List<SemanticsNode> included = <SemanticsNode>[];
for (final SemanticsNode child in children) {
final excluded = <SemanticsNode>[_innerNode!];
final included = <SemanticsNode>[];
for (final child in children) {
assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
if (child.isTagged(RenderViewport.excludeFromScrolling)) {
excluded.add(child);

View File

@@ -2,18 +2,19 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: dangling_library_doc_comments, prefer_initializing_formals
/// @docImport 'package:flutter/material.dart';
///
/// @docImport 'overscroll_indicator.dart';
/// @docImport 'viewport.dart';
// ignore_for_file: dangling_library_doc_comments
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide ScrollableState;
import 'package:flutter/material.dart'
hide EdgeDraggingAutoScroller, Scrollable, ScrollableState;
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
/// to its edge.
@@ -29,7 +30,7 @@ class EdgeDraggingAutoScroller {
required this.velocityScalar,
});
/// The [CustomScrollable] this auto scroller is scrolling.
/// The [Scrollable] this auto scroller is scrolling.
final ScrollableState scrollable;
/// Called when a scroll view is scrolled.
@@ -97,8 +98,7 @@ class EdgeDraggingAutoScroller {
}
Future<void> _scroll() async {
final RenderBox scrollRenderBox =
scrollable.context.findRenderObject()! as RenderBox;
final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox;
final Matrix4 transform = scrollRenderBox.getTransformTo(null);
final Rect globalRect = MatrixUtils.transformRect(
transform,
@@ -123,7 +123,7 @@ class EdgeDraggingAutoScroller {
);
_scrolling = true;
double? newOffset;
const double overDragMax = 20.0;
const overDragMax = 20.0;
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
final Offset viewportOrigin = globalRect.topLeft.translate(
@@ -194,9 +194,7 @@ class EdgeDraggingAutoScroller {
_scrolling = false;
return;
}
final Duration duration = Duration(
milliseconds: (1000 / velocityScalar).round(),
);
final duration = Duration(milliseconds: (1000 / velocityScalar).round());
await scrollable.position.animateTo(
newOffset,
duration: duration,

View File

@@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: prefer_initializing_formals
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart'
show DragStartBehavior, HorizontalDragGestureRecognizer;
import 'package:flutter/material.dart' hide PageView;
import 'package:flutter/material.dart' hide TabBarView, PageView;
/// A page view that displays the widget which corresponds to the currently
/// selected tab.
@@ -38,7 +40,7 @@ class TabBarView<T extends HorizontalDragGestureRecognizer>
required this.horizontalDragGestureRecognizer,
});
final T horizontalDragGestureRecognizer;
final GestureRecognizerFactoryConstructor<T> horizontalDragGestureRecognizer;
/// This widget's selection and animation state.
///
@@ -220,7 +222,7 @@ class _TabBarViewState<T extends HorizontalDragGestureRecognizer>
return;
}
final bool adjacentDestination =
final adjacentDestination =
(_currentIndex! - _controller!.previousIndex).abs() == 1;
if (adjacentDestination) {
_warpToAdjacentTab(_controller!.animationDuration);

View File

@@ -2,10 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
// ignore_for_file: prefer_initializing_formals
import 'package:flutter/material.dart' hide PopScope;
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
abstract class PopScopeState<T extends StatefulWidget> extends State<T>
implements PopEntry<T> {
implements PopEntry<Object> {
ModalRoute<dynamic>? _route;
@override
@@ -14,31 +18,60 @@ abstract class PopScopeState<T extends StatefulWidget> extends State<T>
@override
late final ValueNotifier<bool> canPopNotifier;
void initCanPopNotifier() {
canPopNotifier = ValueNotifier<bool>(false);
}
bool get initCanPop => true;
@override
void initState() {
super.initState();
initCanPopNotifier();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopEntry(this);
_route = nextRoute;
_route?.registerPopEntry(this);
}
canPopNotifier = ValueNotifier<bool>(initCanPop);
_route = (Get.routing.route as ModalRoute)..registerPopEntry(this);
}
@override
void dispose() {
_route?.unregisterPopEntry(this);
_route = null;
canPopNotifier.dispose();
super.dispose();
}
}
// ignore: camel_case_types
typedef popScope = PopScope;
class PopScope extends StatefulWidget {
const PopScope({
super.key,
required this.child,
this.canPop = true,
required this.onPopInvokedWithResult,
});
final Widget child;
final PopInvokedWithResultCallback<Object> onPopInvokedWithResult;
final bool canPop;
@override
State<PopScope> createState() => _PopScopeState();
}
class _PopScopeState<T extends PopScope> extends PopScopeState<T> {
@override
bool get initCanPop => widget.canPop;
@override
void onPopInvokedWithResult(bool didPop, Object? result) {
widget.onPopInvokedWithResult(didPop, result);
}
@override
void didUpdateWidget(T oldWidget) {
super.didUpdateWidget(oldWidget);
canPopNotifier.value = widget.canPop;
}
@override
Widget build(BuildContext context) => widget.child;
}

View File

@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library;
// ignore_for_file: prefer_initializing_formals
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide PopupMenuItem;
class CustomPopupMenuItem<T> extends PopupMenuEntry<T> {
const CustomPopupMenuItem({
@@ -114,7 +114,7 @@ class _CustomPopupMenuDividerState extends State<CustomPopupMenuDivider> {
// dart format off
class _PopupMenuDefaultsM3 extends PopupMenuThemeData {
_PopupMenuDefaultsM3(this.context)
: super(elevation: 3.0);
: super(elevation: 3.0);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
@@ -123,8 +123,8 @@ class _PopupMenuDefaultsM3 extends PopupMenuThemeData {
@override WidgetStateProperty<TextStyle?>? get labelTextStyle {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
// TODO(quncheng): Update this hard-coded value to use the latest tokens.
final TextStyle style = _textTheme.labelLarge!;
// TODO(quncheng): Update this hard-coded value to use the latest tokens.
final TextStyle style = _textTheme.labelLarge!;
if (states.contains(WidgetState.disabled)) {
return style.apply(color: _colors.onSurface.withValues(alpha: 0.38));
}

View File

@@ -2,19 +2,25 @@
// 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
// ignore_for_file: prefer_initializing_formals
/// @docImport 'color_scheme.dart';
library;
import 'dart:async';
import 'dart:math' as math;
import 'dart:async' show Completer;
import 'dart:io' show Platform;
import 'package:PiliPlus/common/widgets/scroll_behavior.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/cupertino.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'
show RefreshScrollPhysics;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart' hide RefreshIndicator;
/// The distance from the child's top or bottom [edgeOffset] where
/// the refresh indicator will settle. During the drag that exposes the refresh
/// indicator, its actual displacement may significantly exceed this value.
///
/// In most cases, [displacement] distance starts counting from the parent's
/// edges. However, if [edgeOffset] is larger than zero then the [displacement]
/// value is calculated from that offset instead of the parent's edge.
double displacement = Pref.refreshDisplacement;
// The over-scroll distance that moves the indicator to its maximum
@@ -33,21 +39,13 @@ const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
// has completed.
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
/// The signature for a function that's called when the user has dragged a
/// [RefreshIndicator] far enough to demonstrate that they want the app to
/// refresh. The returned [Future] must complete when the refresh operation is
/// finished.
///
/// Used by [RefreshIndicator.onRefresh].
typedef RefreshCallback = Future<void> Function();
/// Indicates current status of Material `RefreshIndicator`.
enum RefreshIndicatorStatus {
/// Pointer is down.
drag,
/// Dragged far enough that an up event will run the onRefresh callback.
armed,
// armed,
/// Animating to the indicator's final "displacement".
snap,
@@ -62,19 +60,6 @@ enum RefreshIndicatorStatus {
canceled,
}
/// Used to configure how [RefreshIndicator] can be triggered.
enum RefreshIndicatorTriggerMode {
/// The indicator can be triggered regardless of the scroll position
/// of the [Scrollable] when the drag starts.
anywhere,
/// The indicator can only be triggered if the [Scrollable] is at the edge
/// when the drag starts.
onEdge,
}
enum _IndicatorType { material, adaptive, noSpinner }
/// A widget that supports the Material "swipe to refresh" idiom.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
@@ -149,80 +134,16 @@ class RefreshIndicator extends StatefulWidget {
/// The [semanticsValue] may be used to specify progress on the widget.
const RefreshIndicator({
super.key,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
this.elevation = 2.0,
}) : _indicatorType = _IndicatorType.material,
onStatusChange = null,
assert(elevation >= 0.0);
/// Creates an adaptive [RefreshIndicator] based on whether the target
/// platform is iOS or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// When the descendant overscrolls, a different spinning progress indicator
/// is shown depending on platform. On iOS and macOS,
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
/// [CircularProgressIndicator] appears.
///
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
///
/// Notably the scrollable widget itself will have slightly different behavior
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
const RefreshIndicator.adaptive({
super.key,
this.isClampingScrollPhysics = false,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
this.elevation = 2.0,
}) : _indicatorType = _IndicatorType.adaptive,
onStatusChange = null,
assert(elevation >= 0.0);
/// Creates a [RefreshIndicator] with no spinner and calls `onRefresh` when
/// successfully armed by a drag event.
///
/// Events can be optionally listened by using the `onStatusChange` callback.
const RefreshIndicator.noSpinner({
super.key,
required this.child,
required this.onRefresh,
this.onStatusChange,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
this.elevation = 2.0,
}) : _indicatorType = _IndicatorType.noSpinner,
// The following parameters aren't used because [_IndicatorType.noSpinner] is being used,
// which involves showing no spinner, hence the following parameters are useless since
// their only use is to change the spinner's appearance.
displacement = 0.0,
edgeOffset = 0.0,
color = null,
backgroundColor = null,
strokeWidth = 0.0,
assert(elevation >= 0.0);
}) : assert(elevation >= 0.0);
/// The widget below this widget in the tree.
///
@@ -232,15 +153,6 @@ class RefreshIndicator extends StatefulWidget {
/// Typically a [ListView] or [CustomScrollView].
final Widget child;
/// The distance from the child's top or bottom [edgeOffset] where
/// the refresh indicator will settle. During the drag that exposes the refresh
/// indicator, its actual displacement may significantly exceed this value.
///
/// In most cases, [displacement] distance starts counting from the parent's
/// edges. However, if [edgeOffset] is larger than zero then the [displacement]
/// value is calculated from that offset instead of the parent's edge.
final double displacement;
/// The offset where [RefreshProgressIndicator] starts to appear on drag start.
///
/// Depending whether the indicator is showing on the top or bottom, the value
@@ -262,10 +174,6 @@ class RefreshIndicator extends StatefulWidget {
/// [Future] must complete when the refresh operation is finished.
final RefreshCallback onRefresh;
/// Called to get the current status of the [RefreshIndicator] to update the UI as needed.
/// This is an optional parameter, used to fine tune app cases.
final ValueChanged<RefreshIndicatorStatus?>? onStatusChange;
/// The progress indicator's foreground color. The current theme's
/// [ColorScheme.primary] by default.
final Color? color;
@@ -281,42 +189,18 @@ class RefreshIndicator extends StatefulWidget {
/// else for more complicated layouts.
final ScrollNotificationPredicate notificationPredicate;
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel}
///
/// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]
/// if it is null.
final String? semanticsLabel;
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue}
final String? semanticsValue;
/// Defines [strokeWidth] for `RefreshIndicator`.
///
/// By default, the value of [strokeWidth] is 2.0 pixels.
final double strokeWidth;
final _IndicatorType _indicatorType;
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
///
/// The [RefreshIndicator] can be pulled out in two cases,
/// 1, Keep dragging if the scrollable widget at the edge with zero scroll position
/// when the drag starts.
/// 2, Keep dragging after overscroll occurs if the scrollable widget has
/// a non-zero scroll position when the drag starts.
///
/// If this is [RefreshIndicatorTriggerMode.anywhere], both of the cases above can be triggered.
///
/// If this is [RefreshIndicatorTriggerMode.onEdge], only case 1 can be triggered.
///
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
final RefreshIndicatorTriggerMode triggerMode;
/// Defines the elevation of the underlying [RefreshIndicator].
///
/// Defaults to 2.0.
final double elevation;
final bool isClampingScrollPhysics;
@override
RefreshIndicatorState createState() => RefreshIndicatorState();
}
@@ -334,10 +218,9 @@ class RefreshIndicatorState extends State<RefreshIndicator>
RefreshIndicatorStatus? _status;
late Future<void> _pendingRefreshFuture;
bool? _isIndicatorAtTop;
double? _dragOffset;
late Color _effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
late Color _effectiveValueColor;
// late Color _backgroundColor;
static final Animatable<double> _threeQuarterTween = Tween<double>(
begin: 0.0,
@@ -393,9 +276,10 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
void _setupColorTween() {
final colorScheme = ColorScheme.of(context);
// _backgroundColor = colorScheme.surfaceContainerHighest;
// Reset the current value color.
_effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary;
_effectiveValueColor = widget.color ?? colorScheme.primary;
final Color color = _effectiveValueColor;
if (color.a == 0) {
// Set an always stopped animation instead of a driven tween.
@@ -417,17 +301,13 @@ class RefreshIndicatorState extends State<RefreshIndicator>
// If the notification.dragDetails is null, this scroll is not triggered by
// user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll.
// In this case, we don't want to trigger the refresh indicator.
return ((notification is ScrollStartNotification &&
return _status == null &&
((notification is ScrollStartNotification &&
notification.dragDetails != null) ||
(notification is ScrollUpdateNotification &&
notification.dragDetails != null &&
widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) &&
((notification.metrics.axisDirection == AxisDirection.up &&
notification.metrics.extentAfter == 0.0) ||
(notification.metrics.axisDirection == AxisDirection.down &&
notification.metrics.extentBefore == 0.0)) &&
_status == null &&
_start(notification.metrics.axisDirection);
notification.dragDetails != null)) &&
notification.metrics.extentBefore == 0.0 &&
_start();
}
bool _handleScrollNotification(ScrollNotification notification) {
@@ -437,57 +317,35 @@ class RefreshIndicatorState extends State<RefreshIndicator>
if (_shouldStart(notification)) {
setState(() {
_status = RefreshIndicatorStatus.drag;
widget.onStatusChange?.call(_status);
});
return false;
}
final bool? indicatorAtTopNow =
switch (notification.metrics.axisDirection) {
AxisDirection.down || AxisDirection.up => true,
AxisDirection.left || AxisDirection.right => null,
};
if (indicatorAtTopNow != _isIndicatorAtTop) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
_dismiss(RefreshIndicatorStatus.canceled);
}
} else if (notification is ScrollUpdateNotification) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.scrollDelta!;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.scrollDelta!;
}
if (notification is ScrollUpdateNotification) {
if (_status == RefreshIndicatorStatus.drag) {
_dragOffset = _dragOffset! - notification.scrollDelta!;
_checkDragOffset(notification.metrics.viewportDimension);
}
if (_status == RefreshIndicatorStatus.armed &&
notification.dragDetails == null) {
// On iOS start the refresh when the Scrollable bounces back from the
// overscroll (ScrollNotification indicating this don't have dragDetails
// because the scroll activity is not directly triggered by a drag).
_show();
if (notification.dragDetails == null &&
_valueColor.value!.a == _effectiveValueColor.a) {
// On iOS start the refresh when the Scrollable bounces back from the
// overscroll (ScrollNotification indicating this don't have dragDetails
// because the scroll activity is not directly triggered by a drag).
_show();
}
}
} else if (notification is OverscrollNotification) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.overscroll;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.overscroll;
}
if (_status == RefreshIndicatorStatus.drag) {
_dragOffset = _dragOffset! - notification.overscroll;
_checkDragOffset(notification.metrics.viewportDimension);
}
} else if (notification is ScrollEndNotification) {
switch (_status) {
case RefreshIndicatorStatus.armed:
if (_positionController.value < 1.0) {
_dismiss(RefreshIndicatorStatus.canceled);
} else {
_show();
}
case RefreshIndicatorStatus.drag:
_dismiss(RefreshIndicatorStatus.canceled);
if (_valueColor.value!.a == _effectiveValueColor.a) {
_show();
} else {
_dismiss(RefreshIndicatorStatus.canceled);
}
case RefreshIndicatorStatus.canceled:
case RefreshIndicatorStatus.done:
case RefreshIndicatorStatus.refresh:
@@ -513,20 +371,9 @@ class RefreshIndicatorState extends State<RefreshIndicator>
return false;
}
bool _start(AxisDirection direction) {
bool _start() {
assert(_status == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
switch (direction) {
case AxisDirection.down:
case AxisDirection.up:
_isIndicatorAtTop = true;
case AxisDirection.left:
case AxisDirection.right:
_isIndicatorAtTop = null;
// we do not support horizontal scroll views.
return false;
}
_dragOffset = 0.0;
_scaleController.value = 0.0;
_positionController.value = 0.0;
@@ -535,24 +382,15 @@ class RefreshIndicatorState extends State<RefreshIndicator>
void _checkDragOffset(double containerExtent) {
assert(
_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed,
_status == RefreshIndicatorStatus.drag,
);
double newValue =
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
if (_status == RefreshIndicatorStatus.armed) {
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
}
_positionController.value = clampDouble(
newValue,
0.0,
1.0,
); // This triggers various rebuilds.
if (_status == RefreshIndicatorStatus.drag &&
_valueColor.value!.a == _effectiveValueColor.a) {
_status = RefreshIndicatorStatus.armed;
widget.onStatusChange?.call(_status);
}
}
// Stop showing the refresh indicator.
@@ -567,7 +405,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
);
setState(() {
_status = newMode;
widget.onStatusChange?.call(_status);
});
switch (_status!) {
case RefreshIndicatorStatus.done:
@@ -580,7 +417,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
0.0,
duration: _kIndicatorScaleDuration,
);
case RefreshIndicatorStatus.armed:
case RefreshIndicatorStatus.drag:
case RefreshIndicatorStatus.refresh:
case RefreshIndicatorStatus.snap:
@@ -588,7 +424,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
}
if (mounted && _status == newMode) {
_dragOffset = null;
_isIndicatorAtTop = null;
setState(() {
_status = null;
});
@@ -601,7 +436,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future;
_status = RefreshIndicatorStatus.snap;
widget.onStatusChange?.call(_status);
_positionController
.animateTo(
1.0 / _kDragSizeFactorLimit,
@@ -640,11 +474,11 @@ class RefreshIndicatorState extends State<RefreshIndicator>
/// When initiated in this manner, the refresh indicator is independent of any
/// actual scroll view. It defaults to showing the indicator at the top. To
/// show it at the bottom, set `atTop` to false.
Future<void> show({bool atTop = true}) {
Future<void> show() {
if (_status != RefreshIndicatorStatus.refresh &&
_status != RefreshIndicatorStatus.snap) {
if (_status == null) {
_start(atTop ? AxisDirection.down : AxisDirection.up);
_start();
}
_show();
}
@@ -655,7 +489,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final Widget child = NotificationListener<ScrollNotification>(
Widget child = NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: _handleIndicatorNotification,
@@ -665,10 +499,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
assert(() {
if (_status == null) {
assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
} else {
assert(_dragOffset != null);
assert(_isIndicatorAtTop != null);
}
return true;
}());
@@ -677,75 +509,33 @@ class RefreshIndicatorState extends State<RefreshIndicator>
_status == RefreshIndicatorStatus.refresh ||
_status == RefreshIndicatorStatus.done;
return Stack(
child = Stack(
clipBehavior: Clip.none,
children: <Widget>[
child,
if (_status != null)
Positioned(
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
top: widget.edgeOffset,
left: 0.0,
right: 0.0,
child: SizeTransition(
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
alignment: .bottomStart,
sizeFactor: _positionFactor, // This is what brings it down.
child: Padding(
padding: _isIndicatorAtTop!
? EdgeInsets.only(top: widget.displacement)
: EdgeInsets.only(bottom: widget.displacement),
padding: EdgeInsets.only(top: displacement),
child: Align(
alignment: _isIndicatorAtTop!
? Alignment.topCenter
: Alignment.bottomCenter,
alignment: Alignment.topCenter,
child: ScaleTransition(
scale: _scaleFactor,
child: AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
final Widget materialIndicator =
RefreshProgressIndicator(
semanticsLabel:
widget.semanticsLabel ??
MaterialLocalizations.of(
context,
).refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator
? null
: _value.value,
valueColor: _valueColor,
backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth,
elevation: widget.elevation,
);
final Widget cupertinoIndicator =
CupertinoActivityIndicator(
color: widget.color,
);
switch (widget._indicatorType) {
case _IndicatorType.material:
return materialIndicator;
case _IndicatorType.adaptive:
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return materialIndicator;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return cupertinoIndicator;
}
case _IndicatorType.noSpinner:
return const SizedBox.shrink();
}
},
builder: (context, child) => RefreshProgressIndicator(
value: showIndeterminateIndicator ? null : _value.value,
valueColor: _valueColor,
backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth,
elevation: widget.elevation,
),
),
),
),
@@ -754,16 +544,84 @@ class RefreshIndicatorState extends State<RefreshIndicator>
),
],
);
if (!widget.isClampingScrollPhysics &&
(Platform.isIOS || Platform.isMacOS)) {
return child;
}
return ScrollConfiguration(
behavior: RefreshScrollBehavior(
desktopDragDevices,
scrollPhysics: RefreshScrollPhysics(
parent: const RangeMaintainingScrollPhysics(),
onDrag: _onDrag,
),
),
child: child,
);
}
bool _onDrag(double offset, double viewportDimension) {
if (_positionController.value > 0.0 && _status == .drag) {
_dragOffset = _dragOffset! + offset;
_checkDragOffset(viewportDimension);
return true;
}
return false;
}
// late final _refreshKey = GlobalKey();
// Widget _m3eRefreshProgressIndicator(bool showIndeterminateIndicator) {
// const indicatorMargin = EdgeInsets.all(4);
// const indicatorPadding = EdgeInsets.all(6);
// const indicatorSize = 41.0;
// final progress = _value.value;
// return Padding(
// padding: indicatorMargin,
// child: SizedBox(
// width: indicatorSize,
// height: indicatorSize,
// child: Material(
// type: MaterialType.circle,
// color: _backgroundColor,
// elevation: widget.elevation,
// child: Padding(
// padding: indicatorPadding,
// child: showIndeterminateIndicator
// ? M3ELoadingIndicator(
// childKey: _refreshKey,
// color: _effectiveValueColor,
// morphs: Morphs.refreshMorphs,
// size: null,
// )
// : RawM3ELoadingIndicator(
// key: _refreshKey,
// morph: Morphs.manualMorph,
// progress: progress,
// angle: -progress * math.pi,
// color: _valueColor.value!,
// size: null,
// ),
// ),
// ),
// ),
// );
// }
}
Widget refreshIndicator({
required RefreshCallback onRefresh,
required Widget child,
}) {
return RefreshIndicator(
displacement: displacement,
onRefresh: onRefresh,
child: child,
);
// ignore: camel_case_types
typedef refreshIndicator = RefreshIndicator;
class RefreshScrollBehavior extends CustomScrollBehavior {
const RefreshScrollBehavior(
super.dragDevices, {
required this.scrollPhysics,
});
final RefreshScrollPhysics scrollPhysics;
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return scrollPhysics;
}
}

View File

@@ -1,901 +0,0 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:PiliPlus/common/widgets/flutter/selectable_text/text_selection.dart';
import 'package:flutter/cupertino.dart'
hide TextSelectionGestureDetectorBuilder;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TextSelectionGestureDetectorBuilder;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
class _TextSpanEditingController extends TextEditingController {
_TextSpanEditingController({required TextSpan textSpan})
: _textSpan = textSpan,
super(text: textSpan.toPlainText(includeSemanticsLabels: false));
final TextSpan _textSpan;
@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
// This does not care about composing.
return TextSpan(style: style, children: <TextSpan>[_textSpan]);
}
@override
set text(String? newText) {
// This should never be reached.
throw UnimplementedError();
}
}
class _SelectableTextSelectionGestureDetectorBuilder
extends CustomTextSelectionGestureDetectorBuilder {
_SelectableTextSelectionGestureDetectorBuilder({
required _SelectableTextState state,
}) : _state = state,
super(delegate: state);
final _SelectableTextState _state;
@override
void onSingleTapUp(TapDragUpDetails details) {
if (!delegate.selectionEnabled) {
return;
}
super.onSingleTapUp(details);
_state.widget.onTap?.call();
}
}
/// A run of selectable text with a single style.
///
/// Consider using [SelectionArea] or [SelectableRegion] instead, which enable
/// selection on a widget subtree, including but not limited to [Text] widgets.
///
/// The [SelectableText] widget displays a string of text with a single style.
/// The string might break across multiple lines or might all be displayed on
/// the same line depending on the layout constraints.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
///
/// The [style] argument is optional. When omitted, the text will use the style
/// from the closest enclosing [DefaultTextStyle]. If the given style's
/// [TextStyle.inherit] property is true (the default), the given style will
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
/// behavior is useful, for example, to make the text bold while using the
/// default font family and size.
///
/// {@macro flutter.material.textfield.wantKeepAlive}
///
/// {@tool snippet}
///
/// ```dart
/// const SelectableText(
/// 'Hello! How are you?',
/// textAlign: TextAlign.center,
/// style: TextStyle(fontWeight: FontWeight.bold),
/// )
/// ```
/// {@end-tool}
///
/// Using the [SelectableText.rich] constructor, the [SelectableText] widget can
/// display a paragraph with differently styled [TextSpan]s. The sample
/// that follows displays "Hello beautiful world" with different styles
/// for each word.
///
/// {@tool snippet}
///
/// ```dart
/// const SelectableText.rich(
/// TextSpan(
/// text: 'Hello', // default text style
/// children: <TextSpan>[
/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
/// ],
/// ),
/// )
/// ```
/// {@end-tool}
///
/// ## Interactivity
///
/// To make [SelectableText] react to touch events, use callback [onTap] to achieve
/// the desired behavior.
///
/// ## Scrolling Considerations
///
/// If this [SelectableText] is not a descendant of [Scaffold] and is being used
/// within a [Scrollable] or nested [Scrollable]s, consider placing a
/// [ScrollNotificationObserver] above the root [Scrollable] that contains this
/// [SelectableText] to ensure proper scroll coordination for [SelectableText]
/// and its components like [TextSelectionOverlay].
///
/// See also:
///
/// * [Text], which is the non selectable version of this widget.
/// * [TextField], which is the editable version of this widget.
/// * [SelectionArea], which enables the selection of multiple [Text] widgets
/// and of other widgets.
class SelectableText extends StatefulWidget {
/// Creates a selectable text widget.
///
/// If the [style] argument is null, the text will use the style from the
/// closest enclosing [DefaultTextStyle].
///
/// If the [showCursor], [autofocus], [dragStartBehavior],
/// [selectionHeightStyle], [selectionWidthStyle] and [data] arguments are
/// specified, the [maxLines] argument must be greater than zero.
const SelectableText(
String this.data, {
super.key,
this.focusNode,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
this.textScaleFactor,
this.textScaler,
this.showCursor = false,
this.autofocus = false,
@Deprecated(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
this.toolbarOptions,
this.minLines,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionColor,
this.selectionHeightStyle,
this.selectionWidthStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollPhysics,
this.scrollBehavior,
this.semanticsLabel,
this.textHeightBehavior,
this.textWidthBasis,
this.onSelectionChanged,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
}) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(
textScaler == null || textScaleFactor == null,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
),
textSpan = null;
/// Creates a selectable text widget with a [TextSpan].
///
/// The [TextSpan.children] attribute of the [textSpan] parameter must only
/// contain [TextSpan]s. Other types of [InlineSpan] are not allowed.
const SelectableText.rich(
TextSpan this.textSpan, {
super.key,
this.focusNode,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
this.textScaleFactor,
this.textScaler,
this.showCursor = false,
this.autofocus = false,
@Deprecated(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
this.toolbarOptions,
this.minLines,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionColor,
this.selectionHeightStyle,
this.selectionWidthStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollPhysics,
this.scrollBehavior,
this.semanticsLabel,
this.textHeightBehavior,
this.textWidthBasis,
this.onSelectionChanged,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
}) : assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(
textScaler == null || textScaleFactor == null,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
),
data = null;
/// The text to display.
///
/// This will be null if a [textSpan] is provided instead.
final String? data;
/// The text to display as a [TextSpan].
///
/// This will be null if [data] is provided instead.
final TextSpan? textSpan;
/// Defines the focus for this widget.
///
/// Text is only selectable when widget is focused.
///
/// The [focusNode] is a long-lived object that's typically managed by a
/// [StatefulWidget] parent. See [FocusNode] for more information.
///
/// To give the focus to this widget, provide a [focusNode] and then
/// use the current [FocusScope] to request the focus:
///
/// ```dart
/// FocusScope.of(context).requestFocus(myFocusNode);
/// ```
///
/// This happens automatically when the widget is tapped.
///
/// To be notified when the widget gains or loses the focus, add a listener
/// to the [focusNode]:
///
/// ```dart
/// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
/// ```
///
/// If null, this widget will create its own [FocusNode] with
/// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
/// to be skipped over during focus traversal.
final FocusNode? focusNode;
/// The style to use for the text.
///
/// If null, defaults [DefaultTextStyle] of context.
final TextStyle? style;
/// {@macro flutter.widgets.editableText.strutStyle}
final StrutStyle? strutStyle;
/// {@macro flutter.widgets.editableText.textAlign}
final TextAlign? textAlign;
/// {@macro flutter.widgets.editableText.textDirection}
final TextDirection? textDirection;
/// {@macro flutter.widgets.editableText.textScaleFactor}
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
final double? textScaleFactor;
/// {@macro flutter.painting.textPainter.textScaler}
final TextScaler? textScaler;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
/// {@macro flutter.widgets.editableText.minLines}
final int? minLines;
/// {@macro flutter.widgets.editableText.maxLines}
final int? maxLines;
/// {@macro flutter.widgets.editableText.showCursor}
final bool showCursor;
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
/// {@macro flutter.widgets.editableText.cursorHeight}
final double? cursorHeight;
/// {@macro flutter.widgets.editableText.cursorRadius}
final Radius? cursorRadius;
/// The color of the cursor.
///
/// The cursor indicates the current text insertion point.
///
/// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also
/// null and [ThemeData.platform] is [TargetPlatform.iOS] or
/// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used.
/// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used.
final Color? cursorColor;
/// The color to use when painting the selection.
///
/// If this property is null, this widget gets the selection color from the
/// inherited [DefaultSelectionStyle] (if any); if none, the selection
/// color is derived from the [CupertinoThemeData.primaryColor] on
/// Apple platforms and [ColorScheme.primary] of [ThemeData.colorScheme] on
/// other platforms.
final Color? selectionColor;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle? selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle? selectionWidthStyle;
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final bool enableInteractiveSelection;
/// {@macro flutter.widgets.editableText.selectionControls}
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// Configuration of toolbar options.
///
/// Paste and cut will be disabled regardless.
///
/// If not set, select all and copy will be enabled by default.
@Deprecated(
'Use `contextMenuBuilder` instead. '
'This feature was deprecated after v3.3.0-0.5.pre.',
)
final ToolbarOptions? toolbarOptions;
/// {@macro flutter.widgets.editableText.selectionEnabled}
bool get selectionEnabled => enableInteractiveSelection;
/// Called when the user taps on this selectable text.
///
/// The selectable text builds a [GestureDetector] to handle input events like tap,
/// to trigger focus requests, to move the caret, adjust the selection, etc.
/// Handling some of those events by wrapping the selectable text with a competing
/// GestureDetector is problematic.
///
/// To unconditionally handle taps, without interfering with the selectable text's
/// internal gesture detector, provide this callback.
///
/// To be notified when the text field gains or loses the focus, provide a
/// [focusNode] and add a listener to that.
///
/// To listen to arbitrary pointer events without competing with the
/// selectable text's internal gesture detector, use a [Listener].
final GestureTapCallback? onTap;
/// {@macro flutter.widgets.editableText.scrollPhysics}
final ScrollPhysics? scrollPhysics;
/// {@macro flutter.widgets.editableText.scrollBehavior}
final ScrollBehavior? scrollBehavior;
/// {@macro flutter.widgets.Text.semanticsLabel}
final String? semanticsLabel;
/// {@macro dart.ui.textHeightBehavior}
final TextHeightBehavior? textHeightBehavior;
/// {@macro flutter.painting.textPainter.textWidthBasis}
final TextWidthBasis? textWidthBasis;
/// {@macro flutter.widgets.editableText.onSelectionChanged}
final SelectionChangedCallback? onSelectionChanged;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
final EditableTextContextMenuBuilder? contextMenuBuilder;
static Widget _defaultContextMenuBuilder(
BuildContext context,
EditableTextState editableTextState,
) {
return AdaptiveTextSelectionToolbar.editableText(
editableTextState: editableTextState,
);
}
/// The configuration for the magnifier used when the text is selected.
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
/// on Android, and builds nothing on all other platforms. To suppress the
/// magnifier, consider passing [TextMagnifierConfiguration.disabled].
///
/// {@macro flutter.widgets.magnifier.intro}
final TextMagnifierConfiguration? magnifierConfiguration;
@override
State<SelectableText> createState() => _SelectableTextState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(
DiagnosticsProperty<String>('data', data, defaultValue: null),
)
..add(
DiagnosticsProperty<String>(
'semanticsLabel',
semanticsLabel,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<FocusNode>(
'focusNode',
focusNode,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null),
)
..add(
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false),
)
..add(
DiagnosticsProperty<bool>(
'showCursor',
showCursor,
defaultValue: false,
),
)
..add(IntProperty('minLines', minLines, defaultValue: null))
..add(IntProperty('maxLines', maxLines, defaultValue: null))
..add(
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null),
)
..add(
EnumProperty<TextDirection>(
'textDirection',
textDirection,
defaultValue: null,
),
)
..add(
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null),
)
..add(
DiagnosticsProperty<TextScaler>(
'textScaler',
textScaler,
defaultValue: null,
),
)
..add(
DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0),
)
..add(
DoubleProperty('cursorHeight', cursorHeight, defaultValue: null),
)
..add(
DiagnosticsProperty<Radius>(
'cursorRadius',
cursorRadius,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<Color>(
'cursorColor',
cursorColor,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<Color>(
'selectionColor',
selectionColor,
defaultValue: null,
),
)
..add(
FlagProperty(
'selectionEnabled',
value: selectionEnabled,
defaultValue: true,
ifFalse: 'selection disabled',
),
)
..add(
DiagnosticsProperty<TextSelectionControls>(
'selectionControls',
selectionControls,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<ScrollPhysics>(
'scrollPhysics',
scrollPhysics,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<ScrollBehavior>(
'scrollBehavior',
scrollBehavior,
defaultValue: null,
),
)
..add(
DiagnosticsProperty<TextHeightBehavior>(
'textHeightBehavior',
textHeightBehavior,
defaultValue: null,
),
);
}
}
class _SelectableTextState extends State<SelectableText>
implements TextSelectionGestureDetectorBuilderDelegate {
EditableTextState? get _editableText => editableTextKey.currentState;
late _TextSpanEditingController _controller;
FocusNode? _focusNode;
FocusNode get _effectiveFocusNode =>
widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
bool _showSelectionHandles = false;
late _SelectableTextSelectionGestureDetectorBuilder
_selectionGestureDetectorBuilder;
// API for TextSelectionGestureDetectorBuilderDelegate.
@override
late bool forcePressEnabled;
@override
final GlobalKey<EditableTextState> editableTextKey =
GlobalKey<EditableTextState>();
@override
bool get selectionEnabled => widget.selectionEnabled;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
@override
void initState() {
super.initState();
_selectionGestureDetectorBuilder =
_SelectableTextSelectionGestureDetectorBuilder(state: this);
_controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
);
_controller.addListener(_onControllerChanged);
_effectiveFocusNode.addListener(_handleFocusChanged);
}
@override
void didUpdateWidget(SelectableText oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data ||
widget.textSpan != oldWidget.textSpan) {
_controller.removeListener(_onControllerChanged);
_controller.dispose();
_controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
);
_controller.addListener(_onControllerChanged);
}
if (widget.focusNode != oldWidget.focusNode) {
(oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
(widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
}
if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
_showSelectionHandles = false;
} else {
_showSelectionHandles = true;
}
}
@override
void dispose() {
_effectiveFocusNode.removeListener(_handleFocusChanged);
_focusNode?.dispose();
_controller.dispose();
super.dispose();
}
void _onControllerChanged() {
final bool showSelectionHandles =
!_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed;
if (showSelectionHandles == _showSelectionHandles) {
return;
}
setState(() {
_showSelectionHandles = showSelectionHandles;
});
}
void _handleFocusChanged() {
if (!_effectiveFocusNode.hasFocus &&
SchedulerBinding.instance.lifecycleState == AppLifecycleState.resumed) {
// We should only clear the selection when this SelectableText loses
// focus while the application is currently running. It is possible
// that the application is not currently running, for example on desktop
// platforms, clicking on a different window switches the focus to
// the new window causing the Flutter application to go inactive. In this
// case we want to retain the selection so it remains when we return to
// the Flutter application.
_controller.value = TextEditingValue(text: _controller.value.text);
}
}
void _handleSelectionChanged(
TextSelection selection,
SelectionChangedCause? cause,
) {
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
if (willShowSelectionHandles != _showSelectionHandles) {
setState(() {
_showSelectionHandles = willShowSelectionHandles;
});
}
widget.onSelectionChanged?.call(selection, cause);
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.base);
}
return;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Do nothing.
}
}
/// Toggle the toolbar when a selection handle is tapped.
void _handleSelectionHandleTapped() {
if (_controller.selection.isCollapsed) {
_editableText!.toggleToolbar();
}
}
bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
return false;
}
if (_controller.selection.isCollapsed) {
return false;
}
if (cause == SelectionChangedCause.keyboard) {
return false;
}
if (cause == SelectionChangedCause.longPress) {
return true;
}
if (_controller.text.isNotEmpty) {
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
// TODO(garyq): Assert to block WidgetSpans from being used here are removed,
// but we still do not yet have nice handling of things like carets, clipboard,
// and other features. We should add proper support. Currently, caret handling
// is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
// should be landed in SkParagraph after the switch is complete.
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasDirectionality(context));
assert(
!(widget.style != null &&
!widget.style!.inherit &&
(widget.style!.fontSize == null ||
widget.style!.textBaseline == null)),
'inherit false style must supply fontSize and textBaseline',
);
final ThemeData theme = Theme.of(context);
final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(
context,
);
final FocusNode focusNode = _effectiveFocusNode;
TextSelectionControls? textSelectionControls = widget.selectionControls;
final bool paintCursorAboveText;
final bool cursorOpacityAnimates;
Offset? cursorOffset;
final Color cursorColor;
final Color selectionColor;
Radius? cursorRadius = widget.cursorRadius;
switch (theme.platform) {
case TargetPlatform.iOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = true;
textSelectionControls ??= cupertinoTextSelectionHandleControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
cupertinoTheme.primaryColor;
selectionColor =
selectionStyle.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
0,
);
case TargetPlatform.macOS:
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
forcePressEnabled = false;
textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
cupertinoTheme.primaryColor;
selectionColor =
selectionStyle.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context),
0,
);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
forcePressEnabled = false;
textSelectionControls ??= materialTextSelectionHandleControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
theme.colorScheme.primary;
selectionColor =
selectionStyle.selectionColor ??
theme.colorScheme.primary.withOpacity(0.40);
case TargetPlatform.linux:
case TargetPlatform.windows:
forcePressEnabled = false;
textSelectionControls ??= desktopTextSelectionHandleControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor =
widget.cursorColor ??
selectionStyle.cursorColor ??
theme.colorScheme.primary;
selectionColor =
selectionStyle.selectionColor ??
theme.colorScheme.primary.withOpacity(0.40);
}
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
TextStyle? effectiveTextStyle = widget.style;
if (effectiveTextStyle == null || effectiveTextStyle.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(
widget.style ?? _controller._textSpan.style,
);
}
final TextScaler? effectiveScaler =
widget.textScaler ??
switch (widget.textScaleFactor) {
null => null,
final double textScaleFactor => TextScaler.linear(textScaleFactor),
};
final Widget child = RepaintBoundary(
child: EditableText(
key: editableTextKey,
style: effectiveTextStyle,
readOnly: true,
toolbarOptions: widget.toolbarOptions,
textWidthBasis:
widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
textHeightBehavior:
widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
showSelectionHandles: _showSelectionHandles,
showCursor: widget.showCursor,
controller: _controller,
focusNode: focusNode,
strutStyle: widget.strutStyle ?? const StrutStyle(),
textAlign:
widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
textDirection: widget.textDirection,
textScaler: effectiveScaler,
autofocus: widget.autofocus,
forceLine: false,
minLines: widget.minLines,
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
selectionColor: widget.selectionColor ?? selectionColor,
selectionControls: widget.selectionEnabled
? textSelectionControls
: null,
onSelectionChanged: _handleSelectionChanged,
onSelectionHandleTapped: _handleSelectionHandleTapped,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
cursorOpacityAnimates: cursorOpacityAnimates,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
backgroundCursorColor: CupertinoColors.inactiveGray,
enableInteractiveSelection: widget.enableInteractiveSelection,
magnifierConfiguration:
widget.magnifierConfiguration ??
TextMagnifier.adaptiveMagnifierConfiguration,
dragStartBehavior: widget.dragStartBehavior,
scrollPhysics: widget.scrollPhysics,
scrollBehavior: widget.scrollBehavior,
autofillHints: null,
contextMenuBuilder: widget.contextMenuBuilder,
),
);
return Semantics(
label: widget.semanticsLabel,
excludeSemantics: widget.semanticsLabel != null,
onLongPress: () {
_effectiveFocusNode.requestFocus();
},
child: _selectionGestureDetectorBuilder.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: child,
),
);
}
}

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