Compare commits

..

425 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
0ae4157384 create release
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-19 14:40:40 +08:00
My-Responsitories
6e1ceb1277 feat: like count (#1640) 2025-10-19 05:45:29 +00:00
bggRGjQaUbCoE
71a170deb5 audio notification
Closes #1635

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-19 11:24:14 +08:00
bggRGjQaUbCoE
9482a706da fix #1613
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 21:02:05 +08:00
bggRGjQaUbCoE
0804484a49 check cmd key
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 19:28:14 +08:00
bggRGjQaUbCoE
cdb9bb3dbc check cmd key
Closes #1630

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 19:16:55 +08:00
bggRGjQaUbCoE
6ca0de96f4 opt pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 17:21:14 +08:00
bggRGjQaUbCoE
d908f58528 opt pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 16:29:14 +08:00
bggRGjQaUbCoE
1368733a24 fix check pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 16:17:32 +08:00
bggRGjQaUbCoE
32e71dbf65 fix #1625
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 15:57:41 +08:00
bggRGjQaUbCoE
c9ce1af2c6 Update build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 14:23:17 +08:00
bggRGjQaUbCoE
416f9e6a8d upgrade dm dep
Closes #1619

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 13:25:10 +08:00
bggRGjQaUbCoE
3ae3955f53 rename release files
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 13:14:05 +08:00
bggRGjQaUbCoE
464f008023 fix #1615
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 11:29:31 +08:00
bggRGjQaUbCoE
52498b3e34 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-18 11:29:31 +08:00
Axiaobo
57c57b02a5 build linux arm64 (#1610)
* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* new file:   .github/workflows/linux_arm64.yml

* modified:   .github/workflows/linux.yml
	deleted:    .github/workflows/linux_arm64.yml
	modified:   .github/workflows/win.yml

* new file:   .github/workflows/linux_arm64.yml

* modified:   .github/workflows/linux_arm64.yml

* modified:   .github/workflows/linux_arm64.yml

* modified:   .github/workflows/linux_arm64.yml
	renamed:    .github/workflows/linux.yml -> .github/workflows/linux_x64.yml

* renamed:    .github/workflows/win.yml -> .github/workflows/win_x64.yml

* modified:   .github/workflows/linux_x64.yml

---------

Co-authored-by: Zhang Yujie <Axiaobo7788@163.com>
2025-10-17 23:23:33 +08:00
My-Responsitories
b8c6868043 fix: cross import 2025-10-17 17:24:13 +08:00
My-Responsitories
8200fbf512 tweak 2025-10-17 14:38:56 +08:00
My-Responsitories
8650c96b7b feat: danmaku seekTo (#1603) 2025-10-17 06:37:25 +00:00
bggRGjQaUbCoE
15fe7787ba pause
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-17 13:28:21 +08:00
dom
d83076cb07 Update android.yml 2025-10-17 12:07:07 +08:00
bggRGjQaUbCoE
8b3b4c28a5 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-17 10:43:43 +08:00
bggRGjQaUbCoE
740c001e2f bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-17 10:34:13 +08:00
bggRGjQaUbCoE
096b057f81 fix #1598
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-17 10:34:13 +08:00
bggRGjQaUbCoE
a161fa5e58 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-17 10:34:08 +08:00
bggRGjQaUbCoE
bebf34db23 disable windows thread merging
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-16 19:09:47 +08:00
bggRGjQaUbCoE
b95061434a upgrade danmaku dep
Closes #1583

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-16 18:56:23 +08:00
bggRGjQaUbCoE
f2a05bb970 fix #1593
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-16 18:55:47 +08:00
bggRGjQaUbCoE
6c361a047b live dm action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-16 17:37:49 +08:00
bggRGjQaUbCoE
3fb9e22378 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-16 13:44:13 +08:00
bggRGjQaUbCoE
b2fb4c9afe opt dm action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-16 13:21:15 +08:00
bggRGjQaUbCoE
0862c0fc87 show backbtn on mine page if needed
Closes #1580

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 21:34:21 +08:00
My-Responsitories
77ec78e3fe opt: singleton FollowTypeController (#1578) 2025-10-15 21:13:56 +08:00
Axiaobo
fb59c208e3 package linux rpm (#1575)
* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml

* modified:   .github/workflows/linux.yml
2025-10-15 20:17:46 +08:00
bggRGjQaUbCoE
112a06f92a custom show tray icon
Closes #1569

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 19:21:11 +08:00
bggRGjQaUbCoE
c10c4a6f89 windows single instance
Closes #1574

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 19:06:28 +08:00
bggRGjQaUbCoE
669c807b23 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 18:40:54 +08:00
bggRGjQaUbCoE
c9de79532a handle relation url
Closes #1566

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 18:28:50 +08:00
bggRGjQaUbCoE
32ce2b87db enable tap dm by def
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 18:28:46 +08:00
bggRGjQaUbCoE
4cfcf18bc9 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 18:06:51 +08:00
bggRGjQaUbCoE
14ae61f891 fix typo
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-15 18:06:51 +08:00
My-Responsitories
a2d5ecc51e feat: ImmediateTapGestureRecognizer (#1572) 2025-10-15 18:06:10 +08:00
My-Responsitories
84f972a3ab fix: report 2025-10-15 16:03:10 +08:00
bggRGjQaUbCoE
5249ceccdb opt audio playlist
Closes #1547

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-14 21:39:30 +08:00
bggRGjQaUbCoE
5035495043 pause
Closes #1559

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-14 21:39:30 +08:00
My-Responsitories
25483d71e9 fix: decompress 2025-10-14 21:23:26 +08:00
My-Responsitories
c3fa976b26 tweaks (#1562)
* opt: downloadImg use cache

* opt: uin8 cast

* non null ext
2025-10-14 11:36:43 +00:00
My-Responsitories
43beb518f4 feat: right click fullscreen (#1561) 2025-10-14 11:27:15 +00:00
bggRGjQaUbCoE
11edabb890 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-14 17:09:06 +08:00
bggRGjQaUbCoE
019cd9fda0 opt search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-14 17:08:42 +08:00
My-Responsitories
9d747c8e2c refa: video (#1555)
* refa: video [skip ci]

* fix: scroll [skip ci]

* mod: only left click

* downgrade

* refa: background play & wakelock [skip ci]

* fix: subtitle [skip ci]

* upgrade deps

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

* mod: long press

* tweak

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

* fix [skip ci]

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

* use right pos

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

* delay showing

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

* fix: null danmaku

* remove

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-14 17:05:31 +08:00
bggRGjQaUbCoE
4cf1c25b36 fix #1545
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 20:35:40 +08:00
bggRGjQaUbCoE
6c6ed46aea fix #1541
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 19:32:29 +08:00
bggRGjQaUbCoE
e1473a453e tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 17:03:36 +08:00
bggRGjQaUbCoE
9f6ef0281a tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 14:36:38 +08:00
bggRGjQaUbCoE
84d5a24bc3 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 13:44:32 +08:00
bggRGjQaUbCoE
ed8c39aa76 opt player gesture
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 12:52:50 +08:00
bggRGjQaUbCoE
23d235b8f4 opt player gesture
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 12:39:22 +08:00
bggRGjQaUbCoE
8bea09b78a tap dm when debug
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 11:49:20 +08:00
bggRGjQaUbCoE
897fda875a opt dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-13 10:55:56 +08:00
bggRGjQaUbCoE
510bfe01be opt btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-12 21:55:39 +08:00
My-Responsitories
f6ca007815 feat: tap danmaku (#1534) 2025-10-12 21:51:59 +08:00
bggRGjQaUbCoE
35b34cb2d4 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-12 21:51:14 +08:00
My-Responsitories
5197cca69c tweaks 2025-10-12 18:56:09 +08:00
My-Responsitories
e5f0742bf6 feat: danmaku api (#1530) 2025-10-12 18:41:40 +08:00
bggRGjQaUbCoE
88d207cc24 upgrade danmaku dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-12 18:32:01 +08:00
bggRGjQaUbCoE
931fcb6f8f opt fab
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-12 17:09:50 +08:00
bggRGjQaUbCoE
e4a960ecf9 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-12 16:13:23 +08:00
My-Responsitories
e44419e088 mod: ui (#1521)
* mod: ui

* fix: -400

* tweaks

* update

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

* tweak [skip ci]

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

* tweak [skip ci]

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-12 12:12:44 +08:00
dom
16f577f3fd feat: audio page (#1518)
* feat: audio page

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

* opt ui

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

* impl intro, share, fav

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

* tweaks

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

* load prev/next

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

---------

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-11 22:16:16 +08:00
My-Responsitories
a65edab7d1 opt: env (#1510)
* opt: env

* fix

* fix: regex

* fix: android

* fix

* fix

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

* fastforge define

* fix

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-10 15:52:26 +08:00
bggRGjQaUbCoE
c0bbf8400a upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-10 13:20:53 +08:00
bggRGjQaUbCoE
1dc2da68ac opt msgNotifyMsg
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-10 13:20:47 +08:00
bggRGjQaUbCoE
3d49529272 show user name
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-09 16:48:42 +08:00
bggRGjQaUbCoE
41768656b4 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-09 11:24:44 +08:00
bggRGjQaUbCoE
c7e7b3f9c5 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-09 11:24:39 +08:00
bggRGjQaUbCoE
e0b0a98f0f opt block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-08 23:03:59 +08:00
bggRGjQaUbCoE
ca0eb1716f feat: pgc skip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-08 22:50:08 +08:00
bggRGjQaUbCoE
06d8296939 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-08 16:11:24 +08:00
bggRGjQaUbCoE
322885f284 opt slide
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 21:25:47 +08:00
bggRGjQaUbCoE
4553b86cb4 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 17:47:31 +08:00
bggRGjQaUbCoE
904756b6ea opt gesture
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 14:10:48 +08:00
bggRGjQaUbCoE
2bfa1bb6c2 tweaks
Closes #1505

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 14:10:29 +08:00
bggRGjQaUbCoE
8439a3d85c opt fs
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 12:32:36 +08:00
bggRGjQaUbCoE
454d6b9de1 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 11:52:29 +08:00
bggRGjQaUbCoE
44c7c44a27 tweaks
Closes #1354

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-07 11:52:24 +08:00
dom
40e5e2f372 Update 功能请求.yml 2025-10-06 14:15:34 +08:00
dom
138739781c Update bug-反馈.yml 2025-10-06 14:14:23 +08:00
dom
355d897ef0 Update 功能请求.yml 2025-10-06 14:10:04 +08:00
dom
a06aef2b25 Update bug-反馈.yml 2025-10-06 14:08:19 +08:00
bggRGjQaUbCoE
6ef9a24ed1 opt play btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 15:03:46 +08:00
bggRGjQaUbCoE
4df2bb0073 opt update
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 14:34:57 +08:00
bggRGjQaUbCoE
f93753ccfd tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 14:11:26 +08:00
bggRGjQaUbCoE
52373dc540 dyn uplist
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 12:06:46 +08:00
bggRGjQaUbCoE
203a997583 opt exit desktop pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 11:10:16 +08:00
bggRGjQaUbCoE
b22a406471 opt trackpad pan
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 10:31:37 +08:00
bggRGjQaUbCoE
a441759eb6 check live playurl
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-05 10:31:24 +08:00
bggRGjQaUbCoE
9057401b16 opt video progress indicator
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 21:03:12 +08:00
bggRGjQaUbCoE
6d0017c256 opt queryBySort
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 20:23:58 +08:00
dom
12b27b1d8d Update 功能请求.yml 2025-10-04 19:52:45 +08:00
dom
884bb53d6f Update bug-反馈.yml 2025-10-04 19:52:16 +08:00
bggRGjQaUbCoE
aa356b5376 global escape
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 18:19:02 +08:00
bggRGjQaUbCoE
2aa9b46433 handle trackpad pan
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 17:51:48 +08:00
Axiaobo
42f5a42dd9 build windows setup (#1454)
* modified:   .github/workflows/win.yml
	new file:   windows/Inno_Setup.iss

* modified:   windows/Inno_Setup.iss

* modified:   .github/workflows/win.yml
	new file:   distribute_options.yaml
	deleted:    windows/Inno_Setup.iss
	new file:   windows/packaging/exe/make_config.yaml

* modified:   windows/packaging/exe/make_config.yaml

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml
	new file:   windows/packaging/exe/ChineseSimplified.isl

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml

* modified:   .github/workflows/win.yml

* fix

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 14:19:29 +08:00
bggRGjQaUbCoE
74f0fb471c fix thumbGlowColor
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 12:38:23 +08:00
bggRGjQaUbCoE
c31e772a63 opt progress bar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 12:32:32 +08:00
bggRGjQaUbCoE
32f6d97256 opt play
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 11:17:12 +08:00
bggRGjQaUbCoE
a28db0dd98 remove videoRelation from recommend
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 11:07:14 +08:00
bggRGjQaUbCoE
aba9493ae0 fix #1429
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 10:58:37 +08:00
bggRGjQaUbCoE
4973176868 fix #1448
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 10:20:14 +08:00
bggRGjQaUbCoE
a000e2262c upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 09:49:46 +08:00
My-Responsitories
a5715868b3 tweaks (#1444)
* opt: proxy

* opt: calcWindowPosition

* fix: height depend on svg

* bump

* fix

* ci: cache linux

* string systemProxyPort

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-04 09:44:41 +08:00
bggRGjQaUbCoE
a928e48159 opt fullscreen
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 21:39:55 +08:00
bggRGjQaUbCoE
16c152d306 opt fullscreen
Closes #1442

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 21:18:29 +08:00
bggRGjQaUbCoE
5747dee03d opt mouse/keyboard event
Closes #1443

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 20:38:24 +08:00
bggRGjQaUbCoE
06c545acd4 fix #1441
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 20:01:24 +08:00
bggRGjQaUbCoE
54c3c314e1 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 19:56:18 +08:00
bggRGjQaUbCoE
11c4fae547 fix progress bar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 19:00:04 +08:00
My-Responsitories
8f87d248a1 fix: like details (#1438)
* fix: like details

* check counts

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 16:46:14 +08:00
bggRGjQaUbCoE
ec37af5900 fix #1439
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 16:17:33 +08:00
bggRGjQaUbCoE
1b213793d4 opt mouse event
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 15:52:04 +08:00
bggRGjQaUbCoE
aaa8998cb1 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 15:10:11 +08:00
VillagerTom
94760a4136 tweak gitignore and vscode config (#1433)
* tweak gitignore and vscode config

* restore settings

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 11:48:28 +08:00
bggRGjQaUbCoE
bdbd6cd377 opt player gesture
Closes #1427

opt dyn/reply check

Closes #1430

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 11:26:36 +08:00
bggRGjQaUbCoE
d69d81912d fix #1418
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 18:41:55 +08:00
bggRGjQaUbCoE
198a38b103 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 18:41:55 +08:00
NLsdt
750e67d835 feat: add deb build (#1422) 2025-10-02 18:39:42 +08:00
bggRGjQaUbCoE
5d5adbc73f cache desktop volume
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 10:55:30 +08:00
bggRGjQaUbCoE
8c7db34e5a tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 10:35:41 +08:00
Integral
a18863f292 chore: add config file .fvmrc to specify Flutter version (#1417) 2025-10-02 10:31:07 +08:00
bggRGjQaUbCoE
15ee6a679e bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 10:03:30 +08:00
bggRGjQaUbCoE
4dfeb284e7 update proto
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 10:03:18 +08:00
bggRGjQaUbCoE
eae075c380 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-02 10:02:49 +08:00
bggRGjQaUbCoE
d9bff6237d opt play all
Closes #1383

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 23:07:30 +08:00
bggRGjQaUbCoE
35df23194f fix player listener
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 22:44:14 +08:00
bggRGjQaUbCoE
12a68257a3 fix typo
Closes #1406

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 21:51:03 +08:00
bggRGjQaUbCoE
022108607f tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 21:30:27 +08:00
My-Responsitories
4e15422d2d fix: search (#1412) 2025-10-01 13:29:08 +00:00
bggRGjQaUbCoE
e1944b0c8d opt viewpoint
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 20:06:15 +08:00
bggRGjQaUbCoE
0fd3f3ffd1 upgrade windows mpv
Closes #1341

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 19:23:16 +08:00
bggRGjQaUbCoE
ed9be72172 fix calcWindowPosition
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 13:17:12 +08:00
bggRGjQaUbCoE
5d95e624db fix #1395
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-01 12:56:37 +08:00
bggRGjQaUbCoE
929c51e059 skip segments with keyboard
Closes #1379

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 22:20:58 +08:00
bggRGjQaUbCoE
15b05cc454 fix #1390
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 20:13:18 +08:00
bggRGjQaUbCoE
260742dc4b disable RichTextField undo
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 16:59:05 +08:00
bggRGjQaUbCoE
d833f3651c fix close
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 15:20:42 +08:00
Integral
698e11885a fix: prevent running multiple app instances on Linux (#1385) 2025-09-30 13:58:16 +08:00
bggRGjQaUbCoE
c103551f6a opt gesture
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 13:54:44 +08:00
bggRGjQaUbCoE
299ee09749 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 13:03:48 +08:00
bggRGjQaUbCoE
06b258cff1 build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 13:03:48 +08:00
bggRGjQaUbCoE
be03909fdc opt gesture
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-30 13:03:48 +08:00
My-Responsitories
19f7720fb2 fix: type cast (#1384) 2025-09-30 04:37:15 +00:00
My-Responsitories
89e6d5c160 tweaks (#1381)
* opt: boundary

* opt: subtitle

* tweaks
2025-09-29 14:13:02 +00:00
bggRGjQaUbCoE
1d723b704b opt video gesture
Closes #1374

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-29 15:44:39 +08:00
My-Responsitories
05636b33c0 fix: loudnorm out of range (#1371) 2025-09-29 06:02:37 +00:00
bggRGjQaUbCoE
2817c8f5b1 opt set windows brightness
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-29 13:20:57 +08:00
bggRGjQaUbCoE
fe0c636ad6 custom show window title bar
Closes #1362

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-29 13:20:41 +08:00
bggRGjQaUbCoE
5465492d70 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 23:07:49 +08:00
bggRGjQaUbCoE
862a9fa731 opt push live dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 23:07:43 +08:00
bggRGjQaUbCoE
0aebadb005 scroll to update volume
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 22:18:29 +08:00
bggRGjQaUbCoE
24be7a9cf2 opt handle mouse back
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 22:18:29 +08:00
Integral
9d5f4ad977 docs(README): add Linux support status (#1364) 2025-09-28 22:16:43 +08:00
My-Responsitories
22c57bf468 feat: loudnorm (#1358)
* feat: loudnorm

* fix

* fix: android only

* fix: toString
2025-09-28 22:16:33 +08:00
bggRGjQaUbCoE
046412b709 fix image preview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 18:11:37 +08:00
bggRGjQaUbCoE
b3622ef61d opt refreshDisplacement
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 17:56:14 +08:00
bggRGjQaUbCoE
138b7935f3 opt sub reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 17:51:44 +08:00
bggRGjQaUbCoE
328034f3ed opt handle mouse back
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 17:46:52 +08:00
bggRGjQaUbCoE
e1f748d7e4 opt subtitle
Closes #1357

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 17:39:58 +08:00
bggRGjQaUbCoE
5f8dc76891 opt mouse control
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 17:39:58 +08:00
bggRGjQaUbCoE
2031604ea2 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 16:37:38 +08:00
bggRGjQaUbCoE
efbf392677 opt video title
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 15:22:08 +08:00
bggRGjQaUbCoE
fb8c5f50ba drag to refresh
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 15:11:34 +08:00
bggRGjQaUbCoE
09920f9fb5 mouse back
Closes #1317

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 14:59:34 +08:00
dom
1e2618a33f mac release 2025-09-28 13:37:54 +08:00
bggRGjQaUbCoE
fb79fd9b9d fix #1326
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 12:44:53 +08:00
bggRGjQaUbCoE
8f65a5d202 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-28 12:30:41 +08:00
bggRGjQaUbCoE
b32c1871ae opt filter dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 21:08:39 +08:00
bggRGjQaUbCoE
8e26a7bc9d tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 21:08:39 +08:00
bggRGjQaUbCoE
2333736a72 show article heading
Closes #1338

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 20:27:24 +08:00
bggRGjQaUbCoE
7fedfb8963 pause on minimize
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 20:26:57 +08:00
bggRGjQaUbCoE
670f788558 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 20:26:47 +08:00
bggRGjQaUbCoE
c7e3d9dbc1 build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 20:25:34 +08:00
bggRGjQaUbCoE
0ebb2afe39 fix #1343
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-27 11:47:14 +08:00
My-Responsitories
e3e6bb0e39 feat: webview geetest (#1342)
* feat: webview geetest

* opt: geetest

* fix: linux

* remove pwd mobile check

* fix linux check
2025-09-27 10:57:41 +08:00
bggRGjQaUbCoE
ee8af925be fix win cookie manage
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 20:27:47 +08:00
bggRGjQaUbCoE
34bdfe1641 fix #1337
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 20:03:28 +08:00
bggRGjQaUbCoE
d33e2071b6 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 20:00:54 +08:00
bggRGjQaUbCoE
59fd89ae5d windows webview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 18:13:29 +08:00
bggRGjQaUbCoE
93e64a0988 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 12:57:12 +08:00
bggRGjQaUbCoE
86a79a9733 opt in-app fullscreen
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 10:29:33 +08:00
NLsdt
d961b6d7a9 chore: add linux build CI file (#1332)
* feat: add linux build file

* update

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 10:13:53 +08:00
bggRGjQaUbCoE
a799b15155 opt image crop
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 10:01:25 +08:00
bggRGjQaUbCoE
33c8d69a58 opt scrollBehavior
Closes #1328

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-26 09:47:20 +08:00
NLsdt
7637c44645 linux check (#1331) 2025-09-26 09:46:47 +08:00
My-Responsitories
5fd3d32200 feat: inapp fullscreen (#1330) 2025-09-25 16:18:11 +00:00
My-Responsitories
4ae3bd2845 tweak (#1325)
* tweak

* opt: async

* tweak

* opt: PopularSeries tile

* tweak

* opt: sc

* mod: more account type

* tweak

* opt: qrcode

* tweak

* partial revert: opt: sc

* fix

* fix

* mod: window enqueue
2025-09-26 00:02:55 +08:00
bggRGjQaUbCoE
67c25bd130 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-25 13:48:30 +08:00
bggRGjQaUbCoE
05cd631439 opt update check
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-25 11:32:06 +08:00
bggRGjQaUbCoE
000fa9fe5c upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-25 11:01:02 +08:00
bggRGjQaUbCoE
61bc4ce5b1 opt msg item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-25 10:58:26 +08:00
bggRGjQaUbCoE
11fbd2ebed fix member video jump
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-24 14:22:08 +08:00
bggRGjQaUbCoE
6ced89f30b seek from ugc intro
Closes #1303

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-24 11:13:41 +08:00
bggRGjQaUbCoE
ec1bdb243f fix #1300
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-24 10:20:23 +08:00
My-Responsitories
1345fd36e4 opt: account (#1306)
* opt: account

* opt: account

* opt: live api

* opt: buvid

* Revert "opt: buvid"

This reverts commit da1ea68f8b.

* tweak
2025-09-23 15:58:40 +08:00
bggRGjQaUbCoE
6b4fb0d611 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-23 14:59:53 +08:00
bggRGjQaUbCoE
b9eaa368b1 opt progress bar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-23 11:47:50 +08:00
bggRGjQaUbCoE
66752093e4 bottom sheet patch
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-22 13:11:51 +08:00
bggRGjQaUbCoE
4ca9dfecb4 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-22 12:48:48 +08:00
bggRGjQaUbCoE
2efb04f77e opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-21 21:18:35 +08:00
bggRGjQaUbCoE
f4e3484b01 opt video header
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-21 13:44:27 +08:00
bggRGjQaUbCoE
9f715ddd5b opt player keyboard event
opt triple

opt desktop pip

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-21 13:18:51 +08:00
bggRGjQaUbCoE
787be7ac11 opt switch style
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 21:59:13 +08:00
bggRGjQaUbCoE
c54d77f393 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 17:37:51 +08:00
My-Responsitories
96539cc64c feat: OrderedMultiSelectDialog (#1290)
* tweak

* feat: OrderedMultiSelectDialog
2025-09-20 09:24:45 +00:00
bggRGjQaUbCoE
96586f130f opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 16:27:08 +08:00
bggRGjQaUbCoE
36de899a35 desktop pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 15:31:05 +08:00
bggRGjQaUbCoE
0745d83e4b feat: ai translate
Closes #1285

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 14:10:31 +08:00
bggRGjQaUbCoE
9b171e04be opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 13:16:35 +08:00
bggRGjQaUbCoE
d62d0eddc2 feat: popular series/precious
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 13:16:35 +08:00
bggRGjQaUbCoE
51c605f5d0 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-20 13:16:29 +08:00
Kofua
099c7b4dff fix small black flicker window on startup for macOS (#1287) 2025-09-20 11:08:33 +08:00
bggRGjQaUbCoE
6559aa767d opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-19 12:21:20 +08:00
bggRGjQaUbCoE
c3bcd323fb fix reset sub/dm settings
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-19 12:19:23 +08:00
bggRGjQaUbCoE
5ec04e3a53 opt webdav fileName
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-19 12:19:17 +08:00
bggRGjQaUbCoE
7a4098434f fix ios selectionHandleColor
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 21:02:05 +08:00
bggRGjQaUbCoE
9c552a89e1 custom keyboard control
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 18:23:24 +08:00
bggRGjQaUbCoE
2d625e0241 fix android build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 18:08:24 +08:00
bggRGjQaUbCoE
fef6a8c22a upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 17:38:09 +08:00
bggRGjQaUbCoE
cef7e478f5 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 17:37:53 +08:00
bggRGjQaUbCoE
64e893e36f separate live dm
Closes #1217

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 17:37:53 +08:00
bggRGjQaUbCoE
a6182b20c0 opt emoji tooltip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 15:06:34 +08:00
bggRGjQaUbCoE
b2e3273dba fix #1278
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 14:31:32 +08:00
bggRGjQaUbCoE
ffd1747bb3 opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 14:29:18 +08:00
bggRGjQaUbCoE
59bbe51702 opt viewpoint widget
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 13:19:45 +08:00
bggRGjQaUbCoE
1824c83cd0 opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-18 12:57:17 +08:00
bggRGjQaUbCoE
f05cd0322a bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 21:08:06 +08:00
bggRGjQaUbCoE
e165666155 fix dyn additional panel
tweaks

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 21:07:55 +08:00
bggRGjQaUbCoE
b043dc38c4 opt player
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 17:21:49 +08:00
bggRGjQaUbCoE
d9a0db03f8 opt set window size/pos
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 12:24:35 +08:00
bggRGjQaUbCoE
8a3cf34cb1 cache window size/pos
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 11:36:34 +08:00
bggRGjQaUbCoE
470140a068 multi hwdecs
Closes #1268

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 11:36:34 +08:00
bggRGjQaUbCoE
720591b8fe allow desktop login by pwd
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-17 11:36:34 +08:00
My-Responsitories
349a4dfc0b feat: music search (#1270)
* tweak

* feat: music search
2025-09-16 16:33:33 +00:00
My-Responsitories
3897efd82f opt: hwdec (#1269)
* mod: more HwDecType

* opt: hwdec
2025-09-16 16:29:16 +00:00
bggRGjQaUbCoE
7ca6d2958f upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-16 11:51:53 +08:00
bggRGjQaUbCoE
efac7f7700 add hwdecs
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-16 11:51:53 +08:00
My-Responsitories
9e4a32e3e4 feat: show hwdec (#1261) 2025-09-15 15:51:42 +00:00
My-Responsitories
37fb63c3b1 tweaks (#1252)
* opt: cache

* opt: MediaListPanel

* feat: nested replyreply panel

* tweaks

* opt: abstract class

* opt: PageStorageKey

* opt: contextExt

* opt: EpisodePanel

* opt

* opt: context instead GlobalKey

* feat: jump to reply

* refa: reply_reply

* fix: jump

* fix: index

* update

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

* opt: keepalive

* reapply: nested replyreply

* mod: spacing

* opt: CommonSlidePageState

* fix drag bottomsheet

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

* opt reply jump

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

* opt reply2reply

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

* tweaks

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

* tweaks

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

* reapply: jumpToReply

* fix: padding

* fix: anim

* fix some panels

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

* opt: implements Scaffold

* opt: remove keepalive

* revert: GlobalKey

* tweaks

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-15 18:45:28 +08:00
Kofua
b9a55ccbce Some tweaks for macOS target (#1260)
* fix macOS save video cover

* not hide title bar for macOS
2025-09-14 18:14:19 +08:00
bggRGjQaUbCoE
d3f4ba4b4a refa: reply2reply panel
tweaks

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-13 18:56:15 +08:00
bggRGjQaUbCoE
0f2908dbc1 opt win selection
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-13 13:52:43 +08:00
bggRGjQaUbCoE
d0c108538d opt follow page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-13 13:00:46 +08:00
bggRGjQaUbCoE
b6352a6a43 opt ui
opt video keyboard event

opt code

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-13 12:36:26 +08:00
dom
d6bff33d29 win (#1240)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-12 18:12:21 +08:00
bggRGjQaUbCoE
da17725616 opt query pgc timeline
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-10 19:56:27 +08:00
bggRGjQaUbCoE
f0f5224677 fix #1164
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-07 20:08:12 +08:00
bggRGjQaUbCoE
bd0c620097 opt playback keyboard event
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-07 19:38:18 +08:00
bggRGjQaUbCoE
7a4fc6f7e2 opt borderRadius
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-07 15:42:18 +08:00
Kofua
d6b87b48f5 add physical keyboard controls for playback (#1203) 2025-09-07 15:36:51 +08:00
Kofua
d285f00086 fix dolby videos playing (#1202) 2025-09-07 11:35:12 +08:00
bggRGjQaUbCoE
e02835ddc4 fix parse live dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-06 17:13:17 +08:00
Kofua
b6de7ef40d fix macOS target app name (#1199) 2025-09-06 16:29:48 +08:00
bggRGjQaUbCoE
594a891abd fix #1198
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-06 16:28:03 +08:00
bggRGjQaUbCoE
215ed665de deprecate rcmdDyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-06 15:05:37 +08:00
bggRGjQaUbCoE
89f72df2b9 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-06 15:00:02 +08:00
Kofua
0659ec9d8a opt ai conclusion failure toast (#1197) 2025-09-06 14:13:54 +08:00
bggRGjQaUbCoE
a74cbe215e opt pgc index
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-06 12:18:54 +08:00
bggRGjQaUbCoE
5bf09b98f4 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-06 11:43:05 +08:00
bggRGjQaUbCoE
224bd88473 fix ios volume
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-05 14:46:07 +08:00
bggRGjQaUbCoE
6e6ff369d3 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-05 11:08:12 +08:00
bggRGjQaUbCoE
7e1e42181c fixes
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-05 09:57:01 +08:00
My-Responsitories
172389b12b tweaks (#1187)
* opt: marquee

* fix: bangumi seek

* opt: post panel

* opt: remove deprecated code

* opt: singleton dynController

* fix: music scheme

* feat: MemberVideo jump keep position

* tweak
2025-09-04 12:29:02 +00:00
My-Responsitories
e8a674ca2a feat: support dynaudnorm & webp (#1186)
* feat: support dynaudnorm & webp

* Revert "remove audio_normalization"

This reverts commit 477b59ce89.

* feat: save webp

* mod: strokeWidth

* feat: webp preset

* feat: save webp select qa

* upgrade volume_controller
2025-09-04 12:09:50 +00:00
bggRGjQaUbCoE
f0828ea18c opt live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-04 13:01:10 +08:00
bggRGjQaUbCoE
f0c4d3412d tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-04 13:01:10 +08:00
bggRGjQaUbCoE
28a58ade84 opt remove sc
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 20:12:50 +08:00
bggRGjQaUbCoE
04830c7789 opt dyn panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 16:45:16 +08:00
My-Responsitories
a635767561 opt: scheme 2025-09-03 13:09:14 +08:00
My-Responsitories
2b1759bd10 fix: setState 2025-09-03 13:09:14 +08:00
My-Responsitories
5a6c73b8cf fix: null aid 2025-09-03 13:09:14 +08:00
My-Responsitories
cf4ad87b20 fix action bar 2025-09-03 13:09:14 +08:00
My-Responsitories
47ad1adfdc feat: remove after save 2025-09-03 13:09:14 +08:00
My-Responsitories
f50965862d opt: type 2025-09-03 13:09:14 +08:00
My-Responsitories
0a7f18a035 mod: retry 2025-09-03 13:09:14 +08:00
bggRGjQaUbCoE
f8226fcade opt marquee
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 13:09:14 +08:00
My-Responsitories
498ab2818e mod: marquee use velocity 2025-09-03 13:09:14 +08:00
bggRGjQaUbCoE
8d94c0405f opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 13:09:08 +08:00
bggRGjQaUbCoE
80fa0240e9 feat: super chat
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 08:43:52 +08:00
bggRGjQaUbCoE
e6af0ef15b feat: new img grid view
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 08:07:26 +08:00
dom
3cbfd158e1 Update ios.yml 2025-09-03 08:07:26 +08:00
bggRGjQaUbCoE
2956b43f42 feat: member shop
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-09-03 08:07:26 +08:00
COYG⚡️
86346d568e docs: add star-history (#1185) 2025-09-02 22:51:15 +08:00
bggRGjQaUbCoE
fc6f51787b upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 16:37:20 +08:00
bggRGjQaUbCoE
e72203afdb fix #1173
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 15:31:55 +08:00
bggRGjQaUbCoE
6741333367 opt marquee
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 15:31:55 +08:00
bggRGjQaUbCoE
6e1bc8d0e7 fix #1169
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-29 10:41:33 +08:00
bggRGjQaUbCoE
477b59ce89 remove audio_normalization
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-28 21:37:54 +08:00
bggRGjQaUbCoE
70881ead22 opt fav intro
Closes #1159

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

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

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

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

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

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

* mod: semantics

* opt: DanmakuMsg type

* opt: avoid cast

* opt: unnecessary_lambdas

* opt: use isEven

* opt: logger

* opt: invalid common page

* tweak

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

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

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

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

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

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

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

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

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

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

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

* opt: type 2

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

add audio qa type

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

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

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

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

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

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

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

Closes #1061
Closes #1062

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-08-09 16:42:56 +08:00
746 changed files with 171057 additions and 86661 deletions

3
.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.35.6"
}

View File

@@ -9,10 +9,24 @@ body:
attributes:
label: 检查清单
options:
- label: 之前没有人提交过类似或相同的 bug report。
- label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同问题
required: true
- label: 正在使用最新版本。
required: true
- label: 已排除网络问题
required: true
- label: 已排除账号问题
required: true
- label: 已排除设置问题
required: true
- type: checkboxes
id: assign
attributes:
label: Assign
options:
- label: self-assign
required: false
- type: textarea
id: version
@@ -21,14 +35,6 @@ body:
validations:
required: true
- type: textarea
id: bug
attributes:
label: 问题描述
description: 请提供一个清晰而简明的问题描述。
validations:
required: true
- type: textarea
id: steps
attributes:
@@ -45,6 +51,14 @@ body:
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为
description: 请描述实际的行为或结果。
validations:
required: true
- type: textarea
id: log
attributes:

View File

@@ -9,10 +9,20 @@ body:
attributes:
label: 检查清单
options:
- label: 之前没有人提交过类似或相同功能请求
- label: 搜索了 [历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同功能请求
required: true
- label: 正在使用最新版本。
required: true
- label: 设置中未搜索到该功能
required: true
- type: checkboxes
id: assign
attributes:
label: Assign
options:
- label: self-assign
required: false
- type: textarea
id: desc
@@ -22,14 +32,6 @@ body:
validations:
required: true
- type: textarea
id: propose
attributes:
label: 目标
description: 请描述你希望通过这个功能实现的目标。
validations:
required: true
- type: textarea
id: solution
attributes:

View File

@@ -1,79 +0,0 @@
name: Android Release
on:
workflow_dispatch:
jobs:
android:
runs-on: ubuntu-latest
steps:
- name: 代码迁出
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 构建Java环境
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "17"
- name: 检查缓存
uses: actions/cache@v4
id: cache-flutter
with:
path: /root/flutter-sdk # Flutter SDK 的路径
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
- name: 安装Flutter
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version-file: pubspec.yaml
- name: 下载项目依赖
run: flutter pub get
- name: 更新版本号
run: |
version_name=$(yq e .version pubspec.yaml | cut -d "+" -f 1)
sed -i "s/version: .*/version: $version_name-$(git rev-parse --short HEAD)+$(git rev-list --count HEAD)/g" pubspec.yaml
- name: Write key
run: |
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
echo storeFile='key.jks' >> android/key.properties
echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties
echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties
echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties
fi
- name: flutter build apk
run: |
chmod +x lib/scripts/build.sh
lib/scripts/build.sh
flutter build apk --release --split-per-abi
- name: 上传
uses: actions/upload-artifact@v4
with:
name: app-arm64-v8a
path: |
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
- name: 上传
uses: actions/upload-artifact@v4
with:
name: app-armeabi-v7a
path: |
build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
- name: 上传
uses: actions/upload-artifact@v4
with:
name: app-x86_64
path: |
build/app/outputs/flutter-apk/app-x86_64-release.apk

181
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,181 @@
name: Build
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths-ignore:
- "**.md"
workflow_dispatch:
inputs:
build_android:
description: "Build Android"
required: false
default: true
type: boolean
build_ios:
description: "Build iOS"
required: false
default: true
type: boolean
build_mac:
description: "Build Mac"
required: false
default: true
type: boolean
build_win_x64:
description: "Build Win-x64"
required: false
default: true
type: boolean
build_linux_x64:
description: "Build Linux-x64"
required: false
default: true
type: boolean
build_linux_arm64:
description: "Build Linux-arm64"
required: false
default: true
type: boolean
tag:
description: "tag"
required: false
default: ""
type: string
jobs:
android:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_android == 'true' }}
name: Release Android
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: 代码迁出
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: 构建Java环境
uses: actions/setup-java@v5
with:
distribution: "zulu"
java-version: "17"
cache: "gradle"
cache-dependency-path: |
android/*.gradle*
android/**/gradle-wrapper.properties
- name: 安装Flutter
uses: subosito/flutter-action@v2
id: flutter-action
with:
channel: stable
flutter-version-file: pubspec.yaml
cache: true
- name: apply bottom sheet patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
continue-on-error: true
- name: Write key
if: github.event_name == 'workflow_dispatch'
run: |
if [ ! -z "${{ secrets.SIGN_KEYSTORE_BASE64 }}" ]; then
echo "${{ secrets.SIGN_KEYSTORE_BASE64 }}" | base64 --decode > android/app/key.jks
echo storeFile='key.jks' >> android/key.properties
echo storePassword='${{ secrets.KEYSTORE_PASSWORD }}' >> android/key.properties
echo keyAlias='${{ secrets.KEY_ALIAS }}' >> android/key.properties
echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> android/key.properties
fi
- name: Set and Extract version
shell: pwsh
run: lib/scripts/build.ps1 android
- name: flutter build apk
run: flutter build apk --release --split-per-abi --dart-define-from-file=pili_release.json --pub
- name: rename
run: |
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
abi=$(echo "$file" | sed -E 's|.*app-(.*)-release\.apk|\1|')
mv "$file" "PiliPlus_android_${{ env.version }}_${abi}.apk"
done
shell: bash
- name: Release
if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_android_*.apk
- name: 上传
uses: actions/upload-artifact@v4
with:
name: Android_arm64-v8a
path: |
PiliPlus_android_*_arm64-v8a.apk
- name: 上传
uses: actions/upload-artifact@v4
with:
name: Android_armeabi-v7a
path: |
PiliPlus_android_*_armeabi-v7a.apk
- name: 上传
uses: actions/upload-artifact@v4
with:
name: Android_x86_64
path: |
PiliPlus_android_*_x86_64.apk
ios:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_ios == 'true' }}
uses: ./.github/workflows/ios.yml
permissions: write-all
with:
tag: github.event.inputs.tag
mac:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_mac == 'true' }}
uses: ./.github/workflows/mac.yml
permissions: write-all
with:
tag: github.event.inputs.tag
win_x64:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
uses: ./.github/workflows/win_x64.yml
permissions: write-all
with:
tag: github.event.inputs.tag
linux_x64:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_linux_x64 == 'true' }}
uses: ./.github/workflows/linux_x64.yml
permissions: write-all
with:
tag: github.event.inputs.tag
linux_arm64:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_linux_arm64 == 'true' }}
uses: ./.github/workflows/linux_arm64.yml
permissions: write-all
with:
tag: github.event.inputs.tag

View File

@@ -1,11 +1,14 @@
name: Build for iOS
on:
workflow_dispatch:
workflow_call:
inputs:
branch:
tag:
description: "tag"
required: false
default: 'main'
default: ""
type: string
workflow_dispatch:
jobs:
build-macos-app:
@@ -13,9 +16,8 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
- name: Setup flutter
@@ -24,21 +26,27 @@ jobs:
channel: stable
flutter-version-file: pubspec.yaml
- name: 更新版本号
run: |
version_name=$(yq e '.version' pubspec.yaml | cut -d "+" -f 1)
sed -i '' "s/version: .*/version: $version_name+$(git rev-list --count HEAD)/" pubspec.yaml
- name: Set and Extract version
shell: pwsh
run: lib/scripts/build.ps1
- name: Build iOS
run: |
chmod +x lib/scripts/build.sh
lib/scripts/build.sh
flutter build ios --release --no-codesign
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
ln -sf ./build/ios/iphoneos Payload
zip -r9 ios-release-no-sign.ipa Payload/runner.app
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
- name: Release
if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_ios_*.ipa
- name: Upload ios release
uses: actions/upload-artifact@v4
with:
name: ios-release
path: ios-release-no-sign.ipa
name: iOS-release
path: PiliPlus_ios_*.ipa

210
.github/workflows/linux_arm64.yml vendored Normal file
View File

@@ -0,0 +1,210 @@
name: Build for Linux Arm64
on:
workflow_call:
inputs:
tag:
description: "tag"
required: false
default: ""
type: string
workflow_dispatch:
jobs:
build-linux-app:
name: Release Linux Arm64
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev rpm patchelf
sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \
libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \
libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \
libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \
libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \
libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \
libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \
libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \
libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \
libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \
linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \
x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \
unixodbc-dev libpq-dev libxxhash-dev libaom-dev \
libgtk-3-0 libblkid1 liblzma5 libmpv-dev
shell: bash
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
channel: master
flutter-version-file: pubspec.yaml
cache: true
- name: Set and Extract version
shell: pwsh
run: lib/scripts/build.ps1
#TODO: deb and rpm packages need to be build
- name: Build Linux
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
- name: Package .tar.gz
run: tar -zcvf PiliPlus_linux_${{ env.version }}_arm64.tar.gz -C build/linux/arm64/release/bundle .
- name: Packege deb
run: |
printf "建立构建目录...\n"
mkdir "PiliPlus_linux_${{ env.version }}_arm64"
pushd "PiliPlus_linux_${{ env.version }}_arm64"
mkdir -p opt/PiliPlus
mkdir -p usr/share/applications
mkdir -p usr/share/icons/hicolor/512x512/apps
printf "复制文件...\n"
cp -r ../build/linux/arm64/release/bundle/* opt/PiliPlus
cp -r ../assets/linux/DEBIAN .
cp ../assets/linux/piliplus.desktop usr/share/applications
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
printf "修改控制文件...\n"
# 替换版本号
sed -i "2s/version_need_change/${{ env.version }}/g" DEBIAN/control
# 计算安装大小并替换
SIZE_KB=$(du -s -b --apparent-size . | awk '{print int($1)}')
SIZE_KB=$(($SIZE_KB - $(du -s -b --apparent-size DEBIAN | awk '{print int($1)}')))
SIZE_KB=$(echo $SIZE_KB | awk '{print int($1/1024 + 0.999)}')
printf "\t安装大小: %s KB\n" "$SIZE_KB"
sed -i "9s/size_need_change/${SIZE_KB}/g" DEBIAN/control
printf "生成并写入 md5sums ...\n"
md5sum opt/PiliPlus/piliplus >> DEBIAN/md5sums
md5sum opt/PiliPlus/lib/* >> DEBIAN/md5sums
md5sum opt/PiliPlus/data/icudtl.dat >> DEBIAN/md5sums
printf "设置权限...\n"
chmod 0644 DEBIAN/control
chmod 0644 DEBIAN/md5sums
chmod 0755 DEBIAN/postinst
chmod 0755 DEBIAN/postrm
chmod 0755 DEBIAN/prerm
printf "打包 deb 文件...\n"
popd
dpkg-deb --build --verbose --root-owner-group "PiliPlus_linux_${{ env.version }}_arm64"
printf "完成: PiliPlus_linux_%s_arm64.deb\n" "${{ env.version }}"
shell: bash
- name: Packege rpm
run: |
printf "建立 RPM 构建目录...\n"
RPM_BUILD_ROOT="$PWD/rpm_build"
mkdir -p "$RPM_BUILD_ROOT/BUILD" "$RPM_BUILD_ROOT/RPMS" "$RPM_BUILD_ROOT/SOURCES" "$RPM_BUILD_ROOT/SPECS" "$RPM_BUILD_ROOT/SRPMS"
printf "准备源码归档(仅包含运行时与元数据)...\n"
DATE="$(date '+%a %b %d %Y')"
SRC_DIR="$PWD/piliplus-${{ env.version }}"
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
cp -r build/linux/arm64/release/bundle/* "$SRC_DIR/bundle/"
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
printf "生成 spec 文件...\n"
cat > "$RPM_BUILD_ROOT/SPECS/piliplus.spec" <<EOF
Name: piliplus
Version: ${{ env.version }}
Release: 1%{?dist}
Summary: PiliPlus Linux Version
License: GPL-3.0
Source0: piliplus-${{ env.version }}.tar.gz
Requires: desktop-file-utils, hicolor-icon-theme
%description
使用 Flutter 开发的 BiliBili 第三方客户端
%prep
%setup -q -n piliplus-${{ env.version }}
%build
%install
mkdir -p %{buildroot}/opt/PiliPlus
cp -r bundle/* %{buildroot}/opt/PiliPlus/
# 二进制权限与命令行入口
chmod 755 %{buildroot}/opt/PiliPlus/piliplus
mkdir -p %{buildroot}/usr/bin
ln -sf /opt/PiliPlus/piliplus %{buildroot}/usr/bin/piliplus
# 桌面集成
mkdir -p %{buildroot}/usr/share/applications
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
%post
update-desktop-database -q || true
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
%postun
update-desktop-database -q || true
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
%files
/opt/PiliPlus
/usr/bin/piliplus
/usr/share/applications/piliplus.desktop
/usr/share/icons/hicolor/512x512/apps/piliplus.png
%changelog
* DATE - ${{ env.version }}-1
- Initial RPM release
EOF
sed -i "s/DATE/${DATE}/g" "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
printf "构建 RPM 包...\n"
rpmbuild --define "_topdir $RPM_BUILD_ROOT" -bb "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
printf "移动生成的 RPM...\n"
find "$RPM_BUILD_ROOT/RPMS" -name "*.rpm" -exec mv {} "PiliPlus_linux_${{ env.version }}_arm64.rpm" \;
printf "完成: PiliPlus_linux_%s_arm64.rpm\n" "${{ env.version }}"
shell: bash
- name: Release
if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_linux_*.tar.gz
PiliPlus_linux_*.deb
PiliPlus_linux_*.rpm
- name: Upload linux targz package
uses: actions/upload-artifact@v4
with:
name: Linux_targz_arm64_packege
path: PiliPlus_linux_*.tar.gz
- name: Upload linux deb package
uses: actions/upload-artifact@v4
with:
name: Linux_deb_arm64_package
path: PiliPlus_linux_*.deb
- name: Upload linux rpm package
uses: actions/upload-artifact@v4
with:
name: Linux_rpm_arm64_package
path: PiliPlus_linux_*.rpm

210
.github/workflows/linux_x64.yml vendored Normal file
View File

@@ -0,0 +1,210 @@
name: Build for Linux x64
on:
workflow_call:
inputs:
tag:
description: "tag"
required: false
default: ""
type: string
workflow_dispatch:
jobs:
build-linux-app:
name: Release Linux x64
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y clang cmake libgtk-3-dev ninja-build libayatana-appindicator3-dev unzip webkit2gtk-4.1 libasound2-dev rpm patchelf
sudo apt-get install -y gcc g++ autoconf automake debhelper glslang-dev ladspa-sdk xutils-dev libasound2-dev \
libarchive-dev libbluray-dev libbs2b-dev libcaca-dev libcdio-paranoia-dev libdrm-dev \
libdav1d-dev libdvdnav-dev libegl1-mesa-dev libepoxy-dev libfontconfig-dev libfreetype6-dev \
libfribidi-dev libgl1-mesa-dev libgbm-dev libgme-dev libgsm1-dev libharfbuzz-dev libjpeg-dev \
libbrotli-dev liblcms2-dev libmodplug-dev libmp3lame-dev libopenal-dev \
libopus-dev libopencore-amrnb-dev libopencore-amrwb-dev libpulse-dev librtmp-dev \
libsdl2-dev libsixel-dev libssh-dev libsoxr-dev libspeex-dev libtool \
libv4l-dev libva-dev libvdpau-dev libvorbis-dev libvo-amrwbenc-dev \
libunwind-dev libvpx-dev libwayland-dev libx11-dev libxext-dev \
libxkbcommon-dev libxrandr-dev libxss-dev libxv-dev libxvidcore-dev \
linux-libc-dev nasm ninja-build pkg-config python3 python3-docutils wayland-protocols \
x11proto-core-dev zlib1g-dev libfdk-aac-dev libtheora-dev libwebp-dev \
unixodbc-dev libpq-dev libxxhash-dev libaom-dev \
libgtk-3-0 libblkid1 liblzma5 libmpv-dev
shell: bash
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version-file: pubspec.yaml
cache: true
- name: Set and Extract version
shell: pwsh
run: lib/scripts/build.ps1
#TODO: deb and rpm packages need to be build
- name: Build Linux
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
- name: Package .tar.gz
run: tar -zcvf PiliPlus_linux_${{ env.version }}_amd64.tar.gz -C build/linux/x64/release/bundle .
- name: Packege deb
run: |
printf "建立构建目录...\n"
mkdir "PiliPlus_linux_${{ env.version }}_amd64"
pushd "PiliPlus_linux_${{ env.version }}_amd64"
mkdir -p opt/PiliPlus
mkdir -p usr/share/applications
mkdir -p usr/share/icons/hicolor/512x512/apps
printf "复制文件...\n"
cp -r ../build/linux/x64/release/bundle/* opt/PiliPlus
cp -r ../assets/linux/DEBIAN .
cp ../assets/linux/piliplus.desktop usr/share/applications
cp ../assets/images/logo/logo.png usr/share/icons/hicolor/512x512/apps/piliplus.png
printf "修改控制文件...\n"
# 替换版本号
sed -i "2s/version_need_change/${{ env.version }}/g" DEBIAN/control
# 计算安装大小并替换
SIZE_KB=$(du -s -b --apparent-size . | awk '{print int($1)}')
SIZE_KB=$(($SIZE_KB - $(du -s -b --apparent-size DEBIAN | awk '{print int($1)}')))
SIZE_KB=$(echo $SIZE_KB | awk '{print int($1/1024 + 0.999)}')
printf "\t安装大小: %s KB\n" "$SIZE_KB"
sed -i "9s/size_need_change/${SIZE_KB}/g" DEBIAN/control
printf "生成并写入 md5sums ...\n"
md5sum opt/PiliPlus/piliplus >> DEBIAN/md5sums
md5sum opt/PiliPlus/lib/* >> DEBIAN/md5sums
md5sum opt/PiliPlus/data/icudtl.dat >> DEBIAN/md5sums
printf "设置权限...\n"
chmod 0644 DEBIAN/control
chmod 0644 DEBIAN/md5sums
chmod 0755 DEBIAN/postinst
chmod 0755 DEBIAN/postrm
chmod 0755 DEBIAN/prerm
printf "打包 deb 文件...\n"
popd
dpkg-deb --build --verbose --root-owner-group "PiliPlus_linux_${{ env.version }}_amd64"
printf "完成: PiliPlus_linux_%s_amd64.deb\n" "${{ env.version }}"
shell: bash
- name: Packege rpm
run: |
printf "建立 RPM 构建目录...\n"
RPM_BUILD_ROOT="$PWD/rpm_build"
mkdir -p "$RPM_BUILD_ROOT/BUILD" "$RPM_BUILD_ROOT/RPMS" "$RPM_BUILD_ROOT/SOURCES" "$RPM_BUILD_ROOT/SPECS" "$RPM_BUILD_ROOT/SRPMS"
printf "准备源码归档(仅包含运行时与元数据)...\n"
DATE="$(date '+%a %b %d %Y')"
SRC_DIR="$PWD/piliplus-${{ env.version }}"
mkdir -p "$SRC_DIR/bundle" "$SRC_DIR/assets"
cp -r build/linux/x64/release/bundle/* "$SRC_DIR/bundle/"
cp assets/linux/piliplus.desktop "$SRC_DIR/assets/piliplus.desktop"
cp assets/images/logo/logo.png "$SRC_DIR/assets/piliplus.png"
tar -zcvf "$RPM_BUILD_ROOT/SOURCES/piliplus-${{ env.version }}.tar.gz" -C "$PWD" "piliplus-${{ env.version }}"
printf "生成 spec 文件...\n"
cat > "$RPM_BUILD_ROOT/SPECS/piliplus.spec" <<EOF
Name: piliplus
Version: ${{ env.version }}
Release: 1%{?dist}
Summary: PiliPlus Linux Version
License: GPL-3.0
Source0: piliplus-${{ env.version }}.tar.gz
Requires: desktop-file-utils, hicolor-icon-theme
%description
使用 Flutter 开发的 BiliBili 第三方客户端
%prep
%setup -q -n piliplus-${{ env.version }}
%build
%install
mkdir -p %{buildroot}/opt/PiliPlus
cp -r bundle/* %{buildroot}/opt/PiliPlus/
# 二进制权限与命令行入口
chmod 755 %{buildroot}/opt/PiliPlus/piliplus
mkdir -p %{buildroot}/usr/bin
ln -sf /opt/PiliPlus/piliplus %{buildroot}/usr/bin/piliplus
# 桌面集成
mkdir -p %{buildroot}/usr/share/applications
install -m 644 assets/piliplus.desktop %{buildroot}/usr/share/applications/piliplus.desktop
mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps
install -m 644 assets/piliplus.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/piliplus.png
%post
update-desktop-database -q || true
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
%postun
update-desktop-database -q || true
gtk-update-icon-cache -q -t -f %{_datadir}/icons/hicolor || true
%files
/opt/PiliPlus
/usr/bin/piliplus
/usr/share/applications/piliplus.desktop
/usr/share/icons/hicolor/512x512/apps/piliplus.png
%changelog
* DATE - ${{ env.version }}-1
- Initial RPM release
EOF
sed -i "s/DATE/${DATE}/g" "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
printf "构建 RPM 包...\n"
rpmbuild --define "_topdir $RPM_BUILD_ROOT" -bb "$RPM_BUILD_ROOT/SPECS/piliplus.spec"
printf "移动生成的 RPM...\n"
find "$RPM_BUILD_ROOT/RPMS" -name "*.rpm" -exec mv {} "PiliPlus_linux_${{ env.version }}_amd64.rpm" \;
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
shell: bash
- name: Release
if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_linux_*.tar.gz
PiliPlus_linux_*.deb
PiliPlus_linux_*.rpm
- name: Upload linux targz package
uses: actions/upload-artifact@v4
with:
name: Linux_targz_amd64_packege
path: PiliPlus_linux_*.tar.gz
- name: Upload linux deb package
uses: actions/upload-artifact@v4
with:
name: Linux_deb_amd64_package
path: PiliPlus_linux_*.deb
- name: Upload linux rpm package
uses: actions/upload-artifact@v4
with:
name: Linux_rpm_amd64_package
path: PiliPlus_linux_*.rpm

58
.github/workflows/mac.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Build for Mac
on:
workflow_call:
inputs:
tag:
description: "tag"
required: false
default: ""
type: string
workflow_dispatch:
jobs:
build-mac-app:
name: Release Mac
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version-file: pubspec.yaml
- name: Set and Extract version
shell: pwsh
run: lib/scripts/build.ps1
- name: Build Mac
run: flutter build macos --release --dart-define-from-file=pili_release.json
- name: Prepare Upload
run: |
npm install --global create-dmg
create-dmg build/macos/Build/Products/Release/PiliPlus.app
continue-on-error: true
- name: Rename DMG
run: mv PiliPlus*.dmg PiliPlus_macos_${{ env.version }}.dmg
- name: Release
if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_macos_*.dmg
- name: Upload macos release
uses: actions/upload-artifact@v4
with:
name: macOS-release
path: PiliPlus_macos_*.dmg

80
.github/workflows/win_x64.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Build for Windows x64
on:
workflow_call:
inputs:
tag:
description: "tag"
required: false
default: ""
type: string
workflow_dispatch:
jobs:
build-windows-app:
name: Release Windows x64
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version-file: pubspec.yaml
- name: Add fastforge and Inno Setup
run: |
dart pub global activate fastforge
choco install innosetup
- name: Add Chinese language file for Inno Setup
run: |
Copy-Item "windows/packaging/exe/ChineseSimplified.isl" "C:\Program Files (x86)\Inno Setup 6\Languages\ChineseSimplified.isl"
shell: pwsh
- name: Set and Extract version
shell: pwsh
run: lib/scripts/build.ps1
- name: Build Windows
run: |
fastforge package --platform windows --targets exe --flutter-build-args="dart-define-from-file=pili_release.json"
- name: Prepare Upload
run: |
mkdir -p Release/PiliPlus-Win
mkdir -p PiliPlus-Win-Setup
mv build/windows/x64/runner/Release/* Release/PiliPlus-Win/
mv dist/**/*.exe PiliPlus-Win-Setup/PiliPlus_windows_${{env.version}}_x64_setup.exe
- name: Compress
if: ${{ github.event.inputs.tag != '' }}
run: |
Compress-Archive -Path "Release/PiliPlus-Win" -DestinationPath "PiliPlus_windows_${{env.version}}_x64.zip"
shell: pwsh
- name: Release
if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.tag }}
files: |
PiliPlus_windows_*.zip
PiliPlus-Win-Setup/PiliPlus_windows_*.exe
- name: Upload windows file release
uses: actions/upload-artifact@v4
with:
name: Windows-file-x64-release
path: Release
- name: Upload windows setup release
uses: actions/upload-artifact@v4
with:
name: Windows-setup-x64-release
path: PiliPlus-Win-Setup

15
.gitignore vendored
View File

@@ -19,7 +19,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
.vscode/
# Flutter repo-specific
/bin/cache/
@@ -134,7 +134,16 @@ app.*.symbols
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json
/lib/build_config.dart
!.vscode/launch.json
!.vscode/tasks.json
devtools_options.yaml
# FVM Version Cache
.fvm/
pili_release.json
dist
test.dart

8
.vscode/launch.json vendored
View File

@@ -1,22 +1,22 @@
{
// 使用 IntelliSense 了解相关属性。
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "piliplus",
"name": "PiliPlus",
"request": "launch",
"type": "dart"
},
{
"name": "piliplus (profile mode)",
"name": "PiliPlus (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "piliplus (release mode)",
"name": "PiliPlus (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"

View File

@@ -22,22 +22,18 @@
<br/>
</div>
## 开发环境
```bash
[] Flutter (Channel stable, 3.24.0, on Microsoft Windows [版本 10.0.19045.4046], locale zh-CN)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Xcode - develop for iOS and macOS (Xcode 15.1)
[] Chrome - develop for the web
[] Android Studio (version 2022.3)
[] VS Code (version 1.85.1)
[] Connected device (3 available)
[] Network resources
```
<br/>
## 适配平台
- [x] Android
- [x] iOS
- [x] Pad
- [x] Windows
- [x] Linux
[![Packaging status](https://repology.org/badge/vertical-allrepos/piliplus.svg)](https://repology.org/project/piliplus/versions)
## refactor
@@ -47,6 +43,14 @@
## feat
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] 播放音频
- [x] 跳过番剧片头/片尾
- [x] 安卓端 `loudnorm` 适配 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] Win/Mac 支持极验、短信登录 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] 视频截取动图 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] AI 原声翻译
- [x] SuperChat
- [x] 播放课堂视频
- [x] 发起投票
- [x] 发布动态/评论支持`富文本编辑`/`表情显示`/`@用户`
@@ -135,11 +139,6 @@
## 功能
目前着重移动端(Android、iOS)和Pad端暂时没有适配桌面端、手表端等
<br/>
- [x] 推荐视频列表(app端)
- [x] 最热视频列表
- [x] 热门直播
@@ -239,3 +238,13 @@
<br/>
<br/>
<br/>
## Star History
<a href="https://www.star-history.com/#bggRGjQaUbCoE/PiliPlus&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=bggRGjQaUbCoE/PiliPlus&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=bggRGjQaUbCoE/PiliPlus&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=bggRGjQaUbCoE/PiliPlus&type=Date" />
</picture>
</a>

View File

@@ -61,5 +61,7 @@ linter:
- use_decorated_box
- use_named_constants
- use_null_aware_elements
- unnecessary_lambdas
- use_is_even_rather_than_modulo
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

3
android/.gitignore vendored
View File

@@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
/build
/.kotlin

View File

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

View File

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

View File

@@ -1 +1,3 @@
-keep class com.yalantis.ucrop.util.RectUtils { *; }
-dontwarn javax.annotation.Nullable
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.OpenSSLProvider

View File

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

View File

@@ -36,11 +36,11 @@
</queries>
<application
android:label="PiliPlus"
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
xmlns:tools="http://schemas.android.com/tools"
android:enableOnBackInvokedCallback="true"
android:enableOnBackInvokedCallback="false"
android:allowBackup="false"
android:fullBackupContent="false"
tools:replace="android:allowBackup">
@@ -109,6 +109,7 @@
<data android:host="uper" />
<data android:host="article"
android:pathPattern="/readlist" />
<data android:host="opus" />
<data android:host="advertise" android:path="/home" />
<data android:host="clip" />
<data android:host="search" android:pathPattern=".*" />
@@ -176,7 +177,6 @@
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Ucrop.CropTheme"/>
<receiver

View File

@@ -1,116 +0,0 @@
package com.example.piliplus
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import com.ryanheise.audioservice.AudioServiceActivity
import android.content.ComponentName
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.WindowManager.LayoutParams
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 ->
if (call.method == "back") {
back()
} else if (call.method == "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 (e: Exception) {}
} else if (call.method == "linkVerifySettings") {
try {
val intent = Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:" + context.packageName))
context.startActivity(intent)
} catch (t: Throwable) {
try {
val intent = Intent("android.intent.action.MAIN", Uri.parse("package:" + context.packageName))
intent.setClassName("com.android.settings", "com.android.settings.applications.InstalledAppOpenByDefaultActivity")
context.startActivity(intent)
} catch (t2: Throwable) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + context.packageName))
context.startActivity(intent)
}
}
} else {
result.notImplemented()
}
}
}
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) {
window.attributes.layoutInDisplayCutoutMode =
LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
override fun onDestroy() {
super.onDestroy()
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
methodChannel.invokeMethod("onUserLeaveHint", null)
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
MethodChannel(
flutterEngine!!.getDartExecutor()!!.getBinaryMessenger(),
"floating"
).invokeMethod("onPipChanged", isInPictureInPictureMode)
}
}

View File

@@ -0,0 +1,153 @@
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()
}
}
}
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) {
window.attributes.layoutInDisplayCutoutMode =
LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
override fun onDestroy() {
super.onDestroy()
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
methodChannel.invokeMethod("onUserLeaveHint", null)
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
MethodChannel(
flutterEngine!!.dartExecutor.binaryMessenger,
"floating"
).invokeMethod("onPipChanged", isInPictureInPictureMode)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

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

View File

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

View File

@@ -1,59 +0,0 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android {
if (namespace == null) {
namespace project.group
}
}
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = '17'
}
}
}
Integer pluginCompileSdk = project.android.compileSdk
if (pluginCompileSdk != null) {
if (pluginCompileSdk < 31) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 31 (to work around https://issuetracker.google.com/issues/199180389)."
+ "\nIf there is not a new version of " + project.name + ", consider filing an issue against "
+ project.name
+ " to increase their compileSdk to the latest (otherwise try updating to the latest version)."
)
project.android {
compileSdk 31
}
}
}
}
}
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

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

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

View File

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

View File

@@ -1,25 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.4.1' apply false
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
include ":app"

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/images/loading.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,16 @@
Package: PiliPlus
Version: version_need_change
Maintainer: gh-MzA4Nzk <githubaccount2333@proton.me>
Original-Maintainer: bggRGjQaUbCoE <githubaccount56556@proton.me>
Section: x11
Priority: optional
Architecture: amd64
Essential: no
Installed-Size: size_need_change
Description: third-party Bilibili client developed in Flutter
Homepage: https://github.com/bggRGjQaUbCoE/PiliPlus
Depends: libgtk-3-0t64,
libmpv2,
gir1.2-ayatanaappindicator3-0.1,
libayatana-appindicator3-1

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
ln -sf /opt/PiliPlus/piliplus /usr/bin/piliplus
chmod +x /usr/bin/piliplus
if [ $1 == "config" ] && [ -x /usr/binupdate-mime-database ]; then
echo "updating mime database..."
update-mime-database /usr/share/mime || true
fi
if [ $1 == "config" ] && [ -x /usr/bin/gtk-update-icon-cache ]; then
echo "updating icon cache..."
gtk-update-icon-cache -q -f -t /usr/share/icons/hicolor || true
fi
if [ $1 == "config" ] && [ -x /usr/bin/update-desktop-database ]; then
echo "updating desktop database..."
update-desktop-database -q /usr/share/applications || true
fi
exit 0

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
rm /usr/bin/piliplus
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
if [ -x /usr/bin/update-desktop-database ]; then
echo "updating desktop database..."
update-desktop-database -q /usr/share/applications || true
fi
if [ -x /usr/bin/gtk-update-icon-cache ]; then
echo "updating icon cache..."
gtk-update-icon-cache -q -t /usr/share/icons/hicolor || true
fi
if [ -x /usr/bin/update-mime-database ]; then
echo "updating mime database..."
update-mime-database /usr/share/mime || true
fi
fi
if [ $1 = "purge" ]; then
echo "Removing user data..."
rm -rf /home/*/.local/share/com.example.PiliPlus || true
rm -rf /root/.local/share/com.example.PiliPlus || true
fi
exit 0

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
echo "Stopping PiliPlus if running..."
pkill -x piliplus || true
fi
exit 0

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=PiliPlus
Comment=A third-party Bilibili Client developed in Flutter
Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端
Exec=piliplus
Icon=piliplus
Terminal=false
Categories=Video;AudioVideo;Player;

1
distribute_options.yaml Normal file
View File

@@ -0,0 +1 @@
output: dist/

View File

@@ -1,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

16
lib/build_config.dart Normal file
View File

@@ -0,0 +1,16 @@
class BuildConfig {
static const int versionCode = int.fromEnvironment(
'pili.code',
defaultValue: 1,
);
static const String versionName = String.fromEnvironment(
'pili.name',
defaultValue: 'SNAPSHOT',
);
static const int buildTime = int.fromEnvironment('pili.time');
static const String commitHash = String.fromEnvironment(
'pili.hash',
defaultValue: 'N/A',
);
}

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/common/skeleton/skeleton.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:flutter/material.dart';
class DynamicCardSkeleton extends StatelessWidget {
@@ -20,6 +21,7 @@ class DynamicCardSkeleton extends StatelessWidget {
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
@@ -50,46 +52,38 @@ class DynamicCardSkeleton extends StatelessWidget {
),
],
),
const SizedBox(height: 10),
Container(
color: color,
width: double.infinity,
margin: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: color,
width: double.infinity,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: double.infinity,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: 300,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: 250,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: 100,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
],
),
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
const Spacer(),
Container(
color: color,
width: double.infinity,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: 300,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: 250,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
Container(
color: color,
width: 100,
height: 13,
margin: const EdgeInsets.only(bottom: 7),
),
if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [

View File

@@ -19,15 +19,11 @@ class VideoCardHSkeleton extends StatelessWidget {
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
),
),
Expanded(

View File

@@ -10,18 +10,15 @@ class VideoCardVSkeleton extends StatelessWidget {
final color = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: StyleString.mdRadius,
),
),
),
Padding(

View File

@@ -30,9 +30,8 @@ class VideoReplySkeleton extends StatelessWidget {
],
),
),
Container(
width: double.infinity,
margin: const EdgeInsets.only(
Padding(
padding: const EdgeInsets.only(
top: 4,
left: 57,
right: 6,

View File

@@ -33,7 +33,7 @@ class MultiSelectAppBarWidget extends StatelessWidget
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => ctr.handleSelect(true),
onPressed: () => ctr.handleSelect(checked: true),
child: const Text('全选'),
),
...?children,

View File

@@ -75,10 +75,11 @@ class PBadge extends StatelessWidget {
bgColor = Colors.transparent;
borderColor = theme.secondary;
case PBadgeType.free:
bgColor = Get.isDarkMode
? const Color(0xFFD66011)
: const Color(0xFFFF7F24);
bgColor = theme.freeColor;
color = Colors.white;
case PBadgeType.shop:
bgColor = theme.secondaryContainer.withValues(alpha: 0.5);
color = theme.onSurfaceVariant;
}
late EdgeInsets paddingStyle = const EdgeInsets.symmetric(

View File

@@ -1,50 +1,35 @@
import 'package:flutter/material.dart';
Widget iconButton({
required BuildContext context,
BuildContext? context,
String? tooltip,
required IconData icon,
required Widget icon,
required VoidCallback? onPressed,
double size = 36,
double? iconSize,
Color? bgColor,
Color? iconColor,
}) {
late final theme = Theme.of(context);
Color? backgroundColor = bgColor;
Color? foregroundColor = iconColor;
if (context != null) {
final colorScheme = ColorScheme.of(context);
backgroundColor = colorScheme.secondaryContainer;
foregroundColor = colorScheme.onSecondaryContainer;
}
return SizedBox(
width: size,
height: size,
child: IconButton(
icon: icon,
tooltip: tooltip,
onPressed: onPressed,
icon: Icon(
icon,
size: iconSize ?? size / 2,
color: iconColor ?? theme.colorScheme.onSecondaryContainer,
),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: bgColor ?? theme.colorScheme.secondaryContainer,
iconSize: iconSize ?? size / 2,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
),
),
);
}
Widget mediumButton({
String? tooltip,
IconData? icon,
VoidCallback? onPressed,
}) {
return SizedBox(
width: 34,
height: 34,
child: IconButton(
tooltip: tooltip,
icon: Icon(icon),
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: onPressed,
),
);
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
Widget moreTextButton({
String text = '查看更多',
required VoidCallback onTap,
EdgeInsets? padding,
Color? color,
}) {
Widget child = Text.rich(
style: TextStyle(color: color, height: 1),
strutStyle: const StrutStyle(leading: 0, height: 1),
TextSpan(
children: [
TextSpan(text: text),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
size: 22,
color: color,
Icons.keyboard_arrow_right,
),
),
],
),
);
if (padding != null) {
child = Padding(padding: padding, child: child);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: child,
);
}

View File

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

View File

@@ -15,11 +15,10 @@ class ColorPalette extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final Hct hct = Hct.fromInt(color.value);
final Hct hct = Hct.fromInt(color.toARGB32());
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt());
final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());
final checkbox = Color(Hct.from(hct.hue, 30.0, 40.0).toInt());
Widget coloredBox(Color color) => Expanded(
child: ColoredBox(
color: color,
@@ -51,7 +50,7 @@ class ColorPalette extends StatelessWidget {
width: 23,
height: 23,
decoration: BoxDecoration(
color: checkbox,
color: Color(Hct.from(hct.hue, 30.0, 40.0).toInt()),
shape: BoxShape.circle,
),
child: Icon(

View File

@@ -2,7 +2,7 @@
import 'package:flutter/widgets.dart';
class CustomIcon {
class CustomIcons {
static const IconData coin = _CustomIconData(0xe800);
static const IconData dm_off = _CustomIconData(0xe801);
static const IconData dm_on = _CustomIconData(0xe802);
@@ -10,19 +10,24 @@ class CustomIcon {
static const IconData dyn = _CustomIconData(0xe804);
static const IconData fav = _CustomIconData(0xe805);
static const IconData live_reserve = _CustomIconData(0xe806);
static const IconData share = _CustomIconData(0xe807);
static const IconData share_line = _CustomIconData(0xe808);
static const IconData share_node = _CustomIconData(0xe809);
static const IconData star_favorite_line = _CustomIconData(0xe80a);
static const IconData star_favorite_solid = _CustomIconData(0xe80b);
static const IconData thumbs_down = _CustomIconData(0xe80c);
static const IconData thumbs_down_outline = _CustomIconData(0xe80d);
static const IconData thumbs_up = _CustomIconData(0xe80e);
static const IconData thumbs_up_fill = _CustomIconData(0xe80f);
static const IconData thumbs_up_line = _CustomIconData(0xe810);
static const IconData thumbs_up_outline = _CustomIconData(0xe811);
static const IconData topic_tag = _CustomIconData(0xe812);
static const IconData watch_later = _CustomIconData(0xe813);
static const IconData player_dm_tip_back = _CustomIconData(0xe807);
static const IconData player_dm_tip_copy = _CustomIconData(0xe808);
static const IconData player_dm_tip_like = _CustomIconData(0xe809);
static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a);
static const IconData player_dm_tip_recall = _CustomIconData(0xe80b);
static const IconData share = _CustomIconData(0xe80c);
static const IconData share_line = _CustomIconData(0xe80d);
static const IconData share_node = _CustomIconData(0xe80e);
static const IconData star_favorite_line = _CustomIconData(0xe80f);
static const IconData star_favorite_solid = _CustomIconData(0xe810);
static const IconData thumbs_down = _CustomIconData(0xe811);
static const IconData thumbs_down_outline = _CustomIconData(0xe812);
static const IconData thumbs_up = _CustomIconData(0xe813);
static const IconData thumbs_up_fill = _CustomIconData(0xe814);
static const IconData thumbs_up_line = _CustomIconData(0xe815);
static const IconData thumbs_up_outline = _CustomIconData(0xe816);
static const IconData topic_tag = _CustomIconData(0xe817);
static const IconData watch_later = _CustomIconData(0xe818);
}
class _CustomIconData extends IconData {

View File

@@ -0,0 +1,462 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: uri_does_not_exist_in_doc_import
/// @docImport 'package:flutter/widgets.dart';
///
/// @docImport 'stack.dart';
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class CustomMultiChildLayout extends MultiChildRenderObjectWidget {
/// Creates a custom multi-child layout.
const CustomMultiChildLayout({
super.key,
required this.delegate,
super.children,
});
/// The delegate that controls the layout of the children.
final MultiChildLayoutDelegate delegate;
@override
RenderCustomMultiChildLayoutBox createRenderObject(BuildContext context) {
return RenderCustomMultiChildLayoutBox(delegate: delegate);
}
@override
void updateRenderObject(
BuildContext context,
RenderCustomMultiChildLayoutBox renderObject,
) {
renderObject.delegate = delegate;
}
}
/// A delegate that controls the layout of multiple children.
///
/// Used with [CustomMultiChildLayout] (in the widgets library) and
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
///
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
/// they must produce the same layout. To change the layout, replace the
/// delegate with a different instance whose [shouldRelayout] returns true when
/// given the previous instance.
///
/// Override [getSize] to control the overall size of the layout. The size of
/// the layout cannot depend on layout properties of the children. This was
/// a design decision to simplify the delegate implementations: This way,
/// the delegate implementations do not have to also handle various intrinsic
/// sizing functions if the parent's size depended on the children.
/// If you want to build a custom layout where you define the size of that widget
/// based on its children, then you will have to create a custom render object.
/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and
/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an
/// example implementation.
///
/// Override [performLayout] to size and position the children. An
/// implementation of [performLayout] must call [layoutChild] exactly once for
/// each child, but it may call [layoutChild] on children in an arbitrary order.
/// Typically a delegate will use the size returned from [layoutChild] on one
/// child to determine the constraints for [performLayout] on another child or
/// to determine the offset for [positionChild] for that child or another child.
///
/// Override [shouldRelayout] to determine when the layout of the children needs
/// to be recomputed when the delegate changes.
///
/// The most efficient way to trigger a relayout is to supply a `relayout`
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
/// layout will listen to this value and relayout whenever the Listenable
/// notifies its listeners, such as when an [Animation] ticks. This allows
/// the custom layout to avoid the build phase of the pipeline.
///
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
/// the children that the [CustomMultiChildLayout] manages.
///
/// {@tool snippet}
///
/// Below is an example implementation of [performLayout] that causes one widget
/// (the follower) to be the same size as another (the leader):
///
/// ```dart
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
/// // Typical usage is to define an enum like the one below, and use those
/// // values as the ids.
/// enum _Slot {
/// leader,
/// follower,
/// }
///
/// class FollowTheLeader extends MultiChildLayoutDelegate {
/// @override
/// void performLayout(Size size) {
/// Size leaderSize = Size.zero;
///
/// if (hasChild(_Slot.leader)) {
/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
/// positionChild(_Slot.leader, Offset.zero);
/// }
///
/// if (hasChild(_Slot.follower)) {
/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
/// size.height - leaderSize.height));
/// }
/// }
///
/// @override
/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
/// }
/// ```
/// {@end-tool}
///
/// The delegate gives the leader widget loose constraints, which means the
/// child determines what size to be (subject to fitting within the given size).
/// The delegate then remembers the size of that child and places it in the
/// upper left corner.
///
/// The delegate then gives the follower widget tight constraints, forcing it to
/// match the size of the leader widget. The delegate then places the follower
/// widget in the bottom right corner.
///
/// The leader and follower widget will paint in the order they appear in the
/// child list, regardless of the order in which [layoutChild] is called on
/// them.
///
/// See also:
///
/// * [CustomMultiChildLayout], the widget that uses this delegate.
/// * [RenderCustomMultiChildLayoutBox], render object that uses this
/// delegate.
abstract class MultiChildLayoutDelegate {
/// Creates a layout delegate.
///
/// The layout will update whenever [relayout] notifies its listeners.
MultiChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout;
final Listenable? _relayout;
Map<Object, RenderBox>? _idToChild;
Set<RenderBox>? _debugChildrenNeedingLayout;
/// True if a non-null LayoutChild was provided for the specified id.
///
/// Call this from the [performLayout] method to determine which children
/// are available, if the child list might vary.
///
/// This method cannot be called from [getSize] as the size is not allowed
/// to depend on the children.
bool hasChild(Object childId) => _idToChild![childId] != null;
/// Ask the child to update its layout within the limits specified by
/// the constraints parameter. The child's size is returned.
///
/// Call this from your [performLayout] function to lay out each
/// child. Every child must be laid out using this function exactly
/// once each time the [performLayout] function is called.
Size layoutChild(Object childId, BoxConstraints constraints) {
final RenderBox? child = _idToChild![childId];
assert(() {
if (child == null) {
throw FlutterError(
'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
'There is no child with the id "$childId".',
);
}
if (!_debugChildrenNeedingLayout!.remove(child)) {
throw FlutterError(
'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
'Each child must be laid out exactly once.',
);
}
try {
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
} on AssertionError catch (exception) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".',
),
DiagnosticsProperty<AssertionError>(
'Exception',
exception,
showName: false,
),
ErrorDescription(
'The minimum width and height must be greater than or equal to zero.\n'
'The maximum width must be greater than or equal to the minimum width.\n'
'The maximum height must be greater than or equal to the minimum height.',
),
]);
}
return true;
}());
child!.layout(constraints, parentUsesSize: true);
return child.size;
}
/// Specify the child's origin relative to this origin.
///
/// Call this from your [performLayout] function to position each
/// child. If you do not call this for a child, its position will
/// remain unchanged. Children initially have their position set to
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
void positionChild(Object childId, Offset offset) {
final RenderBox? child = _idToChild![childId];
assert(() {
if (child == null) {
throw FlutterError(
'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
'There is no child with the id "$childId".',
);
}
return true;
}());
final MultiChildLayoutParentData childParentData =
child!.parentData! as MultiChildLayoutParentData;
childParentData.offset = offset;
}
DiagnosticsNode _debugDescribeChild(RenderBox child) {
final MultiChildLayoutParentData childParentData =
child.parentData! as MultiChildLayoutParentData;
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
}
void _callPerformLayout(Size size, RenderBox? firstChild) {
// A particular layout delegate could be called reentrantly, e.g. if it used
// by both a parent and a child. So, we must restore the _idToChild map when
// we return.
final Map<Object, RenderBox>? previousIdToChild = _idToChild;
Set<RenderBox>? debugPreviousChildrenNeedingLayout;
assert(() {
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
_debugChildrenNeedingLayout = <RenderBox>{};
return true;
}());
try {
_idToChild = <Object, RenderBox>{};
RenderBox? child = firstChild;
while (child != null) {
final MultiChildLayoutParentData childParentData =
child.parentData! as MultiChildLayoutParentData;
assert(() {
if (childParentData.id == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.',
),
child!.describeForError('The following child has no ID'),
]);
}
return true;
}());
_idToChild![childParentData.id!] = child;
assert(() {
_debugChildrenNeedingLayout!.add(child!);
return true;
}());
child = childParentData.nextSibling;
}
performLayout(size);
assert(() {
if (_debugChildrenNeedingLayout!.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Each child must be laid out exactly once.'),
DiagnosticsBlock(
name:
'The $this custom multichild layout delegate forgot '
'to lay out the following '
'${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}',
properties: _debugChildrenNeedingLayout!
.map<DiagnosticsNode>(_debugDescribeChild)
.toList(),
),
]);
}
return true;
}());
} finally {
_idToChild = previousIdToChild;
assert(() {
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
return true;
}());
}
}
/// Override this method to return the size of this object given the
/// incoming constraints.
///
/// The size cannot reflect the sizes of the children. If this layout has a
/// fixed width or height the returned size can reflect that; the size will be
/// constrained to the given constraints.
///
/// By default, attempts to size the box to the biggest size
/// possible given the constraints.
Size getSize(BoxConstraints constraints) => constraints.biggest;
/// Override this method to lay out and position all children given this
/// widget's size.
///
/// This method must call [layoutChild] for each child. It should also specify
/// the final position of each child with [positionChild].
void performLayout(Size size);
/// Override this method to return true when the children need to be
/// laid out.
///
/// This should compare the fields of the current delegate and the given
/// `oldDelegate` and return true if the fields are such that the layout would
/// be different.
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
/// Override this method to include additional information in the
/// debugging data printed by [debugDumpRenderTree] and friends.
///
/// By default, returns the [runtimeType] of the class.
@override
String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate');
}
/// Defers the layout of multiple children to a delegate.
///
/// The delegate can determine the layout constraints for each child and can
/// decide where to position each child. The delegate can also determine the
/// size of the parent, but the size of the parent cannot depend on the sizes of
/// the children.
class RenderCustomMultiChildLayoutBox extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
/// Creates a render object that customizes the layout of multiple children.
RenderCustomMultiChildLayoutBox({
List<RenderBox>? children,
required MultiChildLayoutDelegate delegate,
}) : _delegate = delegate {
addAll(children);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MultiChildLayoutParentData) {
child.parentData = MultiChildLayoutParentData();
}
}
/// The delegate that controls the layout of the children.
MultiChildLayoutDelegate get delegate => _delegate;
MultiChildLayoutDelegate _delegate;
set delegate(MultiChildLayoutDelegate newDelegate) {
if (_delegate == newDelegate) {
return;
}
final MultiChildLayoutDelegate oldDelegate = _delegate;
if (newDelegate.runtimeType != oldDelegate.runtimeType ||
newDelegate.shouldRelayout(oldDelegate)) {
markNeedsLayout();
}
_delegate = newDelegate;
if (attached) {
oldDelegate._relayout?.removeListener(markNeedsLayout);
newDelegate._relayout?.addListener(markNeedsLayout);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_delegate._relayout?.addListener(markNeedsLayout);
}
@override
void detach() {
_delegate._relayout?.removeListener(markNeedsLayout);
super.detach();
}
Size _getSize(BoxConstraints constraints) {
assert(constraints.debugAssertIsValid());
return constraints.constrain(_delegate.getSize(constraints));
}
// TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
// figure out the intrinsic dimensions. We really should either not support intrinsics,
// or we should expose intrinsic delegate callbacks and throw if they're not implemented.
@override
double computeMinIntrinsicWidth(double height) {
final double width = _getSize(
BoxConstraints.tightForFinite(height: height),
).width;
if (width.isFinite) {
return width;
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
final double width = _getSize(
BoxConstraints.tightForFinite(height: height),
).width;
if (width.isFinite) {
return width;
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
final double height = _getSize(
BoxConstraints.tightForFinite(width: width),
).height;
if (height.isFinite) {
return height;
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
final double height = _getSize(
BoxConstraints.tightForFinite(width: width),
).height;
if (height.isFinite) {
return height;
}
return 0.0;
}
@override
@protected
Size computeDryLayout(covariant BoxConstraints constraints) {
return _getSize(constraints);
}
@override
void performLayout() {
size = _getSize(constraints);
delegate._callPerformLayout(size, firstChild);
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
@override
bool get isRepaintBoundary => true;
}

View File

@@ -1,19 +1,21 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
class CustomSliverPersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
CustomSliverPersistentHeaderDelegate({
const CustomSliverPersistentHeaderDelegate({
required this.child,
required this.bgColor,
double extent = 45,
this.needRebuild,
this.needRebuild = false,
}) : _minExtent = extent,
_maxExtent = extent;
final double _minExtent;
final double _maxExtent;
final Widget child;
final Color? bgColor;
final bool? needRebuild;
final bool needRebuild;
@override
Widget build(
@@ -28,12 +30,14 @@ class CustomSliverPersistentHeaderDelegate
? DecoratedBox(
decoration: BoxDecoration(
color: bgColor,
boxShadow: [
BoxShadow(
color: bgColor!,
offset: const Offset(0, -2),
),
],
boxShadow: Platform.isIOS
? null
: [
BoxShadow(
color: bgColor!,
offset: const Offset(0, -1),
),
],
),
child: child,
)
@@ -51,6 +55,6 @@ class CustomSliverPersistentHeaderDelegate
@override
bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate.bgColor != bgColor ||
(needRebuild == true && oldDelegate.child != child);
(needRebuild && oldDelegate.child != child);
}
}

View File

@@ -13,7 +13,7 @@ class CustomToast extends StatelessWidget {
final ThemeData theme = Theme.of(context);
return Container(
margin: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 30,
bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
),
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
decoration: BoxDecoration(
@@ -46,7 +46,7 @@ class LoadingWidget extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
decoration: BoxDecoration(
color: theme.dialogBackgroundColor,
color: theme.dialogTheme.backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
child: Column(

View File

@@ -1,6 +1,7 @@
import 'dart:math' as math;
import 'dart:ui' show clampDouble;
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -148,11 +149,21 @@ class CustomTooltipState extends State<CustomTooltip>
@protected
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque,
child: widget.child,
);
Widget result;
if (Utils.isMobile) {
result = Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque,
child: widget.child,
);
} else {
result = MouseRegion(
cursor: MouseCursor.defer,
onEnter: (_) => _scheduleShowTooltip(),
onExit: (_) => _scheduleDismissTooltip(),
child: widget.child,
);
}
return OverlayPortal(
controller: _overlayController,
overlayChildBuilder: _buildCustomTooltipOverlay,
@@ -184,30 +195,34 @@ class _CustomTooltipOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDismiss,
child: CustomMultiChildLayout(
delegate: _CustomMultiTooltipPositionDelegate(
type: type,
target: target,
verticalOffset: verticalOffset,
horizontslOffset: horizontslOffset,
preferBelow: false,
),
children: [
LayoutId(
id: 'overlay',
child: overlayWidget(),
),
if (indicator != null)
LayoutId(
id: 'indicator',
child: indicator!(),
),
],
Widget child = CustomMultiChildLayout(
delegate: _CustomMultiTooltipPositionDelegate(
type: type,
target: target,
verticalOffset: verticalOffset,
horizontslOffset: horizontslOffset,
preferBelow: false,
),
children: [
LayoutId(
id: 'overlay',
child: overlayWidget(),
),
if (indicator != null)
LayoutId(
id: 'indicator',
child: indicator!(),
),
],
);
if (Utils.isMobile) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDismiss,
child: child,
);
}
return child;
}
}

View File

@@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void showConfirmDialog({
Future<void> showConfirmDialog({
required BuildContext context,
required String title,
dynamic content,
required VoidCallback onConfirm,
}) {
showDialog(
return showDialog(
context: context,
builder: (context) {
return AlertDialog(

View File

@@ -1,10 +1,11 @@
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
void autoWrapReportDialog(
Future<void> autoWrapReportDialog(
BuildContext context,
Map<String, Map<int, String>> options,
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid)
@@ -14,144 +15,130 @@ void autoWrapReportDialog(
String? reasonDesc;
bool banUid = false;
late final key = GlobalKey<FormState>();
showDialog(
return showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('举报'),
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
contentPadding: const EdgeInsets.symmetric(vertical: 5),
actionsPadding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 10,
),
content: Form(
key: key,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: SingleChildScrollView(
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(
builder: (context) {
return AlertDialog(
title: const Text('举报'),
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22),
contentPadding: const EdgeInsets.symmetric(vertical: 5),
actionsPadding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 10,
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: SingleChildScrollView(
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
child: Builder(
builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(
left: 22,
right: 22,
bottom: 5,
),
child: Text('请选择举报的理由:'),
),
RadioGroup(
onChanged: (value) {
reasonType = value;
(context as Element).markNeedsBuild();
},
groupValue: reasonType,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: options.entries.map((entry) {
return WrapRadioOptionsGroup<int>(
groupTitle: entry.key,
options: entry.value,
);
}).toList(),
),
),
if (reasonType == 0)
Padding(
padding: const EdgeInsets.only(
left: 22,
top: 5,
right: 22,
bottom: 5,
),
child: Text('请选择举报的理由:'),
),
...options.entries.map(
(entry) => WrapRadioOptionsGroup<int>(
groupTitle: entry.key,
options: entry.value,
selectedValue: reasonType,
onChanged: (value) =>
setState(() => reasonType = value),
child: Form(
key: key,
child: TextFormField(
autofocus: true,
minLines: 2,
maxLines: 4,
initialValue: reasonDesc,
decoration: const InputDecoration(
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.all(10),
),
onChanged: (value) => reasonDesc = value,
validator: (value) =>
value.isNullOrEmpty ? '理由不能为空' : null,
),
),
),
if (reasonType == 0)
ReasonField(
onChanged: (value) => reasonDesc = value,
),
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(left: 14, top: 6),
child: CheckBoxText(
text: '拉黑该用户',
onChanged: (value) => banUid = value,
),
),
],
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
if (reasonType == null ||
(reasonType == 0 && key.currentState?.validate() != true)) {
return;
}
SmartDialog.showLoading();
try {
final data = await onSuccess(reasonType!, reasonDesc, banUid);
SmartDialog.dismiss();
if (data['code'] == 0) {
Get.back();
SmartDialog.showToast('举报成功');
} else {
SmartDialog.showToast(data['message']);
}
} catch (e) {
SmartDialog.dismiss();
SmartDialog.showToast('提交失败:$e');
}
},
child: const Text('确定'),
Padding(
padding: const EdgeInsets.only(left: 14, top: 6),
child: CheckBoxText(
text: '拉黑该用户',
onChanged: (value) => banUid = value,
),
),
],
);
},
),
);
}
class ReasonField extends StatefulWidget {
final ValueChanged<String> onChanged;
String? _validator(String? value) => value.isNullOrEmpty ? '理由不能为空' : null;
const ReasonField({super.key, required this.onChanged});
@override
State<ReasonField> createState() => _ReasonFieldState();
}
class _ReasonFieldState extends State<ReasonField> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 22, top: 5, right: 22),
child: TextFormField(
controller: _controller,
autofocus: true,
minLines: 4,
maxLines: 4,
decoration: const InputDecoration(
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.all(10),
),
onChanged: widget.onChanged,
validator: widget._validator,
),
);
}
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
if (reasonType == null ||
(reasonType == 0 && key.currentState?.validate() != true)) {
return;
}
SmartDialog.showLoading();
try {
final data = await onSuccess(reasonType!, reasonDesc, banUid);
SmartDialog.dismiss();
if (data['code'] == 0) {
Get.back();
SmartDialog.showToast('举报成功');
} else {
SmartDialog.showToast(data['message'].toString());
}
} catch (e) {
SmartDialog.dismiss();
SmartDialog.showToast('提交失败:$e');
if (kDebugMode) rethrow;
}
},
child: const Text('确定'),
),
],
);
},
);
}
class CheckBoxText extends StatefulWidget {
@@ -186,8 +173,8 @@ class _CheckBoxTextState extends State<CheckBoxText> {
onTap: () {
setState(() {
_selected = !_selected;
widget.onChanged(_selected);
});
widget.onChanged(_selected);
},
child: Padding(
padding: const EdgeInsets.all(4),
@@ -246,4 +233,34 @@ class ReportOptions {
0: '其他',
},
};
static Map<String, Map<int, String>> get danmakuReport => const {
'': {
1: '违法违禁',
2: '色情低俗',
3: '赌博诈骗',
4: '人身攻击',
5: '侵犯隐私',
6: '垃圾广告',
7: '引战',
8: '剧透',
9: '恶意刷屏',
10: '视频无关',
12: '青少年不良信息',
13: '违法信息外链',
0: '其它', // 11
},
};
static Map<String, Map<int, String>> get liveDanmakuReport => const {
'': {
1: '违法违规',
2: '低俗色情',
3: '垃圾广告',
4: '辱骂引战',
5: '政治敏感',
6: '青少年不良信息',
7: '其他', // avoid show form
},
};
}

View File

@@ -1,126 +1,122 @@
import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class MemberReportPanel extends StatefulWidget {
const MemberReportPanel({
super.key,
required this.name,
required this.mid,
});
Future<void> showMemberReportDialog(
BuildContext context, {
required Object? name,
required Object mid,
}) {
final Set<int> reason = {};
int? reasonV2;
final dynamic name;
final dynamic mid;
@override
State<MemberReportPanel> createState() => _MemberReportPanelState();
}
class _MemberReportPanelState extends State<MemberReportPanel> {
final List<bool> _reasonList = List.generate(3, (_) => false);
final Set<int> _reason = {};
int? _reasonV2;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'举报: ${widget.name}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 4),
Text('uid: ${widget.mid}'),
const SizedBox(height: 10),
const Text('举报内容(必选,可多选)'),
...List.generate(
3,
(index) => _checkBoxWidget(
_reasonList[index],
(value) {
setState(() => _reasonList[index] = value);
if (value) {
_reason.add(index + 1);
} else {
_reason.remove(index + 1);
}
},
const ['头像违规', '昵称违规', '签名违规'][index],
return showDialog(
context: context,
builder: (context) {
final theme = Theme.of(context);
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
titleTextStyle: theme.textTheme.bodyMedium,
title: Column(
spacing: 4,
children: [
Text(
'举报: $name',
style: const TextStyle(fontSize: 18),
),
),
const Text('举报理由(单选,非必选)'),
...List.generate(
5,
(index) => RadioWidget<int>(
value: index,
groupValue: _reasonV2,
onChanged: (value) {
setState(() => _reasonV2 = value);
},
title: const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
Text('uid: $mid'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
const Text('举报内容(必选,可多选)'),
...List.generate(
3,
(index) => Builder(
builder: (context) => CheckboxListTile(
dense: true,
value: reason.contains(index + 1),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
onChanged: (value) {
if (value!) {
reason.add(index + 1);
} else {
reason.remove(index + 1);
}
(context as Element).markNeedsBuild();
},
title: Text(const ['头像违规', '昵称违规', '签名违规'][index]),
),
),
),
TextButton(
onPressed: () async {
if (_reason.isEmpty) {
SmartDialog.showToast('至少选择一项作为举报内容');
} else {
Get.back();
var result = await MemberHttp.reportMember(
widget.mid,
reason: _reason.join(','),
reasonV2: _reasonV2 != null ? _reasonV2! + 1 : null,
);
if (result['msg'] is String && result['msg'].isNotEmpty) {
SmartDialog.showToast(result['msg']);
} else {
SmartDialog.showToast('举报失败');
}
}
},
child: const Text('确定'),
const Text('举报理由(单选,非必选)'),
Builder(
builder: (context) => RadioGroup<int>(
onChanged: (v) {
reasonV2 = v;
(context as Element).markNeedsBuild();
},
groupValue: reasonV2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
5,
(index) => RadioListTile<int>(
toggleable: true,
controlAffinity: ListTileControlAffinity.leading,
contentPadding: const EdgeInsets.only(left: 4),
dense: true,
value: index,
title: Text(
const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index],
),
),
),
),
),
),
],
),
],
),
);
}
}
Widget _checkBoxWidget(
bool defValue,
ValueChanged onChanged,
String title,
) {
return InkWell(
onTap: () => onChanged(!defValue),
child: Row(
children: [
Checkbox(
value: defValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
),
),
TextButton(
onPressed: () async {
if (reason.isEmpty) {
SmartDialog.showToast('至少选择一项作为举报内容');
} else {
Get.back();
var result = await MemberHttp.reportMember(
mid,
reason: reason.join(','),
reasonV2: reasonV2 != null ? reasonV2! + 1 : null,
);
if (result['msg'] is String && result['msg'].isNotEmpty) {
SmartDialog.showToast(result['msg']);
} else {
SmartDialog.showToast('举报失败');
}
}
},
child: const Text('确定'),
),
],
);
},
);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,180 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
class DynamicSliverAppBar extends StatefulWidget {
const DynamicSliverAppBar({
this.flexibleSpace,
super.key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.backgroundGradient,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.collapsedHeight,
this.expandedHeight,
this.floating = false,
this.pinned = false,
this.snap = false,
this.stretch = false,
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
this.toolbarHeight = kToolbarHeight,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
this.appBarClipper,
this.callback,
});
final ValueChanged<double>? callback;
final Widget? flexibleSpace;
final Widget? leading;
final bool automaticallyImplyLeading;
final Widget? title;
final List<Widget>? actions;
final PreferredSizeWidget? bottom;
final double? elevation;
final double? scrolledUnderElevation;
final Color? shadowColor;
final Color? surfaceTintColor;
final bool forceElevated;
final Color? backgroundColor;
/// If backgroundGradient is non null, backgroundColor will be ignored
final LinearGradient? backgroundGradient;
final Color? foregroundColor;
final IconThemeData? iconTheme;
final IconThemeData? actionsIconTheme;
final bool primary;
final bool? centerTitle;
final bool excludeHeaderSemantics;
final double? titleSpacing;
final double? expandedHeight;
final double? collapsedHeight;
final bool floating;
final bool pinned;
final ShapeBorder? shape;
final double toolbarHeight;
final double? leadingWidth;
final TextStyle? toolbarTextStyle;
final TextStyle? titleTextStyle;
final SystemUiOverlayStyle? systemOverlayStyle;
final bool forceMaterialTransparency;
final Clip? clipBehavior;
final bool snap;
final bool stretch;
final double stretchTriggerOffset;
final AsyncCallback? onStretchTrigger;
final CustomClipper<Path>? appBarClipper;
@override
State<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
}
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
final GlobalKey _childKey = GlobalKey();
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
// to calculate dynamically the size for the sliver app bar
double _height = 0;
@override
void initState() {
super.initState();
_updateHeight();
}
void _updateHeight() {
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
// otherwise this will throw an error
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (_childKey.currentContext == null) return;
setState(() {
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
.size
.height;
widget.callback?.call(_height);
});
});
}
@override
void didChangeDependencies() {
_height = 0;
_updateHeight();
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
if (_height == 0) {
return SliverToBoxAdapter(
child: SizedBox(
key: _childKey,
child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight),
),
);
}
MediaQuery.orientationOf(context);
return SliverAppBar(
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,
collapsedHeight: widget.collapsedHeight,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
stretch: widget.stretch,
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: widget.toolbarHeight,
expandedHeight: _height,
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

@@ -99,12 +99,6 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
// to calculate dynamically the size for the sliver app bar
double _height = 0;
@override
void initState() {
super.initState();
_updateHeight();
}
void _updateHeight() {
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
// otherwise this will throw an error
@@ -119,27 +113,36 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
});
}
Orientation? _orientation;
double? _width;
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.orientationOf(context);
if (orientation != _orientation) {
_orientation = orientation;
void didChangeDependencies() {
super.didChangeDependencies();
final width = MediaQuery.widthOf(context);
if (_width != width) {
_width = width;
_height = 0;
_updateHeight();
}
}
@override
Widget build(BuildContext context) {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
if (_height == 0) {
return SliverToBoxAdapter(
child: SizedBox(
key: _childKey,
child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight),
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: SizedBox(
key: _childKey,
width: _width,
child: widget.flexibleSpace,
),
),
);
}
final padding = MediaQuery.viewPaddingOf(context).top;
return SliverAppBar.medium(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
@@ -159,7 +162,6 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
collapsedHeight: widget.collapsedHeight,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
@@ -167,8 +169,9 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: widget.toolbarHeight,
expandedHeight: _height - MediaQuery.paddingOf(context).top,
toolbarHeight: kToolbarHeight,
collapsedHeight: kToolbarHeight + padding + 1,
expandedHeight: _height - padding,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,

View File

@@ -0,0 +1,180 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer {
ImmediateTapGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
required this.onTapDown,
required this.onTapUp,
required this.onTapCancel,
this.onTap,
});
final GestureTapDownCallback onTapDown;
final GestureTapUpCallback onTapUp;
final GestureTapCancelCallback onTapCancel;
final GestureTapCallback? onTap;
PointerUpEvent? _up;
int _activePointer = 0;
bool _sentTapDown = false;
bool _wonArena = false;
@override
bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => false;
@override
bool isPointerAllowed(PointerDownEvent event) =>
_activePointer == 0 && super.isPointerAllowed(event);
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
_activePointer = event.pointer;
_sentTapDown = false;
_wonArena = false;
}
@override
void handleEvent(PointerEvent event) {
if (event.pointer != _activePointer) {
stopTrackingPointer(event.pointer);
return;
}
if (event is PointerDownEvent) {
_handleTapDown(event);
} else if (event is PointerMoveEvent) {
_handlePointerMove(event);
} else if (event is PointerUpEvent) {
_up = event;
_handlePointerUp(event);
}
stopTrackingIfPointerNoLongerDown(event);
}
void _handleTapDown(PointerDownEvent event) {
if (_sentTapDown) return;
_sentTapDown = true;
final details = TapDownDetails(
globalPosition: event.position,
localPosition: event.localPosition,
kind: event.kind,
);
invokeCallback<void>('onTapDown', () => onTapDown(details));
}
void _handlePointerMove(PointerMoveEvent event) {
if (event.delta.distanceSquared > 2.0) {
_cancelGesture('pointer moved');
stopTrackingPointer(event.pointer);
}
}
void _handlePointerUp(PointerUpEvent event) {
if (_wonArena && _sentTapDown) {
_handleTapUp(event);
}
}
void _handleTapUp(PointerUpEvent event) {
if (_sentTapDown) {
final details = TapUpDetails(
globalPosition: event.position,
localPosition: event.localPosition,
kind: event.kind,
);
invokeCallback<void>('onTapUp', () => onTapUp(details));
if (onTap != null) {
invokeCallback<void>('onTap', onTap!);
}
}
_reset();
}
void _cancelGesture(String reason) {
if (_sentTapDown) {
invokeCallback<void>('onTapCancel: $reason', onTapCancel);
}
_reset();
}
void _reset() {
_activePointer = 0;
_up = null;
_sentTapDown = false;
_wonArena = false;
}
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == _activePointer) {
_wonArena = true;
if (_up != null && _sentTapDown) {
_handleTapUp(_up!);
}
}
}
@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
if (pointer == _activePointer) {
_cancelGesture('gesture rejected by arena');
stopTrackingPointer(pointer);
}
}
@override
void didStopTrackingLastPointer(int pointer) {
// wait for arena
}
@override
void dispose() {
if (_sentTapDown) {
_cancelGesture('disposed');
}
_reset();
super.dispose();
}
@override
String get debugDescription => 'immediate tap';
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(IntProperty('activePointer', _activePointer))
..add(
FlagProperty(
'sentTapDown',
value: _sentTapDown,
ifTrue: 'has sentTapDown',
),
)
..add(FlagProperty('wonArena', value: _wonArena, ifTrue: 'wonArena'))
..add(
DiagnosticsProperty<PointerUpEvent>(
'pointerUpEvent',
_up,
defaultValue: null,
),
);
}
}

View File

@@ -0,0 +1,905 @@
// 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:io' show Platform;
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3;
class MouseInteractiveViewer extends StatefulWidget {
const MouseInteractiveViewer({
super.key,
this.clipBehavior = Clip.hardEdge,
this.panAxis = PanAxis.free,
this.boundaryMargin = EdgeInsets.zero,
this.constrained = true,
this.maxScale = 2.5,
this.minScale = 0.8,
this.interactionEndFrictionCoefficient = _kDrag,
this.pointerSignalFallback,
this.onPointerPanZoomUpdate,
this.onPointerPanZoomEnd,
this.onPointerDown,
this.onInteractionEnd,
this.onInteractionStart,
this.onInteractionUpdate,
this.panEnabled = true,
this.scaleEnabled = true,
this.scaleFactor = kDefaultMouseScrollToScaleFactor,
this.transformationController,
this.alignment,
this.trackpadScrollCausesScale = false,
required this.childKey,
required this.child,
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(maxScale > 0),
assert(maxScale >= minScale);
final Alignment? alignment;
final Clip clipBehavior;
final PanAxis panAxis;
final EdgeInsets boundaryMargin;
final Widget child;
final bool constrained;
final bool panEnabled;
final bool scaleEnabled;
final bool trackpadScrollCausesScale;
final double scaleFactor;
final double maxScale;
final double minScale;
final double interactionEndFrictionCoefficient;
final PointerSignalEventListener? pointerSignalFallback;
final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
final PointerPanZoomEndEventListener? onPointerPanZoomEnd;
final PointerDownEventListener? onPointerDown;
final GestureScaleEndCallback? onInteractionEnd;
final GestureScaleStartCallback? onInteractionStart;
final GestureScaleUpdateCallback? onInteractionUpdate;
final TransformationController? transformationController;
final GlobalKey childKey;
static const double _kDrag = 0.0000135;
@override
State<MouseInteractiveViewer> createState() => _MouseInteractiveViewerState();
}
class _MouseInteractiveViewerState extends State<MouseInteractiveViewer>
with TickerProviderStateMixin {
late TransformationController _transformer =
widget.transformationController ?? TransformationController();
final GlobalKey _parentKey = GlobalKey();
Animation<Offset>? _animation;
Animation<double>? _scaleAnimation;
late Offset _scaleAnimationFocalPoint;
late AnimationController _controller;
late AnimationController _scaleController;
Axis? _currentAxis;
Offset? _referenceFocalPoint;
double? _scaleStart;
double? _rotationStart = 0.0;
double _currentRotation = 0.0;
_GestureType? _gestureType;
static final gestureSettings = DeviceGestureSettings(
touchSlop: Platform.isIOS ? 9 : 4,
);
late final _scaleGestureRecognizer =
ScaleGestureRecognizer(
debugOwner: this,
allowedButtonsFilter: (buttons) => buttons == kPrimaryButton,
trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor),
trackpadScrollCausesScale: widget.trackpadScrollCausesScale,
)
..gestureSettings = gestureSettings
..onStart = _onScaleStart
..onUpdate = _onScaleUpdate
..onEnd = _onScaleEnd;
final bool _rotateEnabled = false;
Rect get _boundaryRect {
assert(widget.childKey.currentContext != null);
final RenderBox childRenderBox =
widget.childKey.currentContext!.findRenderObject()! as RenderBox;
final Size childSize = childRenderBox.size;
final Rect boundaryRect = widget.boundaryMargin.inflateRect(
Offset.zero & childSize,
);
assert(
!boundaryRect.isEmpty,
"InteractiveViewer's child must have nonzero dimensions.",
);
assert(
boundaryRect.isFinite ||
(boundaryRect.left.isInfinite &&
boundaryRect.top.isInfinite &&
boundaryRect.right.isInfinite &&
boundaryRect.bottom.isInfinite),
'boundaryRect must either be infinite in all directions or finite in all directions.',
);
return boundaryRect;
}
Rect get _viewport {
assert(_parentKey.currentContext != null);
final RenderBox parentRenderBox =
_parentKey.currentContext!.findRenderObject()! as RenderBox;
return Offset.zero & parentRenderBox.size;
}
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
if (translation == Offset.zero) {
return matrix.clone();
}
final Offset alignedTranslation;
if (_currentAxis != null) {
alignedTranslation = switch (widget.panAxis) {
PanAxis.horizontal => _alignAxis(translation, Axis.horizontal),
PanAxis.vertical => _alignAxis(translation, Axis.vertical),
PanAxis.aligned => _alignAxis(translation, _currentAxis!),
PanAxis.free => translation,
};
} else {
alignedTranslation = translation;
}
final Matrix4 nextMatrix = matrix.clone()
..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1);
final Quad nextViewport = _transformViewport(nextMatrix, _viewport);
if (_boundaryRect.isInfinite) {
return nextMatrix;
}
final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation(
_boundaryRect,
_currentRotation,
);
final Offset offendingDistance = _exceedsBy(
boundariesAabbQuad,
nextViewport,
);
if (offendingDistance == Offset.zero) {
return nextMatrix;
}
final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix);
final double currentScale = matrix.getMaxScaleOnAxis();
final Offset correctedTotalTranslation = Offset(
nextTotalTranslation.dx - offendingDistance.dx * currentScale,
nextTotalTranslation.dy - offendingDistance.dy * currentScale,
);
final Matrix4 correctedMatrix = matrix.clone()
..setTranslation(
Vector3(
correctedTotalTranslation.dx,
correctedTotalTranslation.dy,
0.0,
),
);
final Quad correctedViewport = _transformViewport(
correctedMatrix,
_viewport,
);
final Offset offendingCorrectedDistance = _exceedsBy(
boundariesAabbQuad,
correctedViewport,
);
if (offendingCorrectedDistance == Offset.zero) {
return correctedMatrix;
}
if (offendingCorrectedDistance.dx != 0.0 &&
offendingCorrectedDistance.dy != 0.0) {
return matrix.clone();
}
final Offset unidirectionalCorrectedTotalTranslation = Offset(
offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
);
return matrix.clone()..setTranslation(
Vector3(
unidirectionalCorrectedTotalTranslation.dx,
unidirectionalCorrectedTotalTranslation.dy,
0.0,
),
);
}
Matrix4 _matrixScale(Matrix4 matrix, double scale) {
if (scale == 1.0) {
return matrix.clone();
}
assert(scale != 0.0);
final double currentScale = _transformer.value.getMaxScaleOnAxis();
final double totalScale = math.max(
currentScale * scale,
math.max(
_viewport.width / _boundaryRect.width,
_viewport.height / _boundaryRect.height,
),
);
final double clampedTotalScale = clampDouble(
totalScale,
widget.minScale,
widget.maxScale,
);
final double clampedScale = clampedTotalScale / currentScale;
return matrix.clone()
..scaleByDouble(clampedScale, clampedScale, clampedScale, 1);
}
Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) {
if (rotation == 0) {
return matrix.clone();
}
final Offset focalPointScene = _transformer.toScene(focalPoint);
return matrix.clone()
..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1)
..rotateZ(-rotation)
..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1);
}
bool _gestureIsSupported(_GestureType? gestureType) {
return switch (gestureType) {
_GestureType.rotate => _rotateEnabled,
_GestureType.scale => widget.scaleEnabled,
_GestureType.pan || null => widget.panEnabled,
};
}
_GestureType _getGestureType(ScaleUpdateDetails details) {
final double scale = !widget.scaleEnabled ? 1.0 : details.scale;
final double rotation = !_rotateEnabled ? 0.0 : details.rotation;
if ((scale - 1).abs() > rotation.abs()) {
return _GestureType.scale;
} else if (rotation != 0.0) {
return _GestureType.rotate;
} else {
return _GestureType.pan;
}
}
// Handle the start of a gesture. All of pan, scale, and rotate are handled
// with GestureDetector's scale gesture.
void _onScaleStart(ScaleStartDetails details) {
widget.onInteractionStart?.call(details);
if (_controller.isAnimating) {
_controller
..stop()
..reset();
_animation?.removeListener(_handleInertiaAnimation);
_animation = null;
}
if (_scaleController.isAnimating) {
_scaleController
..stop()
..reset();
_scaleAnimation?.removeListener(_handleScaleAnimation);
_scaleAnimation = null;
}
_gestureType = null;
_currentAxis = null;
_scaleStart = _transformer.value.getMaxScaleOnAxis();
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
_rotationStart = _currentRotation;
}
// Handle an update to an ongoing gesture. All of pan, scale, and rotate are
// handled with GestureDetector's scale gesture.
void _onScaleUpdate(ScaleUpdateDetails details) {
final double scale = _transformer.value.getMaxScaleOnAxis();
_scaleAnimationFocalPoint = details.localFocalPoint;
final Offset focalPointScene = _transformer.toScene(
details.localFocalPoint,
);
if (_gestureType == _GestureType.pan) {
// When a gesture first starts, it sometimes has no change in scale and
// rotation despite being a two-finger gesture. Here the gesture is
// allowed to be reinterpreted as its correct type after originally
// being marked as a pan.
_gestureType = _getGestureType(details);
} else {
_gestureType ??= _getGestureType(details);
}
if (!_gestureIsSupported(_gestureType)) {
widget.onInteractionUpdate?.call(details);
return;
}
switch (_gestureType!) {
case _GestureType.scale:
assert(_scaleStart != null);
// details.scale gives us the amount to change the scale as of the
// start of this gesture, so calculate the amount to scale as of the
// previous call to _onScaleUpdate.
final double desiredScale = _scaleStart! * details.scale;
final double scaleChange = desiredScale / scale;
_transformer.value = _matrixScale(_transformer.value, scaleChange);
// While scaling, translate such that the user's two fingers stay on
// the same places in the scene. That means that the focal point of
// the scale should be on the same place in the scene before and after
// the scale.
final Offset focalPointSceneScaled = _transformer.toScene(
details.localFocalPoint,
);
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - _referenceFocalPoint!,
);
// details.localFocalPoint should now be at the same location as the
// original _referenceFocalPoint point. If it's not, that's because
// the translate came in contact with a boundary. In that case, update
// _referenceFocalPoint so subsequent updates happen in relation to
// the new effective focal point.
final Offset focalPointSceneCheck = _transformer.toScene(
details.localFocalPoint,
);
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
_referenceFocalPoint = focalPointSceneCheck;
}
case _GestureType.rotate:
if (details.rotation == 0.0) {
widget.onInteractionUpdate?.call(details);
return;
}
final double desiredRotation = _rotationStart! + details.rotation;
_transformer.value = _matrixRotate(
_transformer.value,
_currentRotation - desiredRotation,
details.localFocalPoint,
);
_currentRotation = desiredRotation;
case _GestureType.pan:
assert(_referenceFocalPoint != null);
// details may have a change in scale here when scaleEnabled is false.
// In an effort to keep the behavior similar whether or not scaleEnabled
// is true, these gestures are thrown away.
if (details.scale != 1.0) {
widget.onInteractionUpdate?.call(details);
return;
}
_currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene);
// Translate so that the same point in the scene is underneath the
// focal point before and after the movement.
final Offset translationChange =
focalPointScene - _referenceFocalPoint!;
_transformer.value = _matrixTranslate(
_transformer.value,
translationChange,
);
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
}
widget.onInteractionUpdate?.call(details);
}
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
// are handled with GestureDetector's scale gesture.
void _onScaleEnd(ScaleEndDetails details) {
widget.onInteractionEnd?.call(details);
_scaleStart = null;
_rotationStart = null;
_referenceFocalPoint = null;
_animation?.removeListener(_handleInertiaAnimation);
_scaleAnimation?.removeListener(_handleScaleAnimation);
_controller.reset();
_scaleController.reset();
if (!_gestureIsSupported(_gestureType)) {
_currentAxis = null;
return;
}
switch (_gestureType) {
case _GestureType.pan:
if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
_currentAxis = null;
return;
}
final Vector3 translationVector = _transformer.value.getTranslation();
final Offset translation = Offset(
translationVector.x,
translationVector.y,
);
final FrictionSimulation frictionSimulationX = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dx,
details.velocity.pixelsPerSecond.dx,
);
final FrictionSimulation frictionSimulationY = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dy,
details.velocity.pixelsPerSecond.dy,
);
final double tFinal = _getFinalTime(
details.velocity.pixelsPerSecond.distance,
widget.interactionEndFrictionCoefficient,
);
_animation =
Tween<Offset>(
begin: translation,
end: Offset(
frictionSimulationX.finalX,
frictionSimulationY.finalX,
),
).animate(
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
)
..addListener(_handleInertiaAnimation);
_controller
..duration = Duration(milliseconds: (tFinal * 1000).round())
..forward();
case _GestureType.scale:
if (details.scaleVelocity.abs() < 0.1) {
_currentAxis = null;
return;
}
final double scale = _transformer.value.getMaxScaleOnAxis();
final FrictionSimulation frictionSimulation = FrictionSimulation(
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
details.scaleVelocity / 10,
);
final double tFinal = _getFinalTime(
details.scaleVelocity.abs(),
widget.interactionEndFrictionCoefficient,
effectivelyMotionless: 0.1,
);
_scaleAnimation =
Tween<double>(
begin: scale,
end: frictionSimulation.x(tFinal),
).animate(
CurvedAnimation(
parent: _scaleController,
curve: Curves.decelerate,
),
)
..addListener(_handleScaleAnimation);
_scaleController
..duration = Duration(milliseconds: (tFinal * 1000).round())
..forward();
case _GestureType.rotate || null:
break;
}
}
void _receivedPointerSignal(PointerSignalEvent event) {
final Offset local = event.localPosition;
final Offset global = event.position;
final double scaleChange;
if (event is PointerScrollEvent) {
if (event.kind == PointerDeviceKind.trackpad) {
widget.onInteractionStart?.call(
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
);
final Offset localDelta = PointerEvent.transformDeltaViaPositions(
untransformedEndPosition: global + event.scrollDelta,
untransformedDelta: event.scrollDelta,
transform: event.transform,
);
final Offset focalPointScene = _transformer.toScene(local);
final Offset newFocalPointScene = _transformer.toScene(
local - localDelta,
);
_transformer.value = _matrixTranslate(
_transformer.value,
newFocalPointScene - focalPointScene,
);
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: global - event.scrollDelta,
localFocalPoint: local - localDelta,
focalPointDelta: -localDelta,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
_handlePointerScrollEvent(event);
return;
} else if (event is PointerScaleEvent) {
scaleChange = event.scale;
} else {
return;
}
widget.onInteractionStart?.call(
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: global,
localFocalPoint: local,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
final Offset focalPointScene = _transformer.toScene(local);
_transformer.value = _matrixScale(_transformer.value, scaleChange);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final Offset focalPointSceneScaled = _transformer.toScene(local);
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: global,
localFocalPoint: local,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
}
void _handlePointerScrollEvent(PointerScrollEvent event) {
final Offset local = event.localPosition;
final Offset global = event.position;
if (_gestureIsSupported(_GestureType.scale)) {
late final shift = HardwareKeyboard.instance.isShiftPressed;
if (HardwareKeyboard.instance.isControlPressed) {
_handleMouseWheelScale(event, local, global);
return;
} else if (shift || HardwareKeyboard.instance.isAltPressed) {
_handleMouseWheelPanAsScale(event, local, global, shift);
return;
} else {
widget.pointerSignalFallback?.call(event);
}
}
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: global,
localFocalPoint: local,
scale: math.exp(-event.scrollDelta.dy / widget.scaleFactor),
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
}
void _handleMouseWheelScale(
PointerScrollEvent event,
Offset local,
Offset global,
) {
final double scaleChange = math.exp(
-event.scrollDelta.dy / widget.scaleFactor,
);
final Offset focalPointScene = _transformer.toScene(local);
_transformer.value = _matrixScale(_transformer.value, scaleChange);
final Offset focalPointSceneScaled = _transformer.toScene(local);
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: global,
localFocalPoint: local,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
}
void _handleMouseWheelPanAsScale(
PointerScrollEvent event,
Offset local,
Offset global,
bool flip,
) {
final Offset translation = flip
? event.scrollDelta.flip
: event.scrollDelta;
final Offset focalPointScene = _transformer.toScene(local);
final Offset newFocalPointScene = _transformer.toScene(local - translation);
_transformer.value = _matrixTranslate(
_transformer.value,
newFocalPointScene - focalPointScene,
);
}
void _handleInertiaAnimation() {
if (!_controller.isAnimating) {
_currentAxis = null;
_animation?.removeListener(_handleInertiaAnimation);
_animation = null;
_controller.reset();
return;
}
final Vector3 translationVector = _transformer.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
_transformer.value = _matrixTranslate(
_transformer.value,
_transformer.toScene(_animation!.value) -
_transformer.toScene(translation),
);
}
void _handleScaleAnimation() {
if (!_scaleController.isAnimating) {
_currentAxis = null;
_scaleAnimation?.removeListener(_handleScaleAnimation);
_scaleAnimation = null;
_scaleController.reset();
return;
}
final double desiredScale = _scaleAnimation!.value;
final double scaleChange =
desiredScale / _transformer.value.getMaxScaleOnAxis();
final Offset referenceFocalPoint = _transformer.toScene(
_scaleAnimationFocalPoint,
);
_transformer.value = _matrixScale(_transformer.value, scaleChange);
final Offset focalPointSceneScaled = _transformer.toScene(
_scaleAnimationFocalPoint,
);
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - referenceFocalPoint,
);
}
void _handleTransformation() {
setState(() {});
}
void _onPointerDown(PointerDownEvent event) {
widget.onPointerDown?.call(event);
_scaleGestureRecognizer.addPointer(event);
}
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_scaleController = AnimationController(vsync: this);
_transformer.addListener(_handleTransformation);
}
@override
void didUpdateWidget(MouseInteractiveViewer oldWidget) {
super.didUpdateWidget(oldWidget);
final TransformationController? newController =
widget.transformationController;
if (newController == oldWidget.transformationController) {
return;
}
_transformer.removeListener(_handleTransformation);
if (oldWidget.transformationController == null) {
_transformer.dispose();
}
_transformer = newController ?? TransformationController();
_transformer.addListener(_handleTransformation);
}
@override
void dispose() {
_scaleGestureRecognizer.dispose();
_controller.dispose();
_scaleController.dispose();
_transformer.removeListener(_handleTransformation);
if (widget.transformationController == null) {
_transformer.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(widget.child.key == widget.childKey);
return Listener(
key: _parentKey,
behavior: HitTestBehavior.opaque,
onPointerSignal: _receivedPointerSignal,
onPointerDown: _onPointerDown,
onPointerPanZoomStart: _scaleGestureRecognizer.addPointerPanZoom,
onPointerPanZoomUpdate: widget.onPointerPanZoomUpdate,
onPointerPanZoomEnd: widget.onPointerPanZoomEnd,
child: _InteractiveViewerBuilt(
childKey: widget.childKey,
clipBehavior: widget.clipBehavior,
constrained: widget.constrained,
matrix: _transformer.value,
alignment: widget.alignment,
child: widget.child,
),
);
}
}
class _InteractiveViewerBuilt extends StatelessWidget {
const _InteractiveViewerBuilt({
required this.child,
required this.childKey,
required this.clipBehavior,
required this.constrained,
required this.matrix,
required this.alignment,
});
final Widget child;
final GlobalKey childKey;
final Clip clipBehavior;
final bool constrained;
final Matrix4 matrix;
final Alignment? alignment;
@override
Widget build(BuildContext context) {
Widget child = Transform(
transform: matrix,
alignment: alignment,
child: this.child,
);
if (!constrained) {
child = OverflowBox(
alignment: Alignment.topLeft,
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: child,
);
}
if (clipBehavior != Clip.none) {
child = ClipRect(clipBehavior: clipBehavior, child: child);
}
return child;
}
}
enum _GestureType { pan, scale, rotate }
double _getFinalTime(
double velocity,
double drag, {
double effectivelyMotionless = 10,
}) {
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
}
Offset _getMatrixTranslation(Matrix4 matrix) {
final Vector3 nextTranslation = matrix.getTranslation();
return Offset(nextTranslation.x, nextTranslation.y);
}
Quad _transformViewport(Matrix4 matrix, Rect viewport) {
final Matrix4 inverseMatrix = matrix.clone()..invert();
return Quad.points(
inverseMatrix.transform3(
Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0),
),
inverseMatrix.transform3(
Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0),
),
inverseMatrix.transform3(
Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0),
),
inverseMatrix.transform3(
Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0),
),
);
}
Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) {
final Matrix4 rotationMatrix = Matrix4.identity()
..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1)
..rotateZ(rotation)
..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1);
final Quad boundariesRotated = Quad.points(
rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)),
rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)),
rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)),
rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)),
);
// ignore: invalid_use_of_visible_for_testing_member
return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated);
}
Offset _exceedsBy(Quad boundary, Quad viewport) {
final List<Vector3> viewportPoints = <Vector3>[
viewport.point0,
viewport.point1,
viewport.point2,
viewport.point3,
];
Offset largestExcess = Offset.zero;
for (final Vector3 point in viewportPoints) {
// ignore: invalid_use_of_visible_for_testing_member
final Vector3 pointInside = InteractiveViewer.getNearestPointInside(
point,
boundary,
);
final Offset excess = Offset(
pointInside.x - point.x,
pointInside.y - point.y,
);
if (excess.dx.abs() > largestExcess.dx.abs()) {
largestExcess = Offset(excess.dx, largestExcess.dy);
}
if (excess.dy.abs() > largestExcess.dy.abs()) {
largestExcess = Offset(largestExcess.dx, excess.dy);
}
}
return _round(largestExcess);
}
Offset _round(Offset offset) {
return Offset(
double.parse(offset.dx.toStringAsFixed(9)),
double.parse(offset.dy.toStringAsFixed(9)),
);
}
Offset _alignAxis(Offset offset, Axis axis) {
return switch (axis) {
Axis.horizontal => Offset(offset.dx, 0.0),
Axis.vertical => Offset(0.0, offset.dy),
};
}
Axis? _getPanAxis(Offset point1, Offset point2) {
if (point1 == point2) {
return null;
}
final double x = point2.dx - point1.dx;
final double y = point2.dy - point1.dy;
return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
}
extension on Offset {
Offset get flip => Offset(dy, dx);
}

View File

@@ -0,0 +1,223 @@
// code from cached_network_svg_image;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
class CachedNetworkSVGImage extends StatefulWidget {
CachedNetworkSVGImage(
String url, {
Key? key,
String? cacheKey,
Widget? placeholder,
Widget? errorWidget,
double? width,
double? height,
Map<String, String>? headers,
BoxFit fit = BoxFit.contain,
AlignmentGeometry alignment = Alignment.center,
bool matchTextDirection = false,
bool allowDrawingOutsideViewBox = false,
String? semanticsLabel,
bool excludeFromSemantics = false,
SvgTheme theme = const SvgTheme(),
ColorFilter? colorFilter,
WidgetBuilder? placeholderBuilder,
BaseCacheManager? cacheManager,
}) : _url = url,
_cacheKey = cacheKey,
_placeholder = placeholder,
_errorWidget = errorWidget,
_width = width,
_height = height,
_headers = headers,
_fit = fit,
_alignment = alignment,
_matchTextDirection = matchTextDirection,
_allowDrawingOutsideViewBox = allowDrawingOutsideViewBox,
_semanticsLabel = semanticsLabel,
_excludeFromSemantics = excludeFromSemantics,
_theme = theme,
_colorFilter = colorFilter,
_placeholderBuilder = placeholderBuilder,
_cacheManager = cacheManager ?? DefaultCacheManager(),
super(key: key ?? ValueKey(cacheKey ?? url));
final String _url;
final String? _cacheKey;
final Widget? _placeholder;
final Widget? _errorWidget;
final double? _width;
final double? _height;
final Map<String, String>? _headers;
final BoxFit _fit;
final AlignmentGeometry _alignment;
final bool _matchTextDirection;
final bool _allowDrawingOutsideViewBox;
final String? _semanticsLabel;
final bool _excludeFromSemantics;
final SvgTheme _theme;
final ColorFilter? _colorFilter;
final WidgetBuilder? _placeholderBuilder;
final BaseCacheManager _cacheManager;
@override
State<CachedNetworkSVGImage> createState() => _CachedNetworkSVGImageState();
static Future<void> preCache(
String imageUrl, {
String? cacheKey,
BaseCacheManager? cacheManager,
}) {
final key = cacheKey ?? _generateKeyFromUrl(imageUrl);
cacheManager ??= DefaultCacheManager();
return cacheManager.downloadFile(key);
}
static Future<void> clearCacheForUrl(
String imageUrl, {
String? cacheKey,
BaseCacheManager? cacheManager,
}) {
final key = cacheKey ?? _generateKeyFromUrl(imageUrl);
cacheManager ??= DefaultCacheManager();
return cacheManager.removeFile(key);
}
static Future<void> clearCache({BaseCacheManager? cacheManager}) {
cacheManager ??= DefaultCacheManager();
return cacheManager.emptyCache();
}
static String _generateKeyFromUrl(String url) => url.split('?').first;
}
class _CachedNetworkSVGImageState extends State<CachedNetworkSVGImage> {
bool _isLoading = false;
bool _isError = false;
String? _svgString;
late final String _cacheKey;
double? height;
late TextScaler textScaler;
static final _sizeRegExp = RegExp(
r'height="([\d\.]+)([c-x]{2})?"',
);
@override
void initState() {
super.initState();
_cacheKey =
widget._cacheKey ??
CachedNetworkSVGImage._generateKeyFromUrl(widget._url);
_loadImage();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
textScaler = MediaQuery.textScalerOf(context);
}
Future<void> _loadImage() async {
try {
var file = (await widget._cacheManager.getFileFromCache(_cacheKey))?.file;
file ??= await widget._cacheManager.getSingleFile(
widget._url,
key: _cacheKey,
headers: widget._headers ?? {},
);
final svg = await file.readAsString();
_svgString = svg;
if (widget._width == null && widget._height == null) {
final match = _sizeRegExp.firstMatch(svg);
if (match != null) {
double h = double.parse(match.group(1)!);
final suffix = match.group(2);
if (suffix != null) {
h *= switch (suffix) {
'em' => textScaler.scale(widget._theme.fontSize),
'ex' => textScaler.scale(widget._theme.xHeight),
'pt' => 1.25,
'pc' => 15.0,
'mm' => 3.543307,
'cm' => 35.43307,
'in' => 90.0,
_ => 1.0,
};
}
height = h;
}
}
_isLoading = false;
_setState();
} catch (e) {
log('CachedNetworkSVGImage: $e');
_isError = true;
_isLoading = false;
_setState();
}
}
void _setState() {
if (mounted) {
setState(() {});
} else {
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget._width,
height: widget._height,
child: _buildImage(),
);
}
Widget? _buildImage() {
if (_isLoading) return _buildPlaceholderWidget();
if (_isError) return _buildErrorWidget();
return _buildSVGImage();
}
Widget _buildPlaceholderWidget() => Center(child: widget._placeholder);
Widget _buildErrorWidget() => Center(child: widget._errorWidget);
Widget? _buildSVGImage() {
if (_svgString == null) {
return Center(child: widget._placeholderBuilder?.call(context));
}
return SvgPicture.string(
_svgString!,
fit: widget._fit,
width: widget._width,
height: widget._height ?? height,
alignment: widget._alignment,
matchTextDirection: widget._matchTextDirection,
allowDrawingOutsideViewBox: widget._allowDrawingOutsideViewBox,
semanticsLabel: widget._semanticsLabel,
excludeFromSemantics: widget._excludeFromSemantics,
colorFilter: widget._colorFilter,
placeholderBuilder: widget._placeholderBuilder,
theme: widget._theme,
);
}
}

View File

@@ -0,0 +1,291 @@
/*
* This file is part of PiliPlus
*
* PiliPlus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PiliPlus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:math' show min;
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/custom_layout.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/marquee.dart' show ContextSingleTicker;
import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/utils/context_ext.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart'
hide CustomMultiChildLayout, MultiChildLayoutDelegate;
class ImageModel {
ImageModel({
required num? width,
required num? height,
required this.url,
this.liveUrl,
}) {
this.width = width == null || width == 0 ? 1 : width;
this.height = height == null || height == 0 ? 1 : height;
}
late num width;
late num height;
String url;
String? liveUrl;
bool? _isLongPic;
bool? _isLivePhoto;
bool get isLongPic => _isLongPic ??= (height / width) > _maxRatio;
bool get isLivePhoto =>
_isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
static bool enableLivePhoto = Pref.enableLivePhoto;
}
const double _maxRatio = 22 / 9;
class CustomGridView extends StatelessWidget {
const CustomGridView({
super.key,
this.space = 5,
required this.maxWidth,
required this.picArr,
this.onViewImage,
this.onDismissed,
this.fullScreen = false,
});
final double maxWidth;
final double space;
final List<ImageModel> picArr;
final VoidCallback? onViewImage;
final ValueChanged<int>? onDismissed;
final bool fullScreen;
static bool horizontalPreview = Pref.horizontalPreview;
void onTap(BuildContext context, int index) {
final imgList = picArr.map(
(item) {
bool isLive = item.isLivePhoto;
return SourceModel(
sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage,
url: item.url,
liveUrl: isLive ? item.liveUrl : null,
width: isLive ? item.width.toInt() : null,
height: isLive ? item.height.toInt() : null,
);
},
).toList();
if (horizontalPreview &&
!fullScreen &&
!context.mediaQuerySize.isPortrait) {
final scaffoldState = Scaffold.maybeOf(context);
if (scaffoldState != null) {
PageUtils.onHorizontalPreviewState(
scaffoldState,
ContextSingleTicker(scaffoldState.context),
imgList,
index,
);
return;
}
}
onViewImage?.call();
PageUtils.imageView(
initialPage: index,
imgList: imgList,
onDismissed: onDismissed,
);
}
static BorderRadius borderRadius(
int col,
int length,
int index, {
Radius r = StyleString.imgRadius,
}) {
if (length == 1) return StyleString.mdRadius;
final bool hasUp = index - col >= 0;
final bool hasDown = index + col < length;
final bool isRowStart = (index % col) == 0;
final bool isRowEnd = (index % col) == col - 1 || index == length - 1;
final bool hasLeft = !isRowStart;
final bool hasRight = !isRowEnd && (index + 1) < length;
return BorderRadius.only(
topLeft: !hasUp && !hasLeft ? r : Radius.zero,
topRight: !hasUp && !hasRight ? r : Radius.zero,
bottomLeft: !hasDown && !hasLeft ? r : Radius.zero,
bottomRight: !hasDown && !hasRight ? r : Radius.zero,
);
}
@override
Widget build(BuildContext context) {
double imageWidth;
double imageHeight;
final length = picArr.length;
final isSingle = length == 1;
final isFour = length == 4;
if (length == 2) {
imageWidth = imageHeight = (maxWidth - space) / 2;
} else {
imageHeight = imageWidth = (maxWidth - 2 * space) / 3;
if (isSingle) {
final img = picArr.first;
final width = img.width;
final height = img.height;
final ratioWH = width / height;
final ratioHW = height / width;
imageWidth = ratioWH > 1.5
? maxWidth
: (ratioWH >= 1 || (height > width && ratioHW < 1.5))
? 2 * imageWidth
: 1.5 * imageWidth;
if (width != 1) {
imageWidth = min(imageWidth, width.toDouble());
}
imageHeight = imageWidth * min(ratioHW, _maxRatio);
}
}
final int column = isFour ? 2 : 3;
final int row = isFour ? 2 : (length / 3).ceil();
late final placeHolder = Container(
width: imageWidth,
height: imageHeight,
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.onInverseSurface.withValues(alpha: 0.4),
),
child: Center(
child: Image.asset(
'assets/images/loading.png',
width: imageWidth,
height: imageHeight,
cacheWidth: imageWidth.cacheSize(context),
),
),
);
return Padding(
padding: const EdgeInsets.only(top: 6),
child: SizedBox(
width: maxWidth,
height: imageHeight * row + space * (row - 1),
child: CustomMultiChildLayout(
delegate: _CustomGridViewDelegate(
space: space,
itemCount: length,
column: column,
width: imageWidth,
height: imageHeight,
),
children: List.generate(length, (index) {
final item = picArr[index];
final radius = borderRadius(column, length, index);
return LayoutId(
id: index,
child: Hero(
tag: item.url,
child: GestureDetector(
onTap: () => onTap(context, index),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: radius,
child: NetworkImgLayer(
radius: 0,
src: item.url,
width: imageWidth,
height: imageHeight,
isLongPic: item.isLongPic,
forceUseCacheWidth: item.width <= item.height,
getPlaceHolder: () => placeHolder,
),
),
if (item.isLivePhoto)
const PBadge(
text: 'Live',
right: 8,
bottom: 8,
type: PBadgeType.gray,
)
else if (item.isLongPic)
const PBadge(
text: '长图',
right: 8,
bottom: 8,
),
],
),
),
),
);
}),
),
),
);
}
}
class _CustomGridViewDelegate extends MultiChildLayoutDelegate {
_CustomGridViewDelegate({
required this.space,
required this.itemCount,
required this.column,
required this.width,
required this.height,
});
final double space;
final int itemCount;
final int column;
final double width;
final double height;
@override
void performLayout(Size size) {
final constraints = BoxConstraints.expand(width: width, height: height);
for (int i = 0; i < itemCount; i++) {
layoutChild(i, constraints);
positionChild(
i,
Offset(
(space + width) * (i % column),
(space + height) * (i ~/ column),
),
);
}
}
@override
bool shouldRelayout(_CustomGridViewDelegate oldDelegate) {
return space != oldDelegate.space ||
itemCount != oldDelegate.itemCount ||
column != oldDelegate.column ||
width != oldDelegate.width ||
height != oldDelegate.height;
}
}

View File

@@ -2,7 +2,8 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -18,20 +19,16 @@ void imageSaveDialog({
animationType: SmartAnimationType.centerScale_otherSlide,
builder: (context) {
final theme = Theme.of(context);
late final iconColor = theme.colorScheme.onSurfaceVariant;
Widget iconBtn({
String? tooltip,
required IconData icon,
required Icon icon,
required VoidCallback? onPressed,
}) {
return iconButton(
context: context,
onPressed: onPressed,
iconSize: 20,
icon: icon,
bgColor: Colors.transparent,
iconColor: iconColor,
iconSize: 20,
onPressed: onPressed,
);
}
@@ -67,12 +64,12 @@ void imageSaveDialog({
color: Colors.black.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: IconButton(
child: const IconButton(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
padding: WidgetStatePropertyAll(EdgeInsets.zero),
),
onPressed: SmartDialog.dismiss,
icon: const Icon(
icon: Icon(
Icons.close,
size: 18,
color: Colors.white,
@@ -104,21 +101,22 @@ void imageSaveDialog({
(res) => SmartDialog.showToast(res['msg']),
),
},
icon: Icons.watch_later_outlined,
icon: const Icon(Icons.watch_later_outlined),
),
if (cover?.isNotEmpty == true) ...[
iconBtn(
tooltip: '分享',
onPressed: () {
SmartDialog.dismiss();
ImageUtil.onShareImg(cover!);
},
icon: Icons.share,
),
if (Utils.isMobile)
iconBtn(
tooltip: '分享',
onPressed: () {
SmartDialog.dismiss();
ImageUtils.onShareImg(cover!);
},
icon: const Icon(Icons.share),
),
iconBtn(
tooltip: '保存封面图',
onPressed: () async {
bool saveStatus = await ImageUtil.downloadImg(
bool saveStatus = await ImageUtils.downloadImg(
context,
[cover!],
);
@@ -126,7 +124,7 @@ void imageSaveDialog({
SmartDialog.dismiss();
}
},
icon: Icons.download,
icon: const Icon(Icons.download),
),
],
],

View File

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

View File

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

View File

@@ -1,595 +0,0 @@
import 'dart:async';
import 'dart:collection';
import 'dart:math' as math;
import 'package:flutter/material.dart';
/**
* @Author: Sky24n
* @GitHub: https://github.com/Sky24n
* @Description: NineGridView.
* @Date: 2020/06/16
*/
/// NineGridView Type.
enum NineGridType {
/// normal NineGridView.
normal,
/// like WeChat NineGridView.
weChat,
/// like WeiBo International NineGridView.
weiBo,
/// like WeChat group.
weChatGp,
/// like DingTalk group.
dingTalkGp,
/// like QQ group.
qqGp,
}
/// big images size cache map.
Map<String, Rect> ngvBigImageSizeMap = HashMap();
/// NineGridView.
/// like WeChat, WeiBo International, WeChat group, DingTalk group, QQ group.
///
/// Another [NineGridView](https://github.com/flutterchina/flukit) in [flukit](https://github.com/flutterchina/flukit) UI Kitusing GridView implementation。
class NineGridView extends StatefulWidget {
/// create NineGridView.
/// If you want to show a single big picture.
/// It is recommended to use a medium-quality picture, because the original picture is too large and takes time to load.
/// 单张大图建议使用中等质量图片,因为原图太大加载耗时。
/// you need input (bigImageWidth + bigImageHeight) or (bigImage + bigImageUrl).
const NineGridView({
super.key,
this.width,
this.height,
this.space = 3,
this.arcAngle = 0,
this.initIndex = 1,
this.padding = EdgeInsets.zero,
this.margin = EdgeInsets.zero,
this.alignment,
this.color,
this.decoration,
this.type = NineGridType.weChat,
required this.itemCount,
required this.itemBuilder,
this.bigImageWidth,
this.bigImageHeight,
this.bigImage,
this.bigImageUrl,
});
/// View width.
final double? width;
/// View height.
final double? height;
/// The number of logical pixels between each child.
final double space;
/// QQ group arc angle (0 ~ 180).
final double arcAngle;
/// QQ group init index (0 or 1). def 1.
final int initIndex;
/// View padding.
final EdgeInsets padding;
/// View margin.
final EdgeInsets margin;
/// Align the [child] within the container.
final AlignmentGeometry? alignment;
/// The color to paint behind the [child].
final Color? color;
/// The decoration to paint behind the [child].
final Decoration? decoration;
/// NineGridView type.
final NineGridType type;
/// The total number of children this delegate can provide.
final int itemCount;
/// Called to build children for the view.
final IndexedWidgetBuilder itemBuilder;
/// Single big picture width.
final double? bigImageWidth;
/// Single big picture height.
final double? bigImageHeight;
/// It is recommended to use a medium-quality picture, because the original picture is too large and takes time to load.
/// 单张大图建议使用中等质量图片,因为原图太大加载耗时。
/// Single big picture Image.
final Image? bigImage;
/// Single big picture url.
final String? bigImageUrl;
@override
State<StatefulWidget> createState() {
return _NineGridViewState();
}
}
/// _NineGridViewState.
class _NineGridViewState extends State<NineGridView> {
/// init view size.
Rect _initSize(BuildContext context) {
EdgeInsets padding = widget.padding;
if (widget.itemCount == 0) {
return Rect.fromLTRB(0, 0, padding.horizontal, padding.vertical);
}
double width =
widget.width ??
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
width = width - padding.horizontal;
double space = widget.space;
double itemW;
if (widget.type == NineGridType.weiBo &&
(widget.itemCount == 1 || widget.itemCount == 2)) {
// || itemCount == 4
itemW = (width - space) / 2;
} else {
itemW = (width - space * 2) / 3;
}
bool fourGrid =
(widget.itemCount == 4 && widget.type != NineGridType.normal);
int column = fourGrid ? 2 : math.min(3, widget.itemCount);
int row = fourGrid ? 2 : (widget.itemCount / 3).ceil();
double realWidth =
itemW * column + space * (column - 1) + padding.horizontal;
double realHeight = itemW * row + space * (row - 1) + padding.vertical;
return Rect.fromLTRB(itemW, 0, realWidth, realHeight);
}
/// build nine grid view.
Widget _buildChild(BuildContext context, double itemW) {
double space = widget.space;
int column = (widget.itemCount == 4 && widget.type != NineGridType.normal)
? 2
: 3;
List<Widget> list = [];
for (int i = 0; i < widget.itemCount; i++) {
list.add(
Positioned(
top: (space + itemW) * (i ~/ column),
left: (space + itemW) * (i % column),
child: SizedBox(
width: itemW,
height: itemW,
child: widget.itemBuilder(context, i),
),
),
);
}
return Stack(
clipBehavior: Clip.none,
children: list,
);
}
/// build one child.
Widget? _buildOneChild(BuildContext context) {
double? bigImgWidth = widget.bigImageWidth?.toDouble();
double? bigImgHeight = widget.bigImageHeight?.toDouble();
if (!_isZero(bigImgWidth) && !_isZero(bigImgHeight)) {
return _getOneChild(context, bigImgWidth!, bigImgHeight!);
} else if (widget.bigImage != null) {
String bigImageUrl = widget.bigImageUrl!;
Rect? bigImgRect = ngvBigImageSizeMap[bigImageUrl];
bigImgWidth = bigImgRect?.width;
bigImgHeight = bigImgRect?.height;
if (!_isZero(bigImgWidth) && !_isZero(bigImgHeight)) {
return _getOneChild(context, bigImgWidth!, bigImgHeight!);
} else {
_ImageUtil()
.getImageSize(widget.bigImage)
?.then((rect) {
ngvBigImageSizeMap[bigImageUrl] = rect;
if (!mounted) return;
setState(() {});
})
.catchError((e) {});
}
}
return null;
}
/// get one child.
Widget _getOneChild(BuildContext context, double width, double height) {
Rect rect = _getBigImgSize(width, height);
return SizedBox(
width: rect.width,
height: rect.height,
child: widget.itemBuilder(context, 0),
);
}
/// build weChat group.
Widget _buildWeChatGroup(BuildContext context) {
double width = widget.width! - widget.padding.horizontal;
double space = widget.space;
double itemW;
int column = widget.itemCount < 5 ? 2 : 3;
int row = 0;
if (widget.itemCount == 1) {
row = 1;
itemW = width;
} else if (widget.itemCount < 5) {
row = widget.itemCount == 2 ? 1 : 2;
itemW = (width - space) / 2;
} else if (widget.itemCount < 7) {
row = 2;
itemW = (width - space * 2) / 3;
} else {
row = 3;
itemW = (width - space * 2) / 3;
}
int first = widget.itemCount % column;
List<Widget> list = [];
for (int i = 0; i < widget.itemCount; i++) {
double left;
if (first > 0 && i < first) {
left =
(width - itemW * first - space * (first - 1)) / 2 +
(itemW + space) * i;
} else {
left = (space + itemW) * ((i - first) % column);
}
int itemIndex = (first > 0 && i < first)
? 0
: (first > 0 ? (i + column - first) : i) ~/ column;
double top =
(width - itemW * row - space * (row - 1)) / 2 +
(space + itemW) * itemIndex;
list.add(
Positioned(
top: top,
left: left,
child: SizedBox(
width: itemW,
height: itemW,
child: widget.itemBuilder(context, i),
),
),
);
}
return Stack(
clipBehavior: Clip.none,
children: list,
);
}
/// build dingTalk group.
Widget _buildDingTalkGroup(BuildContext context) {
double width = widget.width! - widget.padding.horizontal;
int itemCount = math.min(4, widget.itemCount);
double itemW = (width - widget.space) / 2;
List<Widget> children = [];
for (int i = 0; i < itemCount; i++) {
children.add(
Positioned(
top: (widget.space + itemW) * (i ~/ 2),
left:
(widget.space + itemW) *
(((itemCount == 3 && i == 2) ? i + 1 : i) % 2),
child: SizedBox(
width: itemCount == 1 ? width : itemW,
height:
(itemCount == 1 || itemCount == 2 || (itemCount == 3 && i == 0))
? width
: itemW,
child: widget.itemBuilder(context, i),
),
),
);
}
return ClipOval(
child: Stack(
clipBehavior: Clip.none,
children: children,
),
);
}
/// build QQ group.
Widget _buildQQGroup(BuildContext context) {
double width = widget.width! - widget.padding.horizontal;
int itemCount = math.min(5, widget.itemCount);
if (itemCount == 1) {
return ClipOval(
child: SizedBox(
width: width,
height: width,
child: widget.itemBuilder(context, 0),
),
);
}
List<Widget> children = [];
double startDegree = 0;
double r = 0;
double r1 = 0;
double centerX = width / 2;
double centerY = width / 2;
switch (itemCount) {
case 2:
startDegree = 135;
r = width / (2 + 2 * math.sin(math.pi / 4));
r1 = r;
break;
case 3:
startDegree = 210;
r = width / (2 + 4 * math.sin(math.pi * (3 - 2) / (2 * 3)));
r1 = r / math.cos(math.pi * (3 - 2) / (2 * 3));
double R =
r *
(1 + math.sin(math.pi / itemCount)) /
math.sin(math.pi / itemCount);
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 3)));
centerY = dy + r + r1;
break;
case 4:
startDegree = 180;
r = width / 4;
r1 = r / math.cos(math.pi / 4);
break;
case 5:
startDegree = 126;
r = width / (2 + 4 * math.sin(math.pi * (5 - 2) / (2 * 5)));
r1 = r / math.cos(math.pi * (5 - 2) / (2 * 5));
double R =
r *
(1 + math.sin(math.pi / itemCount)) /
math.sin(math.pi / itemCount);
double dy = 0.5 * (width - R - r * (1 + 1 / math.tan(math.pi / 5)));
centerY = dy + r + r1;
break;
}
for (int i = 0; i < itemCount; i++) {
double degree1 = (itemCount == 2 || itemCount == 4) ? (-math.pi / 4) : 0;
double x = centerX + r1 * math.sin(degree1 + i * 2 * math.pi / itemCount);
double y = centerY - r1 * math.cos(degree1 + i * 2 * math.pi / itemCount);
double degree = startDegree + i * 2 * 180 / itemCount;
if (degree >= 360) degree = degree % 360;
double previousX = r + 2 * r * math.sin(degree / 180 * math.pi);
double previousY = r - 2 * r * math.cos(degree / 180 * math.pi);
Widget child = Positioned.fromRect(
rect: Rect.fromCircle(center: Offset(x, y), radius: r),
child: ClipPath(
clipper: QQClipper(
total: itemCount,
index: i,
initIndex: widget.initIndex,
previousX: previousX,
previousY: previousY,
degree: degree,
arcAngle: widget.arcAngle,
space: widget.space,
),
child: widget.itemBuilder(context, i),
),
);
children.add(child);
}
return Stack(
clipBehavior: Clip.none,
children: children,
);
}
/// double is zero.
bool _isZero(double? value) {
return value == null || value == 0;
}
/// get big image size.
Rect _getBigImgSize(double originalWidth, double originalHeight) {
double width =
widget.width ??
(MediaQuery.sizeOf(context).width - widget.margin.horizontal);
width = width - widget.padding.horizontal;
double itemW = (width - widget.space * 2) / 3;
// double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
double devicePixelRatio = 1.0;
double tempWidth = originalWidth / devicePixelRatio;
double tempHeight = originalHeight / devicePixelRatio;
double maxW = itemW * 2 + widget.space;
double minW = width / 2;
double relWidth = tempWidth >= maxW ? maxW : math.max(minW, tempWidth);
double relHeight;
double ratio = tempWidth / tempHeight;
if (tempWidth == tempHeight) {
relHeight = relWidth;
} else if (tempWidth > tempHeight) {
relHeight = relWidth / (math.min(ratio, 4 / 3));
} else {
relHeight = relWidth / (math.max(ratio, 3 / 4));
}
return Rect.fromLTRB(0, 0, relWidth, relHeight);
}
@override
Widget build(BuildContext context) {
Widget? child;
double? realWidth = widget.width;
double? realHeight = widget.height;
switch (widget.type) {
case NineGridType.normal:
case NineGridType.weiBo:
case NineGridType.weChat:
Rect size = _initSize(context);
if (widget.itemCount == 1) {
child = _buildOneChild(context);
if (child == null) {
realWidth = size.right;
realHeight = size.bottom;
child = _buildChild(context, size.left);
}
} else {
realWidth = size.right;
realHeight = size.bottom;
child = _buildChild(context, size.left);
}
break;
case NineGridType.weChatGp:
child = _buildWeChatGroup(context);
break;
case NineGridType.dingTalkGp:
child = _buildDingTalkGroup(context);
break;
case NineGridType.qqGp:
child = _buildQQGroup(context);
break;
}
return Container(
alignment: widget.alignment,
color: widget.color,
decoration: widget.decoration,
margin: widget.margin,
padding: widget.padding,
width: realWidth,
height: realHeight,
child: child,
);
}
}
/// image util.
class _ImageUtil {
late ImageStreamListener listener;
late ImageStream imageStream;
/// get image size.
Future<Rect>? getImageSize(Image? image) {
if (image == null) {
return null;
}
Completer<Rect> completer = Completer<Rect>();
listener = ImageStreamListener(
(ImageInfo info, bool synchronousCall) {
imageStream.removeListener(listener);
if (!completer.isCompleted) {
completer.complete(
Rect.fromLTWH(
0,
0,
info.image.width.toDouble(),
info.image.height.toDouble(),
),
);
}
},
onError: (dynamic exception, StackTrace? stackTrace) {
imageStream.removeListener(listener);
if (!completer.isCompleted) {
completer.completeError(exception, stackTrace);
}
},
);
imageStream = image.image.resolve(ImageConfiguration.empty);
imageStream.addListener(listener);
return completer.future;
}
}
/// QQ Clipper.
class QQClipper extends CustomClipper<Path> {
QQClipper({
this.total = 0,
this.index = 0,
this.initIndex = 1,
this.previousX = 0,
this.previousY = 0,
this.degree = 0,
this.arcAngle = 0,
this.space = 0,
}) : assert(arcAngle >= 0 && arcAngle <= 180);
final int total;
final int index;
final int initIndex;
final double previousX;
final double previousY;
final double degree;
final double arcAngle;
final double space;
@override
Path getClip(Size size) {
double r = size.width / 2;
Path path = Path();
List<Offset> points = [];
if (total == 2 && index == initIndex) {
path.addOval(Rect.fromLTRB(0, 0, size.width, size.height));
} else {
/// arcAngle and space, prefer to use arcAngle.
double spaceA = arcAngle > 0
? (arcAngle / 2)
: (math.acos((r - math.min(r, space)) / r) / math.pi * 180);
double startA = degree + spaceA;
double endA = degree - spaceA;
for (double i = startA; i <= 360 + endA; i = i + 1) {
double x1 = r + r * math.sin(d2r(i));
double y1 = r - r * math.cos(d2r(i));
points.add(Offset(x1, y1));
}
double spaceB =
math.atan(
r * math.sin(d2r(spaceA)) / (2 * r - r * math.cos(d2r(spaceA))),
) /
math.pi *
180;
double r1 = (2 * r - r * math.cos(d2r(spaceA))) / math.cos(d2r(spaceB));
double startB = degree - 180 - spaceB;
double endB = degree - 180 + spaceB;
List<Offset> pointsB = [];
for (double i = startB; i < endB; i = i + 1) {
double x1 = previousX + r1 * math.sin(d2r(i));
double y1 = previousY - r1 * math.cos(d2r(i));
pointsB.add(Offset(x1, y1));
}
points.addAll(pointsB.reversed);
path.addPolygon(points, true);
}
return path;
}
/// degree to radian.
double d2r(double degree) {
return degree / 180 * math.pi;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) {
return this != oldClipper;
}
}

View File

@@ -2,28 +2,20 @@
// 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 'editable_text.dart';
/// @docImport 'scroll_view.dart';
/// @docImport 'table.dart';
library;
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3;
// Examples can assume:
// late BuildContext context;
// late Offset? _childWasTappedAt;
// late TransformationController _transformationController;
// Widget child = const Placeholder();
/// A signature for widget builders that take a [Quad] of the current viewport.
///
/// See also:
///
/// * [InteractiveViewer.builder], whose builder is of this type.
/// * [WidgetBuilder], which is similar, but takes no viewport.
typedef InteractiveViewerWidgetBuilder =
Widget Function(BuildContext context, Quad viewport);
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3;
/// A widget that enables pan and zoom interactions with its child.
///
@@ -428,43 +420,19 @@ class InteractiveViewer extends StatefulWidget {
static Quad getAxisAlignedBoundingBox(Quad quad) {
final double minX = math.min(
quad.point0.x,
math.min(
quad.point1.x,
math.min(
quad.point2.x,
quad.point3.x,
),
),
math.min(quad.point1.x, math.min(quad.point2.x, quad.point3.x)),
);
final double minY = math.min(
quad.point0.y,
math.min(
quad.point1.y,
math.min(
quad.point2.y,
quad.point3.y,
),
),
math.min(quad.point1.y, math.min(quad.point2.y, quad.point3.y)),
);
final double maxX = math.max(
quad.point0.x,
math.max(
quad.point1.x,
math.max(
quad.point2.x,
quad.point3.x,
),
),
math.max(quad.point1.x, math.max(quad.point2.x, quad.point3.x)),
);
final double maxY = math.max(
quad.point0.y,
math.max(
quad.point1.y,
math.max(
quad.point2.y,
quad.point3.y,
),
),
math.max(quad.point1.y, math.max(quad.point2.y, quad.point3.y)),
);
return Quad.points(
Vector3(minX, minY, 0),
@@ -529,7 +497,8 @@ class InteractiveViewer extends StatefulWidget {
class _InteractiveViewerState extends State<InteractiveViewer>
with TickerProviderStateMixin {
TransformationController? _transformationController;
late TransformationController _transformer =
widget.transformationController ?? TransformationController();
final GlobalKey _childKey = GlobalKey();
final GlobalKey _parentKey = GlobalKey();
@@ -611,10 +580,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
}
final Matrix4 nextMatrix = matrix.clone()
..translate(
alignedTranslation.dx,
alignedTranslation.dy,
);
..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1);
// Transform the viewport to determine where its four corners will be after
// the child has been transformed.
@@ -712,8 +678,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// Don't allow a scale that results in an overall scale beyond min/max
// scale.
final double currentScale = _transformationController!.value
.getMaxScaleOnAxis();
final double currentScale = _transformer.value.getMaxScaleOnAxis();
final double totalScale = math.max(
currentScale * scale,
// Ensure that the scale cannot make the child so big that it can't fit
@@ -729,7 +694,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
widget.maxScale,
);
final double clampedScale = clampedTotalScale / currentScale;
return matrix.clone()..scale(clampedScale);
return matrix.clone()
..scaleByDouble(clampedScale, clampedScale, clampedScale, 1);
}
// Return a new matrix representing the given matrix after applying the given
@@ -738,13 +704,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
if (rotation == 0) {
return matrix.clone();
}
final Offset focalPointScene = _transformationController!.toScene(
focalPoint,
);
final Offset focalPointScene = _transformer.toScene(focalPoint);
return matrix.clone()
..translate(focalPointScene.dx, focalPointScene.dy)
..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1)
..rotateZ(-rotation)
..translate(-focalPointScene.dx, -focalPointScene.dy);
..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1);
}
// Returns true iff the given _GestureType is enabled.
@@ -776,8 +740,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// with GestureDetector's scale gesture.
void _onScaleStart(ScaleStartDetails details) {
if (widget.isAnimating?.call() == true ||
(details.pointerCount < 2 &&
_transformationController?.value.row0.x == 1.0)) {
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
widget.onPanStart?.call(details);
return;
}
@@ -788,23 +751,21 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_controller
..stop()
..reset();
_animation?.removeListener(_onAnimate);
_animation?.removeListener(_handleInertiaAnimation);
_animation = null;
}
if (_scaleController.isAnimating) {
_scaleController
..stop()
..reset();
_scaleAnimation?.removeListener(_onScaleAnimate);
_scaleAnimation?.removeListener(_handleScaleAnimation);
_scaleAnimation = null;
}
_gestureType = null;
_currentAxis = null;
_scaleStart = _transformationController!.value.getMaxScaleOnAxis();
_referenceFocalPoint = _transformationController!.toScene(
details.localFocalPoint,
);
_scaleStart = _transformer.value.getMaxScaleOnAxis();
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
_rotationStart = _currentRotation;
}
@@ -812,15 +773,14 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// handled with GestureDetector's scale gesture.
void _onScaleUpdate(ScaleUpdateDetails details) {
if (widget.isAnimating?.call() == true ||
(details.pointerCount < 2 &&
_transformationController?.value.row0.x == 1.0)) {
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
widget.onPanUpdate?.call(details);
return;
}
final double scale = _transformationController!.value.getMaxScaleOnAxis();
final double scale = _transformer.value.getMaxScaleOnAxis();
_scaleAnimationFocalPoint = details.localFocalPoint;
final Offset focalPointScene = _transformationController!.toScene(
final Offset focalPointScene = _transformer.toScene(
details.localFocalPoint,
);
@@ -846,20 +806,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// previous call to _onScaleUpdate.
final double desiredScale = _scaleStart! * details.scale;
final double scaleChange = desiredScale / scale;
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
_transformer.value = _matrixScale(_transformer.value, scaleChange);
// While scaling, translate such that the user's two fingers stay on
// the same places in the scene. That means that the focal point of
// the scale should be on the same place in the scene before and after
// the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
final Offset focalPointSceneScaled = _transformer.toScene(
details.localFocalPoint,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - _referenceFocalPoint!,
);
@@ -868,7 +825,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// the translate came in contact with a boundary. In that case, update
// _referenceFocalPoint so subsequent updates happen in relation to
// the new effective focal point.
final Offset focalPointSceneCheck = _transformationController!.toScene(
final Offset focalPointSceneCheck = _transformer.toScene(
details.localFocalPoint,
);
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
@@ -881,15 +838,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
return;
}
final double desiredRotation = _rotationStart! + details.rotation;
_transformationController!.value = _matrixRotate(
_transformationController!.value,
_transformer.value = _matrixRotate(
_transformer.value,
_currentRotation - desiredRotation,
details.localFocalPoint,
);
_currentRotation = desiredRotation;
case _GestureType.pan:
assert(_referenceFocalPoint != null);
if (_referenceFocalPoint == null) {
return;
}
// details may have a change in scale here when scaleEnabled is false.
// In an effort to keep the behavior similar whether or not scaleEnabled
// is true, these gestures are thrown away.
@@ -902,13 +861,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// focal point before and after the movement.
final Offset translationChange =
focalPointScene - _referenceFocalPoint!;
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
_transformer.value = _matrixTranslate(
_transformer.value,
translationChange,
);
_referenceFocalPoint = _transformationController!.toScene(
details.localFocalPoint,
);
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
}
widget.onInteractionUpdate?.call(details);
}
@@ -916,12 +873,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
// are handled with GestureDetector's scale gesture.
void _onScaleEnd(ScaleEndDetails details) {
if (_transformationController?.value.row0.x == 1.0) {
if (_transformer.value.row0.x == 1.0) {
widget.onReset?.call();
}
if (widget.isAnimating?.call() == true ||
(details.pointerCount < 2 &&
_transformationController?.value.row0.x == 1.0)) {
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
widget.onPanEnd?.call(details);
return;
}
@@ -931,8 +887,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_rotationStart = null;
_referenceFocalPoint = null;
_animation?.removeListener(_onAnimate);
_scaleAnimation?.removeListener(_onScaleAnimate);
_animation?.removeListener(_handleInertiaAnimation);
_scaleAnimation?.removeListener(_handleScaleAnimation);
_controller.reset();
_scaleController.reset();
@@ -947,8 +903,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_currentAxis = null;
return;
}
final Vector3 translationVector = _transformationController!.value
.getTranslation();
final Vector3 translationVector = _transformer.value.getTranslation();
final Offset translation = Offset(
translationVector.x,
translationVector.y,
@@ -975,21 +930,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
frictionSimulationY.finalX,
),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
),
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
);
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
_animation!.addListener(_onAnimate);
_animation!.addListener(_handleInertiaAnimation);
_controller.forward();
case _GestureType.scale:
if (details.scaleVelocity.abs() < 0.1) {
_currentAxis = null;
return;
}
final double scale = _transformationController!.value
.getMaxScaleOnAxis();
final double scale = _transformer.value.getMaxScaleOnAxis();
final FrictionSimulation frictionSimulation = FrictionSimulation(
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
@@ -1013,7 +964,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_scaleController.duration = Duration(
milliseconds: (tFinal * 1000).round(),
);
_scaleAnimation!.addListener(_onScaleAnimate);
_scaleAnimation!.addListener(_handleScaleAnimation);
_scaleController.forward();
case _GestureType.rotate || null:
break;
@@ -1022,20 +973,19 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// Handle mousewheel and web trackpad scroll events.
void _receivedPointerSignal(PointerSignalEvent event) {
final Offset local = event.localPosition;
final Offset global = event.position;
final double scaleChange;
if (event is PointerScrollEvent) {
if (event.kind == PointerDeviceKind.trackpad &&
!widget.trackpadScrollCausesScale) {
// Trackpad scroll, so treat it as a pan.
widget.onInteractionStart?.call(
ScaleStartDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
),
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
);
final Offset localDelta = PointerEvent.transformDeltaViaPositions(
untransformedEndPosition: event.position + event.scrollDelta,
untransformedEndPosition: global + event.scrollDelta,
untransformedDelta: event.scrollDelta,
transform: event.transform,
);
@@ -1043,8 +993,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
if (!_gestureIsSupported(_GestureType.pan)) {
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - event.scrollDelta,
focalPoint: global - event.scrollDelta,
localFocalPoint: local - event.scrollDelta,
focalPointDelta: -localDelta,
),
);
@@ -1052,23 +1002,20 @@ class _InteractiveViewerState extends State<InteractiveViewer>
return;
}
final Offset focalPointScene = _transformationController!.toScene(
event.localPosition,
final Offset focalPointScene = _transformer.toScene(local);
final Offset newFocalPointScene = _transformer.toScene(
local - localDelta,
);
final Offset newFocalPointScene = _transformationController!.toScene(
event.localPosition - localDelta,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
_transformer.value = _matrixTranslate(
_transformer.value,
newFocalPointScene - focalPointScene,
);
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - localDelta,
focalPoint: global - event.scrollDelta,
localFocalPoint: local - localDelta,
focalPointDelta: -localDelta,
),
);
@@ -1086,17 +1033,14 @@ class _InteractiveViewerState extends State<InteractiveViewer>
return;
}
widget.onInteractionStart?.call(
ScaleStartDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
),
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
focalPoint: global,
localFocalPoint: local,
scale: scaleChange,
),
);
@@ -1104,95 +1048,75 @@ class _InteractiveViewerState extends State<InteractiveViewer>
return;
}
final Offset focalPointScene = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
final Offset focalPointScene = _transformer.toScene(local);
_transformer.value = _matrixScale(_transformer.value, scaleChange);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
final Offset focalPointSceneScaled = _transformer.toScene(local);
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
focalPoint: global,
localFocalPoint: local,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
}
// Handle inertia drag animation.
void _onAnimate() {
void _handleInertiaAnimation() {
if (!_controller.isAnimating) {
_currentAxis = null;
_animation?.removeListener(_onAnimate);
_animation?.removeListener(_handleInertiaAnimation);
_animation = null;
_controller.reset();
return;
}
// Translate such that the resulting translation is _animation.value.
final Vector3 translationVector = _transformationController!.value
.getTranslation();
final Vector3 translationVector = _transformer.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final Offset translationScene = _transformationController!.toScene(
translation,
);
final Offset animationScene = _transformationController!.toScene(
_animation!.value,
);
final Offset translationChangeScene = animationScene - translationScene;
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
translationChangeScene,
_transformer.value = _matrixTranslate(
_transformer.value,
_transformer.toScene(_animation!.value) -
_transformer.toScene(translation),
);
}
// Handle inertia scale animation.
void _onScaleAnimate() {
void _handleScaleAnimation() {
if (!_scaleController.isAnimating) {
_currentAxis = null;
_scaleAnimation?.removeListener(_onScaleAnimate);
_scaleAnimation?.removeListener(_handleScaleAnimation);
_scaleAnimation = null;
_scaleController.reset();
return;
}
final double desiredScale = _scaleAnimation!.value;
final double scaleChange =
desiredScale / _transformationController!.value.getMaxScaleOnAxis();
final Offset referenceFocalPoint = _transformationController!.toScene(
desiredScale / _transformer.value.getMaxScaleOnAxis();
final Offset referenceFocalPoint = _transformer.toScene(
_scaleAnimationFocalPoint,
);
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
_transformer.value = _matrixScale(_transformer.value, scaleChange);
// While scaling, translate such that the user's two fingers stay on
// the same places in the scene. That means that the focal point of
// the scale should be on the same place in the scene before and after
// the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
final Offset focalPointSceneScaled = _transformer.toScene(
_scaleAnimationFocalPoint,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
_transformer.value = _matrixTranslate(
_transformer.value,
focalPointSceneScaled - referenceFocalPoint,
);
}
void _onTransformationControllerChange() {
void _handleTransformation() {
// A change to the TransformationController's value is a change to the
// state.
setState(() {});
@@ -1201,63 +1125,36 @@ class _InteractiveViewerState extends State<InteractiveViewer>
@override
void initState() {
super.initState();
_transformationController =
widget.transformationController ?? TransformationController();
_transformationController!.addListener(_onTransformationControllerChange);
_controller = AnimationController(
vsync: this,
);
_controller = AnimationController(vsync: this);
_scaleController = AnimationController(vsync: this);
_transformer.addListener(_handleTransformation);
}
@override
void didUpdateWidget(InteractiveViewer oldWidget) {
super.didUpdateWidget(oldWidget);
// Handle all cases of needing to dispose and initialize
// transformationControllers.
if (oldWidget.transformationController == null) {
if (widget.transformationController != null) {
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController!.dispose();
_transformationController = widget.transformationController;
_transformationController!.addListener(
_onTransformationControllerChange,
);
}
} else {
if (widget.transformationController == null) {
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController = TransformationController();
_transformationController!.addListener(
_onTransformationControllerChange,
);
} else if (widget.transformationController !=
oldWidget.transformationController) {
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController = widget.transformationController;
_transformationController!.addListener(
_onTransformationControllerChange,
);
}
final TransformationController? newController =
widget.transformationController;
if (newController == oldWidget.transformationController) {
return;
}
_transformer.removeListener(_handleTransformation);
if (oldWidget.transformationController == null) {
_transformer.dispose();
}
_transformer = newController ?? TransformationController();
_transformer.addListener(_handleTransformation);
}
@override
void dispose() {
_controller.dispose();
_scaleController.dispose();
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformer.removeListener(_handleTransformation);
if (widget.transformationController == null) {
_transformationController!.dispose();
_transformer.dispose();
}
super.dispose();
}
@@ -1270,7 +1167,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
childKey: _childKey,
clipBehavior: widget.clipBehavior,
constrained: widget.constrained,
matrix: _transformationController!.value,
matrix: _transformer.value,
alignment: widget.alignment,
child: widget.child!,
);
@@ -1281,7 +1178,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
assert(!widget.constrained);
child = LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Matrix4 matrix = _transformationController!.value;
final Matrix4 matrix = _transformer.value;
return _InteractiveViewerBuilt(
childKey: _childKey,
clipBehavior: widget.clipBehavior,
@@ -1301,8 +1198,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
key: _parentKey,
onPointerSignal: _receivedPointerSignal,
child: GestureDetector(
behavior:
HitTestBehavior.translucent, // Necessary when panning off screen.
behavior: HitTestBehavior.opaque, // Necessary when panning off screen.
onScaleEnd: _onScaleEnd,
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
@@ -1338,10 +1234,7 @@ class _InteractiveViewerBuilt extends StatelessWidget {
Widget child = Transform(
transform: matrix,
alignment: alignment,
child: KeyedSubtree(
key: childKey,
child: this.child,
),
child: KeyedSubtree(key: childKey, child: this.child),
);
if (!constrained) {
@@ -1355,83 +1248,13 @@ class _InteractiveViewerBuilt extends StatelessWidget {
);
}
return ClipRect(
clipBehavior: clipBehavior,
child: child,
);
}
}
/// A thin wrapper on [ValueNotifier] whose value is a [Matrix4] representing a
/// transformation.
///
/// The [value] defaults to the identity matrix, which corresponds to no
/// transformation.
///
/// See also:
///
/// * [InteractiveViewer.transformationController] for detailed documentation
/// on how to use TransformationController with [InteractiveViewer].
class TransformationController extends ValueNotifier<Matrix4> {
/// Create an instance of [TransformationController].
///
/// The [value] defaults to the identity matrix, which corresponds to no
/// transformation.
TransformationController([Matrix4? value])
: super(value ?? Matrix4.identity());
/// Return the scene point at the given viewport point.
///
/// A viewport point is relative to the parent while a scene point is relative
/// to the child, regardless of transformation. Calling toScene with a
/// viewport point essentially returns the scene coordinate that lies
/// underneath the viewport point given the transform.
///
/// The viewport transforms as the inverse of the child (i.e. moving the child
/// left is equivalent to moving the viewport right).
///
/// This method is often useful when determining where an event on the parent
/// occurs on the child. This example shows how to determine where a tap on
/// the parent occurred on the child.
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// return GestureDetector(
/// onTapUp: (TapUpDetails details) {
/// _childWasTappedAt = _transformationController.toScene(
/// details.localPosition,
/// );
/// },
/// child: InteractiveViewer(
/// transformationController: _transformationController,
/// child: child,
/// ),
/// );
/// }
/// ```
Offset toScene(Offset viewportPoint) {
// On viewportPoint, perform the inverse transformation of the scene to get
// where the point would be in the scene before the transformation.
final Matrix4 inverseMatrix = Matrix4.inverted(value);
final Vector3 untransformed = inverseMatrix.transform3(
Vector3(
viewportPoint.dx,
viewportPoint.dy,
0,
),
);
return Offset(untransformed.x, untransformed.y);
return ClipRect(clipBehavior: clipBehavior, child: child);
}
}
// A classification of relevant user gestures. Each contiguous user gesture is
// represented by exactly one _GestureType.
enum _GestureType {
pan,
scale,
rotate,
}
enum _GestureType { pan, scale, rotate }
// Given a velocity and drag, calculate the time at which motion will come to
// a stop, within the margin of effectivelyMotionless.
@@ -1457,32 +1280,16 @@ Quad _transformViewport(Matrix4 matrix, Rect viewport) {
final Matrix4 inverseMatrix = matrix.clone()..invert();
return Quad.points(
inverseMatrix.transform3(
Vector3(
viewport.topLeft.dx,
viewport.topLeft.dy,
0.0,
),
Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0),
),
inverseMatrix.transform3(
Vector3(
viewport.topRight.dx,
viewport.topRight.dy,
0.0,
),
Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0),
),
inverseMatrix.transform3(
Vector3(
viewport.bottomRight.dx,
viewport.bottomRight.dy,
0.0,
),
Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0),
),
inverseMatrix.transform3(
Vector3(
viewport.bottomLeft.dx,
viewport.bottomLeft.dy,
0.0,
),
Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0),
),
);
}
@@ -1491,9 +1298,9 @@ Quad _transformViewport(Matrix4 matrix, Rect viewport) {
// the given amount.
Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) {
final Matrix4 rotationMatrix = Matrix4.identity()
..translate(rect.size.width / 2, rect.size.height / 2)
..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1)
..rotateZ(rotation)
..translate(-rect.size.width / 2, -rect.size.height / 2);
..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1);
final Quad boundariesRotated = Quad.points(
rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)),
rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)),
@@ -1562,20 +1369,3 @@ Axis? _getPanAxis(Offset point1, Offset point2) {
final double y = point2.dy - point1.dy;
return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
}
/// This enum is used to specify the behavior of the [InteractiveViewer] when
/// the user drags the viewport.
enum PanAxis {
/// The user can only pan the viewport along the horizontal axis.
horizontal,
/// The user can only pan the viewport along the vertical axis.
vertical,
/// The user can pan the viewport along the horizontal and vertical axes
/// but not diagonally.
aligned,
/// The user can pan the viewport freely in any direction.
free,
}

View File

@@ -18,7 +18,7 @@ class InteractiveViewerBoundary extends StatefulWidget {
super.key,
required this.child,
required this.boundaryWidth,
this.controller,
required this.controller,
this.onScaleChanged,
this.onLeftBoundaryHit,
this.onRightBoundaryHit,
@@ -43,7 +43,7 @@ class InteractiveViewerBoundary extends StatefulWidget {
final double boundaryWidth;
/// The [TransformationController] for the [InteractiveViewer].
final custom.TransformationController? controller;
final TransformationController controller;
/// Called when the scale changed after an interaction ended.
final ScaleChanged? onScaleChanged;
@@ -68,7 +68,7 @@ class InteractiveViewerBoundary extends StatefulWidget {
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
with SingleTickerProviderStateMixin {
custom.TransformationController? _controller;
late TransformationController _controller;
double? _scale;
@@ -85,8 +85,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
@override
void initState() {
super.initState();
_controller = widget.controller ?? custom.TransformationController();
_controller = widget.controller;
_animateController = AnimationController(
duration: const Duration(milliseconds: 300),
@@ -98,9 +97,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
@override
void dispose() {
_controller!.dispose();
_animateController.dispose();
super.dispose();
}
@@ -183,7 +180,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
}
void _updateBoundaryDetection() {
final double scale = _controller!.value.row0[0];
final double scale = _controller.value.row0[0];
if (_scale != scale) {
// the scale changed
@@ -196,7 +193,7 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
return;
}
final double xOffset = _controller!.value.row0[3];
final double xOffset = _controller.value.row0[3];
final double boundaryWidth = widget.boundaryWidth;
final double boundaryEnd = boundaryWidth * scale;
final double xPos = boundaryEnd + xOffset;

View File

@@ -1,17 +1,15 @@
import 'dart:io';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer.dart'
as custom;
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactive_viewer_boundary.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
@@ -38,7 +36,7 @@ typedef IndexedFocusedWidgetBuilder =
typedef IndexedTagStringBuilder = String Function(int index);
class InteractiveviewerGallery<T> extends StatefulWidget {
class InteractiveviewerGallery extends StatefulWidget {
const InteractiveviewerGallery({
super.key,
required this.sources,
@@ -48,7 +46,6 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
this.minScale = 1.0,
this.onPageChanged,
this.onDismissed,
this.setStatusBar = true,
this.onClose,
required this.quality,
});
@@ -57,8 +54,6 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
final ValueChanged<bool>? onClose;
final bool setStatusBar;
/// The sources to show.
final List<SourceModel> sources;
@@ -83,8 +78,8 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
with SingleTickerProviderStateMixin {
PageController? _pageController;
custom.TransformationController? _transformationController;
late final PageController _pageController;
late final TransformationController _transformationController;
/// The controller to animate the transformation value of the
/// [InteractiveViewer] when it should reset.
@@ -107,55 +102,32 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
_pageController = PageController(initialPage: widget.initIndex);
_transformationController = custom.TransformationController();
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
)..addListener(listener);
if (widget.setStatusBar) {
setStatusBar();
}
var item = widget.sources[currentIndex.value];
final item = widget.sources[currentIndex.value];
if (item.sourceType == SourceType.livePhoto) {
_onPlay(item.liveUrl!);
}
}
void listener() {
_transformationController!.value = _animation?.value ?? Matrix4.identity();
}
SystemUiMode? mode;
Future<void> setStatusBar() async {
if (Platform.isIOS || Platform.isAndroid) {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
);
}
if (Platform.isAndroid && (await Utils.sdkInt < 29)) {
mode = SystemUiMode.manual;
}
_transformationController.value = _animation?.value ?? Matrix4.identity();
}
@override
void dispose() {
widget.onClose?.call(true);
_player?.dispose();
_pageController?.dispose();
_pageController.dispose();
_animationController
..removeListener(listener)
..dispose();
if (widget.setStatusBar) {
if (Platform.isIOS || Platform.isAndroid) {
SystemChrome.setEnabledSystemUIMode(
mode ?? SystemUiMode.edgeToEdge,
overlays: SystemUiOverlay.values,
);
}
}
_transformationController.dispose();
for (var item in widget.sources) {
if (item.sourceType == SourceType.networkImage) {
CachedNetworkImageProvider(_getActualUrl(item.url)).evict();
@@ -189,7 +161,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
/// When the left boundary has been hit after scaling up the source, the page
/// view swiping gets enabled if it has a page to swipe to.
void _onLeftBoundaryHit() {
if (!_enablePageView && _pageController!.page!.floor() > 0) {
if (!_enablePageView && _pageController.page!.floor() > 0) {
setState(() {
_enablePageView = true;
});
@@ -200,7 +172,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
/// view swiping gets enabled if it has a page to swipe to.
void _onRightBoundaryHit() {
if (!_enablePageView &&
_pageController!.page!.floor() < widget.sources.length - 1) {
_pageController.page!.floor() < widget.sources.length - 1) {
setState(() {
_enablePageView = true;
});
@@ -235,12 +207,12 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
_onPlay(item.liveUrl!);
}
widget.onPageChanged?.call(page);
if (_transformationController!.value != Matrix4.identity()) {
if (_transformationController.value != Matrix4.identity()) {
// animate the reset for the transformation of the interactive viewer
_animation =
Matrix4Tween(
begin: _transformationController!.value,
begin: _transformationController.value,
end: Matrix4.identity(),
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
@@ -252,7 +224,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
String _getActualUrl(String url) {
return _quality != 100
? ImageUtil.thumbnailUrl(url, _quality)
? ImageUtils.thumbnailUrl(url, _quality)
: url.http2https;
}
@@ -261,7 +233,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
widget.onClose!(false);
} else {
Get.back();
widget.onDismissed?.call(_pageController!.page!.floor());
widget.onDismissed?.call(_pageController.page!.floor());
}
}
@@ -275,7 +247,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
children: [
InteractiveViewerBoundary(
controller: _transformationController,
boundaryWidth: MediaQuery.sizeOf(context).width,
boundaryWidth: MediaQuery.widthOf(context),
onScaleChanged: _onScaleChanged,
onLeftBoundaryHit: _onLeftBoundaryHit,
onRightBoundaryHit: _onRightBoundaryHit,
@@ -299,6 +271,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
itemCount: widget.sources.length,
itemBuilder: (BuildContext context, int index) {
final item = widget.sources[index];
final isFileImg = item.sourceType == SourceType.fileImage;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => EasyThrottle.throttle(
@@ -314,9 +287,10 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
const Duration(milliseconds: 555),
onDoubleTap,
),
onLongPress: item.sourceType == SourceType.fileImage
? null
: () => onLongPress(item),
onLongPress: !isFileImg ? () => onLongPress(item) : null,
onSecondaryTap: !isFileImg && !Utils.isMobile
? () => onLongPress(item)
: null,
child: widget.itemBuilder != null
? widget.itemBuilder!(
context,
@@ -335,7 +309,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
right: 0,
child: Container(
padding:
MediaQuery.paddingOf(context) +
MediaQuery.viewPaddingOf(context) +
const EdgeInsets.fromLTRB(12, 8, 20, 8),
decoration: _enablePageView
? BoxDecoration(
@@ -349,77 +323,12 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
),
)
: null,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: onClose,
),
),
if (widget.sources.length > 1)
Align(
alignment: Alignment.center,
child: Obx(
() => Text(
"${currentIndex.value + 1}/${widget.sources.length}",
style: const TextStyle(color: Colors.white),
),
),
),
if (widget.sources[currentIndex.value].sourceType !=
SourceType.fileImage)
Align(
alignment: Alignment.centerRight,
child: PopupMenuButton(
itemBuilder: (context) {
final item = widget.sources[currentIndex.value];
return [
PopupMenuItem(
onTap: () => ImageUtil.onShareImg(item.url),
child: const Text("分享图片"),
),
PopupMenuItem(
onTap: () => Utils.copyText(item.url),
child: const Text("复制链接"),
),
PopupMenuItem(
onTap: () => ImageUtil.downloadImg(
this.context,
[item.url],
),
child: const Text("保存图片"),
),
if (widget.sources.length > 1)
PopupMenuItem(
onTap: () => ImageUtil.downloadImg(
this.context,
widget.sources.map((item) => item.url).toList(),
),
child: const Text("保存全部"),
),
if (item.sourceType == SourceType.livePhoto)
PopupMenuItem(
onTap: () {
ImageUtil.downloadLivePhoto(
context: this.context,
url: item.url,
liveUrl: item.liveUrl!,
width: item.width!,
height: item.height!,
);
},
child: const Text("保存 Live Photo"),
),
];
},
child: const Icon(Icons.more_horiz, color: Colors.white),
),
),
],
alignment: Alignment.center,
child: Obx(
() => Text(
"${currentIndex.value + 1}/${widget.sources.length}",
style: const TextStyle(color: Colors.white),
),
),
),
),
@@ -445,7 +354,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
return CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: ImageUtil.thumbnailUrl(item.url, widget.quality),
imageUrl: ImageUtils.thumbnailUrl(item.url, widget.quality),
);
},
),
@@ -465,7 +374,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
}
void onDoubleTap() {
Matrix4 matrix = _transformationController!.value.clone();
Matrix4 matrix = _transformationController.value.clone();
double currentScale = matrix.row0.x;
double targetScale = widget.minScale;
@@ -502,7 +411,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
_animation =
Matrix4Tween(
begin: _transformationController!.value,
begin: _transformationController.value,
end: matrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
@@ -522,14 +431,15 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
onTap: () {
Get.back();
ImageUtil.onShareImg(item.url);
},
dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)),
),
if (Utils.isMobile)
ListTile(
onTap: () {
Get.back();
ImageUtils.onShareImg(item.url);
},
dense: true,
title: const Text('分享', style: TextStyle(fontSize: 14)),
),
ListTile(
onTap: () {
Get.back();
@@ -541,7 +451,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
ListTile(
onTap: () {
Get.back();
ImageUtil.downloadImg(
ImageUtils.downloadImg(
this.context,
[item.url],
);
@@ -549,11 +459,20 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
dense: true,
title: const Text('保存图片', style: TextStyle(fontSize: 14)),
),
if (widget.sources.length > 1)
if (Utils.isDesktop)
ListTile(
onTap: () {
Get.back();
ImageUtil.downloadImg(
PageUtils.launchURL(item.url);
},
dense: true,
title: const Text('网页打开', style: TextStyle(fontSize: 14)),
)
else if (widget.sources.length > 1)
ListTile(
onTap: () {
Get.back();
ImageUtils.downloadImg(
this.context,
widget.sources.map((item) => item.url).toList(),
);
@@ -565,7 +484,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
ListTile(
onTap: () {
Get.back();
ImageUtil.downloadLivePhoto(
ImageUtils.downloadLivePhoto(
context: this.context,
url: item.url,
liveUrl: item.liveUrl!,

View File

@@ -22,6 +22,14 @@ class _KeepAliveWrapperState extends State<KeepAliveWrapper>
return widget.builder(context);
}
@override
void didUpdateWidget(KeepAliveWrapper oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.wantKeepAlive != widget.wantKeepAlive) {
updateKeepAlive();
}
}
@override
bool get wantKeepAlive => widget.wantKeepAlive;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
import 'dart:math';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LoadingWidget extends StatelessWidget {
const LoadingWidget({
super.key,
this.msg = 'loading...',
required this.progress,
});
///loading msg
final String msg;
final RxDouble progress;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
decoration: BoxDecoration(
color: theme.dialogTheme.backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//loading animation
RepaintBoundary.wrap(
Obx(
() => CustomPaint(
size: const Size.square(40),
painter: ArcPainter(
color: onSurfaceVariant,
strokeWidth: 3,
sweepAngle: progress.value * 2 * pi,
),
),
),
0,
),
//msg
Padding(
padding: const EdgeInsets.only(top: 20),
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
),
],
),
);
}
}

View File

@@ -19,10 +19,7 @@ class HttpError extends StatelessWidget {
Widget build(BuildContext context) {
return isSliver
? SliverToBoxAdapter(child: content(context))
: SizedBox(
width: double.infinity,
child: content(context),
);
: SizedBox(width: double.infinity, child: content(context));
}
Widget content(BuildContext context) {
@@ -60,7 +57,7 @@ class HttpError extends StatelessWidget {
style: TextStyle(color: theme.colorScheme.primary),
),
),
SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
SizedBox(height: 40 + MediaQuery.viewPaddingOf(context).bottom),
],
);
}

View File

@@ -0,0 +1,475 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
class MarqueeText extends StatelessWidget {
final String text;
final TextStyle? style;
final double spacing;
final double velocity;
final ContextSingleTicker? provider;
const MarqueeText(
this.text, {
super.key,
this.style,
this.spacing = 0,
this.velocity = 25,
this.provider,
});
@override
Widget build(BuildContext context) {
return NormalMarquee(
velocity: velocity,
spacing: spacing,
provider: provider,
child: Text(
text,
style: style,
maxLines: 1,
textDirection: TextDirection.ltr,
),
);
}
}
abstract class Marquee extends SingleChildRenderObjectWidget {
final Axis direction;
final Clip clipBehavior;
final double spacing;
final double velocity;
final ContextSingleTicker? provider;
const Marquee({
super.key,
required this.velocity,
required super.child,
this.direction = Axis.horizontal,
this.clipBehavior = Clip.hardEdge,
this.spacing = 0,
this.provider,
});
@override
void updateRenderObject(
BuildContext context,
covariant MarqueeRender renderObject,
) {
renderObject
..direction = direction
..clipBehavior = clipBehavior
..velocity = velocity
..spacing = spacing;
if (provider != null) {
renderObject.provider = provider!;
}
}
}
class NormalMarquee extends Marquee {
const NormalMarquee({
super.key,
required super.velocity,
required super.child,
super.direction,
super.clipBehavior,
super.spacing,
super.provider,
});
@override
RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender(
direction: direction,
velocity: velocity,
clipBehavior: clipBehavior,
spacing: spacing,
provider: provider ?? ContextSingleTicker(context),
);
}
class BounceMarquee extends Marquee {
const BounceMarquee({
super.key,
required super.velocity,
required super.child,
super.direction,
super.clipBehavior,
super.spacing,
super.provider,
});
@override
RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender(
direction: direction,
velocity: velocity,
clipBehavior: clipBehavior,
spacing: spacing,
provider: provider ?? ContextSingleTicker(context),
);
}
abstract class MarqueeRender extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
MarqueeRender({
required Axis direction,
required double velocity,
required double spacing,
required this.clipBehavior,
required ContextSingleTicker provider,
}) : _ticker = provider,
_spacing = spacing,
_velocity = velocity,
_direction = direction,
assert(spacing.isFinite && !spacing.isNaN);
Clip clipBehavior;
Axis _direction;
Axis get direction => _direction;
set direction(Axis value) {
if (_direction == value) return;
_direction = value;
markNeedsLayout();
}
ContextSingleTicker _ticker;
set provider(ContextSingleTicker value) {
if (_ticker == value) return;
if (_ticker._ticker != null) {
if (value._ticker != null) {
value._ticker!.absorbTicker(_ticker._ticker!);
} else {
value
..createTicker(_onTick)
..initStart();
}
}
_ticker.cancel();
_ticker = value;
}
double _velocity;
set velocity(double value) {
if (_velocity == value) return;
_velocity = value;
_simulation = _simulation?.copyWith(initialValue: _delta, velocity: value);
_ticker.reset();
}
double _spacing;
set spacing(double value) {
if (value.isNegative) {
value *= _direction == Axis.horizontal ? -size.width : -size.height;
}
if (_spacing == value) return;
_simulation = _simulation?.copyWith(
initialValue: _delta,
addSize: value - _spacing,
);
_spacing = value;
_ticker.reset();
}
double _delta = 0;
set delta(double value) {
if (_delta == value) return;
_delta = value;
markNeedsPaint();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_ticker.updateTicker();
}
@override
void dispose() {
_ticker.cancel();
super.dispose();
}
late double _distance;
_MarqueeSimulation? _simulation;
@override
void performLayout() {
final child = this.child;
if (child == null) {
size = constraints.smallest;
return;
}
if (_direction == Axis.horizontal) {
child.layout(
BoxConstraints(maxHeight: constraints.maxHeight),
parentUsesSize: true,
);
size = constraints.constrain(child.size);
_distance = child.size.width - size.width;
if (_spacing.isNegative) _spacing *= -size.width;
} else {
child.layout(
BoxConstraints(maxWidth: constraints.maxWidth),
parentUsesSize: true,
);
size = constraints.constrain(child.size);
_distance = child.size.height - size.height;
if (_spacing.isNegative) _spacing *= -size.height;
}
if (_distance > 0) {
updateSize();
_ticker.initIfNeeded(_onTick);
} else {
_ticker.cancel();
}
}
@override
bool get isRepaintBoundary => true;
void paintCenter(PaintingContext context, Offset offset) {
if (_direction == Axis.horizontal) {
context.paintChild(child!, Offset(offset.dx - _distance / 2, offset.dy));
} else {
context.paintChild(child!, Offset(offset.dx, offset.dy - _distance / 2));
}
}
void _onTick(Duration elapsed) {
delta = _simulation!.x(
elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond,
);
}
void updateSize();
}
class _BounceMarqueeRender extends MarqueeRender {
_BounceMarqueeRender({
required super.direction,
required super.velocity,
required super.clipBehavior,
required super.spacing,
required super.provider,
});
@override
void updateSize() {
final size = _distance + _spacing;
if (size == _simulation?.size) return;
_simulation = _MarqueeSimulation(_delta, size, false, _velocity);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child == null) return;
if (_distance > 0) {
final delta = _spacing / 2.0 - _delta;
void paintChild() {
if (_direction == Axis.horizontal) {
context.paintChild(child!, Offset(offset.dx + delta, offset.dy));
} else {
context.paintChild(child!, Offset(offset.dx, offset.dy + delta));
}
}
if (clipBehavior == Clip.none) {
paintChild();
} else {
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
}
} else {
paintCenter(context, offset);
}
}
}
class _NormalMarqueeRender extends MarqueeRender {
_NormalMarqueeRender({
required super.direction,
required super.velocity,
required super.clipBehavior,
required super.spacing,
required super.provider,
});
@override
void updateSize() {
final size =
(_direction == Axis.horizontal
? child!.size.width
: child!.size.height) +
_spacing;
if (size == _simulation?.size) return;
_simulation = _MarqueeSimulation(_delta, size, true, _velocity);
}
@override
void paint(PaintingContext context, Offset offset) {
final child = this.child;
if (child == null) return;
if (_distance > 0) {
void paintChild() {
if (_direction == Axis.horizontal) {
final dx = _delta;
context.paintChild(child, Offset(offset.dx - dx, offset.dy));
if (dx > _distance) {
context.paintChild(
child,
Offset(offset.dx + _simulation!.size - dx, offset.dy),
);
}
} else {
final dy = _delta;
context.paintChild(child, Offset(offset.dx, offset.dy - dy));
if (dy > _distance) {
context.paintChild(
child,
Offset(offset.dx, offset.dy + _simulation!.size - dy),
);
}
}
}
if (clipBehavior == Clip.none) {
paintChild();
} else {
final rect = Rect.fromLTRB(0, 0, size.width, size.height);
context.clipRectAndPaint(rect, clipBehavior, rect, paintChild);
}
} else {
paintCenter(context, offset);
}
}
}
class _MarqueeSimulation extends Simulation {
_MarqueeSimulation(
this.initialValue,
this.size,
this.notBounce,
this.velocity,
);
final double initialValue;
final double size;
final bool notBounce;
final double velocity;
@override
double x(double timeInSeconds) {
assert(timeInSeconds >= 0.0);
final totalX = initialValue + velocity * timeInSeconds;
if (notBounce) return totalX % size;
final doublePeriod = 2.0 * size;
final doubleX = totalX % doublePeriod;
return doubleX < size ? doubleX : doublePeriod - doubleX;
}
@override
double dx(double timeInSeconds) => velocity;
@override
bool isDone(double timeInSeconds) => false;
_MarqueeSimulation copyWith({
final double? initialValue,
final double? addSize,
final bool? notBounce,
final double? velocity,
}) => _MarqueeSimulation(
initialValue ?? this.initialValue,
addSize == null ? size : size + addSize,
notBounce ?? this.notBounce,
velocity ?? this.velocity,
);
}
class ContextSingleTicker implements TickerProvider {
Ticker? _ticker;
BuildContext context;
final bool autoStart;
ContextSingleTicker(this.context, {this.autoStart = true});
void initStart() {
if (autoStart) {
_ticker?.start();
}
}
void startIfNeeded() {
if (_ticker case final ticker?) {
if (!ticker.isActive) {
ticker.start();
}
}
}
void initIfNeeded(TickerCallback onTick) {
if (_ticker == null) {
createTicker(onTick);
initStart();
}
}
@override
Ticker createTicker(TickerCallback onTick) {
assert(() {
if (_ticker == null) {
return true;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'$runtimeType is a SingleTickerProviderStateMixin but multiple tickers were created.',
),
ErrorDescription(
'A SingleTickerProviderStateMixin can only be used as a TickerProvider once.',
),
ErrorHint(
'If a State is used for multiple AnimationController objects, or if it is passed to other '
'objects and those objects might use it more than one time in total, then instead of '
'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.',
),
]);
}());
_ticker = Ticker(
onTick,
debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null,
);
_tickerModeNotifier = TickerMode.getNotifier(context)
..addListener(updateTicker);
updateTicker(); // Sets _ticker.mute correctly.
return _ticker!;
}
void reset() {
_ticker
?..stop()
..start();
}
void cancel() {
_ticker?.dispose();
_ticker = null;
_tickerModeNotifier?.removeListener(updateTicker);
_tickerModeNotifier = null;
}
ValueListenable<bool>? _tickerModeNotifier;
void updateTicker() => _ticker?.muted = !_tickerModeNotifier!.value;
set muted(bool value) => _ticker?.muted = value;
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class MouseBackDetector extends StatelessWidget {
const MouseBackDetector({
super.key,
required this.onTapDown,
required this.child,
});
final Widget child;
final VoidCallback onTapDown;
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (event) {
if (event.buttons == kBackMouseButton) {
onTapDown();
}
},
behavior: HitTestBehavior.translucent,
child: child,
);
}
}

View File

@@ -18,7 +18,6 @@ library;
import 'dart:async';
import 'dart:math' as math;
import 'dart:math' show max;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
@@ -665,10 +664,6 @@ class CustomScrollableState extends State<CustomScrollable>
vsync: this,
reverseDuration: const Duration(milliseconds: 500),
);
_anim = Tween<Offset>(
begin: Offset.zero,
end: const Offset(0, 1),
).animate(_animController);
}
@protected
@@ -782,11 +777,11 @@ class CustomScrollableState extends State<CustomScrollable>
bool? _lastCanDrag;
Axis? _lastAxisDirection;
late bool _isRTL = false;
Offset? _downPos;
bool? _isSliding;
late AnimationController _animController;
late Animation<Offset> _anim;
@override
@protected
@@ -893,7 +888,12 @@ class CustomScrollableState extends State<CustomScrollable>
ScrollHoldController? _hold;
void _handleDragDown(DragDownDetails details) {
if (details.localPosition.dx <= 30) {
final dx = details.localPosition.dx;
const offset = 30;
final isLTR = dx <= offset;
final isRTL = dx >= _maxWidth - offset;
if (isLTR || isRTL) {
_isRTL = isRTL;
_downPos = details.localPosition;
return;
}
@@ -915,19 +915,24 @@ class CustomScrollableState extends State<CustomScrollable>
_downPos = null;
_isSliding = false;
}
} else {
_downPos = null;
_isSliding = false;
}
} else if (_isSliding == true) {
if (localPosition.dx < 0) {
return;
}
final from = _downPos!.dx;
final to = localPosition.dx;
_animController.value =
max(0, (localPosition.dx - _downPos!.dx)) / _maxWidth;
math.max(0, _isRTL ? from - to : to - from) / _maxWidth;
}
}
void _onDismiss() {
if (_isSliding == true) {
if (_animController.value * _maxWidth + _downPos!.dx >= 100) {
final dx = _downPos!.dx;
if (_animController.value * _maxWidth +
(_isRTL ? (_maxWidth - dx) : dx) >=
100) {
Get.back();
} else {
_animController.reverse();
@@ -1172,24 +1177,31 @@ class CustomScrollableState extends State<CustomScrollable>
);
}
return SlideTransition(
position: _anim,
child: Material(
color: widget.bgColor,
child: LayoutBuilder(
builder: (_, constrains) {
_maxWidth = constrains.maxWidth;
return widget.header != null
return LayoutBuilder(
builder: (context, constraints) {
_maxWidth = constraints.maxWidth;
return AnimatedBuilder(
animation: _animController,
builder: (context, child) {
return Align(
alignment: AlignmentDirectional.topStart,
heightFactor: 1 - _animController.value,
child: child,
);
},
child: Material(
color: widget.bgColor,
child: widget.header != null
? Column(
children: [
widget.header!,
Expanded(child: result),
],
)
: result;
},
),
),
: result,
),
);
},
);
}

View File

@@ -244,9 +244,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
);
}
if (mounted) {
setState(() {
_updateChildren();
});
setState(_updateChildren);
}
return Future<void>.value();
}
@@ -286,9 +284,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
}
if (mounted) {
setState(() {
_updateChildren();
});
setState(_updateChildren);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/avatar_badge_type.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:cached_network_image/cached_network_image.dart';
@@ -22,13 +22,13 @@ class PendantAvatar extends StatelessWidget {
required this.avatar,
this.size = 80,
double? badgeSize,
bool? isVip,
bool isVip = false,
int? officialType,
this.garbPendantImage,
this.roomId,
this.onTap,
}) : _badgeType = officialType == null || officialType < 0
? isVip == true
? isVip
? BadgeType.vip
: BadgeType.none
: officialType == 0
@@ -43,16 +43,17 @@ class PendantAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isMemberAvatar = size == 80;
return Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
onTap == null
? _buildAvatar(colorScheme)
? _buildAvatar(colorScheme, isMemberAvatar)
: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: _buildAvatar(colorScheme),
child: _buildAvatar(colorScheme, isMemberAvatar),
),
if (showDynDecorate && !garbPendantImage.isNullOrEmpty)
Positioned(
@@ -63,7 +64,7 @@ class PendantAvatar extends StatelessWidget {
child: CachedNetworkImage(
width: size * 1.75,
height: size * 1.75,
imageUrl: ImageUtil.thumbnailUrl(garbPendantImage),
imageUrl: ImageUtils.thumbnailUrl(garbPendantImage),
),
),
),
@@ -100,12 +101,13 @@ class PendantAvatar extends StatelessWidget {
),
)
else if (_badgeType != BadgeType.none)
_buildBadge(colorScheme),
_buildBadge(colorScheme, isMemberAvatar),
],
);
}
Widget _buildAvatar(ColorScheme colorScheme) => size == 80
Widget _buildAvatar(ColorScheme colorScheme, bool isMemberAvatar) =>
isMemberAvatar
? DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
@@ -131,7 +133,7 @@ class PendantAvatar extends StatelessWidget {
type: ImageType.avatar,
);
Widget _buildBadge(ColorScheme colorScheme) {
Widget _buildBadge(ColorScheme colorScheme, bool isMemberAvatar) {
final child = switch (_badgeType) {
BadgeType.vip => Image.asset(
'assets/images/big-vip.png',
@@ -145,9 +147,10 @@ class PendantAvatar extends StatelessWidget {
semanticLabel: _badgeType.desc,
),
};
final offset = isMemberAvatar ? 2.0 : 0.0;
return Positioned(
right: 0,
bottom: 0,
right: offset,
bottom: offset,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(

View File

@@ -5,52 +5,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// This is where the current time and total time labels should appear in
/// relation to the progress bar.
enum TimeLabelLocation {
/// The time is displayed above the progress bar.
///
/// | 01:23 05:00 |
/// | -------O---------------- |
above,
/// The time is displayed below the progress bar.
///
/// | -------O---------------- |
/// | 01:23 05:00 |
below,
/// The time is displayed on the sides of the progress bar.
///
/// | 01:23 -------O---------------- 05:00 |
sides,
/// The time is not displayed.
///
/// | -------O---------------- |
none,
}
/// The time label on the right hand side can be shown as the [totalTime] or as
/// the [remainingTime]. If the choice is [remainingTime] then this will be
/// shown as a negative number.
///
///
enum TimeLabelType {
/// The time label on the right shows the total time.
///
/// | -------O---------------- |
/// | 01:23 05:00 |
totalTime,
/// The time label on the right shows the remaining time as a
/// negative number.
///
/// | -------O---------------- |
/// | 01:23 -03:37 |
remainingTime,
}
/// The shape of the progress bar at the left and right ends.
enum BarCapShape {
/// The left and right ends of the bar are round.
@@ -83,19 +37,15 @@ class ProgressBar extends LeafRenderObjectWidget {
this.onDragUpdate,
this.onDragEnd,
this.barHeight = 5.0,
this.baseBarColor,
this.progressBarColor,
this.bufferedBarColor,
required this.baseBarColor,
required this.progressBarColor,
required this.bufferedBarColor,
this.barCapShape = BarCapShape.round,
this.thumbRadius = 10.0,
this.thumbColor,
this.thumbGlowColor,
required this.thumbColor,
required this.thumbGlowColor,
this.thumbGlowRadius = 30.0,
this.thumbCanPaintOutsideBar = true,
this.timeLabelLocation,
this.timeLabelType,
this.timeLabelTextStyle,
this.timeLabelPadding = 0.0,
});
/// The elapsed playing time of the media.
@@ -172,20 +122,20 @@ class ProgressBar extends LeafRenderObjectWidget {
/// The color of the progress bar before playback has started.
///
/// By default it is a transparent version of your theme's primary color.
final Color? baseBarColor;
final Color baseBarColor;
/// The color of the progress bar to the left of the current playing
/// [progress].
///
/// By default it is your theme's primary color.
final Color? progressBarColor;
final Color progressBarColor;
/// The color of the progress bar between the [progress] location and the
/// [buffered] location.
///
/// By default it is a transparent version of your theme's primary color,
/// a shade darker than [baseBarColor].
final Color? bufferedBarColor;
final Color bufferedBarColor;
/// The shape of the bar at the left and right ends.
///
@@ -199,12 +149,12 @@ class ProgressBar extends LeafRenderObjectWidget {
/// The color of the circle for the moveable progress bar thumb.
///
/// By default it is your theme's primary color.
final Color? thumbColor;
final Color thumbColor;
/// The color of the pressed-down effect of the moveable progress bar thumb.
///
/// By default it is [thumbColor] with an alpha value of 80.
final Color? thumbGlowColor;
final Color thumbGlowColor;
/// The radius of the circle for the pressed-down effect of the moveable
/// progress bar thumb.
@@ -229,35 +179,8 @@ class ProgressBar extends LeafRenderObjectWidget {
/// is happening during this time, though.
final bool thumbCanPaintOutsideBar;
/// The location for the [progress] and [total] duration text labels.
///
/// By default the labels appear under the progress bar but you can also
/// put them above, on the sides, or remove them altogether.
final TimeLabelLocation? timeLabelLocation;
/// What to display for the time label on the right
///
/// The right time label can show the total time or the remaining time as a
/// negative number. The default is [TimeLabelType.totalTime].
final TimeLabelType? timeLabelType;
/// The [TextStyle] used by the time labels.
///
/// By default it is [TextTheme.bodyLarge].
final TextStyle? timeLabelTextStyle;
/// The extra space between the time labels and the progress bar.
///
/// The default is 0.0. A positive number will move the labels further from
/// the progress bar and a negative number will move them closer.
final double timeLabelPadding;
@override
RenderObject createRenderObject(BuildContext context) {
final theme = Theme.of(context);
final primaryColor = theme.colorScheme.primary;
final textStyle = timeLabelTextStyle ?? theme.textTheme.bodyLarge;
final textScaleFactor = MediaQuery.textScalerOf(context).scale(1);
return _RenderProgressBar(
progress: progress,
total: total,
@@ -267,56 +190,38 @@ class ProgressBar extends LeafRenderObjectWidget {
onDragUpdate: onDragUpdate,
onDragEnd: onDragEnd,
barHeight: barHeight,
baseBarColor: baseBarColor ?? primaryColor.withValues(alpha: 0.24),
progressBarColor: progressBarColor ?? primaryColor,
bufferedBarColor:
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24),
baseBarColor: baseBarColor,
progressBarColor: progressBarColor,
bufferedBarColor: bufferedBarColor,
barCapShape: barCapShape,
thumbRadius: thumbRadius,
thumbColor: thumbColor ?? primaryColor,
thumbGlowColor:
thumbGlowColor ?? (thumbColor ?? primaryColor).withAlpha(80),
thumbColor: thumbColor,
thumbGlowColor: thumbGlowColor,
thumbGlowRadius: thumbGlowRadius,
thumbCanPaintOutsideBar: thumbCanPaintOutsideBar,
timeLabelLocation: timeLabelLocation ?? TimeLabelLocation.below,
timeLabelType: timeLabelType ?? TimeLabelType.totalTime,
timeLabelTextStyle: textStyle,
timeLabelPadding: timeLabelPadding,
textScaleFactor: textScaleFactor,
);
}
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) {
final theme = Theme.of(context);
final primaryColor = theme.colorScheme.primary;
final textStyle = timeLabelTextStyle ?? theme.textTheme.bodyLarge;
final textScaleFactor = MediaQuery.textScalerOf(context).scale(1);
(renderObject as _RenderProgressBar)
..progress = progress
..total = total
..progress = progress
..buffered = buffered ?? Duration.zero
..onSeek = onSeek
..onDragStart = onDragStart
..onDragUpdate = onDragUpdate
..onDragEnd = onDragEnd
..barHeight = barHeight
..baseBarColor = baseBarColor ?? primaryColor.withValues(alpha: 0.24)
..progressBarColor = progressBarColor ?? primaryColor
..bufferedBarColor =
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24)
..baseBarColor = baseBarColor
..progressBarColor = progressBarColor
..bufferedBarColor = bufferedBarColor
..barCapShape = barCapShape
..thumbRadius = thumbRadius
..thumbColor = thumbColor ?? primaryColor
..thumbGlowColor =
thumbGlowColor ?? (thumbColor ?? primaryColor).withAlpha(80)
..thumbColor = thumbColor
..thumbGlowColor = thumbGlowColor
..thumbGlowRadius = thumbGlowRadius
..thumbCanPaintOutsideBar = thumbCanPaintOutsideBar
..timeLabelLocation = timeLabelLocation ?? TimeLabelLocation.below
..timeLabelType = timeLabelType ?? TimeLabelType.totalTime
..timeLabelTextStyle = textStyle
..timeLabelPadding = timeLabelPadding
..textScaleFactor = textScaleFactor;
..thumbCanPaintOutsideBar = thumbCanPaintOutsideBar;
}
@override
@@ -371,11 +276,7 @@ class ProgressBar extends LeafRenderObjectWidget {
ifFalse: 'false',
showName: true,
),
)
..add(StringProperty('timeLabelLocation', timeLabelLocation.toString()))
..add(StringProperty('timeLabelType', timeLabelType.toString()))
..add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle))
..add(DoubleProperty('timeLabelPadding', timeLabelPadding));
);
}
}
@@ -445,11 +346,6 @@ class _RenderProgressBar extends RenderBox {
required Color thumbGlowColor,
double thumbGlowRadius = 30.0,
bool thumbCanPaintOutsideBar = true,
required TimeLabelLocation timeLabelLocation,
required TimeLabelType timeLabelType,
TextStyle? timeLabelTextStyle,
double timeLabelPadding = 0.0,
double textScaleFactor = 1.0,
}) : _total = total,
_buffered = buffered,
_onSeek = onSeek,
@@ -465,12 +361,8 @@ class _RenderProgressBar extends RenderBox {
_thumbColor = thumbColor,
_thumbGlowColor = thumbGlowColor,
_thumbGlowRadius = thumbGlowRadius,
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
_timeLabelLocation = timeLabelLocation,
_timeLabelType = timeLabelType,
_timeLabelTextStyle = timeLabelTextStyle,
_timeLabelPadding = timeLabelPadding,
_textScaleFactor = textScaleFactor {
_paintThumbGlow = thumbGlowRadius > thumbRadius,
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar {
_drag = _EagerHorizontalDragGestureRecognizer()
..onStart = _onDragStart
..onUpdate = _onDragUpdate
@@ -482,6 +374,12 @@ class _RenderProgressBar extends RenderBox {
}
}
@override
void dispose() {
_drag?.dispose();
super.dispose();
}
// This is the gesture recognizer used to move the thumb.
_EagerHorizontalDragGestureRecognizer? _drag;
@@ -495,14 +393,6 @@ class _RenderProgressBar extends RenderBox {
// time as a [progress] update there won't be a conflict.
bool _userIsDraggingThumb = false;
// This padding is always used between the time labels and the progress bar
// when the time labels are on the sides. Any user defined [timeLabelPadding]
// is in addition to this.
double get _defaultSidePadding {
const minPadding = 5.0;
return (_thumbCanPaintOutsideBar) ? thumbRadius + minPadding : minPadding;
}
void _onDragStart(DragStartDetails details) {
if (onDragStart == null) {
return;
@@ -557,20 +447,12 @@ class _RenderProgressBar extends RenderBox {
// only one place to make changes.
void _updateThumbPosition(Offset localPosition) {
final dx = localPosition.dx;
double lengthBefore = 0.0;
double lengthAfter = 0.0;
if (_timeLabelLocation == TimeLabelLocation.sides) {
lengthBefore =
_leftLabelSize.width + _defaultSidePadding + _timeLabelPadding;
lengthAfter =
_rightLabelSize.width + _defaultSidePadding + _timeLabelPadding;
}
// The paint used to draw the bar line draws half of the cap before the
// start of the line (and after the end of the line). The cap radius is
// equal to half of the line width, which in this case is the bar height.
final barCapRadius = _barHeight / 2;
double barStart = lengthBefore + barCapRadius;
double barEnd = size.width - lengthAfter - barCapRadius;
double barStart = barCapRadius;
double barEnd = size.width - barCapRadius;
final barWidth = barEnd - barStart;
final position = (dx - barStart).clamp(0.0, barWidth);
_thumbValue = (position / barWidth);
@@ -588,9 +470,6 @@ class _RenderProgressBar extends RenderBox {
if (_progress == clamp) {
return;
}
if (_labelLengthDifferent(_progress, clamp)) {
_clearLabelCache();
}
if (!_userIsDraggingThumb) {
_progress = clamp;
_thumbValue = _proportionOfTotal(clamp);
@@ -598,58 +477,6 @@ class _RenderProgressBar extends RenderBox {
markNeedsPaint();
}
bool _labelLengthDifferent(Duration first, Duration second) {
return (first.inMinutes < 10 && second.inMinutes >= 10) ||
(first.inMinutes >= 10 && second.inMinutes < 10) ||
(first.inHours == 0 && second.inHours != 0) ||
(first.inHours != 0 && second.inHours == 0) ||
(first.inHours < 10 && second.inHours >= 10) ||
(first.inHours >= 10 && second.inHours < 10);
}
TextPainter? _cachedLeftLabel;
Size get _leftLabelSize {
_cachedLeftLabel ??= _leftTimeLabel();
return _cachedLeftLabel!.size;
}
TextPainter? _cachedRightLabel;
Size get _rightLabelSize {
_cachedRightLabel ??= _rightTimeLabel();
return _cachedRightLabel!.size;
}
void _clearLabelCache() {
_cachedLeftLabel = null;
_cachedRightLabel = null;
}
TextPainter _leftTimeLabel() {
final text = _getTimeString(progress);
return _layoutText(text);
}
TextPainter _rightTimeLabel() {
switch (timeLabelType) {
case TimeLabelType.totalTime:
final text = _getTimeString(total);
return _layoutText(text);
case TimeLabelType.remainingTime:
final remaining = total - progress;
final text = '-${_getTimeString(remaining)}';
return _layoutText(text);
}
}
TextPainter _layoutText(String text) {
TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: _timeLabelTextStyle),
textDirection: TextDirection.ltr,
textScaler: TextScaler.linear(textScaleFactor),
)..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter;
}
/// The total time length of the media.
Duration get total => _total;
Duration _total;
@@ -658,9 +485,6 @@ class _RenderProgressBar extends RenderBox {
if (_total == clamp) {
return;
}
if (_labelLengthDifferent(_total, clamp)) {
_clearLabelCache();
}
_total = clamp;
if (!_userIsDraggingThumb) {
_thumbValue = _proportionOfTotal(progress);
@@ -798,11 +622,13 @@ class _RenderProgressBar extends RenderBox {
}
/// The length of the radius of the pressed-down effect of the moveable thumb.
bool _paintThumbGlow;
double get thumbGlowRadius => _thumbGlowRadius;
double _thumbGlowRadius;
set thumbGlowRadius(double value) {
if (_thumbGlowRadius == value) return;
_thumbGlowRadius = value;
_paintThumbGlow = value > _thumbRadius;
markNeedsLayout();
}
@@ -815,59 +641,6 @@ class _RenderProgressBar extends RenderBox {
markNeedsPaint();
}
/// The position of the duration text labels for the progress and total time.
TimeLabelLocation get timeLabelLocation => _timeLabelLocation;
TimeLabelLocation _timeLabelLocation;
set timeLabelLocation(TimeLabelLocation value) {
if (_timeLabelLocation == value) return;
_timeLabelLocation = value;
markNeedsLayout();
}
/// What to display for the time label on the right
///
/// The right time label can show the total time or the remaining time as a
/// negative number. The default is [TimeLabelType.totalTime].
TimeLabelType get timeLabelType => _timeLabelType;
TimeLabelType _timeLabelType;
set timeLabelType(TimeLabelType value) {
if (_timeLabelType == value) return;
_timeLabelType = value;
_clearLabelCache();
markNeedsLayout();
}
/// The text style for the duration text labels. By default this style is
/// taken from the theme's [textStyle.bodyText1].
TextStyle? get timeLabelTextStyle => _timeLabelTextStyle;
TextStyle? _timeLabelTextStyle;
set timeLabelTextStyle(TextStyle? value) {
if (_timeLabelTextStyle == value) return;
_timeLabelTextStyle = value;
_clearLabelCache();
markNeedsLayout();
}
/// The length of the radius for the circular thumb.
double get timeLabelPadding => _timeLabelPadding;
double _timeLabelPadding;
set timeLabelPadding(double value) {
if (_timeLabelPadding == value) return;
_timeLabelPadding = value;
markNeedsLayout();
}
/// The text scale factor for the `progress` and `total` text labels.
/// By default the value is 1.0.
double get textScaleFactor => _textScaleFactor;
double _textScaleFactor;
set textScaleFactor(double value) {
if (_textScaleFactor == value) return;
_textScaleFactor = value;
_clearLabelCache();
markNeedsLayout();
}
// The smallest that this widget would ever want to be.
static const _minDesiredWidth = 100.0;
@@ -878,10 +651,10 @@ class _RenderProgressBar extends RenderBox {
double computeMaxIntrinsicWidth(double height) => _minDesiredWidth;
@override
double computeMinIntrinsicHeight(double width) => _calculateDesiredHeight();
double computeMinIntrinsicHeight(double width) => _heightWhenNoLabels();
@override
double computeMaxIntrinsicHeight(double width) => _calculateDesiredHeight();
double computeMaxIntrinsicHeight(double width) => _heightWhenNoLabels();
@override
bool hitTestSelf(Offset position) => true;
@@ -902,41 +675,15 @@ class _RenderProgressBar extends RenderBox {
@override
Size computeDryLayout(BoxConstraints constraints) {
final desiredWidth = constraints.maxWidth;
final desiredHeight = _calculateDesiredHeight();
final desiredHeight = _heightWhenNoLabels();
final desiredSize = Size(desiredWidth, desiredHeight);
return constraints.constrain(desiredSize);
}
// When changing these remember to keep the gesture recognizer for the
// thumb in sync.
double _calculateDesiredHeight() {
switch (_timeLabelLocation) {
case TimeLabelLocation.below:
case TimeLabelLocation.above:
return _heightWhenLabelsAboveOrBelow();
case TimeLabelLocation.sides:
return _heightWhenLabelsOnSides();
default:
return _heightWhenNoLabels();
}
}
double _heightWhenLabelsAboveOrBelow() {
return _heightWhenNoLabels() + _textHeight() + _timeLabelPadding;
}
double _heightWhenLabelsOnSides() {
return max(_heightWhenNoLabels(), _textHeight());
}
double _heightWhenNoLabels() {
return max(2 * _thumbRadius, _barHeight);
}
double _textHeight() {
return _leftLabelSize.height;
}
@override
bool get isRepaintBoundary => true;
@@ -946,94 +693,18 @@ class _RenderProgressBar extends RenderBox {
..save()
..translate(offset.dx, offset.dy);
switch (_timeLabelLocation) {
case TimeLabelLocation.above:
case TimeLabelLocation.below:
_drawProgressBarWithLabelsAboveOrBelow(canvas);
break;
case TimeLabelLocation.sides:
_drawProgressBarWithLabelsOnSides(canvas);
break;
default:
_drawProgressBarWithoutLabels(canvas);
}
_drawProgressBarWithoutLabels(canvas);
canvas.restore();
}
/// Draw the progress bar and labels vertically aligned:
///
/// | -------O---------------- |
/// | 01:23 05:00 |
///
/// Or like this:
///
/// | 01:23 05:00 |
/// | -------O---------------- |
void _drawProgressBarWithLabelsAboveOrBelow(Canvas canvas) {
// calculate sizes
final barWidth = size.width;
final barHeight = _heightWhenNoLabels();
// whether to paint the labels below the progress bar or above it
final isLabelBelow = _timeLabelLocation == TimeLabelLocation.below;
// current time label
final labelDy = (isLabelBelow) ? barHeight + _timeLabelPadding : 0.0;
final leftLabelOffset = Offset(0, labelDy);
_leftTimeLabel().paint(canvas, leftLabelOffset);
// total or remaining time label
final rightLabelDx = size.width - _rightLabelSize.width;
final rightLabelOffset = Offset(rightLabelDx, labelDy);
_rightTimeLabel().paint(canvas, rightLabelOffset);
// progress bar
final barDy = (isLabelBelow)
? 0.0
: _leftLabelSize.height + _timeLabelPadding;
_drawProgressBar(canvas, Offset(0, barDy), Size(barWidth, barHeight));
}
/// Draw the progress bar and labels horizontally aligned:
///
/// | 01:23 -------O---------------- 05:00 |
///
void _drawProgressBarWithLabelsOnSides(Canvas canvas) {
// left time label
final leftLabelSize = _leftLabelSize;
final verticalOffset = size.height / 2 - leftLabelSize.height / 2;
final leftLabelOffset = Offset(0, verticalOffset);
_leftTimeLabel().paint(canvas, leftLabelOffset);
// right time label
final rightLabelSize = _rightLabelSize;
final rightLabelWidth = rightLabelSize.width;
final totalLabelDx = size.width - rightLabelWidth;
final totalLabelOffset = Offset(totalLabelDx, verticalOffset);
_rightTimeLabel().paint(canvas, totalLabelOffset);
// progress bar
final leftLabelWidth = leftLabelSize.width;
final barHeight = _heightWhenNoLabels();
final barWidth =
size.width -
2 * _defaultSidePadding -
2 * _timeLabelPadding -
leftLabelWidth -
rightLabelWidth;
final barDy = size.height / 2 - barHeight / 2;
final barDx = leftLabelWidth + _defaultSidePadding + _timeLabelPadding;
_drawProgressBar(canvas, Offset(barDx, barDy), Size(barWidth, barHeight));
}
/// Draw the progress bar without labels like this:
///
/// | -------O---------------- |
///
void _drawProgressBarWithoutLabels(Canvas canvas) {
final barWidth = size.width;
final barHeight = 2 * _thumbRadius;
final barHeight = _heightWhenNoLabels();
_drawProgressBar(canvas, Offset.zero, Size(barWidth, barHeight));
}
@@ -1105,7 +776,7 @@ class _RenderProgressBar extends RenderBox {
thumbDx = thumbDx.clamp(_thumbRadius, localSize.width - _thumbRadius);
}
final center = Offset(thumbDx, localSize.height / 2);
if (_userIsDraggingThumb) {
if (_userIsDraggingThumb && _paintThumbGlow) {
final thumbGlowPaint = Paint()..color = thumbGlowColor;
canvas.drawCircle(center, thumbGlowRadius, thumbGlowPaint);
}
@@ -1119,19 +790,6 @@ class _RenderProgressBar extends RenderBox {
return (duration.inMilliseconds / total.inMilliseconds).clamp(0.0, 1.0);
}
String _getTimeString(Duration time) {
final minutes = time.inMinutes
.remainder(Duration.minutesPerHour)
.toString();
final seconds = time.inSeconds
.remainder(Duration.secondsPerMinute)
.toString()
.padLeft(2, '0');
return time.inHours > 0
? "${time.inHours}:${minutes.padLeft(2, "0")}:$seconds"
: "$minutes:$seconds";
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
@@ -1160,16 +818,16 @@ class _RenderProgressBar extends RenderBox {
void increaseAction() {
final newValue = _thumbValue + _semanticActionUnit;
_thumbValue = (newValue).clamp(0.0, 1.0);
onSeek?.call(_currentThumbDuration());
markNeedsPaint();
markNeedsSemanticsUpdate();
onSeek?.call(_currentThumbDuration());
}
void decreaseAction() {
final newValue = _thumbValue - _semanticActionUnit;
_thumbValue = (newValue).clamp(0.0, 1.0);
onSeek?.call(_currentThumbDuration());
markNeedsPaint();
markNeedsSemanticsUpdate();
onSeek?.call(_currentThumbDuration());
}
}

View File

@@ -18,6 +18,26 @@ class Segment {
this.from,
this.to,
]);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is Segment) {
return start == other.start &&
end == other.end &&
color == other.color &&
title == other.title &&
url == other.url &&
from == other.from &&
to == other.to;
}
return false;
}
@override
int get hashCode => Object.hash(start, end, color, title, url, from, to);
}
class SegmentProgressBar extends CustomPainter {
@@ -126,7 +146,7 @@ class SegmentProgressBar extends CustomPainter {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
bool shouldRepaint(SegmentProgressBar oldDelegate) {
return segmentColors != oldDelegate.segmentColors;
}
}

View File

@@ -4,13 +4,12 @@ import 'package:flutter/material.dart';
Widget videoProgressIndicator(double progress) => ClipRect(
clipper: ProgressClipper(),
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
borderRadius: const BorderRadius.vertical(bottom: StyleString.imgRadius),
child: LinearProgressIndicator(
minHeight: 10,
value: progress,
// ignore: deprecated_member_use
year2023: true,
stopIndicatorColor: Colors.transparent,
),
),

View File

@@ -1,43 +1,88 @@
import 'package:flutter/material.dart';
class RadioWidget<T> extends StatelessWidget {
class RadioWidget<T> extends StatefulWidget {
final T value;
final T? groupValue;
final ValueChanged<T?> onChanged;
final String title;
final bool tristate;
final EdgeInsetsGeometry? padding;
final MainAxisSize mainAxisSize;
const RadioWidget({
super.key,
required this.value,
this.groupValue,
required this.onChanged,
required this.title,
this.tristate = false,
this.padding,
this.mainAxisSize = MainAxisSize.min,
});
Widget _child() => Row(
children: [
Radio<T>(
value: value,
groupValue: groupValue,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Text(title),
],
);
@override
State<RadioWidget<T>> createState() => RadioWidgetState<T>();
}
class RadioWidgetState<T> extends State<RadioWidget<T>> with RadioClient<T> {
late final _RadioRegistry<T> _radioRegistry = _RadioRegistry<T>(this);
@override
final focusNode = FocusNode();
@override
T get radioValue => widget.value;
bool get checked => radioValue == registry!.groupValue;
@override
bool get tristate => widget.tristate;
@override
void dispose() {
registry = null;
focusNode.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
registry = RadioGroup.maybeOf(context);
assert(registry != null);
}
void _handleTap() {
if (checked) {
if (tristate) registry!.onChanged(null);
return;
}
registry!.onChanged(radioValue);
}
@override
Widget build(BuildContext context) {
final child = Row(
mainAxisSize: widget.mainAxisSize,
children: [
Focus(
parentNode: focusNode,
canRequestFocus: false,
skipTraversal: true,
includeSemantics: true,
descendantsAreFocusable: false,
descendantsAreTraversable: false,
child: Radio<T>(
value: radioValue,
groupRegistry: _radioRegistry,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
Text(widget.title),
],
);
return InkWell(
onTap: () => onChanged(value),
child: padding != null
? Padding(
padding: padding!,
child: _child(),
)
: _child(),
onTap: _handleTap,
focusNode: focusNode,
child: widget.padding == null
? child
: Padding(padding: widget.padding!, child: child),
);
}
}
@@ -45,16 +90,12 @@ class RadioWidget<T> extends StatelessWidget {
class WrapRadioOptionsGroup<T> extends StatelessWidget {
final String groupTitle;
final Map<T, String> options;
final T? selectedValue;
final ValueChanged<T?> onChanged;
final EdgeInsetsGeometry? itemPadding;
const WrapRadioOptionsGroup({
super.key,
required this.groupTitle,
required this.options,
required this.selectedValue,
required this.onChanged,
this.itemPadding,
});
@@ -75,14 +116,10 @@ class WrapRadioOptionsGroup<T> extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap(
children: options.entries.map((entry) {
return IntrinsicWidth(
child: RadioWidget<T>(
value: entry.key,
groupValue: selectedValue,
onChanged: onChanged,
title: entry.value,
padding: itemPadding ?? const EdgeInsets.only(right: 10),
),
return RadioWidget<T>(
value: entry.key,
title: entry.value,
padding: itemPadding ?? const EdgeInsets.only(right: 10),
);
}).toList(),
),
@@ -91,3 +128,27 @@ class WrapRadioOptionsGroup<T> extends StatelessWidget {
);
}
}
/// A registry to controls internal [Radio] and hides it from [RadioGroup]
/// ancestor.
///
/// [RadioListTile] implements the [RadioClient] directly to register to
/// [RadioGroup] ancestor. Therefore, it has to hide the internal [Radio] from
/// participate in the [RadioGroup] ancestor.
class _RadioRegistry<T> extends RadioGroupRegistry<T> {
_RadioRegistry(this.state);
final RadioWidgetState<T> state;
@override
T? get groupValue => state.registry!.groupValue;
@override
ValueChanged<T?> get onChanged => state.registry!.onChanged;
@override
void registerClient(RadioClient<T> radio) {}
@override
void unregisterClient(RadioClient<T> radio) {}
}

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