Compare commits

..

360 Commits

Author SHA1 Message Date
dom
4ac855d393 Release 1.1.6
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 16:43:37 +08:00
dom
7381939c0f upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 16:43:37 +08:00
dom
a380bcd96a tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 12:38:52 +08:00
dom
d253ef468b tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 11:39:23 +08:00
систем
e8145ef65a fix: initialize controller before super (#1827) 2026-02-07 11:32:02 +08:00
dom
0c175abc0b opt image physics
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 10:55:00 +08:00
dom
946a5a1e47 opt image gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 10:40:21 +08:00
dom
29e7e0e556 feat: vertical tabbar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 09:28:02 +08:00
dom
cc1704a021 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-07 09:25:23 +08:00
My-Responsitories
7ab2cf973f tweaks (#1826)
* opt: dataStatus

* tweaks

* opt: ui

* update

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-06 14:25:45 +08:00
dom
32386bf146 fix check bottom bar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-06 12:52:35 +08:00
dom
40269da391 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-06 12:34:08 +08:00
dom
42e082bbc6 scroll to current rank
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-06 11:39:31 +08:00
dom
1ad710c1cf opt settings
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-06 10:48:36 +08:00
dom
cfa925549e tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-05 16:54:27 +08:00
dom
ca387787b3 opt handle reply note
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-05 11:19:36 +08:00
dom
29a9b22c29 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-05 11:18:17 +08:00
dom
672375b925 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-04 19:54:44 +08:00
wuhenzha
c099738802 fix: respect title bar setting after exiting PiP (#1825) 2026-02-04 19:50:52 +08:00
dom
50561b8dc1 opt persist header
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-03 18:28:06 +08:00
dom
2596859778 custom dyn text menu
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-03 10:21:34 +08:00
dom
3d453bafdb modal barrier patch
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-03 10:21:34 +08:00
систем
18e0b93ca7 feat: add copy/move support to fav/later search results (#1822)
* feat: add copy/move support to fav/later search results

* update

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-03 10:21:01 +08:00
My-Responsitories
7260a387f9 tweaks (#1821)
* opt: _onTapUp

* opt: set contains

* opt: elementAtOrNull
2026-02-02 20:12:12 +08:00
dom
37fa165f59 opt pip
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-01 17:06:21 +08:00
dom
8f08104f37 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-02-01 11:57:13 +08:00
lesetong
6ee4deab05 Fix duplicate items in Up list when showAllUp is enabled. (#1819)
Overrode UpItem's == operator and hashCode using 'mid' to prevent
redundant entries.
2026-02-01 00:18:18 +08:00
My-Responsitories
77fff92939 opt: binary search fontsize (#1818)
* opt: permission

* opt: opt: binary search fontsize

* use transform

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

* fix

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

* opt: matrix

* opt [skip ci]

* tweaks [skip ci]

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-02-01 00:01:34 +08:00
dom
8964197b73 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-31 13:20:51 +08:00
dom
dbc7bcd0dd upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-31 13:20:51 +08:00
dom
207ad2753c opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-30 15:16:22 +08:00
My-Responsitories
d6e6e52df2 tweaks (#1816)
* opt: getFileName

* opt: audio-pitch-correction

* opt: spring dialog

* opt: account dialog

* update [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-30 10:31:26 +08:00
систем
9442b17d63 opt select account (#1815)
* feat: switchAccountDialog pages simple-detaile

* update

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-29 22:06:10 +08:00
dom
058ff44e39 opt autosync
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 19:35:50 +08:00
dom
48c7dc0eed custom autosync
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 18:12:08 +08:00
dom
99634a66ab tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 18:12:03 +08:00
nakixii
21fad89cde Add autosync property with value '30' (#1813) 2026-01-29 15:22:00 +08:00
dom
5979ddb60c tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 14:55:16 +08:00
dom
bcbfe5c849 SpringDescription with duration
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 12:35:58 +08:00
dom
1640732f5d opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 12:35:58 +08:00
dom
9567910611 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 10:24:16 +08:00
dom
d1713504a0 opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-29 10:16:16 +08:00
dom
bce73d9f16 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 15:46:06 +08:00
dom
6f30d2e331 opt reply
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 12:37:21 +08:00
dom
556bda0d68 opt video intro
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 11:46:47 +08:00
dom
9d5eb55e26 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-28 11:46:42 +08:00
dom
110469961d opt video scheme
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-27 11:40:00 +08:00
dom
fa348db7c5 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-27 11:19:49 +08:00
dom
3eac565b5e fix slide
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:52:49 +08:00
dom
af40e489bc opt ao
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:38:48 +08:00
dom
361eb4c614 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:15:47 +08:00
dom
7ace981f24 upgrade dep
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-26 14:15:47 +08:00
My-Responsitories
bfb2becb2d opt: ao (#1811)
* opt: ao

* multi select

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-26 14:11:48 +08:00
My-Responsitories
038f03a4e7 tweaks (#1810)
* tweak

* opt: image quality

* opt: VideoPlayerServiceHandler

* fixes

* update

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

* fix get file name

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-25 15:21:33 +08:00
dom
219228f8b5 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-25 11:59:17 +08:00
dom
1f64de5954 opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-25 11:59:17 +08:00
dom
e9b5cffa91 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-25 11:59:12 +08:00
dom
68872f7b14 opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-24 16:23:14 +08:00
dom
bd158619a4 opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-24 15:40:21 +08:00
dom
310f497c30 opt slide
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-24 15:20:01 +08:00
dom
30ee413852 fix web rcmd
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-24 13:45:30 +08:00
dom
0ab07a713e tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-24 13:45:25 +08:00
My-Responsitories
7eaf05839a fixes (#1809)
* Revert "opt gesture"

This reverts commit bd97f9a500.

* revert: late init

* update [skip ci]

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-24 11:46:00 +08:00
该昵称己被占用_
777c3c2278 fix typo (#1808)
把中文段落中的“, ”改为“,”。
2026-01-24 11:45:44 +08:00
dom
b9b54ce4f7 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-23 13:08:32 +08:00
dom
92e5fae29c opt pop
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-22 12:35:34 +08:00
dom
05e8ded86a upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-21 21:32:15 +08:00
dom
7a65b777c9 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-21 21:32:10 +08:00
dom
0b1f6c4d0e tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-21 13:38:10 +08:00
My-Responsitories
923af32c96 tweaks (#1806)
* opt: nonnull case

* fix: ImageGrid

* opt: distanceSquared
2026-01-21 13:34:44 +08:00
dom
4eae7e698f opt live border indicator
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-20 15:13:32 +08:00
dom
5a61dbe30c opt emoji tooltip
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-20 15:13:32 +08:00
dom
036dbcaf21 opt image grid
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-20 15:13:32 +08:00
dom
bd97f9a500 opt gesture
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-20 15:13:32 +08:00
dom
33278a74b2 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-20 15:13:27 +08:00
dom
397f887b91 fix video progress indicator
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 12:21:25 +08:00
dom
ebe793ccfc fix progress behavior
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 12:12:54 +08:00
dom
68464e4e34 refa: segment progressbar
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 11:39:25 +08:00
dom
395893fc7d refa: video progress indicator
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 11:38:28 +08:00
dom
f5657d2d4c refa custom painter
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 11:38:27 +08:00
dom
a3ddc83430 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 11:37:45 +08:00
dom
d2f8aff421 opt ui
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-19 11:37:33 +08:00
My-Responsitories
25148509d2 opt: aaudio (#1805)
* opt: aaudio
2026-01-18 09:29:34 +08:00
dom
2879d0dc00 upgrade deps
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-15 16:41:23 +08:00
dom
90349189ee fix image grid
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-15 16:41:23 +08:00
dom
bdc524e486 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-15 16:41:17 +08:00
dom
cb58822009 feat: edit dyn
feat: set pub setting

feat: set reply interaction

Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-15 15:03:19 +08:00
dom
4a2679a589 opt scale
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-11 21:40:25 +08:00
dom
09bd1edeb3 tweaks
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-11 15:27:10 +08:00
KoishiMoe
00da3c4a0e fix: ipa can't be installed by altstore/sidestore (#1803) 2026-01-11 10:46:18 +08:00
My-Responsitories
c40d794180 tweaks (#1802)
* opt: uuid

* tweak

* opt: SlideDialog

* mod: fvmrc [skip ci]

* Revert "mod: fvmrc [skip ci]"

This reverts commit 500fd7f454.

* Revert "opt: SlideDialog"

This reverts commit b435a312a6.

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-11 10:45:51 +08:00
dom
34a839d9e2 fix menu position
fix sc

opt ui

Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-10 18:04:30 +08:00
dom
f06d0605ce fix chat panel container
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-10 12:38:15 +08:00
dom
ef975de624 mark deleted sc
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-10 11:47:40 +08:00
dom
d10c737a38 show img menu
opt img placeholder

opt player gesture

opt pref

tweaks

Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-10 10:21:06 +08:00
s
28b69a06fa feat: Add desktop scaling and fix linux postinst (#1800)
* fix: resolve Linux window close handler to prevent app hang

- Add delete-event callback that properly quits the application when window is closed

* feat: Add desktop scaling and fix linux postinst

- Implement desktop interface scaling in main.dart using FittedBox.
- Add desktop scaling setting UI.
- Add desktopScale to storage preference.
- Fix typos and logic in Linux postinst script.
- Update piliplus.desktop with StartupWMClass.

* update

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

---------

Signed-off-by: Shao Guohao <shao.gh.98@gmail.com>
Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-10 10:03:51 +08:00
dom
069cf555ea bump flutter
Signed-off-by: dom <githubaccount56556@proton.me>
2026-01-09 11:48:04 +08:00
KoishiMoe
836ab311d6 feat: add option to turn off dynamic interactions (#1798)
* add option to turn off dynamic interactions

* update

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-09 11:35:47 +08:00
KoishiMoe
dbc11c36df fix: permission dialog (#1799)
* don't request photo permission on A13+

saving to system album requires no additional permission

* fix permission dialog

* update

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

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-01-09 11:07:39 +08:00
Kofua
fffce10b31 update sponsor block api (#1797)
* update sponsor block api

* update

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-08 11:33:07 +08:00
dom
de85e82bfa Update build.yml 2026-01-07 12:12:39 +08:00
bggRGjQaUbCoE
9855b35b65 opt ui
fix

report im msg

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-07 11:32:24 +08:00
bggRGjQaUbCoE
5a0b045a1f opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-06 13:10:02 +08:00
bggRGjQaUbCoE
c226f8f6df upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-06 13:10:02 +08:00
bggRGjQaUbCoE
fd06fa9cc4 report sc
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-06 13:09:58 +08:00
s
2b5f111fb1 fix: resolve Linux window close handler to prevent app hang (#1795)
- Add delete-event callback that properly quits the application when window is closed
2026-01-03 18:42:30 +08:00
bggRGjQaUbCoE
9f5ce5ae37 fix find sc index
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-03 15:28:19 +08:00
Vixb
3d95165d46 feat: support more dolby id (#1794) 2026-01-03 12:16:09 +08:00
bggRGjQaUbCoE
cfb72f27ac tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-03 11:29:47 +08:00
bggRGjQaUbCoE
bcacc41db3 live dm action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-03 11:01:06 +08:00
bggRGjQaUbCoE
b2da99e334 fix dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-02 14:58:52 +08:00
bggRGjQaUbCoE
041af37bb0 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-02 12:06:09 +08:00
bggRGjQaUbCoE
80e007bac6 add static2Scroll option
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2026-01-02 12:06:05 +08:00
bggRGjQaUbCoE
87c7699324 fix dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-31 14:01:49 +08:00
bggRGjQaUbCoE
11912c5f62 fix level
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-31 12:44:03 +08:00
bggRGjQaUbCoE
236a8b3023 fix dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-31 12:14:01 +08:00
bggRGjQaUbCoE
63e4bac204 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-31 12:13:38 +08:00
My-Responsitories
2e11247af4 fix: font size 2025-12-30 14:13:45 +08:00
My-Responsitories
13f377f680 fix: font size 2025-12-30 14:07:18 +08:00
bggRGjQaUbCoE
b9d594bc8b tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-29 21:04:52 +08:00
bggRGjQaUbCoE
2a52157c3f show live rank
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-29 21:04:10 +08:00
bggRGjQaUbCoE
a037d8e793 opt dyn publish
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-29 21:04:10 +08:00
My-Responsitories
49b7ea14c3 refa: danmaku & feat: scroll fixed velocity (#1791) 2025-12-29 21:03:24 +08:00
bggRGjQaUbCoE
0a40d11133 opt SpringDescription
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-28 10:58:31 +08:00
bggRGjQaUbCoE
dff6b6486d do not check uploadPictureIconState
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 20:54:57 +08:00
bggRGjQaUbCoE
b51c646415 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 20:54:41 +08:00
My-Responsitories
25acf3a9bb fix: dynamic openInBrowser (#1790) 2025-12-27 20:51:40 +08:00
bggRGjQaUbCoE
7ec90e9a22 opt dyn more text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 14:07:49 +08:00
bggRGjQaUbCoE
645ce0b7b3 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 13:52:36 +08:00
bggRGjQaUbCoE
864fef5881 fix check uploadPictureIconState
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 12:40:35 +08:00
bggRGjQaUbCoE
eea232c6db show dyn interaction
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 12:40:35 +08:00
bggRGjQaUbCoE
25fca498fc opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-27 12:40:30 +08:00
bggRGjQaUbCoE
c9a02f9c74 fix retry
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-26 10:43:27 +08:00
bggRGjQaUbCoE
99602eea95 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-26 10:43:27 +08:00
bggRGjQaUbCoE
b5fe0faeec Revert "opt view dyn reply"
This reverts commit 161bf2eedb.
2025-12-26 10:43:27 +08:00
bggRGjQaUbCoE
20a36e8f9a tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-25 13:46:21 +08:00
bggRGjQaUbCoE
161bf2eedb opt view dyn reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-24 18:36:17 +08:00
bggRGjQaUbCoE
fcf4e72d8e fix vote card
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-24 12:47:50 +08:00
bggRGjQaUbCoE
b46cb69df4 opt reload reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-24 12:47:30 +08:00
My-Responsitories
43c7620b4c fix: cacheIndex 2025-12-24 01:05:59 +08:00
bggRGjQaUbCoE
1a8f65b075 opt bar set
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-23 21:12:51 +08:00
bggRGjQaUbCoE
259e7080f8 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-23 17:31:05 +08:00
My-Responsitories
7da6f05a50 tweak 2025-12-23 14:17:22 +08:00
My-Responsitories
521ca3ad18 tweaks (#1788)
* tweak

* opt: show bar

* opt: crc32

* opt: appsign

* opt: Get

* opt: compress only if large

* opt: wbi

* tweak

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

---------

Signed-off-by: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com>
Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-23 12:57:19 +08:00
bggRGjQaUbCoE
31e5692dff upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-22 10:58:45 +08:00
bggRGjQaUbCoE
191bcbc525 fix parse dyn emoji
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-22 10:58:45 +08:00
bggRGjQaUbCoE
a0f3b3e442 tweaks
cache season fav state

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-22 10:58:40 +08:00
bggRGjQaUbCoE
5bcd822251 opt live follow list
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-22 10:43:52 +08:00
My-Responsitories
d80324655e opt: cache image (#1787) 2025-12-22 10:43:32 +08:00
bggRGjQaUbCoE
952d168022 fix grpc contentType
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-19 11:22:26 +08:00
bggRGjQaUbCoE
af723e161c tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-19 11:22:13 +08:00
dom
3ff521e103 Update build.yml 2025-12-18 22:29:19 +08:00
My-Responsitories
b4a5d985f5 opt: isolate parse danmaku & feat: grpc account (#1785)
* opt: isolate parse danmaku

* feat: grpc account
2025-12-18 22:27:40 +08:00
bggRGjQaUbCoE
1e0e2d2d6e tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-18 12:30:57 +08:00
My-Responsitories
d7f7611af4 opt: color (#1782)
* fixes

* opt: color

* fix
2025-12-18 11:08:03 +08:00
My-Responsitories
11cdb67050 feat: show network type (#1781) 2025-12-17 21:58:42 +08:00
bggRGjQaUbCoE
53cf9d54c4 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 21:39:34 +08:00
bggRGjQaUbCoE
2e73688688 add superChatType
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 19:54:30 +08:00
My-Responsitories
ce5e85e64b tweaks (#1780)
* opt: sized

* fix: self send

* feat: ctrl enter to send

* opt: checked

* opt: download notifier

* opt: Future.syncValue

* mod: account

* mod: loading state

* opt: DebounceStreamMixin

* opt: report

* opt: enum map

* opt: file handler

* opt: dyn color

* opt: Uint8List subview

* opt: FileExt

* opt: computeLuminance

* opt: isNullOrEmpty

* opt: Get context

* update [skip ci]

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

* opt dynamicColor [skip ci]

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

* fixes [skip ci]

* update

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

* update

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

---------

Signed-off-by: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com>
Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 17:01:10 +08:00
bggRGjQaUbCoE
02e0d34127 increase desktop max volume
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 14:05:30 +08:00
bggRGjQaUbCoE
830f3b60e0 opt theme
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 13:09:15 +08:00
bggRGjQaUbCoE
b4fb7d14d4 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 13:02:01 +08:00
lesetong
ab1e5cb62a Add multi-select support to pmshare panel (#1779)
* Add multi-select support to share panel

- Replace single selection index with per-user selected flag
- Allow sending to multiple selected users
- Add sending state to prevent multiple clicks
- Update default selection logic to mark first user as selected

* 简化代码逻辑

* update

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

---------

Signed-off-by: lesetong <oscarlbw@qq.com>
Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-17 12:46:36 +08:00
bggRGjQaUbCoE
348a9e014e opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-16 18:42:20 +08:00
bggRGjQaUbCoE
0baf3fcd36 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-16 15:56:54 +08:00
bggRGjQaUbCoE
13818533a7 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-16 14:13:40 +08:00
bggRGjQaUbCoE
0dd3689d65 opt opus text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-16 11:21:03 +08:00
bggRGjQaUbCoE
23b6850778 opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-15 20:29:46 +08:00
bggRGjQaUbCoE
d8ca89ac8f upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-15 20:11:07 +08:00
bggRGjQaUbCoE
ae06d5f7f2 opt live header
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 17:02:01 +08:00
bggRGjQaUbCoE
62506d3eb5 disable alwaysOnTop on dispose
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 16:36:02 +08:00
bggRGjQaUbCoE
f7c61d63a0 remove deprecated pref keys
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 16:14:10 +08:00
bggRGjQaUbCoE
f46437f891 fix get block color
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 15:57:57 +08:00
bggRGjQaUbCoE
1cd949c365 use ValueGetter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 14:14:27 +08:00
bggRGjQaUbCoE
bc5ce11449 fix PopupMenuText
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 14:04:00 +08:00
Vixb
cef4beaa0c feat: sync segment type with upstream (#1777)
* feat: sync segment type with upstream

* update

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

---------

Signed-off-by: Vixb <xzx8023@outlook.com>
Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 13:48:00 +08:00
bggRGjQaUbCoE
02bd68f697 opt desktop pip
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 12:31:51 +08:00
bggRGjQaUbCoE
2bc3275c1f opt reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-14 11:14:01 +08:00
bggRGjQaUbCoE
ec107063c3 opt pay coin
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-13 20:42:27 +08:00
bggRGjQaUbCoE
4c2fd38d6c upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-13 17:10:46 +08:00
bggRGjQaUbCoE
1a6653ba93 opt reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-13 15:53:14 +08:00
bggRGjQaUbCoE
74d5e03a34 show followee votes
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-13 15:53:14 +08:00
My-Responsitories
2b4b1debe6 tweak 2025-12-13 14:51:07 +08:00
My-Responsitories
17883eb77e opt: LoadingState (#1776) 2025-12-13 12:43:32 +08:00
bggRGjQaUbCoE
3741fe54ff upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-10 18:00:01 +08:00
bggRGjQaUbCoE
ec11af3827 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-10 17:57:39 +08:00
My-Responsitories
890dc58dc3 refa: settings model (#1773)
* opt: MediaQuery

* refa: settings model
2025-12-10 16:41:31 +08:00
bggRGjQaUbCoE
b12bdf2eb8 opt log page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-10 10:51:33 +08:00
bggRGjQaUbCoE
59c7f8a030 opt onChangeAccount
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-10 10:51:33 +08:00
bggRGjQaUbCoE
50cf74ccf7 fix play next audio
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-10 10:51:33 +08:00
LiPolymer
15b5c0a874 feat: modify recommend page's card width separately (#1771)
* feat: modify recommend card width setting separately
2025-12-10 10:51:16 +08:00
My-Responsitories
244ef22f54 feat: load config from text (#1772)
* feat: load config from text

* opt: login utils

* update

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-09 22:09:57 +08:00
bggRGjQaUbCoE
b4daf5fbd8 reduce log snackbar duration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-08 23:07:30 +08:00
bggRGjQaUbCoE
0519ec0e4b build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-08 23:07:30 +08:00
My-Responsitories
ff4f97de1a opt: parse sys msg (#1770) 2025-12-08 23:06:46 +08:00
My-Responsitories
773bdafec3 opt: more linter (#1765)
* opt: more linter

* fix [skip ci]

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-07 23:46:42 +08:00
bggRGjQaUbCoE
3787f99d35 opt download next
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-07 12:29:12 +08:00
bggRGjQaUbCoE
2cb8331528 cache follow order type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-07 11:48:47 +08:00
bggRGjQaUbCoE
5b6443cfa4 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-07 11:48:42 +08:00
bggRGjQaUbCoE
6fd8212d8b upgrade actions/checkout
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-07 10:42:36 +08:00
My-Responsitories
0d273f6909 refa: logfile (#1764)
* refa: logfile

* opt: log page

* opt: raf log file

* remove old log

* update

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-06 22:33:00 +08:00
bggRGjQaUbCoE
255e39b709 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-06 10:04:57 +08:00
bggRGjQaUbCoE
ea52dd4484 fix typos
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-06 10:04:52 +08:00
bggRGjQaUbCoE
b4a46133be opt set pageTransition
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-04 17:11:59 +08:00
bggRGjQaUbCoE
7c1644efc4 upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-04 11:26:17 +08:00
bggRGjQaUbCoE
775e1aa97d do not show others rank
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-04 11:19:50 +08:00
bggRGjQaUbCoE
2a55d4390a opt list
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-04 11:16:51 +08:00
bggRGjQaUbCoE
d57a34a4e1 fix member list jump
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 22:05:06 +08:00
bggRGjQaUbCoE
2785248615 opt up panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 21:47:12 +08:00
bggRGjQaUbCoE
c42468e2c8 opt update down progress
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 19:05:00 +08:00
bggRGjQaUbCoE
196ddf3f5f opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 17:36:47 +08:00
bggRGjQaUbCoE
27302435be specify window class name
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 17:36:47 +08:00
My-Responsitories
2b3ec77e92 opt: unnecessary_non_null_assertion (#1762) 2025-12-03 17:35:42 +08:00
bggRGjQaUbCoE
b7a277a57c refa: member fav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 14:15:02 +08:00
bggRGjQaUbCoE
9c8e5b53e7 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-03 14:14:38 +08:00
bggRGjQaUbCoE
001b746f65 change dynamicColor def value
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 17:06:52 +08:00
bggRGjQaUbCoE
a78214de3c sort video language
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 16:58:26 +08:00
bggRGjQaUbCoE
d88ffb1127 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 16:58:26 +08:00
dom
f05b901009 fix & opt appsign (#1761)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 16:58:13 +08:00
bggRGjQaUbCoE
430837eef6 opt live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 11:46:38 +08:00
bggRGjQaUbCoE
fa583ebd0f tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 11:46:31 +08:00
bggRGjQaUbCoE
d2dcba5a59 upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-02 11:45:57 +08:00
bggRGjQaUbCoE
fb5116d525 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-12-01 14:14:29 +08:00
bggRGjQaUbCoE
a48f6b1ca5 opt update block type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-30 11:41:34 +08:00
bggRGjQaUbCoE
fc0af3f284 remove seek announce
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-30 11:41:34 +08:00
bggRGjQaUbCoE
2288e11398 fix dm block type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-30 11:06:20 +08:00
bggRGjQaUbCoE
d95283c4ac upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-28 20:49:28 +08:00
bggRGjQaUbCoE
4b56bd5a87 fix download status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-28 16:50:53 +08:00
My-Responsitories
62bb605ee8 tweak: danmaku (#1756)
* fix: danmaku like

* opt: danmaku merge

* remove: showSpecialDanmaku
2025-11-28 16:50:37 +08:00
bggRGjQaUbCoE
0f8da1999a opt multi select
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-28 11:59:12 +08:00
bggRGjQaUbCoE
21a2373a5c update dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-28 11:24:03 +08:00
bggRGjQaUbCoE
2ca5310825 reduce fullscreen sc duration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-28 11:24:03 +08:00
dom
9ccaa3072b opt download (#1755)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-27 21:00:13 +08:00
bggRGjQaUbCoE
ded78e534f upgrade protobuf
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-26 17:11:35 +08:00
bggRGjQaUbCoE
9b0a43efc9 upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-26 11:50:08 +08:00
bggRGjQaUbCoE
10808c2a84 show live online count
update live title

update live watchedshow

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-26 11:47:00 +08:00
My-Responsitories
38a7afd63a opt: player controller (#1753)
* opt: player controller

* tweak [skip ci]

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-26 11:45:16 +08:00
bggRGjQaUbCoE
54b26d20fa upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-23 12:43:53 +08:00
bggRGjQaUbCoE
ad2bc78ebd opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-23 12:43:46 +08:00
bggRGjQaUbCoE
c4aca389a8 fixes
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-22 22:35:37 +08:00
bggRGjQaUbCoE
cb8333d4c0 show vote status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-22 19:35:27 +08:00
bggRGjQaUbCoE
2f5eed6998 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-22 13:56:06 +08:00
bggRGjQaUbCoE
935c53e452 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-22 09:50:32 +08:00
bggRGjQaUbCoE
dd0ccb327b show battery level
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-22 09:50:27 +08:00
bggRGjQaUbCoE
919134759b tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-21 17:53:46 +08:00
bggRGjQaUbCoE
c1d42b498a opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-21 17:46:17 +08:00
bggRGjQaUbCoE
a7e67796f1 fix theme type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-21 14:07:22 +08:00
bggRGjQaUbCoE
6692c9e851 set dm for live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-21 14:03:20 +08:00
bggRGjQaUbCoE
ace949aaa0 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-21 13:26:57 +08:00
bggRGjQaUbCoE
fbd9687432 upgrade kgp
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-21 13:26:47 +08:00
bggRGjQaUbCoE
460a8262c1 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-20 19:45:55 +08:00
bggRGjQaUbCoE
c8de503fae fix dyn showmore
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-20 15:27:52 +08:00
bggRGjQaUbCoE
a60cd51ff4 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-20 13:27:31 +08:00
bggRGjQaUbCoE
aad980ce23 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-20 13:27:15 +08:00
bggRGjQaUbCoE
e7cda7b9fa upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 18:34:54 +08:00
bggRGjQaUbCoE
1d368b7a8b update richtextfield
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 18:34:49 +08:00
bggRGjQaUbCoE
725d7055bf update flutter widgets
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 17:45:24 +08:00
bggRGjQaUbCoE
1fb798db4e opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 10:19:36 +08:00
bggRGjQaUbCoE
8e1d5e0dd5 opt live back btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 09:35:14 +08:00
bggRGjQaUbCoE
2d9a1310b9 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 09:35:09 +08:00
bggRGjQaUbCoE
588ec7babd upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 09:35:04 +08:00
My-Responsitories
2be13e7283 refa: sb & feat: sb portVideo (WIP) (#1751)
* refa: sb

* feat: sb portVideo (WIP)

* fix: keep-alive

* revert: ua version

* fix

* tweak [skip ci]

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-19 09:30:04 +08:00
bggRGjQaUbCoE
d5d95671ff ios uiscene migration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-18 18:22:55 +08:00
bggRGjQaUbCoE
a0eccda6ff set player proxy
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-18 18:22:55 +08:00
bggRGjQaUbCoE
ec82c86210 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 22:06:30 +08:00
bggRGjQaUbCoE
de03bef226 show video restore btn if translated
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 21:50:03 +08:00
My-Responsitories
0f8166620e opt: notify-debugger-on-exception (#1750) 2025-11-17 21:49:36 +08:00
bggRGjQaUbCoE
76c2de4394 fix THREE_DOT_ITEM_TYPE_UP_HELPER action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 21:10:20 +08:00
bggRGjQaUbCoE
0d38ded981 show lock btn for live
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 18:51:40 +08:00
bggRGjQaUbCoE
646888c06f fix import
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 18:15:20 +08:00
bggRGjQaUbCoE
332f6f1bb4 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 18:04:30 +08:00
bggRGjQaUbCoE
aaab5371b2 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 17:35:29 +08:00
JianGuo Wang
ad931d7ea2 add media notification handling for offline videos (#1748)
* feat: add media notification handling for offline videos

* update

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

* update

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 17:35:00 +08:00
My-Responsitories
377e430d74 refa: report error (#1747)
* refa: report error

* remove some reports [skip ci]

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-17 17:20:29 +08:00
My-Responsitories
a797467606 upgrade dep (#1746) 2025-11-16 15:02:20 +00:00
My-Responsitories
5ee83d902d opt: exclude analysis flutter widget (#1745) 2025-11-16 14:33:40 +00:00
My-Responsitories
27ae296b28 refa: cdn (#1743)
* refa: cdn

* feat: live cdn (WIP)

* tweaks

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

* add live quality [skip ci]

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

* mod: replace durl host

* tweak [skip ci]

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-15 20:12:21 +08:00
My-Responsitories
e589f27195 fix; set subtitle before init (#1742)
* tweka

* fix; set subtitle before init
2025-11-15 07:34:23 +00:00
bggRGjQaUbCoE
c89d6a5a59 fix wakelock
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-15 11:50:17 +08:00
bggRGjQaUbCoE
861365930d reformat
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-14 09:22:43 +08:00
bggRGjQaUbCoE
0d4d92a202 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-14 09:22:43 +08:00
bggRGjQaUbCoE
4c6ad0e385 increase webdav timeout
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-14 09:22:43 +08:00
iKirby
ad45e995e2 fix preferred cdn & Add more PCDN url patterns (#1739)
* Fix preferred cdn not used after changing quality

* Add more PCDN url patterns
2025-11-14 09:21:51 +08:00
bggRGjQaUbCoE
50a035a479 opt get season status
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-13 14:27:52 +08:00
bggRGjQaUbCoE
c0dbd6cbb2 migration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-13 11:04:46 +08:00
bggRGjQaUbCoE
686af4a330 bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-13 10:07:44 +08:00
bggRGjQaUbCoE
46aad06e34 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-13 10:05:27 +08:00
bggRGjQaUbCoE
3921b2304d opt download task
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-13 09:38:23 +08:00
My-Responsitories
bca5b0419c tweaks (#1738)
* feat: edit dm filter

* opt: browser

* feat: sb userInfo

* mod: tvPlayUrl
2025-11-13 09:36:50 +08:00
bggRGjQaUbCoE
9754b061dd check dm state
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-12 20:21:11 +08:00
My-Responsitories
407b31c5c1 refa: download video (#1737)
* opt: save pb danmaku

* refa: download video

* opt: replaceAll

* fix: wait delete

* opt: remove completer

* fix: index.json

* tweaks

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

---------

Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-12 19:12:17 +08:00
bggRGjQaUbCoE
37b1228552 feat: dlna
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-12 19:03:01 +08:00
bggRGjQaUbCoE
0acd9ca767 upgrade dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-12 10:50:29 +08:00
My-Responsitories
8f3c9f029c opt: const page (#1736) 2025-11-12 07:33:28 +08:00
BH8GUQ
9310732343 Mark offline cache as completed in README (#1735) 2025-11-11 17:46:11 +08:00
bggRGjQaUbCoE
e767e506f3 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-11 17:43:50 +08:00
bggRGjQaUbCoE
ef3a612338 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-11 09:32:08 +08:00
bggRGjQaUbCoE
d66a42a0aa opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-11 09:32:03 +08:00
bggRGjQaUbCoE
0f06de0047 fix save subtitle on win
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-10 12:09:59 +08:00
bggRGjQaUbCoE
963181fef2 feat: load file sub
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-10 11:31:43 +08:00
bggRGjQaUbCoE
ffd4f9ee73 feat: video download
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-09 22:06:19 +08:00
bggRGjQaUbCoE
976622df89 fix pm send btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-09 16:13:42 +08:00
bggRGjQaUbCoE
13c220338c set system brightness
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-09 15:48:52 +08:00
bggRGjQaUbCoE
1291dc77c8 stop audio service
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-09 11:34:00 +08:00
bggRGjQaUbCoE
08e5477e74 enable audio cdn by def
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-08 16:55:28 +08:00
bggRGjQaUbCoE
c4c6a2243e disable win single alt key event
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-08 16:55:28 +08:00
bggRGjQaUbCoE
58791e3e91 show vote option pic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-08 16:55:28 +08:00
bggRGjQaUbCoE
d5bb4bc149 fix fan/follow params
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-07 19:55:59 +08:00
bggRGjQaUbCoE
3d1199363b Release 1.1.5
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-07 16:45:13 +08:00
bggRGjQaUbCoE
f225fa33e1 update proto
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-06 15:08:19 +08:00
bggRGjQaUbCoE
e85c8b3dde tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-02 12:37:20 +08:00
bggRGjQaUbCoE
737be8dcac del cache
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-01 18:26:58 +08:00
bggRGjQaUbCoE
77dd939172 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-11-01 15:57:17 +08:00
bggRGjQaUbCoE
0a5965a423 fix parse dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-31 18:57:31 +08:00
bggRGjQaUbCoE
a53be6814c opt tooltip style
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-31 15:12:28 +08:00
bggRGjQaUbCoE
415b8e9da3 fix audio action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-31 12:28:59 +08:00
bggRGjQaUbCoE
f034c24d13 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-30 17:32:18 +08:00
bggRGjQaUbCoE
1ac93d6269 upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-30 17:32:02 +08:00
bggRGjQaUbCoE
906c8f7999 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-29 18:28:10 +08:00
bggRGjQaUbCoE
c904a5ded8 del
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-29 18:28:04 +08:00
bggRGjQaUbCoE
0c9486f6b4 build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 23:15:48 +08:00
bggRGjQaUbCoE
576740a502 opt ui
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 21:53:07 +08:00
bggRGjQaUbCoE
b3f9f43b57 fix #1729
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 21:52:52 +08:00
bggRGjQaUbCoE
e7424bcc66 fix buvidActive
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 17:21:41 +08:00
bggRGjQaUbCoE
209ec70ea9 opt icon
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 17:21:00 +08:00
bggRGjQaUbCoE
3b4e251034 opt desktop pip
Closes #1478

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 13:49:09 +08:00
bggRGjQaUbCoE
86beb879a2 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 13:17:16 +08:00
bggRGjQaUbCoE
321d434141 build
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-28 11:04:12 +08:00
Axiaobo
b9d17e27b1 fix #1718 (#1723) 2025-10-28 10:45:15 +08:00
My-Responsitories
2f6f6da6c0 opt: tap (#1719)
* opt: tap

* revert: remove suspendedDm
2025-10-27 21:32:39 +08:00
bggRGjQaUbCoE
c3d3fa67f7 fix #1716
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-27 18:16:09 +08:00
bggRGjQaUbCoE
032dfd69be fix #1712
fix #1641

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-27 18:16:09 +08:00
bggRGjQaUbCoE
e9dc154642 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-27 18:16:03 +08:00
My-Responsitories
b43840b636 mod: save panel (#1706) 2025-10-25 10:23:42 +00:00
My-Responsitories
1a9d8e35ba feat: show aiConclusion (#1698) 2025-10-25 06:54:21 +00:00
My-Responsitories
ccb61415f5 tweaks (#1697)
* opt: play status

* opt: comment
2025-10-25 06:45:19 +00:00
My-Responsitories
08944241bb fix: danmaku (#1696)
* fix: post danmaku

* mod: tap danmaku

* mod: delete danmaku
2025-10-25 06:41:47 +00:00
bggRGjQaUbCoE
63030147ea bump flutter
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-25 12:16:11 +08:00
bggRGjQaUbCoE
8ff71c44ca opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-25 12:16:11 +08:00
bggRGjQaUbCoE
4eaf16f500 disable search default by def
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-24 20:22:06 +08:00
Axiaobo
1a9c8a62f2 package linux appimage (#1688)
* modified:   .github/workflows/linux_x64.yml

* modified:   .github/workflows/linux_arm64.yml

* modified:   .github/workflows/linux_arm64.yml
	modified:   lib/plugin/pl_player/controller.dart

* modified:   lib/plugin/pl_player/controller.dart

* modified:   lib/plugin/pl_player/controller.dart

* modified:   lib/plugin/pl_player/controller.dart

---------

Co-authored-by: Xiaobo Ch. <Axiaobo7788@163.com>
2025-10-24 18:36:46 +08:00
bggRGjQaUbCoE
4256c2b023 opt ui
Closes #1680

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-24 14:34:13 +08:00
bggRGjQaUbCoE
bbcf0dec1b upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-23 23:21:33 +08:00
bggRGjQaUbCoE
da52cac2c6 tweaks
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-23 23:21:28 +08:00
bggRGjQaUbCoE
e8a32a6149 custom audio order
Closes #1636

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-21 17:22:33 +08:00
bggRGjQaUbCoE
a71a7b66f8 opt req
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-21 17:01:18 +08:00
bggRGjQaUbCoE
9808f50816 show audio playlist parts
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-21 11:35:41 +08:00
bggRGjQaUbCoE
cf86bb7e13 add match entrance
Closes #1628

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-21 10:20:42 +08:00
bggRGjQaUbCoE
ff065254ae fix get window pos
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-21 10:20:00 +08:00
bggRGjQaUbCoE
39b4c1a59b show rank fold items
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-21 10:19:17 +08:00
bggRGjQaUbCoE
28f10e0a4b check uploadPictureIconState
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-20 19:34:27 +08:00
bggRGjQaUbCoE
12c0ed5baf fix #1616
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-20 15:41:20 +08:00
bggRGjQaUbCoE
23272d285b tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-20 15:39:48 +08:00
bggRGjQaUbCoE
67b4ed65ab opt audio playlist
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-20 13:15:26 +08:00
My-Responsitories
7524b3d168 fix: hide danmaku (#1654) 2025-10-20 12:17:59 +08:00
bggRGjQaUbCoE
340a933e70 fix #1648
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-20 10:23:13 +08:00
bggRGjQaUbCoE
488ca29fc1 opt dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-19 21:31:27 +08:00
bggRGjQaUbCoE
cc00b2cc39 feat: dyn show more
Closes #1629

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-19 20:27:42 +08:00
bggRGjQaUbCoE
287cea4d6c tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-19 20:11:44 +08:00
bggRGjQaUbCoE
39e556891a upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-19 15:33:02 +08:00
928 changed files with 43165 additions and 29268 deletions

2
.fvmrc
View File

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

View File

@@ -41,12 +41,6 @@ on:
default: true default: true
type: boolean type: boolean
build_linux_arm64:
description: "Build Linux-arm64"
required: false
default: true
type: boolean
tag: tag:
description: "tag" description: "tag"
required: false required: false
@@ -62,7 +56,7 @@ jobs:
steps: steps:
- name: 代码迁出 - name: 代码迁出
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -89,6 +83,11 @@ jobs:
run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff run: git apply $GITHUB_WORKSPACE/lib/scripts/bottom_sheet_patch.diff
continue-on-error: true continue-on-error: true
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Write key - name: Write key
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: | run: |
@@ -116,7 +115,7 @@ jobs:
shell: bash shell: bash
- name: Release - name: Release
if: ${{ github.event.inputs.tag != '' }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ github.event.inputs.tag }} tag_name: ${{ github.event.inputs.tag }}
@@ -125,21 +124,21 @@ jobs:
PiliPlus_android_*.apk PiliPlus_android_*.apk
- name: 上传 - name: 上传
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Android_arm64-v8a name: Android_arm64-v8a
path: | path: |
PiliPlus_android_*_arm64-v8a.apk PiliPlus_android_*_arm64-v8a.apk
- name: 上传 - name: 上传
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Android_armeabi-v7a name: Android_armeabi-v7a
path: | path: |
PiliPlus_android_*_armeabi-v7a.apk PiliPlus_android_*_armeabi-v7a.apk
- name: 上传 - name: 上传
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Android_x86_64 name: Android_x86_64
path: | path: |
@@ -150,32 +149,25 @@ jobs:
uses: ./.github/workflows/ios.yml uses: ./.github/workflows/ios.yml
permissions: write-all permissions: write-all
with: with:
tag: github.event.inputs.tag tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
mac: mac:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_mac == 'true' }} if: ${{ github.event.inputs.build_mac == 'true' }}
uses: ./.github/workflows/mac.yml uses: ./.github/workflows/mac.yml
permissions: write-all permissions: write-all
with: with:
tag: github.event.inputs.tag tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
win_x64: win_x64:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }} if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_win_x64 == 'true' }}
uses: ./.github/workflows/win_x64.yml uses: ./.github/workflows/win_x64.yml
permissions: write-all permissions: write-all
with: with:
tag: github.event.inputs.tag tag: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
linux_x64: linux_x64:
if: ${{ github.event_name == 'pull_request' || github.event.inputs.build_linux_x64 == 'true' }} if: ${{ github.event.inputs.build_linux_x64 == 'true' }}
uses: ./.github/workflows/linux_x64.yml uses: ./.github/workflows/linux_x64.yml
permissions: write-all permissions: write-all
with: with:
tag: github.event.inputs.tag tag: ${{ github.event_name == 'workflow_dispatch' && 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

@@ -16,7 +16,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -30,10 +30,17 @@ jobs:
shell: pwsh shell: pwsh
run: lib/scripts/build.ps1 run: lib/scripts/build.ps1
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Build iOS - name: Build iOS
run: | run: |
flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json flutter build ios --release --no-codesign --dart-define-from-file=pili_release.json
ln -sf ./build/ios/iphoneos Payload ln -sf ./build/ios/iphoneos Payload
# make AltSign happy...
find Payload/Runner.app/Frameworks -type d -name "*.framework" -exec codesign --force --sign - --preserve-metadata=identifier,entitlements {} \;
zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app zip -r9 PiliPlus_ios_${{env.version}}.ipa Payload/runner.app
- name: Release - name: Release
@@ -46,7 +53,7 @@ jobs:
PiliPlus_ios_*.ipa PiliPlus_ios_*.ipa
- name: Upload ios release - name: Upload ios release
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: iOS-release name: iOS-release
path: PiliPlus_ios_*.ipa path: PiliPlus_ios_*.ipa

View File

@@ -1,210 +0,0 @@
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

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -51,6 +51,11 @@ jobs:
shell: pwsh shell: pwsh
run: lib/scripts/build.ps1 run: lib/scripts/build.ps1
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
#TODO: deb and rpm packages need to be build #TODO: deb and rpm packages need to be build
- name: Build Linux - name: Build Linux
run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json run: flutter build linux --release -v --pub --dart-define-from-file=pili_release.json
@@ -180,6 +185,49 @@ jobs:
printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}" printf "完成: PiliPlus_linux_%s_amd64.rpm\n" "${{ env.version }}"
shell: bash shell: bash
- name: Package AppImage
run: |
printf "下载 appimagetool...\n"
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
printf "建立 AppDir 目录结构...\n"
APPDIR="PiliPlus.AppDir"
mkdir -p "$APPDIR/usr/bin"
mkdir -p "$APPDIR/usr/lib"
mkdir -p "$APPDIR/usr/share/applications"
mkdir -p "$APPDIR/usr/share/icons/hicolor/512x512/apps"
printf "复制应用文件...\n"
cp -r build/linux/x64/release/bundle/* "$APPDIR/usr/bin/"
printf "复制桌面文件和图标...\n"
cp assets/linux/piliplus.desktop "$APPDIR/piliplus.desktop"
cp assets/linux/piliplus.desktop "$APPDIR/usr/share/applications/piliplus.desktop"
cp assets/images/logo/logo.png "$APPDIR/piliplus.png"
cp assets/images/logo/logo.png "$APPDIR/usr/share/icons/hicolor/512x512/apps/piliplus.png"
printf "创建 AppRun 启动脚本...\n"
cat > "$APPDIR/AppRun" <<'APPRUN_EOF'
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export PATH="${HERE}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
exec "${HERE}/usr/bin/piliplus" "$@"
APPRUN_EOF
chmod +x "$APPDIR/AppRun"
printf "修改桌面文件中的 Exec 路径...\n"
sed -i 's|Exec=piliplus|Exec=piliplus|g' "$APPDIR/piliplus.desktop"
sed -i 's|Icon=piliplus|Icon=piliplus|g' "$APPDIR/piliplus.desktop"
printf "打包 AppImage...\n"
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APPDIR" "PiliPlus_linux_${{ env.version }}_amd64.AppImage"
printf "完成: PiliPlus_linux_%s_amd64.AppImage\n" "${{ env.version }}"
shell: bash
- name: Release - name: Release
if: ${{ github.event.inputs.tag != '' }} if: ${{ github.event.inputs.tag != '' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@@ -190,21 +238,28 @@ jobs:
PiliPlus_linux_*.tar.gz PiliPlus_linux_*.tar.gz
PiliPlus_linux_*.deb PiliPlus_linux_*.deb
PiliPlus_linux_*.rpm PiliPlus_linux_*.rpm
PiliPlus_linux_*.AppImage
- name: Upload linux targz package - name: Upload linux targz package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Linux_targz_amd64_packege name: Linux_targz_amd64_packege
path: PiliPlus_linux_*.tar.gz path: PiliPlus_linux_*.tar.gz
- name: Upload linux deb package - name: Upload linux deb package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Linux_deb_amd64_package name: Linux_deb_amd64_package
path: PiliPlus_linux_*.deb path: PiliPlus_linux_*.deb
- name: Upload linux rpm package - name: Upload linux rpm package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Linux_rpm_amd64_package name: Linux_rpm_amd64_package
path: PiliPlus_linux_*.rpm path: PiliPlus_linux_*.rpm
- name: Upload linux AppImage package
uses: actions/upload-artifact@v5
with:
name: Linux_AppImage_amd64_package
path: PiliPlus_linux_*.AppImage

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -30,6 +30,11 @@ jobs:
shell: pwsh shell: pwsh
run: lib/scripts/build.ps1 run: lib/scripts/build.ps1
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $GITHUB_WORKSPACE/lib/scripts/modal-barrier-patch.diff
continue-on-error: true
- name: Build Mac - name: Build Mac
run: flutter build macos --release --dart-define-from-file=pili_release.json run: flutter build macos --release --dart-define-from-file=pili_release.json
@@ -52,7 +57,7 @@ jobs:
PiliPlus_macos_*.dmg PiliPlus_macos_*.dmg
- name: Upload macos release - name: Upload macos release
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: macOS-release name: macOS-release
path: PiliPlus_macos_*.dmg path: PiliPlus_macos_*.dmg

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -26,6 +26,12 @@ jobs:
channel: stable channel: stable
flutter-version-file: pubspec.yaml flutter-version-file: pubspec.yaml
- name: apply modal barrier patch
working-directory: ${{ env.FLUTTER_ROOT }}
run: git apply $env:GITHUB_WORKSPACE\lib\scripts\modal-barrier-patch.diff
shell: pwsh
continue-on-error: true
- name: Add fastforge and Inno Setup - name: Add fastforge and Inno Setup
run: | run: |
dart pub global activate fastforge dart pub global activate fastforge
@@ -68,13 +74,13 @@ jobs:
PiliPlus-Win-Setup/PiliPlus_windows_*.exe PiliPlus-Win-Setup/PiliPlus_windows_*.exe
- name: Upload windows file release - name: Upload windows file release
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Windows-file-x64-release name: Windows-file-x64-release
path: Release path: Release
- name: Upload windows setup release - name: Upload windows setup release
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: Windows-setup-x64-release name: Windows-setup-x64-release
path: PiliPlus-Win-Setup path: PiliPlus-Win-Setup

View File

@@ -43,6 +43,9 @@
## feat ## feat
- [x] 编辑动态
- [x] DLNA 投屏
- [x] 离线缓存/播放
- [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories) - [x] 移动端支持点击弹幕悬停,点赞、复制、举报 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] 播放音频 - [x] 播放音频
- [x] 跳过番剧片头/片尾 - [x] 跳过番剧片头/片尾
@@ -151,7 +154,7 @@
- [x] 粉丝、关注用户、拉黑用户查看 - [x] 粉丝、关注用户、拉黑用户查看
- [x] 用户主页查看 - [x] 用户主页查看
- [x] 关注/取关用户 - [x] 关注/取关用户
- [ ] 离线缓存 - [x] 离线缓存
- [x] 稍后再看 - [x] 稍后再看
- [x] 观看记录 - [x] 观看记录
- [x] 我的收藏 - [x] 我的收藏
@@ -216,8 +219,8 @@
## 声明 ## 声明
此项目PiliPlus是个人为了兴趣而开发, 仅用于学习和测试请于下载后24小时内删除。 此项目PiliPlus是个人为了兴趣而开发仅用于学习和测试请于下载后24小时内删除。
所用API皆从官方网站收集, 不提供任何破解内容。 所用API皆从官方网站收集不提供任何破解内容。
在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala) 在此致敬原作者:[guozhigq/pilipala](https://github.com/guozhigq/pilipala)
在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX) 在此致敬上游作者:[orz12/PiliPalaX](https://github.com/orz12/PiliPalaX)
本仓库做了更激进的修改,感谢原作者的开源精神。 本仓库做了更激进的修改,感谢原作者的开源精神。

View File

@@ -12,7 +12,8 @@ include: package:flutter_lints/flutter.yaml
analyzer: analyzer:
exclude: exclude:
- lib/grpc/bilibili/** - lib/grpc/bilibili/**
- lib/grpc/google/** # - lib/grpc/google/**
# - lib/common/widgets/flutter/**
formatter: formatter:
trailing_commas: preserve trailing_commas: preserve
@@ -63,5 +64,13 @@ linter:
- use_null_aware_elements - use_null_aware_elements
- unnecessary_lambdas - unnecessary_lambdas
- use_is_even_rather_than_modulo - use_is_even_rather_than_modulo
- unnecessary_async
- unnecessary_await_in_return
- unnecessary_getters_setters
- prefer_const_literals_to_create_immutables
- no_literal_bool_comparisons
- use_truncating_division
- use_string_buffers
- unnecessary_statements
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@@ -210,4 +210,5 @@
--> -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
</manifest> </manifest>

View File

@@ -43,7 +43,10 @@ class MainActivity : AudioServiceActivity() {
val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>() val cookies = call.argument<List<String>>("cookies") ?: emptyList<String>()
val intent = Intent().apply { val intent = Intent().apply {
component = ComponentName("icu.freedomIntrovert.biliSendCommAntifraud", "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity") component = ComponentName(
"icu.freedomIntrovert.biliSendCommAntifraud",
"icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity"
)
putExtra("action", action) putExtra("action", action)
putExtra("oid", oid.toLong()) putExtra("oid", oid.toLong())
putExtra("type", type) putExtra("type", type)
@@ -52,23 +55,27 @@ class MainActivity : AudioServiceActivity() {
putExtra("parent", parent.toLong()) putExtra("parent", parent.toLong())
putExtra("ctime", ctime.toLong()) putExtra("ctime", ctime.toLong())
putExtra("comment_text", commentText) putExtra("comment_text", commentText)
if(pictures != null) if (pictures != null)
putExtra("pictures", pictures) putExtra("pictures", pictures)
putExtra("source_id", sourceId) putExtra("source_id", sourceId)
putExtra("uid", uid.toLong()) putExtra("uid", uid.toLong())
putStringArrayListExtra("cookies", ArrayList(cookies)) putStringArrayListExtra("cookies", ArrayList(cookies))
} }
startActivity(intent) startActivity(intent)
} catch (_: Exception) {} } catch (_: Exception) {
}
} }
"linkVerifySettings" -> { "linkVerifySettings" -> {
val uri = ("package:" + context.packageName).toUri() val uri = ("package:" + context.packageName).toUri()
try { try {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri) Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
} else { } else {
Intent("android.intent.action.MAIN", uri).setClassName("com.android.settings", Intent("android.intent.action.MAIN", uri).setClassName(
"com.android.settings.applications.InstalledAppOpenByDefaultActivity") "com.android.settings",
"com.android.settings.applications.InstalledAppOpenByDefaultActivity"
)
} }
context.startActivity(intent) context.startActivity(intent)
} catch (_: Throwable) { } catch (_: Throwable) {
@@ -76,33 +83,47 @@ class MainActivity : AudioServiceActivity() {
context.startActivity(intent) context.startActivity(intent)
} }
} }
"music" -> { "music" -> {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply { val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply {
putExtra(SearchManager.QUERY, title) putExtra(SearchManager.QUERY, title)
putExtra(MediaStore.EXTRA_MEDIA_TITLE, title) putExtra(MediaStore.EXTRA_MEDIA_TITLE, title)
call.argument<String?>("artist")?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) } call.argument<String?>("artist")
call.argument<String?>("album")?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) } ?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) }
call.argument<String?>("album")
?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) }
addCategory(Intent.CATEGORY_DEFAULT) addCategory(Intent.CATEGORY_DEFAULT)
} }
try { try {
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { if (packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY
) != null
) {
startActivity(intent) startActivity(intent)
result.success(true) result.success(true)
return@setMethodCallHandler return@setMethodCallHandler
} }
} catch (_: Throwable) {} } catch (_: Throwable) {
}
try { try {
intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { if (packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY
) != null
) {
startActivity(intent) startActivity(intent)
result.success(true) result.success(true)
return@setMethodCallHandler return@setMethodCallHandler
} }
} catch (_: Throwable) {} } catch (_: Throwable) {
}
result.success(false) result.success(false)
} }
"setPipAutoEnterEnabled" -> { "setPipAutoEnterEnabled" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val params = PictureInPictureParams.Builder() val params = PictureInPictureParams.Builder()
@@ -111,6 +132,7 @@ class MainActivity : AudioServiceActivity() {
setPictureInPictureParams(params) setPictureInPictureParams(params)
} }
} }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@@ -133,6 +155,7 @@ class MainActivity : AudioServiceActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java))
super.onDestroy() super.onDestroy()
android.os.Process.killProcess(android.os.Process.myPid()) android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0) exitProcess(0)
@@ -143,7 +166,10 @@ class MainActivity : AudioServiceActivity() {
methodChannel.invokeMethod("onUserLeaveHint", null) methodChannel.invokeMethod("onUserLeaveHint", null)
} }
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration?
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
MethodChannel( MethodChannel(
flutterEngine!!.dartExecutor.binaryMessenger, flutterEngine!!.dartExecutor.binaryMessenger,

View File

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

View File

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

View File

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

View File

@@ -6,4 +6,5 @@ Comment[zh_CN]=使用 Flutter 开发的 BiliBili 第三方客户端
Exec=piliplus Exec=piliplus
Icon=piliplus Icon=piliplus
Terminal=false Terminal=false
StartupWMClass=com.example.piliplus
Categories=Video;AudioVideo;Player; Categories=Video;AudioVideo;Player;

View File

@@ -1,11 +0,0 @@
## 1.0.0
### 初始版本
+ 直播、推荐、动态功能
+ 投稿、番剧播放功能
+ 播放器手势支持
+ 画质、音质、解码格式支持
+ 点赞、投币、收藏功能
+ 关注/取关、用户主页功能
+ 评论功能
+ 历史记录、稍后再看功能

View File

@@ -1,7 +0,0 @@
## 1.0.1
### 修复
+ 升级播放器依赖
+ android平台 AV1格式视频支持
+ 视频全屏功能

View File

@@ -1,4 +0,0 @@
## 1.0.10
### 修复
+ 长按倍速抬起后未恢复默认倍速

View File

@@ -1,26 +0,0 @@
## 1.0.11
### 新功能
+ 适配了原生媒体通知栏 @Daydreamer-riri
+ 视频主题图标 @Daydreamer-riri
+ 关闭软件后自动画中画播放
+ UP主分组管理
+ md2样式底栏
+
### 修复
+ 历史记录记忆播放
+ 部分类型视频连播
+ 播放速度选择框不支持返回手势
+ 播放速度选择框不支持返回手势
+ 视频播放速度总是显示1.0X
+ 评论页面计数错误
+ 退出视频还有声音
### 优化
+ 视频加载速度
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,11 +0,0 @@
## 1.0.12
### 修复
+ iOS端视频播放时没有声音
+ 超过6分钟弹幕不显示
+ 视频详情页网络异常
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,22 +0,0 @@
## 1.0.13
### 新功能
+ 视频详情页稍后再看
+ 发送弹幕 感谢@orz12
+ 消息展示
+ up主页显示获赞数
+ up主页显示合集
+ 视频详情页「ai总结」增加开关
### 修复
+ 首页推荐问题(需要重新登录)
+ 长按倍速逻辑
+ 视频详情页网络异常
### 优化
+ 设置面板样式 感谢@GuMengYu @KoolShow
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,28 +0,0 @@
## 1.0.14
圣诞节快乐~ 🎉
大部分内容由@orz12提供,感谢👏
### 修复
+ 全屏弹幕消失
+ iOS全屏/退出全屏视频暂停
+ 个人主页关注状态
+ 视频合集向下滑动UI问题
+ 媒体库滑动底栏不隐藏
+ 个人主页动态加载问题 * 2
+ 未登录状态访问个人主页异常
+ 视频搜索标题特殊字符转义
+ iOS闪退
+ 消息页面夜间模式异常
+ 消息页面含有撤回消息时异常
+ 弹幕速度
### 优化
+ 全屏播放方案优化
+ 弹幕加载逻辑优化
+ 点赞、投币逻辑优化
+ 进度条及播放时间渲染优化
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,22 +0,0 @@
## 1.0.15
元旦快乐~ 🎉
### 功能
+ 转发动态评论展示
+ 推荐、最热、收藏视频增肌日期显示
### 修复
+ 全屏播放相关问题
+ 评论区@用户展示问题
+ 登录状态闪退问题
+ pip意外触发问题
+ 动态页tab切换样式问题
### 优化
+ 首页默认使用web端推荐
+ 取消iOS路由切换效果
+ 视频分享中添加Up主
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,15 +0,0 @@
## 1.0.16
### 功能
+ toast 背景支持透明度调节
### 修复
+ web端推荐未展示【已关注】
+ up主动态页异常
+ 未打开自动播放时,视频详情页异常
+ 视频暂停状态取消自动ip
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,39 +0,0 @@
## 1.0.17
### 功能
+ 视频全屏时隐藏进度条
+ 动态内容增加投稿跳转
+ 未开启自动播放时点击封面播放
+ 弹幕发送标识
+ 定时关闭
+ 推荐视频卡片拉黑up功能
+ 首页tabbar编辑排序
### 修复
+ 连续跳转搜索页未刷新
+ 搜索结果为空时页面异常
+ 评论区链接解析
+ 视频全屏状态栏背景色
+ 私信对话气泡位置
+ 设置up关注分组样式
+ 每次推荐请求数据相同
+ iOS代理网络异常
+ 双击切换播放状态无声
+ 设置自定义倍速白屏
+ 免登录查看1080p
### 优化
+ 首页web端推荐观看数展示
+ 首页web端推荐接口更新
+ 首页样式
+ 搜索页跳转
+ 弹幕资源优化
+ 图片渲染占用内存优化(部分)
+ 两次返回退出应用
+ schame 补充
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,16 +0,0 @@
## 1.0.18
### 功能
### 修复
### 优化
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,15 +0,0 @@
## 1.0.19
### 修复
+ 视频404、评论加载错误
+ bvav转换
### 优化
+ 视频详情页内存占用
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,19 +0,0 @@
## 1.0.2
### 新功能
+ 自动检查更新
+ 封面图片保存
+ 动态跳转番剧
+ 历史记录番剧记忆播放
+ 一键清空稍后再看
### 修复
+ 切换分P cid未切换
+ cookie存储问题
+ 登录/退出登录问题
### 优化
+ 页面空/异常状态样式
+ 退出登录提示
+ 请求节流
+ 全屏播放

View File

@@ -1,19 +0,0 @@
## 1.0.3
建议卸载1.0.2版本,重新安装
### 新功能
+ 底部播放进度条设置
+ 复制图片链接
### 修复
+ 用户数据格式修改
+ video Fit
+ 没有audio 资源的视频异常
+ 评论区域图片无法点击
+ 视频进度条拖动问题
### 优化
+ 页面空/异常状态样式
+ 部分页面样式
+ 图片预览页面样式

View File

@@ -1,21 +0,0 @@
## 1.0.4
### 新功能
+ 热搜刷新
+ 视频搜索排序、筛选
+ app字体大小自定义
+ app主题色自定义
+ 「课堂」类动态渲染
### 修复
+ 搜索词联想richText渲染异常
+ 部分动态点赞异常
+ 默认视频解码格式
+ 搜索页面返回搜索词未清空
+ 动态详情评论加载异常
+ 动态页面下拉刷新数据异常
### 优化
+ 一些样式修改
+ 取消热搜词缓存

View File

@@ -1,30 +0,0 @@
## 1.0.5
主要是bug修复跟一部分小功能弹幕功能需要下一版。
问题反馈请前往QQ频道或提交issues。
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
### 新功能
+ 高帧率支持
+ 默认评论排序设置
+ 默认动态类别设置
+ 动态合集查看
+ 同时观看人数
+ iOS路由切换效果
### 修复
+ 收藏夹翻页
+ 首页搜索框频繁点击消失
+ 评论排序切换空白
+ 快速返回首页
+ 重复进入个人中心页面数据未刷新
+ 动态goods数据异常
+ 大会员切换番剧
+ 高画质codes匹配
### 优化
+ 倍速选择
+ 播放器亮度记忆
+ 下载对应版本apk

View File

@@ -1,34 +0,0 @@
## 1.0.6
问题反馈、功能建议请查看「关于」页面。
### 新功能
+ 首页单列布局
+ 首页推荐展示播放量、弹幕数
+ 简单弹幕功能实现(持续开发中...
+ 评论区搜索关键词开关 issues#46
+ 热搜榜隐藏功能 issues#35
+ 自动全屏 issues#37
+ 快速收藏功能
+ 双击快进/快退开关
+ 评论链接跳转视频
+ 支持移除单个稍后再看
+ app scheme外链跳转
### 修复
+ 杜比、无损音频切换
+ 收藏夹展示 issues#42
+ 搜索建议次 issues#47
### 优化
+ 倍速选择优化
+ 导航条沉浸
+ 取消Hero动画
+ 视频锁定逻辑
+ 登录逻辑优化
+ 图片预览样式
+ +评论区用户点击范围
+ 关注、粉丝页面优化
+ 关闭自动播放时播放器初始化逻辑

View File

@@ -1,22 +0,0 @@
## 1.0.7
默认倍速、直播弹幕、专栏等功能开发中
### 新功能
+ 弹幕设置、屏蔽功能
+ 不是很完美的后台播放功能
+ 不是很完美的画中画(pip)功能Android端
### 修复
+ 动态页面加载异常
+ 网络异常时页面空白
+ 竖屏全屏状态栏问题
+ iOS端代理请求异常
### 优化
+ 图片预览
+ 全屏播放时自动旋转
+ 转发内容增加视频标题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,24 +0,0 @@
## 1.0.8
直播弹幕、循环播放等功能开发中
### 新功能
+ 用户拉黑功能
+ gif图片保存
+ 删除已看历史记录
### 修复
+ 弹幕数量较少
+ 弹幕屏蔽设置自动记忆
+ 动态页面渲染
+ 用户主页数据错乱
+ 大家都在搜空白
+ 默认自动全屏,顶部操作栏丢失
### 优化
+ 全屏状态栏区域显示优化
+ 图片保存至PiliPala文件夹
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,28 +0,0 @@
## 1.0.9
### 新功能
+ 自定义倍速、默认倍速
+ 历史记录搜索
+ 收藏夹搜索
+ 历史记录多选删除
+ 视频循环播放
+ 免登录看1080P
+ 评论区视频链接跳转
+ up主分组
+ up主投稿搜索
### 修复
+ 搜索视频标题乱码
+ 屏幕帧率
+ 动态页面渲染
### 优化
+ 快进手势
+ 视频简介链接匹配
+ 视频全屏时安全区域
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@@ -1,10 +0,0 @@
PiliPlus is a third-party Bilibili client developed in Flutter,
fork from PiliPalaX (https://github.com/orz12/PiliPalaX).
Top Features:
* List of recommended videos
* List of hottest videos
* Popular live streams
* List of bangumis
* Block videos from blacklisted users

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1 +0,0 @@
A third-party Bilibili client developed in Flutter

View File

@@ -1 +0,0 @@
PiliPlus

View File

@@ -1,10 +0,0 @@
PiliPlus 是使用 Flutter 开发的 BiliBili 第三方客户端,
是由PiliPalaX仓库fork并进行了差异化开发的版本
主要功能:
* 推荐视频列表
* 最热视频列表
* 热门直播
* 番剧列表
* 屏蔽黑名单内用户视频

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1 +0,0 @@
使用 Flutter 开发的 BiliBili 第三方客户端

View File

@@ -1 +0,0 @@
PiliPlus

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>11.0</string> <string>13.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,83 +1,135 @@
PODS: PODS:
- appscheme (1.0.4): - app_links (7.0.0):
- Flutter - Flutter
- audio_service (0.0.1): - audio_service (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- audio_session (0.0.1): - audio_session (0.0.1):
- Flutter - Flutter
- auto_orientation (0.0.1): - auto_orientation (0.0.1):
- Flutter - Flutter
- chat_bottom_container (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_mailer (0.0.1): - flutter_mailer (0.0.1):
- Flutter - Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_volume_controller (0.0.1): - flutter_volume_controller (0.0.1):
- Flutter - Flutter
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
- Flutter - Flutter
- Toast - gt3_flutter_plugin (0.0.9):
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- gt3_flutter_plugin (0.0.8):
- Flutter - Flutter
- GT3Captcha-iOS - GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3) - GT3Captcha-iOS (0.15.8.3)
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.8.0)
- image_picker_ios (0.0.1):
- Flutter
- live_photo_maker (0.0.3):
- Flutter
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_native_event_loop (1.0.0): - media_kit_native_event_loop (1.0.0):
- Flutter - Flutter
- media_kit_video (0.0.1): - media_kit_video (0.0.1):
- Flutter - Flutter
- OrderedSet (6.0.3)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.1.1): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1): - saver_gallery (0.0.1):
- Flutter - Flutter
- screen_brightness_ios (0.1.0): - screen_brightness_ios (0.1.0):
- Flutter - Flutter
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- sqflite (0.0.3): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FlutterMacOS
- status_bar_control (3.2.1): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- system_proxy (0.0.1): - FlutterMacOS
- Flutter - SwiftyGif (5.4.5)
- Toast (4.1.0) - TOCropViewController (2.8.0)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1):
- Flutter
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- Flutter - Flutter
- webview_cookie_manager (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- chat_bottom_container (from `.symlinks/plugins/chat_bottom_container/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- live_photo_maker (from `.symlinks/plugins/live_photo_maker/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@@ -87,45 +139,58 @@ DEPENDENCIES:
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`) - saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_cookie_manager (from `.symlinks/plugins/webview_cookie_manager/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - DKImagePickerController
- DKPhotoGallery
- GT3Captcha-iOS - GT3Captcha-iOS
- ReachabilitySwift - OrderedSet
- Toast - SDWebImage
- SwiftyGif
- TOCropViewController
EXTERNAL SOURCES: EXTERNAL SOURCES:
appscheme: app_links:
:path: ".symlinks/plugins/appscheme/ios" :path: ".symlinks/plugins/app_links/ios"
audio_service: audio_service:
:path: ".symlinks/plugins/audio_service/ios" :path: ".symlinks/plugins/audio_service/darwin"
audio_session: audio_session:
:path: ".symlinks/plugins/audio_session/ios" :path: ".symlinks/plugins/audio_session/ios"
auto_orientation: auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios" :path: ".symlinks/plugins/auto_orientation/ios"
chat_bottom_container:
:path: ".symlinks/plugins/chat_bottom_container/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_mailer: flutter_mailer:
:path: ".symlinks/plugins/flutter_mailer/ios" :path: ".symlinks/plugins/flutter_mailer/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_volume_controller: flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios" :path: ".symlinks/plugins/flutter_volume_controller/ios"
fluttertoast: fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
gt3_flutter_plugin: gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios" :path: ".symlinks/plugins/gt3_flutter_plugin/ios"
image_cropper:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
live_photo_maker:
:path: ".symlinks/plugins/live_photo_maker/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop: media_kit_native_event_loop:
@@ -144,57 +209,55 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/screen_brightness_ios/ios" :path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
sqflite: shared_preferences_foundation:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
status_bar_control: sqflite_darwin:
:path: ".symlinks/plugins/status_bar_control/ios" :path: ".symlinks/plugins/sqflite_darwin/darwin"
system_proxy:
:path: ".symlinks/plugins/system_proxy/ios"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"
wakelock_plus: wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
webview_cookie_manager:
:path: ".symlinks/plugins/webview_cookie_manager/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 app_links: 6d01271b3907b0ee7325c5297c75d697c4226c4d
audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910
audio_session: 4f3e461722055d21515cf3261b64c973c062f345 audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a chat_bottom_container: d8b077152c91b0ab90001e900748ea50353a5520
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
image_cropper: b8ef14d3fcff4040b0f9da2ca28d98219a5cba0e
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
live_photo_maker: 7d57bfc70a120b4673c10871f354f4b1b6fde5fd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 6a6f7794b67f07c4f1e24f6374b2d8ad367ffb39
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
Toast: ec33c32b8688982cecc6348adeae667c1b9938da SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be PODFILE CHECKSUM: f62db4fb414ebdecb264109948f76dfef35fdc3d
COCOAPODS: 1.14.3 COCOAPODS: 1.16.2

View File

@@ -156,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -48,6 +48,7 @@
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">

View File

@@ -1,14 +1,17 @@
import UIKit
import Flutter import Flutter
import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
application.applicationSupportsShakeToEdit = false // Disable shake to undo application.applicationSupportsShakeToEdit = false // Disable shake to undo
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }

View File

@@ -2,6 +2,27 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>FlutterDeepLinkingEnabled</key> <key>FlutterDeepLinkingEnabled</key>
<false/> <false/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>

View File

@@ -1,4 +1,4 @@
class BuildConfig { abstract final class BuildConfig {
static const int versionCode = int.fromEnvironment( static const int versionCode = int.fromEnvironment(
'pili.code', 'pili.code',
defaultValue: 1, defaultValue: 1,

View File

@@ -1,18 +1,23 @@
import 'package:PiliPlus/http/constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class StyleString { abstract final class StyleString {
static const double cardSpace = 8; static const double cardSpace = 8;
static const double safeSpace = 12; static const double safeSpace = 12;
static const BorderRadius mdRadius = BorderRadius.all(imgRadius); static const BorderRadius mdRadius = BorderRadius.all(imgRadius);
static const Radius imgRadius = Radius.circular(10); static const Radius imgRadius = Radius.circular(10);
static const double aspectRatio = 16 / 10; static const double aspectRatio = 16 / 10;
static const double aspectRatio16x9 = 16 / 9;
static const double imgMaxRatio = 22 / 9;
static const bottomSheetRadius = BorderRadius.vertical( static const bottomSheetRadius = BorderRadius.vertical(
top: Radius.circular(18), top: Radius.circular(18),
); );
static const dialogFixedConstraints = BoxConstraints(
minWidth: 420,
maxWidth: 420,
);
} }
class Constants { abstract final class Constants {
static const appName = 'PiliPlus'; static const appName = 'PiliPlus';
static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus'; static const sourceCodeUrl = 'https://github.com/bggRGjQaUbCoE/PiliPlus';
@@ -21,9 +26,9 @@ class Constants {
static const String appKey = 'dfca71928277209b'; static const String appKey = 'dfca71928277209b';
// 59b43e04ad6965f34319062b478f83dd TV端 // 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e'; static const String appSec = 'b5475a8825547a4fc26c7d518eaaa02e';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; // static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi = // static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png'; // 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
static const String traceId = static const String traceId =
'11111111111111111111111111111111:1111111111111111:0:0'; '11111111111111111111111111111111:1111111111111111:0:0';
@@ -41,9 +46,7 @@ class Constants {
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}'; '{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
static const baseHeaders = { static const baseHeaders = {
'connection': 'keep-alive', // 'referer': HttpString.baseUrl,
'accept-encoding': 'br,gzip',
'referer': HttpString.baseUrl,
'env': 'prod', 'env': 'prod',
'app-key': 'android64', 'app-key': 'android64',
'x-bili-aurora-zone': 'sh001', 'x-bili-aurora-zone': 'sh001',

View File

@@ -9,6 +9,13 @@ class DynamicCardSkeleton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final color = theme.colorScheme.onInverseSurface; final color = theme.colorScheme.onInverseSurface;
final buttonStyle = TextButton.styleFrom(
tapTargetSize: .padded,
padding: const .symmetric(horizontal: 15),
foregroundColor: theme.colorScheme.outline.withValues(
alpha: 0.2,
),
);
return Skeleton( return Skeleton(
child: Container( child: Container(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12), padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
@@ -86,29 +93,19 @@ class DynamicCardSkeleton extends StatelessWidget {
if (GlobalData().dynamicsWaterfallFlow) const Spacer(), if (GlobalData().dynamicsWaterfallFlow) const Spacer(),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: const ['转发', '评论', '点赞']
for (var i = 0; i < 3; i++) .map(
TextButton.icon( (e) => TextButton.icon(
onPressed: () {}, onPressed: () {},
icon: const Icon( icon: const Icon(
Icons.radio_button_unchecked_outlined, Icons.radio_button_unchecked_outlined,
size: 20, size: 20,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: theme.colorScheme.outline.withValues(
alpha: 0.2,
), ),
style: buttonStyle,
label: Text(e),
), ),
label: Text( )
i == 0 .toList(),
? '转发'
: i == 1
? '评论'
: '点赞',
),
),
],
), ),
], ],
), ),

View File

@@ -11,7 +11,7 @@ class Skeleton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.surface.withAlpha(10); final color = Theme.of(context).colorScheme.surface.withAlpha(10);
var shimmerGradient = LinearGradient( final shimmerGradient = LinearGradient(
colors: [ colors: [
Colors.transparent, Colors.transparent,
color, color,
@@ -62,7 +62,6 @@ class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_shimmerController = AnimationController.unbounded(vsync: this) _shimmerController = AnimationController.unbounded(vsync: this)
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000)); ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
} }

View File

@@ -7,19 +7,21 @@ class MultiSelectAppBarWidget extends StatelessWidget
final MultiSelectBase ctr; final MultiSelectBase ctr;
final bool? visible; final bool? visible;
final AppBar child; final AppBar child;
final List<Widget>? children; final List<Widget>? actions;
const MultiSelectAppBarWidget({ const MultiSelectAppBarWidget({
super.key, super.key,
required this.ctr, required this.ctr,
this.visible, this.visible,
this.children, this.actions,
required this.child, required this.child,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (visible ?? ctr.enableMultiSelect.value) { if (visible ?? ctr.enableMultiSelect.value) {
final style = TextButton.styleFrom(visualDensity: VisualDensity.compact);
final colorScheme = ColorScheme.of(context);
return AppBar( return AppBar(
bottom: child.bottom, bottom: child.bottom,
leading: IconButton( leading: IconButton(
@@ -30,21 +32,22 @@ class MultiSelectAppBarWidget extends StatelessWidget
title: Obx(() => Text('已选: ${ctr.checkedCount}')), title: Obx(() => Text('已选: ${ctr.checkedCount}')),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom( style: style,
visualDensity: VisualDensity.compact,
),
onPressed: () => ctr.handleSelect(checked: true), onPressed: () => ctr.handleSelect(checked: true),
child: const Text('全选'), child: const Text('全选'),
), ),
...?children, ...?actions,
TextButton( TextButton(
style: TextButton.styleFrom( style: style,
visualDensity: VisualDensity.compact, onPressed: () {
), if (ctr.checkedCount == 0) {
onPressed: ctr.onRemove, return;
}
ctr.onRemove();
},
child: Text( child: Text(
'移除', '移除',
style: TextStyle(color: Get.theme.colorScheme.error), style: TextStyle(color: colorScheme.error),
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),

View File

@@ -0,0 +1,56 @@
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/model_owner.dart';
import 'package:flutter/material.dart';
Widget avatars({
required ColorScheme colorScheme,
required Iterable<Owner> users,
}) {
const gap = 6.0;
const size = 22.0;
const padding = 0.8;
const offset = size - gap;
const imgSize = size - 2 * padding;
if (users.length == 1) {
return NetworkImgLayer(
src: users.first.face,
width: imgSize,
height: imgSize,
type: .avatar,
);
} else {
final decoration = BoxDecoration(
shape: .circle,
border: Border.all(color: colorScheme.surface),
);
return SizedBox(
height: size,
width: offset * users.length + gap,
child: Stack(
clipBehavior: .none,
children: users.indexed
.map(
(e) => Positioned(
top: 0,
bottom: 0,
width: size,
left: e.$1 * offset,
child: DecoratedBox(
decoration: decoration,
child: Padding(
padding: const .all(padding),
child: NetworkImgLayer(
src: e.$2.face,
width: imgSize,
height: imgSize,
type: .avatar,
),
),
),
),
)
.toList(),
),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter/gestures.dart' show kBackMouseButton;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show KeyDownEvent;
class BackDetector extends StatelessWidget {
const BackDetector({
super.key,
required this.onBack,
required this.child,
});
final Widget child;
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Focus(
canRequestFocus: false,
onKeyEvent: _onKeyEvent,
child: Listener(
behavior: .translucent,
onPointerDown: _onPointerDown,
child: child,
),
);
}
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
if (event.logicalKey == .escape && event is KeyDownEvent) {
onBack();
return .handled;
}
return .ignored;
}
void _onPointerDown(PointerDownEvent event) {
if (event.buttons == kBackMouseButton) {
onBack();
}
}
}

View File

@@ -1,7 +1,7 @@
import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PBadge extends StatelessWidget { class PBadge extends StatelessWidget {
final String? text; final String? text;
@@ -59,7 +59,7 @@ class PBadge extends StatelessWidget {
bgColor = Colors.black45; bgColor = Colors.black45;
color = Colors.white; color = Colors.white;
case PBadgeType.error: case PBadgeType.error:
if (Get.isDarkMode) { if (theme.isDark) {
bgColor = theme.errorContainer; bgColor = theme.errorContainer;
color = theme.onErrorContainer; color = theme.onErrorContainer;
} else { } else {

View File

@@ -1,39 +1,32 @@
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class ColorPalette extends StatelessWidget { class ColorPalette extends StatelessWidget {
final Color color; final ColorScheme colorScheme;
final bool selected; final bool selected;
final bool showBgColor;
const ColorPalette({ const ColorPalette({
super.key, super.key,
required this.color, required this.colorScheme,
required this.selected, required this.selected,
this.showBgColor = true,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final primary = colorScheme.primary;
final Hct hct = Hct.fromInt(color.toARGB32()); final tertiary = colorScheme.tertiary;
final primary = Color(Hct.from(hct.hue, 20.0, 90.0).toInt()); final primaryContainer = colorScheme.primaryContainer;
final tertiary = Color(Hct.from(hct.hue + 50, 20.0, 85.0).toInt());
final primaryContainer = Color(Hct.from(hct.hue, 30.0, 50.0).toInt());
Widget coloredBox(Color color) => Expanded(
child: ColoredBox(
color: color,
child: const SizedBox.expand(),
),
);
Widget child = ClipOval( Widget child = ClipOval(
child: Column( child: Column(
children: [ children: [
coloredBox(primary), _coloredBox(primary),
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
coloredBox(tertiary), _coloredBox(tertiary),
coloredBox(primaryContainer), _coloredBox(primaryContainer),
], ],
), ),
), ),
@@ -50,7 +43,7 @@ class ColorPalette extends StatelessWidget {
width: 23, width: 23,
height: 23, height: 23,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(Hct.from(hct.hue, 30.0, 40.0).toInt()), color: colorScheme.surfaceContainer,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
@@ -62,15 +55,25 @@ class ColorPalette extends StatelessWidget {
], ],
); );
} }
return Container( if (showBgColor) {
width: 50, return Container(
height: 50, width: 50,
padding: const EdgeInsets.all(6), height: 50,
decoration: BoxDecoration( padding: const EdgeInsets.all(6),
color: theme.colorScheme.onInverseSurface, decoration: BoxDecoration(
borderRadius: StyleString.mdRadius, color: colorScheme.onInverseSurface,
), borderRadius: StyleString.mdRadius,
child: child, ),
); child: child,
);
}
return child;
} }
static Widget _coloredBox(Color color) => Expanded(
child: ColoredBox(
color: color,
child: const SizedBox.expand(),
),
);
} }

View File

@@ -0,0 +1,158 @@
/*
* This file is part of PiliPlus
*
* PiliPlus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PiliPlus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PiliPlus. If not, see <https://www.gnu.org/licenses/>.
*/
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
class CroppedImage extends LeafRenderObjectWidget {
const CroppedImage({
super.key,
required this.size,
required this.image,
required this.srcRect,
required this.dstRect,
required this.rrect,
required this.imgPaint,
required this.borderPaint,
});
final Size size;
final ui.Image image;
final Rect srcRect;
final Rect dstRect;
final RRect rrect;
final Paint imgPaint;
final Paint borderPaint;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCroppedImage(
preferredSize: size,
image: image,
srcRect: srcRect,
dstRect: dstRect,
rrect: rrect,
imgPaint: imgPaint,
borderPaint: borderPaint,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderCroppedImage renderObject,
) {
renderObject
..preferredSize = size
..image = image
..srcRect = srcRect
..dstRect = dstRect
..rrect = rrect
..imgPaint = imgPaint
..borderPaint = borderPaint;
}
}
class RenderCroppedImage extends RenderBox {
RenderCroppedImage({
required Size preferredSize,
required ui.Image image,
required Rect srcRect,
required Rect dstRect,
required RRect rrect,
required Paint imgPaint,
required Paint borderPaint,
}) : _preferredSize = preferredSize,
_image = image,
_srcRect = srcRect,
_dstRect = dstRect,
_rrect = rrect,
_imgPaint = imgPaint,
_borderPaint = borderPaint;
Size _preferredSize;
Size get preferredSize => _preferredSize;
set preferredSize(Size value) {
if (_preferredSize == value) return;
_preferredSize = value;
markNeedsLayout();
}
ui.Image _image;
ui.Image get image => _image;
set image(ui.Image value) {
if (_image == value) return;
_image = value;
markNeedsPaint();
}
Rect _srcRect;
Rect get srcRect => _srcRect;
set srcRect(Rect value) {
if (_srcRect == value) return;
_srcRect = value;
markNeedsPaint();
}
Rect _dstRect;
Rect get dstRect => _dstRect;
set dstRect(Rect value) {
if (_dstRect == value) return;
_dstRect = value;
markNeedsPaint();
}
RRect _rrect;
RRect get rrect => _rrect;
set rrect(RRect value) {
if (_rrect == value) return;
_rrect = value;
markNeedsPaint();
}
Paint _imgPaint;
Paint get imgPaint => _imgPaint;
set imgPaint(Paint value) {
if (_imgPaint == value) return;
_imgPaint = value;
markNeedsPaint();
}
Paint _borderPaint;
Paint get borderPaint => _borderPaint;
set borderPaint(Paint value) {
if (_borderPaint == value) return;
_borderPaint = value;
markNeedsPaint();
}
@override
void performLayout() {
size = constraints.constrain(_preferredSize);
}
@override
void paint(PaintingContext context, Offset offset) {
context.canvas
..drawImageRect(image, srcRect, dstRect, _imgPaint)
..drawRRect(rrect, _borderPaint);
}
@override
bool get isRepaintBoundary => true;
}

View File

@@ -0,0 +1,113 @@
import 'dart:math' show pi;
import 'package:flutter/widgets.dart';
class Arc extends LeafRenderObjectWidget {
const Arc({
super.key,
required this.size,
required this.color,
required this.progress,
this.strokeWidth = 2,
});
final double size;
final Color color;
final double progress;
final double strokeWidth;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderArc(
preferredSize: size,
color: color,
progress: progress,
strokeWidth: strokeWidth,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderArc renderObject,
) {
renderObject
..preferredSize = size
..color = color
..progress = progress
..strokeWidth = strokeWidth;
}
}
class RenderArc extends RenderBox {
RenderArc({
required double preferredSize,
required Color color,
required double progress,
required double strokeWidth,
}) : _preferredSize = preferredSize,
_color = color,
_progress = progress,
_strokeWidth = strokeWidth;
Color _color;
Color get color => _color;
set color(Color value) {
if (_color == value) return;
_color = value;
markNeedsPaint();
}
double _progress;
double get progress => _progress;
set progress(double value) {
if (_progress == value) return;
_progress = value;
markNeedsPaint();
}
double _strokeWidth;
double get strokeWidth => _strokeWidth;
set strokeWidth(double value) {
if (_strokeWidth == value) return;
_strokeWidth = value;
markNeedsPaint();
}
double _preferredSize;
double get preferredSize => _preferredSize;
set preferredSize(double value) {
if (_preferredSize == value) return;
_preferredSize = value;
markNeedsLayout();
}
@override
void performLayout() {
size = constraints.constrainDimensions(_preferredSize, _preferredSize);
}
@override
void paint(PaintingContext context, Offset offset) {
if (progress == 0) {
return;
}
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke;
final radius = size.width / 2;
final rect = Rect.fromCircle(
center: Offset(radius, radius),
radius: radius,
);
const startAngle = -pi / 2;
context.canvas.drawArc(rect, startAngle, progress * 2 * pi, false, paint);
}
@override
bool get isRepaintBoundary => true;
}

View File

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

View File

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

View File

@@ -50,6 +50,7 @@ class LoadingWidget extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(15)), borderRadius: const BorderRadius.all(Radius.circular(15)),
), ),
child: Column( child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
//loading animation //loading animation
@@ -59,10 +60,7 @@ class LoadingWidget extends StatelessWidget {
), ),
//msg //msg
Container( Text(msg, style: TextStyle(color: onSurfaceVariant)),
margin: const EdgeInsets.only(top: 20),
child: Text(msg, style: TextStyle(color: onSurfaceVariant)),
),
], ],
), ),
); );

View File

@@ -1,135 +1,63 @@
import 'dart:math' as math; import 'package:PiliPlus/utils/platform_utils.dart';
import 'dart:ui' show clampDouble;
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'
show
enum TooltipType { top, right } ContainerRenderObjectMixin,
RenderBoxContainerDefaultsMixin,
MultiChildLayoutParentData;
import 'package:flutter/widgets.dart';
class CustomTooltip extends StatefulWidget { class CustomTooltip extends StatefulWidget {
const CustomTooltip({ const CustomTooltip({
super.key, super.key,
this.type = TooltipType.top,
required this.overlayWidget, required this.overlayWidget,
required this.child, required this.child,
this.indicator, required this.indicator,
}); });
final TooltipType type;
final Widget child; final Widget child;
final Widget Function() overlayWidget; final ValueGetter<Widget> overlayWidget;
final Widget Function()? indicator; final ValueGetter<Widget> indicator;
static final List<CustomTooltipState> _openedTooltips =
<CustomTooltipState>[];
static bool dismissAllToolTips() {
if (_openedTooltips.isNotEmpty) {
final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
for (final CustomTooltipState state in openedTooltips) {
assert(state.mounted);
state._scheduleDismissTooltip();
}
return true;
}
return false;
}
@override @override
State<CustomTooltip> createState() => CustomTooltipState(); State<CustomTooltip> createState() => _CustomTooltipState();
} }
class CustomTooltipState extends State<CustomTooltip> class _CustomTooltipState extends State<CustomTooltip> {
with SingleTickerProviderStateMixin {
static const Duration _fadeInDuration = Duration(milliseconds: 150);
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
final OverlayPortalController _overlayController = OverlayPortalController(); final OverlayPortalController _overlayController = OverlayPortalController();
AnimationController? _backingController;
AnimationController get _controller {
return _backingController ??= AnimationController(
duration: _fadeInDuration,
reverseDuration: _fadeOutDuration,
vsync: this,
)..addStatusListener(_handleStatusChanged);
}
CurvedAnimation? _backingOverlayAnimation;
CurvedAnimation get _overlayAnimation {
return _backingOverlayAnimation ??= CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
);
}
LongPressGestureRecognizer? _longPressRecognizer; LongPressGestureRecognizer? _longPressRecognizer;
LongPressGestureRecognizer get longPressRecognizer =>
AnimationStatus _animationStatus = AnimationStatus.dismissed; _longPressRecognizer ??= LongPressGestureRecognizer()
void _handleStatusChanged(AnimationStatus status) { ..onLongPress = _scheduleShowTooltip;
assert(mounted);
switch ((_animationStatus.isDismissed, status.isDismissed)) {
case (false, true):
CustomTooltip._openedTooltips.remove(this);
_overlayController.hide();
case (true, false):
_overlayController.show();
CustomTooltip._openedTooltips.add(this);
case (true, true) || (false, false):
break;
}
_animationStatus = status;
}
void _scheduleShowTooltip() { void _scheduleShowTooltip() {
_controller.forward(); _overlayController.show();
} }
void _scheduleDismissTooltip() { void _scheduleDismissTooltip() {
_controller.reverse(); _overlayController.hide();
} }
void _handlePointerDown(PointerDownEvent event) { void _handlePointerDown(PointerDownEvent event) {
assert(mounted); assert(mounted);
const Set<PointerDeviceKind> triggerModeDeviceKinds = <PointerDeviceKind>{ longPressRecognizer.addPointer(event);
PointerDeviceKind.invertedStylus,
PointerDeviceKind.stylus,
PointerDeviceKind.touch,
PointerDeviceKind.unknown,
PointerDeviceKind.trackpad,
};
_longPressRecognizer ??= LongPressGestureRecognizer(
debugOwner: this,
supportedDevices: triggerModeDeviceKinds,
);
_longPressRecognizer!
..onLongPress = _scheduleShowTooltip
..addPointer(event);
} }
Widget _buildCustomTooltipOverlay(BuildContext context) { Widget _buildCustomTooltipOverlay(
final OverlayState overlayState = Overlay.of( BuildContext context,
context, OverlayChildLayoutInfo layoutInfo,
debugRequiredFor: widget, ) {
final target = MatrixUtils.transformPoint(
layoutInfo.childPaintTransform,
layoutInfo.childSize.topCenter(Offset.zero),
); );
final RenderBox box = this.context.findRenderObject()! as RenderBox;
final Offset target = box.localToGlobal(
box.size.center(Offset.zero),
ancestor: overlayState.context.findRenderObject(),
);
final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay( final _CustomTooltipOverlay overlayChild = _CustomTooltipOverlay(
verticalOffset: box.size.height / 2,
horizontslOffset: box.size.width / 2,
type: widget.type,
animation: _overlayAnimation,
target: target, target: target,
onDismiss: _scheduleDismissTooltip, onDismiss: _scheduleDismissTooltip,
overlayWidget: widget.overlayWidget, overlayWidget: widget.overlayWidget,
indicator: widget.indicator, indicator: widget.indicator,
); );
return SelectionContainer.maybeOf(context) == null return SelectionContainer.maybeOf(context) == null
? overlayChild ? overlayChild
: SelectionContainer.disabled(child: overlayChild); : SelectionContainer.disabled(child: overlayChild);
@@ -138,11 +66,10 @@ class CustomTooltipState extends State<CustomTooltip>
@protected @protected
@override @override
void dispose() { void dispose() {
CustomTooltip._openedTooltips.remove(this); _longPressRecognizer
_longPressRecognizer?.onLongPressCancel = null; ?..onLongPress = null
_longPressRecognizer?.dispose(); ..dispose();
_backingController?.dispose(); _longPressRecognizer = null;
_backingOverlayAnimation?.dispose();
super.dispose(); super.dispose();
} }
@@ -150,7 +77,7 @@ class CustomTooltipState extends State<CustomTooltip>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget result; Widget result;
if (Utils.isMobile) { if (PlatformUtils.isMobile) {
result = Listener( result = Listener(
onPointerDown: _handlePointerDown, onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@@ -164,7 +91,7 @@ class CustomTooltipState extends State<CustomTooltip>
child: widget.child, child: widget.child,
); );
} }
return OverlayPortal( return OverlayPortal.overlayChildLayoutBuilder(
controller: _overlayController, controller: _overlayController,
overlayChildBuilder: _buildCustomTooltipOverlay, overlayChildBuilder: _buildCustomTooltipOverlay,
child: result, child: result,
@@ -174,232 +101,241 @@ class CustomTooltipState extends State<CustomTooltip>
class _CustomTooltipOverlay extends StatelessWidget { class _CustomTooltipOverlay extends StatelessWidget {
const _CustomTooltipOverlay({ const _CustomTooltipOverlay({
required this.verticalOffset,
required this.horizontslOffset,
required this.type,
required this.animation,
required this.target, required this.target,
required this.onDismiss, required this.onDismiss,
required this.overlayWidget, required this.overlayWidget,
this.indicator, required this.indicator,
}); });
final double verticalOffset;
final double horizontslOffset;
final TooltipType type;
final Animation<double> animation;
final Offset target; final Offset target;
final VoidCallback onDismiss; final VoidCallback onDismiss;
final Widget Function() overlayWidget; final ValueGetter<Widget> overlayWidget;
final Widget Function()? indicator; final ValueGetter<Widget> indicator;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = CustomMultiChildLayout( return _ToolTip(
delegate: _CustomMultiTooltipPositionDelegate( target: target,
type: type, preferBelow: false,
target: target, onTap: PlatformUtils.isMobile ? onDismiss : null,
verticalOffset: verticalOffset,
horizontslOffset: horizontslOffset,
preferBelow: false,
),
children: [ children: [
LayoutId( indicator(),
id: 'overlay', overlayWidget(),
child: overlayWidget(),
),
if (indicator != null)
LayoutId(
id: 'indicator',
child: indicator!(),
),
], ],
); );
if (Utils.isMobile) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDismiss,
child: child,
);
}
return child;
} }
} }
class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate { class _ToolTip extends MultiChildRenderObjectWidget {
_CustomMultiTooltipPositionDelegate({ const _ToolTip({
required this.type, super.children,
this.onTap,
required this.target, required this.target,
required this.verticalOffset,
required this.horizontslOffset,
required this.preferBelow, required this.preferBelow,
}); });
final TooltipType type; final VoidCallback? onTap;
final Offset target; final Offset target;
final double verticalOffset;
final double horizontslOffset;
final bool preferBelow; final bool preferBelow;
@override @override
void performLayout(Size size) { RenderObject createRenderObject(BuildContext context) {
switch (type) { return _RenderToolTip(
case TooltipType.top: onTap: onTap,
Size? indicatorSize; target: target,
if (hasChild('indicator')) { preferBelow: preferBelow,
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size)); );
} }
if (hasChild('overlay')) { @override
final overlaySize = layoutChild( void updateRenderObject(BuildContext context, _RenderToolTip renderObject) {
'overlay', renderObject
BoxConstraints.loose(size), ..onTap = onTap
); ..target = target
Offset offset = positionDependentBox( ..preferBelow = preferBelow;
type: type, }
size: size, }
childSize: overlaySize,
target: target,
verticalOffset: verticalOffset,
horizontslOffset: horizontslOffset,
preferBelow: preferBelow,
);
if (indicatorSize != null) {
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
positionChild(
'indicator',
Offset(
target.dx - indicatorSize.width / 2,
offset.dy + overlaySize.height - 1,
),
);
}
positionChild('overlay', offset);
}
case TooltipType.right:
Size? indicatorSize;
if (hasChild('indicator')) {
indicatorSize = layoutChild('indicator', BoxConstraints.loose(size));
}
if (hasChild('overlay')) { class _RenderToolTip extends RenderBox
final overlaySize = layoutChild( with
'overlay', ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
BoxConstraints.loose(size), RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
); _RenderToolTip({
Offset offset = positionDependentBox( VoidCallback? onTap,
type: type, required Offset target,
size: size, required bool preferBelow,
childSize: overlaySize, }) : _target = target,
target: target, _preferBelow = preferBelow,
verticalOffset: verticalOffset, _hitTestSelf = onTap != null {
horizontslOffset: horizontslOffset, if (onTap != null) {
preferBelow: preferBelow, _tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap;
); }
if (indicatorSize != null) { }
offset = Offset(offset.dx + indicatorSize.height - 1, offset.dy);
positionChild( TapGestureRecognizer? _tapGestureRecognizer;
'indicator',
Offset( set onTap(VoidCallback? value) {
offset.dx - indicatorSize.width + 1, _tapGestureRecognizer?.onTap = value;
target.dy - indicatorSize.height / 2, }
),
); @override
} void dispose() {
positionChild('overlay', offset); _tapGestureRecognizer
} ?..onTap = null
..dispose();
_tapGestureRecognizer = null;
super.dispose();
}
final bool _hitTestSelf;
@override
bool hitTestSelf(Offset position) => _hitTestSelf;
@override
void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry) {
if (event is PointerDownEvent) {
_tapGestureRecognizer?.addPointer(event);
}
}
Offset _target;
Offset get target => _target;
set target(Offset value) {
if (_target == value) return;
_target = value;
markNeedsPaint();
}
bool _preferBelow;
bool get preferBelow => _preferBelow;
set preferBelow(bool value) {
if (_preferBelow == value) return;
_preferBelow = value;
markNeedsPaint();
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! MultiChildLayoutParentData) {
child.parentData = MultiChildLayoutParentData();
} }
} }
@override @override
bool shouldRelayout(_CustomMultiTooltipPositionDelegate oldDelegate) { void performLayout() {
return target != oldDelegate.target || size = constraints.constrain(constraints.biggest);
verticalOffset != oldDelegate.verticalOffset ||
preferBelow != oldDelegate.preferBelow; final c = BoxConstraints.loose(size);
RenderBox indicator = firstChild!..layout(c, parentUsesSize: true);
RenderBox overlay = lastChild!..layout(c, parentUsesSize: true);
final indicatorSize = indicator.size;
final overlaySize = overlay.size;
final indicatorParentData =
indicator.parentData as MultiChildLayoutParentData;
final overlayParentData = overlay.parentData as MultiChildLayoutParentData;
Offset offset = positionDependentBox(
size: size,
childSize: overlaySize,
target: target,
preferBelow: preferBelow,
);
offset = Offset(offset.dx, offset.dy - indicatorSize.height + 1);
overlayParentData.offset = offset;
indicatorParentData.offset = Offset(
target.dx - indicatorSize.width / 2,
offset.dy + overlaySize.height - 1,
);
}
@override
void paint(PaintingContext context, Offset offset) {
RenderBox? child = firstChild;
while (child != null) {
final childParentData = child.parentData as MultiChildLayoutParentData;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
@override
bool get isRepaintBoundary => true;
}
class Triangle extends LeafRenderObjectWidget {
const Triangle({
super.key,
required this.color,
required this.size,
});
final Color color;
final Size size;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderTriangle(
color: color,
preferredSize: size,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderTriangle renderObject,
) {
renderObject
..color = color
..preferredSize = size;
} }
} }
class TrianglePainter extends CustomPainter { class RenderTriangle extends RenderBox {
TrianglePainter(this.color, {this.type = TooltipType.top}); RenderTriangle({
final TooltipType type; required Color color,
final Color color; required Size preferredSize,
}) : _color = color,
_preferredSize = preferredSize;
Color _color;
Color get color => _color;
set color(Color value) {
if (_color == value) return;
_color = value;
markNeedsPaint();
}
Size _preferredSize;
set preferredSize(Size value) {
if (_preferredSize == value) return;
_preferredSize = value;
markNeedsLayout();
}
@override @override
void paint(Canvas canvas, Size size) { void performLayout() {
size = constraints.constrain(_preferredSize);
}
@override
void paint(PaintingContext context, Offset offset) {
final size = this.size;
final paint = Paint() final paint = Paint()
..color = color ..color = color
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
Path path; final path = Path()
switch (type) { ..moveTo(0, 0)
case TooltipType.top: ..lineTo(size.width, 0)
path = Path() ..lineTo(size.width / 2, size.height)
..moveTo(0, 0) ..close();
..lineTo(size.width, 0)
..lineTo(size.width / 2, size.height)
..close();
case TooltipType.right:
path = Path()
..moveTo(0, size.height / 2)
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..close();
}
canvas.drawPath(path, paint); context.canvas.drawPath(path, paint);
} }
@override @override
bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color; bool get isRepaintBoundary => true;
}
Offset positionDependentBox({
required TooltipType type,
required Size size,
required Size childSize,
required Offset target,
required bool preferBelow,
double verticalOffset = 0.0,
double horizontslOffset = 0.0,
double margin = 10.0,
}) {
switch (type) {
case TooltipType.top:
// VERTICAL DIRECTION
final bool fitsBelow =
target.dy + verticalOffset + childSize.height <= size.height - margin;
final bool fitsAbove =
target.dy - verticalOffset - childSize.height >= margin;
final bool tooltipBelow = fitsAbove == fitsBelow
? preferBelow
: fitsBelow;
final double y;
if (tooltipBelow) {
y = math.min(target.dy + verticalOffset, size.height - margin);
} else {
y = math.max(target.dy - verticalOffset - childSize.height, margin);
} // HORIZONTAL DIRECTION
final double flexibleSpace = size.width - childSize.width;
final double x = flexibleSpace <= 2 * margin
// If there's not enough horizontal space for margin + child, center the
// child.
? flexibleSpace / 2.0
: clampDouble(
target.dx - childSize.width / 2,
margin,
flexibleSpace - margin,
);
return Offset(x, y);
case TooltipType.right:
final double dy = math.max(margin, target.dy - childSize.height / 2);
final double dx = math.min(
target.dx + horizontslOffset,
size.width - childSize.width - margin,
);
return Offset(dx, dy);
}
} }

View File

@@ -1,41 +1,44 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
Future<void> showConfirmDialog({ Future<bool> showConfirmDialog({
required BuildContext context, required BuildContext context,
required String title, required String title,
dynamic content, Object? content,
required VoidCallback onConfirm, // @Deprecated('use `bool result = await showConfirmDialog()` instead')
}) { VoidCallback? onConfirm,
return showDialog( }) async {
context: context, assert(content is String? || content is Widget);
builder: (context) { return await showDialog<bool>(
return AlertDialog( context: context,
title: Text(title), builder: (context) => AlertDialog(
content: content is String title: Text(title),
? Text(content) content: content is String
: content is Widget ? Text(content)
? content : content is Widget
: null, ? content
actions: [ : null,
TextButton( actions: [
onPressed: Get.back, TextButton(
child: Text( onPressed: Get.back,
'取消', child: Text(
style: TextStyle(color: Theme.of(context).colorScheme.outline), '取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
), ),
), TextButton(
TextButton( onPressed: () {
onPressed: () { Get.back(result: true);
Get.back(); onConfirm?.call();
onConfirm(); },
}, child: const Text('确认'),
child: const Text('确认'), ),
), ],
], ),
); ) ??
}, false;
);
} }
void showPgcFollowDialog({ void showPgcFollowDialog({

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/common/widgets/radio_widget.dart'; import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:flutter/foundation.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -8,92 +9,81 @@ import 'package:get/get.dart';
Future<void> autoWrapReportDialog( Future<void> autoWrapReportDialog(
BuildContext context, BuildContext context,
Map<String, Map<int, String>> options, Map<String, Map<int, String>> options,
Future<Map> Function(int reasonType, String? reasonDesc, bool banUid) Future<LoadingState> Function(int reasonType, String? reasonDesc, bool banUid)
onSuccess, onSuccess, {
) { bool ban = true,
}) {
int? reasonType; int? reasonType;
String? reasonDesc; String? reasonDesc;
bool banUid = false; bool banUid = false;
late final key = GlobalKey<FormState>(); late final key = GlobalKey<FormFieldState<String>>();
return showDialog( return showDialog(
context: context, context: context,
builder: (context) { builder: (context) => AlertDialog(
return AlertDialog( title: const Text('举报'),
title: const Text('举报'), titlePadding: const .only(left: 22, top: 16, right: 22),
titlePadding: const EdgeInsets.only(left: 22, top: 16, right: 22), contentPadding: const .symmetric(vertical: 5),
contentPadding: const EdgeInsets.symmetric(vertical: 5), actionsPadding: const .only(left: 16, right: 16, bottom: 10),
actionsPadding: const EdgeInsets.only( content: Column(
left: 16, mainAxisSize: MainAxisSize.min,
right: 16, crossAxisAlignment: CrossAxisAlignment.start,
bottom: 10, children: [
), Flexible(
content: Column( child: SingleChildScrollView(
mainAxisSize: MainAxisSize.min, child: AnimatedSize(
crossAxisAlignment: CrossAxisAlignment.start, duration: const Duration(milliseconds: 200),
children: [ child: Builder(
Flexible( builder: (context) => Column(
child: SingleChildScrollView( crossAxisAlignment: CrossAxisAlignment.start,
child: AnimatedSize( children: [
duration: const Duration(milliseconds: 200), const Padding(
child: Builder( padding: .only(left: 22, right: 22, bottom: 5),
builder: (context) => Column( child: Text('请选择举报的理由:'),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ RadioGroup(
const Padding( onChanged: (value) {
padding: EdgeInsets.only( reasonType = value;
left: 22, (context as Element).markNeedsBuild();
right: 22, },
bottom: 5, groupValue: reasonType,
), child: Column(
child: Text('请选择举报的理由:'), crossAxisAlignment: CrossAxisAlignment.start,
children: options.entries.map((entry) {
return WrapRadioOptionsGroup<int>(
groupTitle: entry.key,
options: entry.value,
);
}).toList(),
), ),
RadioGroup( ),
onChanged: (value) { if (reasonType == 0)
reasonType = value; Padding(
(context as Element).markNeedsBuild(); padding: const .only(left: 22, top: 5, right: 22),
}, child: TextFormField(
groupValue: reasonType, key: key,
child: Column( autofocus: true,
crossAxisAlignment: CrossAxisAlignment.start, minLines: 2,
children: options.entries.map((entry) { maxLines: 4,
return WrapRadioOptionsGroup<int>( initialValue: reasonDesc,
groupTitle: entry.key, decoration: const InputDecoration(
options: entry.value, labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
); border: OutlineInputBorder(),
}).toList(), contentPadding: .all(10),
labelStyle: TextStyle(fontSize: 14),
floatingLabelStyle: TextStyle(fontSize: 14),
),
onChanged: (value) => reasonDesc = value,
validator: (value) =>
value.isNullOrEmpty ? '理由不能为空' : null,
), ),
), ),
if (reasonType == 0) ],
Padding(
padding: const EdgeInsets.only(
left: 22,
top: 5,
right: 22,
),
child: Form(
key: key,
child: TextFormField(
autofocus: true,
minLines: 2,
maxLines: 4,
initialValue: reasonDesc,
decoration: const InputDecoration(
labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.all(10),
),
onChanged: (value) => reasonDesc = value,
validator: (value) =>
value.isNullOrEmpty ? '理由不能为空' : null,
),
),
),
],
),
), ),
), ),
), ),
), ),
),
if (ban)
Padding( Padding(
padding: const EdgeInsets.only(left: 14, top: 6), padding: const EdgeInsets.only(left: 14, top: 6),
child: CheckBoxText( child: CheckBoxText(
@@ -101,43 +91,42 @@ Future<void> autoWrapReportDialog(
onChanged: (value) => banUid = value, 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'].toString());
}
} catch (e) {
SmartDialog.dismiss();
SmartDialog.showToast('提交失败:$e');
if (kDebugMode) rethrow;
}
},
child: const Text('确定'),
),
], ],
); ),
}, actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: ColorScheme.of(context).outline),
),
),
TextButton(
onPressed: () async {
if (reasonType == null ||
(reasonType == 0 && key.currentState?.validate() != true)) {
return;
}
SmartDialog.showLoading();
try {
final res = await onSuccess(reasonType!, reasonDesc, banUid);
SmartDialog.dismiss();
if (res.isSuccess) {
Get.back();
SmartDialog.showToast('举报成功');
} else {
res.toast();
}
} catch (e, s) {
SmartDialog.dismiss();
SmartDialog.showToast('提交失败:$e');
Utils.reportError(e, s);
}
},
child: const Text('确定'),
),
],
),
); );
} }
@@ -168,7 +157,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = ColorScheme.of(context);
return InkWell( return InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -201,7 +190,7 @@ class _CheckBoxTextState extends State<CheckBoxText> {
} }
} }
class ReportOptions { abstract final class ReportOptions {
// from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js // from https://s1.hdslb.com/bfs/seed/jinkela/comment-h5/static/js/605.chunks.js
static Map<String, Map<int, String>> get commentReport => const { static Map<String, Map<int, String>> get commentReport => const {
'违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'}, '违反法律法规': {9: '违法违规', 2: '色情', 10: '低俗', 12: '赌博诈骗', 23: '违法信息外链'},
@@ -263,4 +252,16 @@ class ReportOptions {
7: '其他', // avoid show form 7: '其他', // avoid show form
}, },
}; };
static Map<String, Map<int, String>> get imMsgReport => const {
'': {
1: '色情低俗',
2: '政治敏感',
3: '违法有害',
4: '广告骚扰',
5: '人身攻击',
6: '诈骗',
0: '其他问题',
},
};
} }

View File

@@ -3,6 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
const _reason = ['头像违规', '昵称违规', '签名违规'];
const _reasonV2 = ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗', '违规引流外链'];
Future<void> showMemberReportDialog( Future<void> showMemberReportDialog(
BuildContext context, { BuildContext context, {
required Object? name, required Object? name,
@@ -17,13 +21,11 @@ Future<void> showMemberReportDialog(
final theme = Theme.of(context); final theme = Theme.of(context);
return AlertDialog( return AlertDialog(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(vertical: 16),
horizontal: 20,
vertical: 16,
),
titleTextStyle: theme.textTheme.bodyMedium, titleTextStyle: theme.textTheme.bodyMedium,
title: Column( title: Column(
spacing: 4, spacing: 4,
crossAxisAlignment: .start,
children: [ children: [
Text( Text(
'举报: $name', '举报: $name',
@@ -34,53 +36,101 @@ Future<void> showMemberReportDialog(
), ),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
const Text('举报内容(必选,可多选)'), const Padding(
padding: .only(left: 18),
child: Text('举报内容(必选,可多选)'),
),
...List.generate( ...List.generate(
3, 3,
(index) => Builder( (index) => Builder(
builder: (context) => CheckboxListTile( builder: (context) {
dense: true, final checked = reason.contains(index + 1);
value: reason.contains(index + 1), return ListTile(
controlAffinity: ListTileControlAffinity.leading, dense: true,
contentPadding: EdgeInsets.zero, minTileHeight: 40,
onChanged: (value) { onTap: () {
if (value!) { if (!checked) {
reason.add(index + 1); reason.add(index + 1);
} else { } else {
reason.remove(index + 1); reason.remove(index + 1);
} }
(context as Element).markNeedsBuild(); (context as Element).markNeedsBuild();
}, },
title: Text(const ['头像违规', '昵称违规', '签名违规'][index]), title: Row(
), spacing: 8,
children: [
checked
? Icon(
size: 22,
Icons.check_box,
color: theme.colorScheme.primary,
)
: Icon(
size: 22,
Icons.check_box_outline_blank,
color: theme.colorScheme.onSurfaceVariant,
),
Expanded(
child: Text(
_reason[index],
style: const TextStyle(fontSize: 14),
),
),
],
),
);
},
), ),
), ),
const Text('举报理由(单选,非必选)'), const Padding(
padding: .only(left: 18),
child: Text('举报理由(单选,非必选)'),
),
Builder( Builder(
builder: (context) => RadioGroup<int>( builder: (context) => Column(
onChanged: (v) { crossAxisAlignment: .start,
reasonV2 = v; children: List.generate(
(context as Element).markNeedsBuild(); _reasonV2.length,
}, (index) {
groupValue: reasonV2, final checked = index == reasonV2;
child: Column( return ListTile(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(
5,
(index) => RadioListTile<int>(
toggleable: true,
controlAffinity: ListTileControlAffinity.leading,
contentPadding: const EdgeInsets.only(left: 4),
dense: true, dense: true,
value: index, minTileHeight: 40,
title: Text( onTap: () {
const ['色情低俗', '不实信息', '违禁', '人身攻击', '赌博诈骗'][index], if (checked) {
reasonV2 = null;
} else {
reasonV2 = index;
}
(context as Element).markNeedsBuild();
},
title: Row(
spacing: 8,
children: [
checked
? Icon(
size: 22,
Icons.radio_button_checked,
color: theme.colorScheme.primary,
)
: Icon(
size: 22,
Icons.radio_button_off,
color: theme.colorScheme.onSurfaceVariant,
),
Expanded(
child: Text(
_reasonV2[index],
style: const TextStyle(fontSize: 14),
),
),
],
), ),
), );
), },
), ),
), ),
), ),
@@ -96,21 +146,16 @@ Future<void> showMemberReportDialog(
), ),
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () {
if (reason.isEmpty) { if (reason.isEmpty) {
SmartDialog.showToast('至少选择一项作为举报内容'); SmartDialog.showToast('至少选择一项作为举报内容');
} else { } else {
Get.back(); Get.back();
var result = await MemberHttp.reportMember( MemberHttp.reportMember(
mid, mid,
reason: reason.join(','), reason: reason.join(','),
reasonV2: reasonV2 != null ? reasonV2! + 1 : null, reasonV2: reasonV2 != null ? reasonV2! + 1 : null,
); );
if (result['msg'] is String && result['msg'].isNotEmpty) {
SmartDialog.showToast(result['msg']);
} else {
SmartDialog.showToast('举报失败');
}
} }
}, },
child: const Text('确定'), child: const Text('确定'),

View File

@@ -4,61 +4,141 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget { class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
final Color? color;
final double lineLengthScale;
final StrokeCap strokeCap;
const DisabledIcon({ const DisabledIcon({
super.key, super.key,
required T child, required T child,
this.disable = false,
this.color, this.color,
this.iconSize,
double? lineLengthScale, double? lineLengthScale,
StrokeCap? strokeCap, StrokeCap? strokeCap,
}) : lineLengthScale = lineLengthScale ?? 0.9, }) : lineLengthScale = lineLengthScale ?? 0.9,
strokeCap = strokeCap ?? StrokeCap.butt, strokeCap = strokeCap ?? StrokeCap.butt,
super(child: child); super(child: child);
final bool disable;
final Color? color;
final double? iconSize;
final StrokeCap strokeCap;
final double lineLengthScale;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
late final iconTheme = IconTheme.of(context);
return RenderMaskedIcon( return RenderMaskedIcon(
color ?? disable: disable,
(child is Icon iconSize:
? (child as Icon).color ?? IconTheme.of(context).color! iconSize ??
: IconTheme.of(context).color!), (child is Icon ? (child as Icon).size : null) ??
lineLengthScale, iconTheme.size ??
strokeCap, 24.0,
color:
color ??
(child is Icon ? (child as Icon).color : null) ??
iconTheme.color!,
strokeCap: strokeCap,
lineLengthScale: lineLengthScale,
); );
} }
T enable() => child as T; @override
void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) {
late final iconTheme = IconTheme.of(context);
renderObject
..disable = disable
..iconSize =
iconSize ??
(child is Icon ? (child as Icon?)?.size : null) ??
iconTheme.size ??
24.0
..color =
color ??
(child is Icon ? (child as Icon?)?.color : null) ??
iconTheme.color!
..strokeCap = strokeCap
..lineLengthScale = lineLengthScale;
}
} }
class RenderMaskedIcon extends RenderProxyBox { class RenderMaskedIcon extends RenderProxyBox {
final Color color; RenderMaskedIcon({
final double lineLengthScale; required bool disable,
final StrokeCap strokeCap; required double iconSize,
required Color color,
required StrokeCap strokeCap,
required double lineLengthScale,
}) : _disable = disable,
_iconSize = iconSize,
_color = color,
_strokeCap = strokeCap,
_lineLengthScale = lineLengthScale;
RenderMaskedIcon(this.color, this.lineLengthScale, this.strokeCap); bool _disable;
bool get disable => _disable;
set disable(bool value) {
if (_disable == value) return;
_disable = value;
markNeedsPaint();
}
double _iconSize;
double get iconSize => _iconSize;
set iconSize(double value) {
if (_iconSize == value) return;
_iconSize = value;
markNeedsPaint();
}
Color _color;
Color get color => _color;
set color(Color value) {
if (_color == value) return;
_color = value;
markNeedsPaint();
}
StrokeCap _strokeCap;
StrokeCap get strokeCap => _strokeCap;
set strokeCap(StrokeCap value) {
if (_strokeCap == value) return;
_strokeCap = value;
markNeedsPaint();
}
double _lineLengthScale;
double get lineLengthScale => _lineLengthScale;
set lineLengthScale(double value) {
if (_lineLengthScale == value) return;
_lineLengthScale = value;
markNeedsPaint();
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final strokeWidth = size.width / 12; if (!disable) {
return super.paint(context, offset);
}
final canvas = context.canvas; final canvas = context.canvas;
Size size = this.size;
final exceedWidth = size.width > _iconSize;
final exceedHeight = size.height > _iconSize;
if (exceedWidth || exceedHeight) {
final dx = exceedWidth ? (size.width - _iconSize) / 2.0 : 0.0;
final dy = exceedHeight ? (size.height - _iconSize) / 2.0 : 0.0;
size = Size.square(_iconSize);
offset = Offset(dx, dy);
} else if (size.width < _iconSize && size.height < _iconSize) {
size = Size.square(_iconSize);
}
final strokeWidth = size.width / 12;
var rect = offset & size; var rect = offset & size;
final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4 final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4
// final path = Path.combine(
// PathOperation.difference,
// Path()..addRect(rect),
// Path()..moveTo(rect.left, rect.top)
// ..relativeLineTo(sqrt2Width, 0)
// ..lineTo(rect.right, rect.bottom - sqrt2Width)
// ..lineTo(rect.right, rect.bottom)
// ..close(),
// );
final path = Path.combine( final path = Path.combine(
PathOperation.union, PathOperation.union,
Path() // bottom Path() // bottom
@@ -75,9 +155,9 @@ class RenderMaskedIcon extends RenderProxyBox {
canvas canvas
..save() ..save()
..clipPath(path, doAntiAlias: false); ..clipPath(path, doAntiAlias: false);
super.paint(context, offset); super.paint(context, .zero);
context.canvas.restore(); canvas.restore();
final linePaint = Paint() final linePaint = Paint()
..color = color ..color = color
@@ -94,9 +174,7 @@ class RenderMaskedIcon extends RenderProxyBox {
linePaint, linePaint,
); );
} }
}
extension DisabledIconExt on Icon { @override
DisabledIcon<Icon> disable([double? lineLengthScale]) => bool get isRepaintBoundary => true;
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
} }

View File

@@ -1,8 +1,8 @@
import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
class DynamicSliverAppBarMedium extends StatefulWidget { class DynamicSliverAppBarMedium extends StatefulWidget {
const DynamicSliverAppBarMedium({ const DynamicSliverAppBarMedium({
this.flexibleSpace, this.flexibleSpace,
@@ -43,10 +43,10 @@ class DynamicSliverAppBarMedium extends StatefulWidget {
this.forceMaterialTransparency = false, this.forceMaterialTransparency = false,
this.clipBehavior, this.clipBehavior,
this.appBarClipper, this.appBarClipper,
this.callback, this.onPerformLayout,
}); });
final ValueChanged<double>? callback; final ValueChanged<double>? onPerformLayout;
final Widget? flexibleSpace; final Widget? flexibleSpace;
final Widget? leading; final Widget? leading;
final bool automaticallyImplyLeading; final bool automaticallyImplyLeading;
@@ -93,56 +93,43 @@ class DynamicSliverAppBarMedium extends StatefulWidget {
} }
class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> { class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
final GlobalKey _childKey = GlobalKey(); double? _height;
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
// to calculate dynamically the size for the sliver app bar
double _height = 0;
void _updateHeight() {
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
// otherwise this will throw an error
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (_childKey.currentContext == null) return;
setState(() {
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
.size
.height;
widget.callback?.call(_height);
});
});
}
double? _width; double? _width;
late double _topPadding;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_topPadding = MediaQuery.viewPaddingOf(context).top;
final width = MediaQuery.widthOf(context); final width = MediaQuery.widthOf(context);
if (_width != width) { if (_width != width) {
_width = width; _width = width;
_height = 0; _height = null;
_updateHeight();
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height if (_height == null) {
if (_height == 0) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: UnconstrainedBox( child: OnlyLayoutWidget(
alignment: Alignment.topLeft, onPerformLayout: (Size size) {
child: SizedBox( if (!mounted) return;
key: _childKey, _height = size.height;
width: _width, widget.onPerformLayout?.call(_height!);
child: widget.flexibleSpace, setState(() {});
},
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: SizedBox(
width: _width,
child: widget.flexibleSpace,
),
), ),
), ),
); );
} }
final padding = MediaQuery.viewPaddingOf(context).top;
return SliverAppBar.medium( return SliverAppBar.medium(
leading: widget.leading, leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading, automaticallyImplyLeading: widget.automaticallyImplyLeading,
@@ -170,8 +157,8 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
onStretchTrigger: widget.onStretchTrigger, onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape, shape: widget.shape,
toolbarHeight: kToolbarHeight, toolbarHeight: kToolbarHeight,
collapsedHeight: kToolbarHeight + padding + 1, collapsedHeight: kToolbarHeight + _topPadding + 1,
expandedHeight: _height - padding, expandedHeight: _height! - _topPadding,
leadingWidth: widget.leadingWidth, leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle, toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle, titleTextStyle: widget.titleTextStyle,

View File

@@ -13,7 +13,7 @@ library;
import 'dart:math' as math; import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/dyn/ink_well.dart'; import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide InkWell; import 'package:flutter/material.dart' hide InkWell;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';

View File

@@ -21,9 +21,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
// Examples can assume:
// late BuildContext context;
abstract class _ParentInkResponseState { abstract class _ParentInkResponseState {
void markChildInkResponsePressed( void markChildInkResponsePressed(
_ParentInkResponseState childState, _ParentInkResponseState childState,
@@ -144,6 +141,7 @@ class InkResponse extends StatelessWidget {
this.onTapCancel, this.onTapCancel,
this.onDoubleTap, this.onDoubleTap,
this.onLongPress, this.onLongPress,
this.onLongPressUp,
this.onSecondaryTap, this.onSecondaryTap,
this.onSecondaryTapUp, this.onSecondaryTapUp,
this.onSecondaryTapDown, this.onSecondaryTapDown,
@@ -197,6 +195,19 @@ class InkResponse extends StatelessWidget {
/// Called when the user long-presses on this part of the material. /// Called when the user long-presses on this part of the material.
final GestureLongPressCallback? onLongPress; final GestureLongPressCallback? onLongPress;
/// Called when the user lifts their finger after a long press on the button.
///
/// This callback is triggered at the end of a long press gesture, specifically
/// after the user holds a long press and then releases it. It does not include
/// position details.
///
/// Common use cases include performing an action only after the long press completes,
/// such as displaying a context menu or confirming a held gesture.
///
/// See also:
/// * [onLongPress], which is triggered when the long press gesture is first recognized.
final GestureLongPressUpCallback? onLongPressUp;
/// Called when the user taps this part of the material with a secondary button. /// Called when the user taps this part of the material with a secondary button.
/// ///
/// See also: /// See also:
@@ -237,7 +248,7 @@ class InkResponse extends StatelessWidget {
/// become highlighted and false if this part of the material has stopped /// become highlighted and false if this part of the material has stopped
/// being highlighted. /// being highlighted.
/// ///
/// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a /// If all of [onTap], [onDoubleTap], [onLongPress], and [onLongPressUp] become null while a
/// gesture is ongoing, then [onTapCancel] will be fired and /// gesture is ongoing, then [onTapCancel] will be fired and
/// [onHighlightChanged] will be fired with the value false _during the /// [onHighlightChanged] will be fired with the value false _during the
/// build_. This means, for instance, that in that scenario [State.setState] /// build_. This means, for instance, that in that scenario [State.setState]
@@ -493,6 +504,7 @@ class InkResponse extends StatelessWidget {
onTapCancel: onTapCancel, onTapCancel: onTapCancel,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onLongPress: onLongPress, onLongPress: onLongPress,
onLongPressUp: onLongPressUp,
onSecondaryTap: onSecondaryTap, onSecondaryTap: onSecondaryTap,
onSecondaryTapUp: onSecondaryTapUp, onSecondaryTapUp: onSecondaryTapUp,
onSecondaryTapDown: onSecondaryTapDown, onSecondaryTapDown: onSecondaryTapDown,
@@ -550,6 +562,7 @@ class _InkResponseStateWidget extends StatefulWidget {
this.onTapCancel, this.onTapCancel,
this.onDoubleTap, this.onDoubleTap,
this.onLongPress, this.onLongPress,
this.onLongPressUp,
this.onSecondaryTap, this.onSecondaryTap,
this.onSecondaryTapUp, this.onSecondaryTapUp,
this.onSecondaryTapDown, this.onSecondaryTapDown,
@@ -588,6 +601,7 @@ class _InkResponseStateWidget extends StatefulWidget {
final GestureTapCallback? onTapCancel; final GestureTapCallback? onTapCancel;
final GestureTapCallback? onDoubleTap; final GestureTapCallback? onDoubleTap;
final GestureLongPressCallback? onLongPress; final GestureLongPressCallback? onLongPress;
final GestureLongPressUpCallback? onLongPressUp;
final GestureTapCallback? onSecondaryTap; final GestureTapCallback? onSecondaryTap;
final GestureTapUpCallback? onSecondaryTapUp; final GestureTapUpCallback? onSecondaryTapUp;
final GestureTapDownCallback? onSecondaryTapDown; final GestureTapDownCallback? onSecondaryTapDown;
@@ -628,6 +642,7 @@ class _InkResponseStateWidget extends StatefulWidget {
if (onTap != null) 'tap', if (onTap != null) 'tap',
if (onDoubleTap != null) 'double tap', if (onDoubleTap != null) 'double tap',
if (onLongPress != null) 'long press', if (onLongPress != null) 'long press',
if (onLongPressUp != null) 'long press up',
if (onTapDown != null) 'tap down', if (onTapDown != null) 'tap down',
if (onTapUp != null) 'tap up', if (onTapUp != null) 'tap up',
if (onTapCancel != null) 'tap cancel', if (onTapCancel != null) 'tap cancel',
@@ -1099,6 +1114,12 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
} }
void handleLongPressUp() {
_currentSplash?.confirm();
_currentSplash = null;
widget.onLongPressUp?.call();
}
void handleSecondaryTap() { void handleSecondaryTap() {
_currentSplash?.confirm(); _currentSplash?.confirm();
_currentSplash = null; _currentSplash = null;
@@ -1140,6 +1161,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
return widget.onTap != null || return widget.onTap != null ||
widget.onDoubleTap != null || widget.onDoubleTap != null ||
widget.onLongPress != null || widget.onLongPress != null ||
widget.onLongPressUp != null ||
widget.onTapUp != null || widget.onTapUp != null ||
widget.onTapDown != null; widget.onTapDown != null;
} }
@@ -1276,6 +1298,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
onLongPress: widget.onLongPress != null onLongPress: widget.onLongPress != null
? handleLongPress ? handleLongPress
: null, : null,
onLongPressUp: widget.onLongPressUp != null
? handleLongPressUp
: null,
onSecondaryTapDown: _secondaryEnabled onSecondaryTapDown: _secondaryEnabled
? handleSecondaryTapDown ? handleSecondaryTapDown
: null, : null,
@@ -1387,6 +1412,7 @@ class InkWell extends InkResponse {
super.onTap, super.onTap,
super.onDoubleTap, super.onDoubleTap,
super.onLongPress, super.onLongPress,
super.onLongPressUp,
super.onTapDown, super.onTapDown,
super.onTapUp, super.onTapUp,
super.onTapCancel, super.onTapCancel,

View File

@@ -12,7 +12,7 @@ library;
import 'dart:ui' show lerpDouble; import 'dart:ui' show lerpDouble;
import 'package:PiliPlus/common/widgets/dyn/button.dart'; import 'package:PiliPlus/common/widgets/flutter/dyn/button.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton; import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;

View File

@@ -335,8 +335,10 @@ class ListTile extends StatelessWidget {
this.contentPadding, this.contentPadding,
this.enabled = true, this.enabled = true,
this.onTap, this.onTap,
this.onTapUp,
this.onLongPress, this.onLongPress,
this.onSecondaryTap, this.onSecondaryTap,
this.onSecondaryTapUp,
this.onFocusChange, this.onFocusChange,
this.mouseCursor, this.mouseCursor,
this.selected = false, this.selected = false,
@@ -562,6 +564,8 @@ class ListTile extends StatelessWidget {
/// Inoperative if [enabled] is false. /// Inoperative if [enabled] is false.
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
final GestureTapUpCallback? onTapUp;
/// Called when the user long-presses on this list tile. /// Called when the user long-presses on this list tile.
/// ///
/// Inoperative if [enabled] is false. /// Inoperative if [enabled] is false.
@@ -569,6 +573,8 @@ class ListTile extends StatelessWidget {
final GestureTapCallback? onSecondaryTap; final GestureTapCallback? onSecondaryTap;
final GestureTapUpCallback? onSecondaryTapUp;
/// {@macro flutter.material.inkwell.onFocusChange} /// {@macro flutter.material.inkwell.onFocusChange}
final ValueChanged<bool>? onFocusChange; final ValueChanged<bool>? onFocusChange;
@@ -910,14 +916,16 @@ class ListTile extends StatelessWidget {
// Show basic cursor when ListTile isn't enabled or gesture callbacks are null. // Show basic cursor when ListTile isn't enabled or gesture callbacks are null.
final Set<WidgetState> mouseStates = <WidgetState>{ final Set<WidgetState> mouseStates = <WidgetState>{
if (!enabled || (onTap == null && onLongPress == null)) if (!enabled ||
(onTap == null &&
onTapUp == null &&
onLongPress == null &&
onSecondaryTap == null &&
onSecondaryTapUp == null))
WidgetState.disabled, WidgetState.disabled,
}; };
final MouseCursor effectiveMouseCursor = final MouseCursor effectiveMouseCursor =
WidgetStateProperty.resolveAs<MouseCursor?>( WidgetStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates) ??
mouseCursor,
mouseStates,
) ??
tileTheme.mouseCursor?.resolve(mouseStates) ?? tileTheme.mouseCursor?.resolve(mouseStates) ??
WidgetStateMouseCursor.clickable.resolve(mouseStates); WidgetStateMouseCursor.clickable.resolve(mouseStates);
@@ -984,8 +992,10 @@ class ListTile extends StatelessWidget {
return InkWell( return InkWell(
customBorder: shape ?? tileTheme.shape, customBorder: shape ?? tileTheme.shape,
onTap: enabled ? onTap : null, onTap: enabled ? onTap : null,
onTapUp: enabled ? onTapUp : null,
onLongPress: enabled ? onLongPress : null, onLongPress: enabled ? onLongPress : null,
onSecondaryTap: enabled ? onSecondaryTap : null, onSecondaryTap: enabled ? onSecondaryTap : null,
onSecondaryTapUp: enabled ? onSecondaryTapUp : null,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
mouseCursor: effectiveMouseCursor, mouseCursor: effectiveMouseCursor,
canRequestFocus: enabled, canRequestFocus: enabled,
@@ -1330,12 +1340,7 @@ class _RenderListTile extends RenderBox
@override @override
Iterable<RenderBox> get children { Iterable<RenderBox> get children {
final RenderBox? title = childForSlot(_ListTileSlot.title); final RenderBox? title = childForSlot(_ListTileSlot.title);
return <RenderBox>[ return <RenderBox>[?leading, ?title, ?subtitle, ?trailing];
?leading,
?title,
?subtitle,
?trailing,
];
} }
bool get isDense => _isDense; bool get isDense => _isDense;

View File

@@ -10,8 +10,9 @@
/// @docImport 'text.dart'; /// @docImport 'text.dart';
library; library;
import 'package:PiliPlus/common/widgets/page/scrollable.dart'; import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/gestures.dart'
show DragStartBehavior, HorizontalDragGestureRecognizer;
import 'package:flutter/material.dart' hide Scrollable, ScrollableState; import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@@ -41,18 +42,18 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
/// ///
/// You can use a [PageController] to control which page is visible in the view. /// You can use a [PageController] to control which page is visible in the view.
/// In addition to being able to control the pixel offset of the content inside /// In addition to being able to control the pixel offset of the content inside
/// the [CustomPageView], a [PageController] also lets you control the offset in terms /// the [PageView], a [PageController] also lets you control the offset in terms
/// of pages, which are increments of the viewport size. /// of pages, which are increments of the viewport size.
/// ///
/// The [PageController] can also be used to control the /// The [PageController] can also be used to control the
/// [PageController.initialPage], which determines which page is shown when the /// [PageController.initialPage], which determines which page is shown when the
/// [CustomPageView] is first constructed, and the [PageController.viewportFraction], /// [PageView] is first constructed, and the [PageController.viewportFraction],
/// which determines the size of the pages as a fraction of the viewport size. /// which determines the size of the pages as a fraction of the viewport size.
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A} /// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// Here is an example of [CustomPageView]. It creates a centered [Text] in each of the three pages /// Here is an example of [PageView]. It creates a centered [Text] in each of the three pages
/// which scroll horizontally. /// which scroll horizontally.
/// ///
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart ** /// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
@@ -61,7 +62,7 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
/// ## Persisting the scroll position during a session /// ## Persisting the scroll position during a session
/// ///
/// Scroll views attempt to persist their scroll position using [PageStorage]. /// Scroll views attempt to persist their scroll position using [PageStorage].
/// For a [CustomPageView], this can be disabled by setting [PageController.keepPage] /// For a [PageView], this can be disabled by setting [PageController.keepPage]
/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for /// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
/// the [key] of this widget is recommended to help disambiguate different /// the [key] of this widget is recommended to help disambiguate different
/// scroll views from each other. /// scroll views from each other.
@@ -74,7 +75,8 @@ const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
/// * [GridView], for a scrollable grid of boxes. /// * [GridView], for a scrollable grid of boxes.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch /// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController]. /// the scroll position without using a [ScrollController].
class CustomPageView extends StatefulWidget { class PageView<T extends HorizontalDragGestureRecognizer>
extends StatefulWidget {
/// Creates a scrollable list that works page by page from an explicit [List] /// Creates a scrollable list that works page by page from an explicit [List]
/// of widgets. /// of widgets.
/// ///
@@ -88,12 +90,12 @@ class CustomPageView extends StatefulWidget {
/// See the documentation at [SliverChildListDelegate.children] for more details. /// See the documentation at [SliverChildListDelegate.children] for more details.
/// ///
/// {@template flutter.widgets.PageView.allowImplicitScrolling} /// {@template flutter.widgets.PageView.allowImplicitScrolling}
/// If [allowImplicitScrolling] is true, the [CustomPageView] will participate in /// If [allowImplicitScrolling] is true, the [PageView] will participate in
/// accessibility scrolling more like a [ListView], where implicit scroll /// accessibility scrolling more like a [ListView], where implicit scroll
/// actions will move to the next page rather than into the contents of the /// actions will move to the next page rather than into the contents of the
/// [CustomPageView]. /// [PageView].
/// {@endtemplate} /// {@endtemplate}
CustomPageView({ PageView({
super.key, super.key,
this.scrollDirection = Axis.horizontal, this.scrollDirection = Axis.horizontal,
this.reverse = false, this.reverse = false,
@@ -109,12 +111,10 @@ class CustomPageView extends StatefulWidget {
this.hitTestBehavior = HitTestBehavior.opaque, this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior, this.scrollBehavior,
this.padEnds = true, this.padEnds = true,
this.header, required this.horizontalDragGestureRecognizer,
this.bgColor = Colors.transparent,
}) : childrenDelegate = SliverChildListDelegate(children); }) : childrenDelegate = SliverChildListDelegate(children);
final Widget? header; final T horizontalDragGestureRecognizer;
final Color bgColor;
/// Creates a scrollable list that works page by page using widgets that are /// Creates a scrollable list that works page by page using widgets that are
/// created on demand. /// created on demand.
@@ -123,7 +123,7 @@ class CustomPageView extends StatefulWidget {
/// number of children because the builder is called only for those children /// number of children because the builder is called only for those children
/// that are actually visible. /// that are actually visible.
/// ///
/// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum /// Providing a non-null [itemCount] lets the [PageView] compute the maximum
/// scroll extent. /// scroll extent.
/// ///
/// [itemBuilder] will be called only with indices greater than or equal to /// [itemBuilder] will be called only with indices greater than or equal to
@@ -141,7 +141,7 @@ class CustomPageView extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling} /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
CustomPageView.builder({ PageView.builder({
super.key, super.key,
this.scrollDirection = Axis.horizontal, this.scrollDirection = Axis.horizontal,
this.reverse = false, this.reverse = false,
@@ -159,8 +159,7 @@ class CustomPageView extends StatefulWidget {
this.hitTestBehavior = HitTestBehavior.opaque, this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior, this.scrollBehavior,
this.padEnds = true, this.padEnds = true,
this.header, required this.horizontalDragGestureRecognizer,
this.bgColor = Colors.transparent,
}) : childrenDelegate = SliverChildBuilderDelegate( }) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
findChildIndexCallback: findChildIndexCallback, findChildIndexCallback: findChildIndexCallback,
@@ -171,14 +170,14 @@ class CustomPageView extends StatefulWidget {
/// model. /// model.
/// ///
/// {@tool dartpad} /// {@tool dartpad}
/// This example shows a [CustomPageView] that uses a custom [SliverChildBuilderDelegate] to support child /// This example shows a [PageView] that uses a custom [SliverChildBuilderDelegate] to support child
/// reordering. /// reordering.
/// ///
/// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart ** /// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
/// {@end-tool} /// {@end-tool}
/// ///
/// {@macro flutter.widgets.PageView.allowImplicitScrolling} /// {@macro flutter.widgets.PageView.allowImplicitScrolling}
const CustomPageView.custom({ const PageView.custom({
super.key, super.key,
this.scrollDirection = Axis.horizontal, this.scrollDirection = Axis.horizontal,
this.reverse = false, this.reverse = false,
@@ -194,8 +193,7 @@ class CustomPageView extends StatefulWidget {
this.hitTestBehavior = HitTestBehavior.opaque, this.hitTestBehavior = HitTestBehavior.opaque,
this.scrollBehavior, this.scrollBehavior,
this.padEnds = true, this.padEnds = true,
this.header, required this.horizontalDragGestureRecognizer,
this.bgColor = Colors.transparent,
}); });
/// Controls whether the widget's pages will respond to /// Controls whether the widget's pages will respond to
@@ -265,10 +263,10 @@ class CustomPageView extends StatefulWidget {
/// Called whenever the page in the center of the viewport changes. /// Called whenever the page in the center of the viewport changes.
final ValueChanged<int>? onPageChanged; final ValueChanged<int>? onPageChanged;
/// A delegate that provides the children for the [CustomPageView]. /// A delegate that provides the children for the [PageView].
/// ///
/// The [PageView.custom] constructor lets you specify this delegate /// The [PageView.custom] constructor lets you specify this delegate
/// explicitly. The [CustomPageView] and [PageView.builder] constructors create a /// explicitly. The [PageView] and [PageView.builder] constructors create a
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
/// respectively. /// respectively.
final SliverChildDelegate childrenDelegate; final SliverChildDelegate childrenDelegate;
@@ -304,10 +302,11 @@ class CustomPageView extends StatefulWidget {
final bool padEnds; final bool padEnds;
@override @override
State<CustomPageView> createState() => _CustomPageViewState(); State<PageView<T>> createState() => _PageViewState<T>();
} }
class _CustomPageViewState extends State<CustomPageView> { class _PageViewState<T extends HorizontalDragGestureRecognizer>
extends State<PageView<T>> {
int _lastReportedPage = 0; int _lastReportedPage = 0;
late PageController _controller; late PageController _controller;
@@ -332,7 +331,7 @@ class _CustomPageViewState extends State<CustomPageView> {
} }
@override @override
void didUpdateWidget(CustomPageView oldWidget) { void didUpdateWidget(PageView<T> oldWidget) {
if (oldWidget.controller != widget.controller) { if (oldWidget.controller != widget.controller) {
if (oldWidget.controller == null) { if (oldWidget.controller == null) {
_controller.dispose(); _controller.dispose();
@@ -388,9 +387,7 @@ class _CustomPageViewState extends State<CustomPageView> {
} }
return false; return false;
}, },
child: CustomScrollable( child: Scrollable<T>(
header: widget.header,
bgColor: widget.bgColor,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection, axisDirection: axisDirection,
controller: _controller, controller: _controller,
@@ -419,6 +416,7 @@ class _CustomPageViewState extends State<CustomPageView> {
], ],
); );
}, },
horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
), ),
); );
} }

View File

@@ -19,40 +19,39 @@ library;
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/flutter/page/scrollable_helpers.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Scrollable, ScrollableState; import 'package:flutter/material.dart'
hide Scrollable, ScrollableState, EdgeDraggingAutoScroller;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart';
export 'package:flutter/physics.dart' show Tolerance;
// The return type of _performEnsureVisible. // The return type of _performEnsureVisible.
// //
// The list of futures represents each pending ScrollPosition call to // The list of futures represents each pending ScrollPosition call to
// ensureVisible. The returned ScrollableState's context is used to find the // ensureVisible. The returned ScrollableState's context is used to find the
// next potential ancestor Scrollable. // next potential ancestor Scrollable.
typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState); typedef _EnsureVisibleResults = (List<Future<void>>, ScrollableState);
/// A widget that manages scrolling in one dimension and informs the [Viewport] /// A widget that manages scrolling in one dimension and informs the [Viewport]
/// through which the content is viewed. /// through which the content is viewed.
/// ///
/// [CustomScrollable] implements the interaction model for a scrollable widget, /// [Scrollable] implements the interaction model for a scrollable widget,
/// including gesture recognition, but does not have an opinion about how the /// including gesture recognition, but does not have an opinion about how the
/// viewport, which actually displays the children, is constructed. /// viewport, which actually displays the children, is constructed.
/// ///
/// It's rare to construct a [CustomScrollable] directly. Instead, consider [ListView] /// It's rare to construct a [Scrollable] directly. Instead, consider [ListView]
/// or [GridView], which combine scrolling, viewporting, and a layout model. To /// or [GridView], which combine scrolling, viewporting, and a layout model. To
/// combine layout models (or to use a custom layout mode), consider using /// combine layout models (or to use a custom layout mode), consider using
/// [CustomScrollView]. /// [CustomScrollView].
/// ///
/// The static [CustomScrollable.of] and [CustomScrollable.ensureVisible] functions are /// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are
/// often used to interact with the [CustomScrollable] widget inside a [ListView] or /// often used to interact with the [Scrollable] widget inside a [ListView] or
/// a [GridView]. /// a [GridView].
/// ///
/// To further customize scrolling behavior with a [CustomScrollable]: /// To further customize scrolling behavior with a [Scrollable]:
/// ///
/// 1. You can provide a [viewportBuilder] to customize the child model. For /// 1. You can provide a [viewportBuilder] to customize the child model. For
/// example, [SingleChildScrollView] uses a viewport that displays a single /// example, [SingleChildScrollView] uses a viewport that displays a single
@@ -62,7 +61,7 @@ typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
/// 2. You can provide a custom [ScrollController] that creates a custom /// 2. You can provide a custom [ScrollController] that creates a custom
/// [ScrollPosition] subclass. For example, [PageView] uses a /// [ScrollPosition] subclass. For example, [PageView] uses a
/// [PageController], which creates a page-oriented scroll position subclass /// [PageController], which creates a page-oriented scroll position subclass
/// that keeps the same page visible when the [CustomScrollable] resizes. /// that keeps the same page visible when the [Scrollable] resizes.
/// ///
/// ## Persisting the scroll position during a session /// ## Persisting the scroll position during a session
/// ///
@@ -70,7 +69,7 @@ typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
/// This can be disabled by setting [ScrollController.keepScrollOffset] to false /// This can be disabled by setting [ScrollController.keepScrollOffset] to false
/// on the [controller]. If it is enabled, using a [PageStorageKey] for the /// on the [controller]. If it is enabled, using a [PageStorageKey] for the
/// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is /// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is
/// recommended to help disambiguate different [CustomScrollable]s from each other. /// recommended to help disambiguate different [Scrollable]s from each other.
/// ///
/// See also: /// See also:
/// ///
@@ -86,9 +85,10 @@ typedef _EnsureVisibleResults = (List<Future<void>>, CustomScrollableState);
/// child. /// child.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch /// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController]. /// the scroll position without using a [ScrollController].
class CustomScrollable extends StatefulWidget { class Scrollable<T extends HorizontalDragGestureRecognizer>
extends StatefulWidget {
/// Creates a widget that scrolls. /// Creates a widget that scrolls.
const CustomScrollable({ const Scrollable({
super.key, super.key,
this.axisDirection = AxisDirection.down, this.axisDirection = AxisDirection.down,
this.controller, this.controller,
@@ -102,19 +102,15 @@ class CustomScrollable extends StatefulWidget {
this.scrollBehavior, this.scrollBehavior,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.hitTestBehavior = HitTestBehavior.opaque, this.hitTestBehavior = HitTestBehavior.opaque,
this.enableSlide, required this.horizontalDragGestureRecognizer,
this.header,
this.bgColor = Colors.transparent,
}) : assert(semanticChildCount == null || semanticChildCount >= 0); }) : assert(semanticChildCount == null || semanticChildCount >= 0);
final Widget? header; final T horizontalDragGestureRecognizer;
final bool? enableSlide;
final Color bgColor;
/// {@template flutter.widgets.Scrollable.axisDirection} /// {@template flutter.widgets.Scrollable.axisDirection}
/// The direction in which this widget scrolls. /// The direction in which this widget scrolls.
/// ///
/// For example, if the [CustomScrollable.axisDirection] is [AxisDirection.down], /// For example, if the [Scrollable.axisDirection] is [AxisDirection.down],
/// increasing the scroll position will cause content below the bottom of the /// increasing the scroll position will cause content below the bottom of the
/// viewport to become visible through the viewport. Similarly, if the /// viewport to become visible through the viewport. Similarly, if the
/// axisDirection is [AxisDirection.right], increasing the scroll position /// axisDirection is [AxisDirection.right], increasing the scroll position
@@ -137,12 +133,12 @@ class CustomScrollable extends StatefulWidget {
/// scroll position (see [ScrollController.offset]), or change it (see /// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]). /// [ScrollController.animateTo]).
/// ///
/// If null, a [ScrollController] will be created internally by [CustomScrollable] /// If null, a [ScrollController] will be created internally by [Scrollable]
/// in order to create and manage the [ScrollPosition]. /// in order to create and manage the [ScrollPosition].
/// ///
/// See also: /// See also:
/// ///
/// * [CustomScrollable.ensureVisible], which animates the scroll position to /// * [Scrollable.ensureVisible], which animates the scroll position to
/// reveal a given [BuildContext]. /// reveal a given [BuildContext].
/// {@endtemplate} /// {@endtemplate}
final ScrollController? controller; final ScrollController? controller;
@@ -157,8 +153,8 @@ class CustomScrollable extends StatefulWidget {
/// the ambient [ScrollConfiguration]. /// the ambient [ScrollConfiguration].
/// ///
/// If an explicit [ScrollBehavior] is provided to /// If an explicit [ScrollBehavior] is provided to
/// [CustomScrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior /// [Scrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior
/// will take precedence after [CustomScrollable.physics]. /// will take precedence after [Scrollable.physics].
/// ///
/// The physics can be changed dynamically, but new physics will only take /// The physics can be changed dynamically, but new physics will only take
/// effect if the _class_ of the provided object changes. Merely constructing /// effect if the _class_ of the provided object changes. Merely constructing
@@ -193,7 +189,7 @@ class CustomScrollable extends StatefulWidget {
/// scroll when the scrollable is asked to scroll via the keyboard using a /// scroll when the scrollable is asked to scroll via the keyboard using a
/// [ScrollAction]. /// [ScrollAction].
/// ///
/// If not supplied, the [CustomScrollable] will scroll a default amount when a /// If not supplied, the [Scrollable] will scroll a default amount when a
/// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow, /// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow,
/// etc.), or otherwise invoked by a [ScrollAction]. /// etc.), or otherwise invoked by a [ScrollAction].
/// ///
@@ -204,7 +200,7 @@ class CustomScrollable extends StatefulWidget {
final ScrollIncrementCalculator? incrementCalculator; final ScrollIncrementCalculator? incrementCalculator;
/// {@template flutter.widgets.scrollable.excludeFromSemantics} /// {@template flutter.widgets.scrollable.excludeFromSemantics}
/// Whether the scroll actions introduced by this [CustomScrollable] are exposed /// Whether the scroll actions introduced by this [Scrollable] are exposed
/// in the semantics tree. /// in the semantics tree.
/// ///
/// Text fields with an overflow are usually scrollable to make sure that the /// Text fields with an overflow are usually scrollable to make sure that the
@@ -219,10 +215,10 @@ class CustomScrollable extends StatefulWidget {
final bool excludeFromSemantics; final bool excludeFromSemantics;
/// {@template flutter.widgets.scrollable.hitTestBehavior} /// {@template flutter.widgets.scrollable.hitTestBehavior}
/// Defines the behavior of gesture detector used in this [CustomScrollable]. /// Defines the behavior of gesture detector used in this [Scrollable].
/// ///
/// This defaults to [HitTestBehavior.opaque] which means it prevents targets /// This defaults to [HitTestBehavior.opaque] which means it prevents targets
/// behind this [CustomScrollable] from receiving events. /// behind this [Scrollable] from receiving events.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// See also: /// See also:
@@ -304,7 +300,7 @@ class CustomScrollable extends StatefulWidget {
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
/// ///
/// This is passed to decorators in [ScrollableDetails], and does not directly affect /// This is passed to decorators in [ScrollableDetails], and does not directly affect
/// clipping of the [CustomScrollable]. This reflects the same [Clip] that is provided /// clipping of the [Scrollable]. This reflects the same [Clip] that is provided
/// to [ScrollView.clipBehavior] and is supplied to the [Viewport]. /// to [ScrollView.clipBehavior] and is supplied to the [Viewport].
final Clip clipBehavior; final Clip clipBehavior;
@@ -314,7 +310,7 @@ class CustomScrollable extends StatefulWidget {
Axis get axis => axisDirectionToAxis(axisDirection); Axis get axis => axisDirectionToAxis(axisDirection);
@override @override
CustomScrollableState createState() => CustomScrollableState(); ScrollableState<T> createState() => ScrollableState<T>();
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -334,31 +330,31 @@ class CustomScrollable extends StatefulWidget {
/// ScrollableState? scrollable = Scrollable.maybeOf(context); /// ScrollableState? scrollable = Scrollable.maybeOf(context);
/// ``` /// ```
/// ///
/// Calling this method will create a dependency on the [CustomScrollableState] /// Calling this method will create a dependency on the [ScrollableState]
/// that is returned, if there is one. This is typically the closest /// that is returned, if there is one. This is typically the closest
/// [CustomScrollable], but may be a more distant ancestor if [axis] is used to /// [Scrollable], but may be a more distant ancestor if [axis] is used to
/// target a specific [CustomScrollable]. /// target a specific [Scrollable].
/// ///
/// Using the optional [Axis] is useful when Scrollables are nested and the /// Using the optional [Axis] is useful when Scrollables are nested and the
/// target [CustomScrollable] is not the closest instance. When [axis] is provided, /// target [Scrollable] is not the closest instance. When [axis] is provided,
/// the nearest enclosing [CustomScrollableState] in that [Axis] is returned, or /// the nearest enclosing [ScrollableState] in that [Axis] is returned, or
/// null if there is none. /// null if there is none.
/// ///
/// This finds the nearest _ancestor_ [CustomScrollable] of the `context`. This /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
/// means that if the `context` is that of a [CustomScrollable], it will _not_ find /// means that if the `context` is that of a [Scrollable], it will _not_ find
/// _that_ [CustomScrollable]. /// _that_ [Scrollable].
/// ///
/// See also: /// See also:
/// ///
/// * [CustomScrollable.of], which is similar to this method, but asserts /// * [Scrollable.of], which is similar to this method, but asserts
/// if no [CustomScrollable] ancestor is found. /// if no [Scrollable] ancestor is found.
static CustomScrollableState? maybeOf(BuildContext context, {Axis? axis}) { static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) {
// This is the context that will need to establish the dependency. // This is the context that will need to establish the dependency.
final BuildContext originalContext = context; final BuildContext originalContext = context;
InheritedElement? element = context InheritedElement? element = context
.getElementForInheritedWidgetOfExactType<_ScrollableScope>(); .getElementForInheritedWidgetOfExactType<_ScrollableScope>();
while (element != null) { while (element != null) {
final CustomScrollableState scrollable = final ScrollableState scrollable =
(element.widget as _ScrollableScope).scrollable; (element.widget as _ScrollableScope).scrollable;
if (axis == null || if (axis == null ||
axisDirectionToAxis(scrollable.axisDirection) == axis) { axisDirectionToAxis(scrollable.axisDirection) == axis) {
@@ -382,28 +378,28 @@ class CustomScrollable extends StatefulWidget {
/// ScrollableState scrollable = Scrollable.of(context); /// ScrollableState scrollable = Scrollable.of(context);
/// ``` /// ```
/// ///
/// Calling this method will create a dependency on the [CustomScrollableState] /// Calling this method will create a dependency on the [ScrollableState]
/// that is returned, if there is one. This is typically the closest /// that is returned, if there is one. This is typically the closest
/// [CustomScrollable], but may be a more distant ancestor if [axis] is used to /// [Scrollable], but may be a more distant ancestor if [axis] is used to
/// target a specific [CustomScrollable]. /// target a specific [Scrollable].
/// ///
/// Using the optional [Axis] is useful when Scrollables are nested and the /// Using the optional [Axis] is useful when Scrollables are nested and the
/// target [CustomScrollable] is not the closest instance. When [axis] is provided, /// target [Scrollable] is not the closest instance. When [axis] is provided,
/// the nearest enclosing [CustomScrollableState] in that [Axis] is returned. /// the nearest enclosing [ScrollableState] in that [Axis] is returned.
/// ///
/// This finds the nearest _ancestor_ [CustomScrollable] of the `context`. This /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
/// means that if the `context` is that of a [CustomScrollable], it will _not_ find /// means that if the `context` is that of a [Scrollable], it will _not_ find
/// _that_ [CustomScrollable]. /// _that_ [Scrollable].
/// ///
/// If no [CustomScrollable] ancestor is found, then this method will assert in /// If no [Scrollable] ancestor is found, then this method will assert in
/// debug mode, and throw an exception in release mode. /// debug mode, and throw an exception in release mode.
/// ///
/// See also: /// See also:
/// ///
/// * [CustomScrollable.maybeOf], which is similar to this method, but returns null /// * [Scrollable.maybeOf], which is similar to this method, but returns null
/// if no [CustomScrollable] ancestor is found. /// if no [Scrollable] ancestor is found.
static CustomScrollableState of(BuildContext context, {Axis? axis}) { static ScrollableState of(BuildContext context, {Axis? axis}) {
final CustomScrollableState? scrollableState = maybeOf(context, axis: axis); final ScrollableState? scrollableState = maybeOf(context, axis: axis);
assert(() { assert(() {
if (scrollableState == null) { if (scrollableState == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
@@ -439,16 +435,16 @@ class CustomScrollable extends StatefulWidget {
/// This also means that the value returned is only good for the point in time /// This also means that the value returned is only good for the point in time
/// when it is called, and callers will not get updated if the value changes. /// when it is called, and callers will not get updated if the value changes.
/// ///
/// The heuristic used is determined by the [physics] of this [CustomScrollable] /// The heuristic used is determined by the [physics] of this [Scrollable]
/// via [ScrollPhysics.recommendDeferredLoading]. That method is called with /// via [ScrollPhysics.recommendDeferredLoading]. That method is called with
/// the current [ScrollPosition.activity]'s [ScrollActivity.velocity]. /// the current [ScrollPosition.activity]'s [ScrollActivity.velocity].
/// ///
/// The optional [Axis] allows targeting of a specific [CustomScrollable] of that /// The optional [Axis] allows targeting of a specific [Scrollable] of that
/// axis, useful when Scrollables are nested. When [axis] is provided, /// axis, useful when Scrollables are nested. When [axis] is provided,
/// [ScrollPosition.recommendDeferredLoading] is called for the nearest /// [ScrollPosition.recommendDeferredLoading] is called for the nearest
/// [CustomScrollable] in that [Axis]. /// [Scrollable] in that [Axis].
/// ///
/// If there is no [CustomScrollable] in the widget tree above the [context], this /// If there is no [Scrollable] in the widget tree above the [context], this
/// method returns false. /// method returns false.
static bool recommendDeferredLoadingForContext( static bool recommendDeferredLoadingForContext(
BuildContext context, { BuildContext context, {
@@ -470,7 +466,7 @@ class CustomScrollable extends StatefulWidget {
/// Scrolls all scrollables that enclose the given context so as to make the /// Scrolls all scrollables that enclose the given context so as to make the
/// given context visible. /// given context visible.
/// ///
/// If a [CustomScrollable] enclosing the provided [BuildContext] is a /// If a [Scrollable] enclosing the provided [BuildContext] is a
/// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure /// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure
/// the target is made visible. /// the target is made visible.
static Future<void> ensureVisible( static Future<void> ensureVisible(
@@ -491,7 +487,7 @@ class CustomScrollable extends StatefulWidget {
// //
// Also see https://github.com/flutter/flutter/issues/65100 // Also see https://github.com/flutter/flutter/issues/65100
RenderObject? targetRenderObject; RenderObject? targetRenderObject;
CustomScrollableState? scrollable = CustomScrollable.maybeOf(context); ScrollableState? scrollable = Scrollable.maybeOf(context);
while (scrollable != null) { while (scrollable != null) {
final List<Future<void>> newFutures; final List<Future<void>> newFutures;
(newFutures, scrollable) = scrollable._performEnsureVisible( (newFutures, scrollable) = scrollable._performEnsureVisible(
@@ -506,7 +502,7 @@ class CustomScrollable extends StatefulWidget {
targetRenderObject ??= context.findRenderObject(); targetRenderObject ??= context.findRenderObject();
context = scrollable.context; context = scrollable.context;
scrollable = CustomScrollable.maybeOf(context); scrollable = Scrollable.maybeOf(context);
} }
if (futures.isEmpty || duration == Duration.zero) { if (futures.isEmpty || duration == Duration.zero) {
@@ -528,7 +524,7 @@ class _ScrollableScope extends InheritedWidget {
required super.child, required super.child,
}); });
final CustomScrollableState scrollable; final ScrollableState scrollable;
final ScrollPosition position; final ScrollPosition position;
@override @override
@@ -537,30 +533,31 @@ class _ScrollableScope extends InheritedWidget {
} }
} }
/// State object for a [CustomScrollable] widget. /// State object for a [Scrollable] widget.
/// ///
/// To manipulate a [CustomScrollable] widget's scroll position, use the object /// To manipulate a [Scrollable] widget's scroll position, use the object
/// obtained from the [position] property. /// obtained from the [position] property.
/// ///
/// To be informed of when a [CustomScrollable] widget is scrolling, use a /// To be informed of when a [Scrollable] widget is scrolling, use a
/// [NotificationListener] to listen for [ScrollNotification] notifications. /// [NotificationListener] to listen for [ScrollNotification] notifications.
/// ///
/// This class is not intended to be subclassed. To specialize the behavior of a /// This class is not intended to be subclassed. To specialize the behavior of a
/// [CustomScrollable], provide it with a [ScrollPhysics]. /// [Scrollable], provide it with a [ScrollPhysics].
class CustomScrollableState extends State<CustomScrollable> class ScrollableState<T extends HorizontalDragGestureRecognizer>
extends State<Scrollable<T>>
with TickerProviderStateMixin, RestorationMixin with TickerProviderStateMixin, RestorationMixin
implements ScrollContext { implements ScrollContext {
// GETTERS // GETTERS
/// The manager for this [CustomScrollable] widget's viewport position. /// The manager for this [Scrollable] widget's viewport position.
/// ///
/// To control what kind of [ScrollPosition] is created for a [CustomScrollable], /// To control what kind of [ScrollPosition] is created for a [Scrollable],
/// provide it with custom [ScrollController] that creates the appropriate /// provide it with custom [ScrollController] that creates the appropriate
/// [ScrollPosition] in its [ScrollController.createScrollPosition] method. /// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
ScrollPosition get position => _position!; ScrollPosition get position => _position!;
ScrollPosition? _position; ScrollPosition? _position;
/// The resolved [ScrollPhysics] of the [CustomScrollableState]. /// The resolved [ScrollPhysics] of the [ScrollableState].
ScrollPhysics? get resolvedPhysics => _physics; ScrollPhysics? get resolvedPhysics => _physics;
ScrollPhysics? _physics; ScrollPhysics? _physics;
@@ -660,10 +657,6 @@ class CustomScrollableState extends State<CustomScrollable>
_fallbackScrollController = ScrollController(); _fallbackScrollController = ScrollController();
} }
super.initState(); super.initState();
_animController = AnimationController(
vsync: this,
reverseDuration: const Duration(milliseconds: 500),
);
} }
@protected @protected
@@ -677,7 +670,7 @@ class CustomScrollableState extends State<CustomScrollable>
super.didChangeDependencies(); super.didChangeDependencies();
} }
bool _shouldUpdatePosition(CustomScrollable oldWidget) { bool _shouldUpdatePosition(Scrollable oldWidget) {
if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) { if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) {
return true; return true;
} }
@@ -704,7 +697,7 @@ class CustomScrollableState extends State<CustomScrollable>
@protected @protected
@override @override
void didUpdateWidget(CustomScrollable oldWidget) { void didUpdateWidget(Scrollable<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) { if (widget.controller != oldWidget.controller) {
@@ -746,8 +739,6 @@ class CustomScrollableState extends State<CustomScrollable>
position.dispose(); position.dispose();
_persistedScrollOffset.dispose(); _persistedScrollOffset.dispose();
_animController.dispose();
super.dispose(); super.dispose();
} }
@@ -777,12 +768,6 @@ class CustomScrollableState extends State<CustomScrollable>
bool? _lastCanDrag; bool? _lastCanDrag;
Axis? _lastAxisDirection; Axis? _lastAxisDirection;
late bool _isRTL = false;
Offset? _downPos;
bool? _isSliding;
late AnimationController _animController;
@override @override
@protected @protected
void setCanDrag(bool value) { void setCanDrag(bool value) {
@@ -829,32 +814,27 @@ class CustomScrollableState extends State<CustomScrollable>
}; };
case Axis.horizontal: case Axis.horizontal:
_gestureRecognizers = <Type, GestureRecognizerFactory>{ _gestureRecognizers = <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: T: GestureRecognizerFactoryWithHandlers<T>(
GestureRecognizerFactoryWithHandlers< () => widget.horizontalDragGestureRecognizer,
HorizontalDragGestureRecognizer (T instance) {
>( instance
() => HorizontalDragGestureRecognizer( ..onDown = _handleDragDown
supportedDevices: _configuration.dragDevices, ..onStart = _handleDragStart
), ..onUpdate = _handleDragUpdate
(HorizontalDragGestureRecognizer instance) { ..onEnd = _handleDragEnd
instance ..onCancel = _handleDragCancel
..onDown = _handleDragDown ..minFlingDistance = _physics?.minFlingDistance
..onStart = _handleDragStart ..minFlingVelocity = _physics?.minFlingVelocity
..onUpdate = _handleDragUpdate ..maxFlingVelocity = _physics?.maxFlingVelocity
..onEnd = _handleDragEnd ..velocityTrackerBuilder = _configuration
..onCancel = _handleDragCancel .velocityTrackerBuilder(context)
..minFlingDistance = _physics?.minFlingDistance ..dragStartBehavior = widget.dragStartBehavior
..minFlingVelocity = _physics?.minFlingVelocity ..multitouchDragStrategy = _configuration
..maxFlingVelocity = _physics?.maxFlingVelocity .getMultitouchDragStrategy(context)
..velocityTrackerBuilder = _configuration ..gestureSettings = _mediaQueryGestureSettings
.velocityTrackerBuilder(context) ..supportedDevices = _configuration.dragDevices;
..dragStartBehavior = widget.dragStartBehavior },
..multitouchDragStrategy = _configuration ),
.getMultitouchDragStrategy(context)
..gestureSettings = _mediaQueryGestureSettings
..supportedDevices = _configuration.dragDevices;
},
),
}; };
} }
} }
@@ -888,65 +868,12 @@ class CustomScrollableState extends State<CustomScrollable>
ScrollHoldController? _hold; ScrollHoldController? _hold;
void _handleDragDown(DragDownDetails details) { void _handleDragDown(DragDownDetails details) {
final dx = details.localPosition.dx;
const offset = 30;
final isLTR = dx <= offset;
final isRTL = dx >= _maxWidth - offset;
if (isLTR || isRTL) {
_isRTL = isRTL;
_downPos = details.localPosition;
return;
}
assert(_drag == null); assert(_drag == null);
assert(_hold == null); assert(_hold == null);
_hold = position.hold(_disposeHold); _hold = position.hold(_disposeHold);
} }
void _onPan(Offset localPosition) {
if (_isSliding == false) {
return;
} else if (_isSliding == null) {
if (_downPos != null) {
Offset cumulativeDelta = localPosition - _downPos!;
if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) {
_downPos = localPosition;
_isSliding = true;
} else {
_downPos = null;
_isSliding = false;
}
} else {
_downPos = null;
_isSliding = false;
}
} else if (_isSliding == true) {
final from = _downPos!.dx;
final to = localPosition.dx;
_animController.value =
math.max(0, _isRTL ? from - to : to - from) / _maxWidth;
}
}
void _onDismiss() {
if (_isSliding == true) {
final dx = _downPos!.dx;
if (_animController.value * _maxWidth +
(_isRTL ? (_maxWidth - dx) : dx) >=
100) {
Get.back();
} else {
_animController.reverse();
}
}
_downPos = null;
_isSliding = null;
}
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
if (_downPos != null) {
_onPan(details.localPosition);
return;
}
// It's possible for _hold to become null between _handleDragDown and // It's possible for _hold to become null between _handleDragDown and
// _handleDragStart, for example if some user code calls jumpTo or otherwise // _handleDragStart, for example if some user code calls jumpTo or otherwise
// triggers a new activity to begin. // triggers a new activity to begin.
@@ -960,20 +887,12 @@ class CustomScrollableState extends State<CustomScrollable>
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
if (_downPos != null) {
_onPan(details.localPosition);
return;
}
// _drag might be null if the drag activity ended and called _disposeDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); assert(_hold == null || _drag == null);
_drag?.update(details); _drag?.update(details);
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
if (_downPos != null) {
_onDismiss();
return;
}
// _drag might be null if the drag activity ended and called _disposeDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null); assert(_hold == null || _drag == null);
_drag?.end(details); _drag?.end(details);
@@ -981,10 +900,6 @@ class CustomScrollableState extends State<CustomScrollable>
} }
void _handleDragCancel() { void _handleDragCancel() {
if (_downPos != null) {
_onDismiss();
return;
}
if (_gestureDetectorKey.currentContext == null) { if (_gestureDetectorKey.currentContext == null) {
// The cancel was caused by the GestureDetector getting disposed, which // The cancel was caused by the GestureDetector getting disposed, which
// means we will get disposed momentarily as well and shouldn't do // means we will get disposed momentarily as well and shouldn't do
@@ -1098,8 +1013,6 @@ class CustomScrollableState extends State<CustomScrollable>
return false; return false;
} }
late double _maxWidth;
Widget _buildChrome(BuildContext context, Widget child) { Widget _buildChrome(BuildContext context, Widget child) {
final ScrollableDetails details = ScrollableDetails( final ScrollableDetails details = ScrollableDetails(
direction: widget.axisDirection, direction: widget.axisDirection,
@@ -1177,32 +1090,7 @@ class CustomScrollableState extends State<CustomScrollable>
); );
} }
return LayoutBuilder( return result;
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,
),
);
},
);
} }
// Returns the Future from calling ensureVisible for the ScrollPosition, as // Returns the Future from calling ensureVisible for the ScrollPosition, as
@@ -1250,7 +1138,7 @@ class _ScrollableSelectionHandler extends StatefulWidget {
required this.child, required this.child,
}); });
final CustomScrollableState state; final ScrollableState state;
final ScrollPosition position; final ScrollPosition position;
final Widget child; final Widget child;
final SelectionRegistrar registrar; final SelectionRegistrar registrar;
@@ -1323,7 +1211,7 @@ class _ScrollableSelectionContainerDelegate
// An eye-balled value for a smooth scrolling speed. // An eye-balled value for a smooth scrolling speed.
static const double _kDefaultSelectToScrollVelocityScalar = 30; static const double _kDefaultSelectToScrollVelocityScalar = 30;
final CustomScrollableState state; final ScrollableState state;
final EdgeDraggingAutoScroller _autoScroller; final EdgeDraggingAutoScroller _autoScroller;
bool _scheduledLayoutChange = false; bool _scheduledLayoutChange = false;
Offset? _currentDragStartRelatedToOrigin; Offset? _currentDragStartRelatedToOrigin;
@@ -1682,7 +1570,7 @@ class _ScrollableSelectionContainerDelegate
bool _globalPositionInScrollable(Offset globalPosition) { bool _globalPositionInScrollable(Offset globalPosition) {
final RenderBox box = state.context.findRenderObject()! as RenderBox; final RenderBox box = state.context.findRenderObject()! as RenderBox;
final Offset localPosition = box.globalToLocal(globalPosition); final Offset localPosition = box.globalToLocal(globalPosition);
final Rect rect = Rect.fromLTWH(0, 0, box.size.width, box.size.height); final Rect rect = Rect.fromLTRB(0, 0, box.size.width, box.size.height);
return rect.contains(localPosition); return rect.contains(localPosition);
} }
@@ -1775,7 +1663,7 @@ class _ScrollableSelectionContainerDelegate
} }
} }
Offset _getDeltaToScrollOrigin(CustomScrollableState scrollableState) { Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) {
return switch (scrollableState.axisDirection) { return switch (scrollableState.axisDirection) {
AxisDirection.up => Offset(0, -scrollableState.position.pixels), AxisDirection.up => Offset(0, -scrollableState.position.pixels),
AxisDirection.down => Offset(0, scrollableState.position.pixels), AxisDirection.down => Offset(0, scrollableState.position.pixels),
@@ -1794,7 +1682,7 @@ Offset _getDeltaToScrollOrigin(CustomScrollableState scrollableState) {
/// [RenderObject.describeSemanticsConfiguration]. /// [RenderObject.describeSemanticsConfiguration].
/// ///
/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport, /// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport,
/// two semantics nodes will be used to represent the [CustomScrollable]: The outer /// two semantics nodes will be used to represent the [Scrollable]: The outer
/// node will contain all children, that are excluded from scrolling. The inner /// node will contain all children, that are excluded from scrolling. The inner
/// node, which is annotated with the scrolling actions, will house the /// node, which is annotated with the scrolling actions, will house the
/// scrollable children. /// scrollable children.
@@ -1937,7 +1825,7 @@ class _RenderScrollSemantics extends RenderProxyBox {
if (child.isTagged(RenderViewport.excludeFromScrolling)) { if (child.isTagged(RenderViewport.excludeFromScrolling)) {
excluded.add(child); excluded.add(child);
} else { } else {
if (!child.hasFlag(SemanticsFlag.isHidden)) { if (!child.flagsCollection.isHidden) {
firstVisibleIndex ??= child.indexInParent; firstVisibleIndex ??= child.indexInParent;
} }
included.add(child); included.add(child);
@@ -1982,222 +1870,3 @@ class _RestorableScrollOffset extends RestorableValue<double?> {
@override @override
bool get enabled => value != null; bool get enabled => value != null;
} }
// 2D SCROLLING
/// Specifies how to configure the [DragGestureRecognizer]s of a
/// [TwoDimensionalScrollable].
// TODO(Piinks): Add sample code, https://github.com/flutter/flutter/issues/126298
enum DiagonalDragBehavior {
/// This behavior will not allow for any diagonal scrolling.
///
/// Drag gestures in one direction or the other will lock the input axis until
/// the gesture is released.
none,
/// This behavior will only allow diagonal scrolling on a weighted
/// scale per gesture event.
///
/// This means that after initially evaluating the drag gesture, the weighted
/// evaluation (based on [kTouchSlop]) stands until the gesture is released.
weightedEvent,
/// This behavior will only allow diagonal scrolling on a weighted
/// scale that is evaluated throughout a gesture event.
///
/// This means that during each update to the drag gesture, the scrolling
/// axis will be allowed to scroll diagonally if it exceeds the
/// [kTouchSlop].
weightedContinuous,
/// This behavior allows free movement in any and all directions when
/// dragging.
free,
}
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
/// to its edge.
///
/// The scroll velocity is controlled by the [velocityScalar]:
///
/// velocity = (distance of overscroll) * [velocityScalar].
class EdgeDraggingAutoScroller {
/// Creates a auto scroller that scrolls the [scrollable].
EdgeDraggingAutoScroller(
this.scrollable, {
this.onScrollViewScrolled,
required this.velocityScalar,
});
/// The [CustomScrollable] this auto scroller is scrolling.
final CustomScrollableState scrollable;
/// Called when a scroll view is scrolled.
///
/// The scroll view may be scrolled multiple times in a row until the drag
/// target no longer triggers the auto scroll. This callback will be called
/// in between each scroll.
final VoidCallback? onScrollViewScrolled;
/// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
/// The velocity scalar per pixel over scroll.
///
/// It represents how the velocity scale with the over scroll distance. The
/// auto-scroll velocity = (distance of overscroll) * velocityScalar.
/// {@endtemplate}
final double velocityScalar;
late Rect _dragTargetRelatedToScrollOrigin;
/// Whether the auto scroll is in progress.
bool get scrolling => _scrolling;
bool _scrolling = false;
double _offsetExtent(Offset offset, Axis scrollDirection) {
return switch (scrollDirection) {
Axis.horizontal => offset.dx,
Axis.vertical => offset.dy,
};
}
double _sizeExtent(Size size, Axis scrollDirection) {
return switch (scrollDirection) {
Axis.horizontal => size.width,
Axis.vertical => size.height,
};
}
AxisDirection get _axisDirection => scrollable.axisDirection;
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
/// Starts the auto scroll if the [dragTarget] is close to the edge.
///
/// The scroll starts to scroll the [scrollable] if the target rect is close
/// to the edge of the [scrollable]; otherwise, it remains stationary.
///
/// If the scrollable is already scrolling, calling this method updates the
/// previous dragTarget to the new value and continues scrolling if necessary.
void startAutoScrollIfNecessary(Rect dragTarget) {
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
_dragTargetRelatedToScrollOrigin = dragTarget.translate(
deltaToOrigin.dx,
deltaToOrigin.dy,
);
if (_scrolling) {
// The change will be picked up in the next scroll.
return;
}
assert(!_scrolling);
_scroll();
}
/// Stop any ongoing auto scrolling.
void stopAutoScroll() {
_scrolling = false;
}
Future<void> _scroll() async {
final RenderBox scrollRenderBox =
scrollable.context.findRenderObject()! as RenderBox;
final Rect globalRect = MatrixUtils.transformRect(
scrollRenderBox.getTransformTo(null),
Rect.fromLTWH(
0,
0,
scrollRenderBox.size.width,
scrollRenderBox.size.height,
),
);
assert(
globalRect.size.width >= _dragTargetRelatedToScrollOrigin.size.width &&
globalRect.size.height >=
_dragTargetRelatedToScrollOrigin.size.height,
'Drag target size is larger than scrollable size, which may cause bouncing',
);
_scrolling = true;
double? newOffset;
const double overDragMax = 20.0;
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
final Offset viewportOrigin = globalRect.topLeft.translate(
deltaToOrigin.dx,
deltaToOrigin.dy,
);
final double viewportStart = _offsetExtent(
viewportOrigin,
_scrollDirection,
);
final double viewportEnd =
viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
final double proxyStart = _offsetExtent(
_dragTargetRelatedToScrollOrigin.topLeft,
_scrollDirection,
);
final double proxyEnd = _offsetExtent(
_dragTargetRelatedToScrollOrigin.bottomRight,
_scrollDirection,
);
switch (_axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
if (proxyEnd > viewportEnd &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyStart < viewportStart &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(
viewportStart - proxyStart,
overDragMax,
);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
case AxisDirection.right:
case AxisDirection.down:
if (proxyStart < viewportStart &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(
viewportStart - proxyStart,
overDragMax,
);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyEnd > viewportEnd &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
}
if (newOffset == null ||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
// Drag should not trigger scroll.
_scrolling = false;
return;
}
final Duration duration = Duration(
milliseconds: (1000 / velocityScalar).round(),
);
await scrollable.position.animateTo(
newOffset,
duration: duration,
curve: Curves.linear,
);
onScrollViewScrolled?.call();
if (_scrolling) {
await _scroll();
}
}
}

View File

@@ -0,0 +1,210 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/material.dart';
///
/// @docImport 'overscroll_indicator.dart';
/// @docImport 'viewport.dart';
// ignore_for_file: dangling_library_doc_comments
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide ScrollableState;
/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close
/// to its edge.
///
/// The scroll velocity is controlled by the [velocityScalar]:
///
/// velocity = (distance of overscroll) * [velocityScalar].
class EdgeDraggingAutoScroller {
/// Creates a auto scroller that scrolls the [scrollable].
EdgeDraggingAutoScroller(
this.scrollable, {
this.onScrollViewScrolled,
required this.velocityScalar,
});
/// The [CustomScrollable] this auto scroller is scrolling.
final ScrollableState scrollable;
/// Called when a scroll view is scrolled.
///
/// The scroll view may be scrolled multiple times in a row until the drag
/// target no longer triggers the auto scroll. This callback will be called
/// in between each scroll.
final VoidCallback? onScrollViewScrolled;
/// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
/// The velocity scalar per pixel over scroll.
///
/// It represents how the velocity scale with the over scroll distance. The
/// auto-scroll velocity = (distance of overscroll) * velocityScalar.
/// {@endtemplate}
final double velocityScalar;
late Rect _dragTargetRelatedToScrollOrigin;
/// Whether the auto scroll is in progress.
bool get scrolling => _scrolling;
bool _scrolling = false;
double _offsetExtent(Offset offset, Axis scrollDirection) {
return switch (scrollDirection) {
Axis.horizontal => offset.dx,
Axis.vertical => offset.dy,
};
}
double _sizeExtent(Size size, Axis scrollDirection) {
return switch (scrollDirection) {
Axis.horizontal => size.width,
Axis.vertical => size.height,
};
}
AxisDirection get _axisDirection => scrollable.axisDirection;
Axis get _scrollDirection => axisDirectionToAxis(_axisDirection);
/// Starts the auto scroll if the [dragTarget] is close to the edge.
///
/// The scroll starts to scroll the [scrollable] if the target rect is close
/// to the edge of the [scrollable]; otherwise, it remains stationary.
///
/// If the scrollable is already scrolling, calling this method updates the
/// previous dragTarget to the new value and continues scrolling if necessary.
void startAutoScrollIfNecessary(Rect dragTarget) {
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
_dragTargetRelatedToScrollOrigin = dragTarget.translate(
deltaToOrigin.dx,
deltaToOrigin.dy,
);
if (_scrolling) {
// The change will be picked up in the next scroll.
return;
}
assert(!_scrolling);
_scroll();
}
/// Stop any ongoing auto scrolling.
void stopAutoScroll() {
_scrolling = false;
}
Future<void> _scroll() async {
final RenderBox scrollRenderBox =
scrollable.context.findRenderObject()! as RenderBox;
final Matrix4 transform = scrollRenderBox.getTransformTo(null);
final Rect globalRect = MatrixUtils.transformRect(
transform,
Rect.fromLTRB(
0,
0,
scrollRenderBox.size.width,
scrollRenderBox.size.height,
),
);
final Rect transformedDragTarget = MatrixUtils.transformRect(
transform,
_dragTargetRelatedToScrollOrigin,
);
assert(
(globalRect.size.width + precisionErrorTolerance) >=
transformedDragTarget.size.width &&
(globalRect.size.height + precisionErrorTolerance) >=
transformedDragTarget.size.height,
'Drag target size is larger than scrollable size, which may cause bouncing',
);
_scrolling = true;
double? newOffset;
const double overDragMax = 20.0;
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
final Offset viewportOrigin = globalRect.topLeft.translate(
deltaToOrigin.dx,
deltaToOrigin.dy,
);
final double viewportStart = _offsetExtent(
viewportOrigin,
_scrollDirection,
);
final double viewportEnd =
viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
final double proxyStart = _offsetExtent(
_dragTargetRelatedToScrollOrigin.topLeft,
_scrollDirection,
);
final double proxyEnd = _offsetExtent(
_dragTargetRelatedToScrollOrigin.bottomRight,
_scrollDirection,
);
switch (_axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
if (proxyEnd > viewportEnd &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyStart < viewportStart &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(
viewportStart - proxyStart,
overDragMax,
);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
case AxisDirection.right:
case AxisDirection.down:
if (proxyStart < viewportStart &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(
viewportStart - proxyStart,
overDragMax,
);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyEnd > viewportEnd &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
}
if (newOffset == null ||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
// Drag should not trigger scroll.
_scrolling = false;
return;
}
final Duration duration = Duration(
milliseconds: (1000 / velocityScalar).round(),
);
await scrollable.position.animateTo(
newOffset,
duration: duration,
curve: Curves.linear,
);
onScrollViewScrolled?.call();
if (_scrolling) {
await _scroll();
}
}
}

View File

@@ -2,12 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsRole; import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:PiliPlus/common/widgets/page/page_view.dart';
import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/gestures.dart'
import 'package:flutter/material.dart' hide TabBarView, PageView; show DragStartBehavior, HorizontalDragGestureRecognizer;
import 'package:flutter/material.dart' hide PageView;
/// A page view that displays the widget which corresponds to the currently /// A page view that displays the widget which corresponds to the currently
/// selected tab. /// selected tab.
@@ -23,11 +22,12 @@ import 'package:flutter/material.dart' hide TabBarView, PageView;
/// [children] list and the length of the [TabBar.tabs] list. /// [children] list and the length of the [TabBar.tabs] list.
/// ///
/// To see a sample implementation, visit the [TabController] documentation. /// To see a sample implementation, visit the [TabController] documentation.
class CustomTabBarView extends StatefulWidget { class TabBarView<T extends HorizontalDragGestureRecognizer>
extends StatefulWidget {
/// Creates a page view with one child per tab. /// Creates a page view with one child per tab.
/// ///
/// The length of [children] must be the same as the [controller]'s length. /// The length of [children] must be the same as the [controller]'s length.
const CustomTabBarView({ const TabBarView({
super.key, super.key,
required this.children, required this.children,
this.controller, this.controller,
@@ -35,13 +35,10 @@ class CustomTabBarView extends StatefulWidget {
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0, this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.scrollDirection = Axis.horizontal, required this.horizontalDragGestureRecognizer,
this.header,
this.bgColor = Colors.transparent,
}); });
final Widget? header; final T horizontalDragGestureRecognizer;
final Color bgColor;
/// This widget's selection and animation state. /// This widget's selection and animation state.
/// ///
@@ -77,13 +74,12 @@ class CustomTabBarView extends StatefulWidget {
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
final Clip clipBehavior; final Clip clipBehavior;
final Axis scrollDirection;
@override @override
State<CustomTabBarView> createState() => _CustomTabBarViewState(); State<TabBarView<T>> createState() => _TabBarViewState<T>();
} }
class _CustomTabBarViewState extends State<CustomTabBarView> { class _TabBarViewState<T extends HorizontalDragGestureRecognizer>
extends State<TabBarView<T>> {
TabController? _controller; TabController? _controller;
PageController? _pageController; PageController? _pageController;
late List<Widget> _childrenWithKey; late List<Widget> _childrenWithKey;
@@ -168,7 +164,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
} }
@override @override
void didUpdateWidget(CustomTabBarView oldWidget) { void didUpdateWidget(TabBarView<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) { if (widget.controller != oldWidget.controller) {
_updateTabController(); _updateTabController();
@@ -203,7 +199,7 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
void _updateChildren() { void _updateChildren() {
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList( _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
widget.children.map<Widget>((Widget child) { widget.children.map<Widget>((Widget child) {
return Semantics(role: SemanticsRole.tabPanel, child: child); return Semantics(role: .tabPanel, child: child);
}).toList(), }).toList(),
); );
} }
@@ -361,16 +357,14 @@ class _CustomTabBarViewState extends State<CustomTabBarView> {
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification, onNotification: _handleScrollNotification,
child: CustomPageView( child: PageView<T>(
scrollDirection: widget.scrollDirection,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
clipBehavior: widget.clipBehavior, clipBehavior: widget.clipBehavior,
controller: _pageController, controller: _pageController,
physics: widget.physics == null physics: widget.physics == null
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics()) ? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
: const PageScrollPhysics().applyTo(widget.physics), : const PageScrollPhysics().applyTo(widget.physics),
header: widget.header, horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer,
bgColor: widget.bgColor,
children: _childrenWithKey, children: _childrenWithKey,
), ),
); );

View File

@@ -0,0 +1,44 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
abstract class PopScopeState<T extends StatefulWidget> extends State<T>
implements PopEntry<T> {
ModalRoute<dynamic>? _route;
@override
void onPopInvoked(bool didPop) {}
@override
late final ValueNotifier<bool> canPopNotifier;
void initCanPopNotifier() {
canPopNotifier = ValueNotifier<bool>(false);
}
@override
void initState() {
super.initState();
initCanPopNotifier();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopEntry(this);
_route = nextRoute;
_route?.registerPopEntry(this);
}
}
@override
void dispose() {
_route?.unregisterPopEntry(this);
canPopNotifier.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,157 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library;
import 'package:flutter/material.dart';
class CustomPopupMenuItem<T> extends PopupMenuEntry<T> {
const CustomPopupMenuItem({
super.key,
this.value,
this.height = kMinInteractiveDimension,
required this.child,
});
final T? value;
@override
final double height;
final Widget? child;
@override
bool represents(T? value) => value == this.value;
@override
CustomPopupMenuItemState<T, CustomPopupMenuItem<T>> createState() =>
CustomPopupMenuItemState<T, CustomPopupMenuItem<T>>();
}
class CustomPopupMenuItemState<T, W extends CustomPopupMenuItem<T>>
extends State<W> {
@protected
@override
Widget build(BuildContext context) {
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
const Set<WidgetState> states = <WidgetState>{};
final style =
popupMenuTheme.labelTextStyle?.resolve(states)! ??
_PopupMenuDefaultsM3(context).labelTextStyle!.resolve(states)!;
return ListTileTheme.merge(
contentPadding: .zero,
titleTextStyle: style,
child: AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: widget.height),
child: Padding(
padding: _PopupMenuDefaultsM3.menuItemPadding,
child: Align(
alignment: AlignmentDirectional.centerStart,
child: widget.child,
),
),
),
),
);
}
}
class CustomPopupMenuDivider extends PopupMenuEntry<Never> {
const CustomPopupMenuDivider({
super.key,
required this.height,
this.thickness,
this.indent,
this.endIndent,
this.radius,
});
@override
final double height;
final double? thickness;
final double? indent;
final double? endIndent;
final BorderRadiusGeometry? radius;
@override
bool represents(void value) => false;
@override
State<CustomPopupMenuDivider> createState() => _CustomPopupMenuDividerState();
}
class _CustomPopupMenuDividerState extends State<CustomPopupMenuDivider> {
@override
Widget build(BuildContext context) {
return Divider(
height: widget.height,
thickness: widget.thickness,
indent: widget.indent,
color: ColorScheme.of(context).outline.withValues(alpha: 0.2),
endIndent: widget.endIndent,
radius: widget.radius,
);
}
}
// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// dart format off
class _PopupMenuDefaultsM3 extends PopupMenuThemeData {
_PopupMenuDefaultsM3(this.context)
: super(elevation: 3.0);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
late final TextTheme _textTheme = _theme.textTheme;
@override WidgetStateProperty<TextStyle?>? get labelTextStyle {
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
// TODO(quncheng): Update this hard-coded value to use the latest tokens.
final TextStyle style = _textTheme.labelLarge!;
if (states.contains(WidgetState.disabled)) {
return style.apply(color: _colors.onSurface.withValues(alpha: 0.38));
}
return style.apply(color: _colors.onSurface);
});
}
@override
Color? get color => _colors.surfaceContainer;
@override
Color? get shadowColor => _colors.shadow;
@override
Color? get surfaceTintColor => Colors.transparent;
@override
ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
// TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs
// Update this when the token is available.
@override
EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0);
// TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs
// Update this when the token is available.
static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0);
}// dart format on
// END GENERATED TOKEN PROPERTIES - PopupMenu

View File

@@ -1,3 +1,12 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: uri_does_not_exist_in_doc_import
/// @docImport 'color_scheme.dart';
library;
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
@@ -6,25 +15,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart' hide RefreshIndicator; import 'package:flutter/material.dart' hide RefreshIndicator;
Widget refreshIndicator({ double displacement = Pref.refreshDisplacement;
required RefreshCallback onRefresh,
required Widget child,
}) {
return RefreshIndicator(
displacement: displacement,
onRefresh: onRefresh,
child: child,
);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The over-scroll distance that moves the indicator to its maximum // The over-scroll distance that moves the indicator to its maximum
// displacement, as a percentage of the scrollable's container extent. // displacement, as a percentage of the scrollable's container extent.
double displacement = Pref.refreshDisplacement;
double kDragContainerExtentPercentage = Pref.refreshDragPercentage; double kDragContainerExtentPercentage = Pref.refreshDragPercentage;
// How much the scroll's drag gesture can overshoot the RefreshIndicator's // How much the scroll's drag gesture can overshoot the RefreshIndicator's
@@ -403,15 +397,15 @@ class RefreshIndicatorState extends State<RefreshIndicator>
_effectiveValueColor = _effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary; widget.color ?? Theme.of(context).colorScheme.primary;
final Color color = _effectiveValueColor; final Color color = _effectiveValueColor;
if (color.alpha == 0x00) { if (color.a == 0) {
// Set an always stopped animation instead of a driven tween. // Set an always stopped animation instead of a driven tween.
_valueColor = AlwaysStoppedAnimation<Color>(color); _valueColor = AlwaysStoppedAnimation<Color>(color);
} else { } else {
// Respect the alpha of the given color. // Respect the alpha of the given color.
_valueColor = _positionController.drive( _valueColor = _positionController.drive(
ColorTween( ColorTween(
begin: color.withAlpha(0), begin: color.withValues(alpha: 0),
end: color.withAlpha(color.alpha), end: color,
).chain( ).chain(
CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)), CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)),
), ),
@@ -555,7 +549,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
1.0, 1.0,
); // This triggers various rebuilds. ); // This triggers various rebuilds.
if (_status == RefreshIndicatorStatus.drag && if (_status == RefreshIndicatorStatus.drag &&
_valueColor.value!.alpha == _effectiveValueColor.alpha) { _valueColor.value!.a == _effectiveValueColor.a) {
_status = RefreshIndicatorStatus.armed; _status = RefreshIndicatorStatus.armed;
widget.onStatusChange?.call(_status); widget.onStatusChange?.call(_status);
} }
@@ -749,7 +743,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
} }
case _IndicatorType.noSpinner: case _IndicatorType.noSpinner:
return Container(); return const SizedBox.shrink();
} }
}, },
), ),
@@ -762,3 +756,14 @@ class RefreshIndicatorState extends State<RefreshIndicator>
); );
} }
} }
Widget refreshIndicator({
required RefreshCallback onRefresh,
required Widget child,
}) {
return RefreshIndicator(
displacement: displacement,
onRefresh: onRefresh,
child: child,
);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,147 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_region.dart';
import 'package:flutter/cupertino.dart'
hide
SelectableRegion,
SelectableRegionState,
SelectableRegionContextMenuBuilder;
import 'package:flutter/material.dart'
hide
SelectableRegion,
SelectableRegionState,
SelectableRegionContextMenuBuilder;
import 'package:flutter/rendering.dart';
/// A widget that introduces an area for user selections with adaptive selection
/// controls.
///
/// This widget creates a [SelectableRegion] with platform-adaptive selection
/// controls.
///
/// Flutter widgets are not selectable by default. To enable selection for
/// a specific screen, consider wrapping the body of the [Route] with a
/// [SelectionArea].
///
/// The [SelectionArea] widget must have a [Localizations] ancestor that
/// contains a [MaterialLocalizations] delegate; using the [MaterialApp] widget
/// ensures that such an ancestor is present.
///
/// {@tool dartpad}
/// This example shows how to make a screen selectable.
///
/// ** See code in examples/api/lib/material/selection_area/selection_area.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [SelectableRegion], which provides an overview of the selection system.
/// * [SelectableText], which enables selection on a single run of text.
/// * [SelectionListener], which enables accessing the [SelectionDetails] of
/// the selectable subtree it wraps.
class SelectionArea extends StatefulWidget {
/// Creates a [SelectionArea].
///
/// If [selectionControls] is null, a platform specific one is used.
const SelectionArea({
super.key,
this.focusNode,
this.selectionControls,
this.contextMenuBuilder = _defaultContextMenuBuilder,
this.magnifierConfiguration,
this.onSelectionChanged,
required this.child,
});
/// The configuration for the magnifier in the selection region.
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
/// on Android, and builds nothing on all other platforms. To suppress the
/// magnifier, consider passing [TextMagnifierConfiguration.disabled].
///
/// {@macro flutter.widgets.magnifier.intro}
final TextMagnifierConfiguration? magnifierConfiguration;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// The delegate to build the selection handles and toolbar.
///
/// If it is null, the platform specific selection control is used.
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, will build a default menu based on the ambient
/// [ThemeData.platform].
///
/// {@tool dartpad}
/// This example shows how to build a custom context menu for any selected
/// content in a SelectionArea.
///
/// ** See code in examples/api/lib/material/context_menu/selectable_region_toolbar_builder.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [AdaptiveTextSelectionToolbar], which is built by default.
final SelectableRegionContextMenuBuilder? contextMenuBuilder;
/// Called when the selected content changes.
final ValueChanged<SelectedContent?>? onSelectionChanged;
/// The child widget this selection area applies to.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
static Widget _defaultContextMenuBuilder(
BuildContext context,
SelectableRegionState selectableRegionState,
) => AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: selectableRegionState.contextMenuButtonItems,
anchors: selectableRegionState.contextMenuAnchors,
);
@override
State<StatefulWidget> createState() => SelectionAreaState();
}
/// State for a [SelectionArea].
class SelectionAreaState extends State<SelectionArea> {
final GlobalKey<SelectableRegionState> _selectableRegionKey =
GlobalKey<SelectableRegionState>();
/// The [State] of the [SelectableRegion] for which this [SelectionArea] wraps.
SelectableRegionState get selectableRegion =>
_selectableRegionKey.currentState!;
@protected
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final TextSelectionControls controls =
widget.selectionControls ??
switch (Theme.of(context).platform) {
TargetPlatform.android ||
TargetPlatform.fuchsia => materialTextSelectionHandleControls,
TargetPlatform.linux ||
TargetPlatform.windows => desktopTextSelectionHandleControls,
TargetPlatform.iOS => cupertinoTextSelectionHandleControls,
TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls,
};
return SelectableRegion(
key: _selectableRegionKey,
selectionControls: controls,
focusNode: widget.focusNode,
contextMenuBuilder: widget.contextMenuBuilder,
magnifierConfiguration:
widget.magnifierConfiguration ??
TextMagnifier.adaptiveMagnifierConfiguration,
onSelectionChanged: widget.onSelectionChanged,
child: widget.child,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart';
import 'package:flutter/material.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/selection_area.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart' hide SelectableText, SelectionArea;
Widget selectableText( Widget selectableText(
String text, { String text, {
TextStyle? style, TextStyle? style,
}) { }) {
if (Utils.isDesktop) { if (PlatformUtils.isDesktop) {
return SelectionArea( return SelectionArea(
child: Text( child: Text(
style: style, style: style,
@@ -16,6 +18,7 @@ Widget selectableText(
return SelectableText( return SelectableText(
style: style, style: style,
text, text,
scrollPhysics: const NeverScrollableScrollPhysics(),
); );
} }
@@ -23,7 +26,7 @@ Widget selectableRichText(
TextSpan textSpan, { TextSpan textSpan, {
TextStyle? style, TextStyle? style,
}) { }) {
if (Utils.isDesktop) { if (PlatformUtils.isDesktop) {
return SelectionArea( return SelectionArea(
child: Text.rich( child: Text.rich(
style: style, style: style,
@@ -34,5 +37,6 @@ Widget selectableRichText(
return SelectableText.rich( return SelectableText.rich(
style: style, style: style,
textSpan, textSpan,
scrollPhysics: const NeverScrollableScrollPhysics(),
); );
} }

View File

@@ -0,0 +1,422 @@
// 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:math' as math;
import 'package:PiliPlus/common/widgets/flutter/selectable_text/tap_and_drag.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'
hide
BaseTapAndDragGestureRecognizer,
TapAndHorizontalDragGestureRecognizer,
TapAndPanGestureRecognizer;
import 'package:flutter/material.dart';
class CustomTextSelectionGestureDetectorBuilder
extends TextSelectionGestureDetectorBuilder {
CustomTextSelectionGestureDetectorBuilder({required super.delegate});
@override
Widget buildGestureDetector({
Key? key,
HitTestBehavior? behavior,
required Widget child,
}) {
return TextSelectionGestureDetector(
key: key,
onTapTrackStart: onTapTrackStart,
onTapTrackReset: onTapTrackReset,
onTapDown: onTapDown,
onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
onSecondaryTap: onSecondaryTap,
onSecondaryTapDown: onSecondaryTapDown,
onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel,
onUserTap: onUserTap,
onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
onSingleLongTapCancel: onSingleLongTapCancel,
onDoubleTapDown: onDoubleTapDown,
onTripleTapDown: onTripleTapDown,
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
onUserTapAlwaysCalled: onUserTapAlwaysCalled,
behavior: behavior,
child: child,
);
}
}
/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
/// double tap will only recognize one or the other. This widget detects both:
/// the first tap and then any subsequent taps that occurs within a time limit
/// after the first.
///
/// See also:
///
/// * [TextField], a Material text field which uses this gesture detector.
/// * [CupertinoTextField], a Cupertino text field which uses this gesture
/// detector.
class TextSelectionGestureDetector extends StatefulWidget {
/// Create a [TextSelectionGestureDetector].
///
/// Multiple callbacks can be called for one sequence of input gesture.
const TextSelectionGestureDetector({
super.key,
this.onTapTrackStart,
this.onTapTrackReset,
this.onTapDown,
this.onForcePressStart,
this.onForcePressEnd,
this.onSecondaryTap,
this.onSecondaryTapDown,
this.onSingleTapUp,
this.onSingleTapCancel,
this.onUserTap,
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.onSingleLongTapCancel,
this.onDoubleTapDown,
this.onTripleTapDown,
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
this.onUserTapAlwaysCalled = false,
this.behavior,
required this.child,
});
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackStart}
/// Callback used to indicate that a tap tracking has started upon
/// a [PointerDownEvent].
/// {@endtemplate}
final VoidCallback? onTapTrackStart;
/// {@template flutter.gestures.selectionrecognizers.TextSelectionGestureDetector.onTapTrackReset}
/// Callback used to indicate that a tap tracking has been reset which
/// happens on the next [PointerDownEvent] after the timer between two taps
/// elapses, the recognizer loses the arena, the gesture is cancelled or
/// the recognizer is disposed of.
/// {@endtemplate}
final VoidCallback? onTapTrackReset;
/// Called for every tap down including every tap down that's part of a
/// double click or a long press, except touches that include enough movement
/// to not qualify as taps (e.g. pans and flings).
final GestureTapDragDownCallback? onTapDown;
/// Called when a pointer has tapped down and the force of the pointer has
/// just become greater than [ForcePressGestureRecognizer.startPressure].
final GestureForcePressStartCallback? onForcePressStart;
/// Called when a pointer that had previously triggered [onForcePressStart] is
/// lifted off the screen.
final GestureForcePressEndCallback? onForcePressEnd;
/// Called for a tap event with the secondary mouse button.
final GestureTapCallback? onSecondaryTap;
/// Called for a tap down event with the secondary mouse button.
final GestureTapDownCallback? onSecondaryTapDown;
/// Called for the first tap in a series of taps, consecutive taps do not call
/// this method.
///
/// For example, if the detector was configured with [onTapDown] and
/// [onDoubleTapDown], three quick taps would be recognized as a single tap
/// down, followed by a tap up, then a double tap down, followed by a single tap down.
final GestureTapDragUpCallback? onSingleTapUp;
/// Called for each touch that becomes recognized as a gesture that is not a
/// short tap, such as a long tap or drag. It is called at the moment when
/// another gesture from the touch is recognized.
final GestureCancelCallback? onSingleTapCancel;
/// Called for the first tap in a series of taps when [onUserTapAlwaysCalled] is
/// disabled, which is the default behavior.
///
/// When [onUserTapAlwaysCalled] is enabled, this is called for every tap,
/// including consecutive taps.
final GestureTapCallback? onUserTap;
/// Called for a single long tap that's sustained for longer than
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
/// double-tap-hold, which calls [onDoubleTapDown] instead.
final GestureLongPressStartCallback? onSingleLongTapStart;
/// Called after [onSingleLongTapStart] when the pointer is dragged.
final GestureLongPressMoveUpdateCallback? onSingleLongTapMoveUpdate;
/// Called after [onSingleLongTapStart] when the pointer is lifted.
final GestureLongPressEndCallback? onSingleLongTapEnd;
/// Called after [onSingleLongTapStart] when the pointer is canceled.
final GestureLongPressCancelCallback? onSingleLongTapCancel;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous short tap.
final GestureTapDragDownCallback? onDoubleTapDown;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous double-tap.
final GestureTapDragDownCallback? onTripleTapDown;
/// Called when a mouse starts dragging to select text.
final GestureTapDragStartCallback? onDragSelectionStart;
/// Called repeatedly as a mouse moves while dragging.
final GestureTapDragUpdateCallback? onDragSelectionUpdate;
/// Called when a mouse that was previously dragging is released.
final GestureTapDragEndCallback? onDragSelectionEnd;
/// Whether [onUserTap] will be called for all taps including consecutive taps.
///
/// Defaults to false, so [onUserTap] is only called for each distinct tap.
final bool onUserTapAlwaysCalled;
/// How this gesture detector should behave during hit testing.
///
/// This defaults to [HitTestBehavior.deferToChild].
final HitTestBehavior? behavior;
/// Child below this widget.
final Widget child;
@override
State<StatefulWidget> createState() => _TextSelectionGestureDetectorState();
}
class _TextSelectionGestureDetectorState
extends State<TextSelectionGestureDetector> {
// Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
// which can grow to be infinitely large, to a value between 1 and 3. The value
// that the raw count is converted to is based on the default observed behavior
// on the native platforms.
//
// This method should be used in all instances when details.consecutiveTapCount
// would be used.
static int _getEffectiveConsecutiveTapCount(int rawCount) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
// From observation, these platform's reset their tap count to 0 when
// the number of consecutive taps exceeds 3. For example on Debian Linux
// with GTK, when going past a triple click, on the fourth click the
// selection is moved to the precise click position, on the fifth click
// the word at the position is selected, and on the sixth click the
// paragraph at the position is selected.
return rawCount <= 3
? rawCount
: (rawCount % 3 == 0 ? 3 : rawCount % 3);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// From observation, these platform's either hold their tap count at 3.
// For example on macOS, when going past a triple click, the selection
// should be retained at the paragraph that was first selected on triple
// click.
return math.min(rawCount, 3);
case TargetPlatform.windows:
// From observation, this platform's consecutive tap actions alternate
// between double click and triple click actions. For example, after a
// triple click has selected a paragraph, on the next click the word at
// the clicked position will be selected, and on the next click the
// paragraph at the position is selected.
return rawCount < 2 ? rawCount : 2 + rawCount % 2;
}
}
void _handleTapTrackStart() {
widget.onTapTrackStart?.call();
}
void _handleTapTrackReset() {
widget.onTapTrackReset?.call();
}
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
void _handleTapDown(TapDragDownDetails details) {
widget.onTapDown?.call(details);
// This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
return widget.onDoubleTapDown?.call(details);
}
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
return widget.onTripleTapDown?.call(details);
}
}
void _handleTapUp(TapDragUpDetails details) {
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
widget.onSingleTapUp?.call(details);
widget.onUserTap?.call();
} else if (widget.onUserTapAlwaysCalled) {
widget.onUserTap?.call();
}
}
void _handleTapCancel() {
widget.onSingleTapCancel?.call();
}
void _handleDragStart(TapDragStartDetails details) {
widget.onDragSelectionStart?.call(details);
}
void _handleDragUpdate(TapDragUpdateDetails details) {
widget.onDragSelectionUpdate?.call(details);
}
void _handleDragEnd(TapDragEndDetails details) {
widget.onDragSelectionEnd?.call(details);
}
void _forcePressStarted(ForcePressDetails details) {
widget.onForcePressStart?.call(details);
}
void _forcePressEnded(ForcePressDetails details) {
widget.onForcePressEnd?.call(details);
}
void _handleLongPressStart(LongPressStartDetails details) {
widget.onSingleLongTapStart?.call(details);
}
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
widget.onSingleLongTapMoveUpdate?.call(details);
}
void _handleLongPressEnd(LongPressEndDetails details) {
widget.onSingleLongTapEnd?.call(details);
}
void _handleLongPressCancel() {
widget.onSingleLongTapCancel?.call();
}
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures =
<Type, GestureRecognizerFactory>{};
gestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onSecondaryTap = widget.onSecondaryTap
..onSecondaryTapDown = widget.onSecondaryTapDown;
},
);
if (widget.onSingleLongTapStart != null ||
widget.onSingleLongTapMoveUpdate != null ||
widget.onSingleLongTapEnd != null ||
widget.onSingleLongTapCancel != null) {
gestures[LongPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
debugOwner: this,
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.touch},
),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = _handleLongPressStart
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
..onLongPressEnd = _handleLongPressEnd
..onLongPressCancel = _handleLongPressCancel;
},
);
}
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
gestures[TapAndHorizontalDragGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<
TapAndHorizontalDragGestureRecognizer
>(
() => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
(TapAndHorizontalDragGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..eagerVictoryOnDrag =
defaultTargetPlatform != TargetPlatform.iOS
..onTapTrackStart = _handleTapTrackStart
..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
gestures[TapAndPanGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
() => TapAndPanGestureRecognizer(debugOwner: this),
(TapAndPanGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..onTapTrackStart = _handleTapTrackStart
..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
}
}
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
instance
..onStart = widget.onForcePressStart != null
? _forcePressStarted
: null
..onEnd = widget.onForcePressEnd != null
? _forcePressEnded
: null;
},
);
}
return RawGestureDetector(
gestures: gestures,
excludeFromSemantics: true,
behavior: widget.behavior,
child: widget.child,
);
}
}

View File

@@ -9,7 +9,6 @@
/// @docImport 'editable.dart'; /// @docImport 'editable.dart';
library; library;
import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' import 'dart:ui'
as ui as ui
@@ -46,6 +45,15 @@ typedef _TextBoundaryAtPositionInText =
const String _kEllipsis = '\u2026'; const String _kEllipsis = '\u2026';
class _UnspecifiedTextScaler extends TextScaler {
const _UnspecifiedTextScaler();
@override
Never get textScaleFactor => throw UnimplementedError();
@override
Never scale(double fontSize) => throw UnimplementedError();
}
/// A render object that displays a paragraph of text. /// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox class RenderParagraph extends RenderBox
with with
@@ -68,7 +76,7 @@ class RenderParagraph extends RenderBox
'This feature was deprecated after v3.12.0-2.0.pre.', 'This feature was deprecated after v3.12.0-2.0.pre.',
) )
double textScaleFactor = 1.0, double textScaleFactor = 1.0,
TextScaler textScaler = TextScaler.noScaling, TextScaler textScaler = const _UnspecifiedTextScaler(),
int? maxLines, int? maxLines,
Locale? locale, Locale? locale,
StrutStyle? strutStyle, StrutStyle? strutStyle,
@@ -78,18 +86,16 @@ class RenderParagraph extends RenderBox
Color? selectionColor, Color? selectionColor,
SelectionRegistrar? registrar, SelectionRegistrar? registrar,
required Color primary, required Color primary,
VoidCallback? onShowMore,
}) : assert(text.debugAssertIsValid()), }) : assert(text.debugAssertIsValid()),
assert(maxLines == null || maxLines > 0),
assert( assert(
maxLines == null || identical(textScaler, const _UnspecifiedTextScaler()) ||
(maxLines > 0 && textScaleFactor == 1.0,
overflow != TextOverflow.ellipsis &&
overflow != TextOverflow.fade),
),
assert(
identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.', 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
), ),
_primary = primary, _primary = primary,
_onShowMore = onShowMore,
_softWrap = softWrap, _softWrap = softWrap,
_overflow = overflow, _overflow = overflow,
_selectionColor = selectionColor, _selectionColor = selectionColor,
@@ -97,7 +103,7 @@ class RenderParagraph extends RenderBox
text: text, text: text,
textAlign: textAlign, textAlign: textAlign,
textDirection: textDirection, textDirection: textDirection,
textScaler: textScaler == TextScaler.noScaling textScaler: textScaler == const _UnspecifiedTextScaler()
? TextScaler.linear(textScaleFactor) ? TextScaler.linear(textScaleFactor)
: textScaler, : textScaler,
maxLines: maxLines, maxLines: maxLines,
@@ -144,7 +150,22 @@ class RenderParagraph extends RenderBox
/// The text to display. /// The text to display.
InlineSpan get text => _textPainter.text!; InlineSpan get text => _textPainter.text!;
set text(InlineSpan value) { set text(({InlineSpan text, Color primary}) params) {
final value = params.text;
_primary = params.primary;
if (_morePainter case final textPainter?) {
final textSpan = _moreTextSpan(value.style);
switch (textPainter.text!.compareTo(textSpan)) {
case RenderComparison.paint:
textPainter.text = textSpan;
case RenderComparison.layout:
textPainter
..text = textSpan
..layout();
default:
}
}
switch (_textPainter.text!.compareTo(value)) { switch (_textPainter.text!.compareTo(value)) {
case RenderComparison.identical: case RenderComparison.identical:
return; return;
@@ -294,6 +315,8 @@ class RenderParagraph extends RenderBox
_disposeSelectableFragments(); _disposeSelectableFragments();
_textPainter.dispose(); _textPainter.dispose();
_textIntrinsicsCache?.dispose(); _textIntrinsicsCache?.dispose();
_tapGestureRecognizer?.dispose();
_tapGestureRecognizer = null;
_morePainter?.dispose(); _morePainter?.dispose();
_morePainter = null; _morePainter = null;
super.dispose(); super.dispose();
@@ -386,6 +409,9 @@ class RenderParagraph extends RenderBox
if (_textPainter.textScaler == value) { if (_textPainter.textScaler == value) {
return; return;
} }
_morePainter
?..textScaler = value
..layout();
_textPainter.textScaler = value; _textPainter.textScaler = value;
_overflowShader = null; _overflowShader = null;
markNeedsLayout(); markNeedsLayout();
@@ -553,6 +579,18 @@ class RenderParagraph extends RenderBox
@override @override
@protected @protected
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
if (_tapGestureRecognizer != null) {
if (_morePainter case final textPainter?) {
late final height = _textPainter.height;
if (position.dx < textPainter.width &&
position.dy > height &&
position.dy < height + textPainter.height) {
result.add(HitTestEntry(textPainter.text as TextSpan));
return true;
}
}
}
final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position); final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position);
// The hit-test can't fall through the horizontal gaps between visually // The hit-test can't fall through the horizontal gaps between visually
// adjacent characters on the same line, even with a large letter-spacing or // adjacent characters on the same line, even with a large letter-spacing or
@@ -673,16 +711,20 @@ class RenderParagraph extends RenderBox
Color _primary; Color _primary;
set primary(Color primary) { VoidCallback? _onShowMore;
if (_primary != primary) { set onShowMore(VoidCallback? onShowMore) {
_primary = primary; if (_onShowMore != onShowMore) {
_morePainter?.text = _moreTextSpan; _onShowMore = onShowMore;
_tapGestureRecognizer?.onTap = onShowMore;
} }
} }
TextSpan get _moreTextSpan => TextSpan( TapGestureRecognizer? _tapGestureRecognizer;
style: text.style!.copyWith(color: _primary),
TextSpan _moreTextSpan([TextStyle? style]) => TextSpan(
style: (style ?? text.style!).copyWith(color: _primary),
text: '查看更多', text: '查看更多',
recognizer: _tapGestureRecognizer,
); );
TextPainter? _morePainter; TextPainter? _morePainter;
@@ -709,8 +751,11 @@ class RenderParagraph extends RenderBox
size.height < textSize.height || _textPainter.didExceedMaxLines; size.height < textSize.height || _textPainter.didExceedMaxLines;
if (didOverflowHeight) { if (didOverflowHeight) {
if (_onShowMore != null) {
_tapGestureRecognizer ??= TapGestureRecognizer()..onTap = _onShowMore;
}
_morePainter ??= TextPainter( _morePainter ??= TextPainter(
text: _moreTextSpan, text: _moreTextSpan(),
textDirection: textDirection, textDirection: textDirection,
textScaler: textScaler, textScaler: textScaler,
locale: locale, locale: locale,
@@ -812,6 +857,11 @@ class RenderParagraph extends RenderBox
} }
} }
assert(() {
_textPainter.debugPaintTextLayoutBoxes = debugPaintTextLayoutBoxes;
return true;
}());
_textPainter.paint(context.canvas, offset); _textPainter.paint(context.canvas, offset);
paintInlineChildren(context, offset); paintInlineChildren(context, offset);
@@ -983,8 +1033,9 @@ class RenderParagraph extends RenderBox
} }
if (needsAssembleSemanticsNode) { if (needsAssembleSemanticsNode) {
config.explicitChildNodes = true; config
config.isSemanticBoundary = true; ..explicitChildNodes = true
..isSemanticBoundary = true;
} else if (needsChildConfigurationsDelegate) { } else if (needsChildConfigurationsDelegate) {
config.childConfigurationsDelegate = config.childConfigurationsDelegate =
_childSemanticsConfigurationsDelegate; _childSemanticsConfigurationsDelegate;
@@ -1013,8 +1064,9 @@ class RenderParagraph extends RenderBox
AttributedString(buffer.toString(), attributes: attributes), AttributedString(buffer.toString(), attributes: attributes),
]; ];
} }
config.attributedLabel = _cachedAttributedLabels![0]; config
config.textDirection = textDirection; ..attributedLabel = _cachedAttributedLabels![0]
..textDirection = textDirection;
} }
} }
@@ -1096,7 +1148,7 @@ class RenderParagraph extends RenderBox
// can be re-used when [assembleSemanticsNode] is called again. This ensures // can be re-used when [assembleSemanticsNode] is called again. This ensures
// stable ids for the [SemanticsNode]s of [TextSpan]s across // stable ids for the [SemanticsNode]s of [TextSpan]s across
// [assembleSemanticsNode] invocations. // [assembleSemanticsNode] invocations.
LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes; Map<Key, SemanticsNode>? _cachedChildNodes;
@override @override
void assembleSemanticsNode( void assembleSemanticsNode(
@@ -1113,8 +1165,7 @@ class RenderParagraph extends RenderBox
int placeholderIndex = 0; int placeholderIndex = 0;
int childIndex = 0; int childIndex = 0;
RenderBox? child = firstChild; RenderBox? child = firstChild;
final LinkedHashMap<Key, SemanticsNode> newChildCache = final Map<Key, SemanticsNode> newChildCache = <Key, SemanticsNode>{};
LinkedHashMap<Key, SemanticsNode>();
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!); _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
for (final InlineSpanSemanticsInformation info for (final InlineSpanSemanticsInformation info
in _cachedCombinedSemanticsInfos!) { in _cachedCombinedSemanticsInfos!) {
@@ -1184,8 +1235,9 @@ class RenderParagraph extends RenderBox
onDoubleTap: final VoidCallback? handler, onDoubleTap: final VoidCallback? handler,
): ):
if (handler != null) { if (handler != null) {
configuration.onTap = handler; configuration
configuration.isLink = true; ..onTap = handler
..isLink = true;
} }
case LongPressGestureRecognizer( case LongPressGestureRecognizer(
onLongPress: final GestureLongPressCallback? onLongPress, onLongPress: final GestureLongPressCallback? onLongPress,
@@ -1255,29 +1307,30 @@ class RenderParagraph extends RenderBox
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(EnumProperty<TextAlign>('textAlign', textAlign)); properties
properties.add(EnumProperty<TextDirection>('textDirection', textDirection)); ..add(EnumProperty<TextAlign>('textAlign', textAlign))
properties.add( ..add(EnumProperty<TextDirection>('textDirection', textDirection))
FlagProperty( ..add(
'softWrap', FlagProperty(
value: softWrap, 'softWrap',
ifTrue: 'wrapping at box width', value: softWrap,
ifFalse: 'no wrapping except at line break characters', ifTrue: 'wrapping at box width',
showName: true, ifFalse: 'no wrapping except at line break characters',
), showName: true,
); ),
properties.add(EnumProperty<TextOverflow>('overflow', overflow)); )
properties.add( ..add(EnumProperty<TextOverflow>('overflow', overflow))
DiagnosticsProperty<TextScaler>( ..add(
'textScaler', DiagnosticsProperty<TextScaler>(
textScaler, 'textScaler',
defaultValue: TextScaler.noScaling, textScaler,
), defaultValue: TextScaler.noScaling,
); ),
properties.add( )
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null), ..add(
); DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); )
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
} }
} }
@@ -1738,8 +1791,7 @@ class _SelectableFragment
final TextPosition? existingSelectionEnd = _textSelectionEnd; final TextPosition? existingSelectionEnd = _textSelectionEnd;
_setSelectionPosition(null, isEnd: isEnd); _setSelectionPosition(null, isEnd: isEnd);
final Matrix4 transform = paragraph.getTransformTo(null); final Matrix4 transform = paragraph.getTransformTo(null)..invert();
transform.invert();
final Offset localPosition = MatrixUtils.transformPoint( final Offset localPosition = MatrixUtils.transformPoint(
transform, transform,
globalPosition, globalPosition,
@@ -1812,8 +1864,7 @@ class _SelectableFragment
required bool isEnd, required bool isEnd,
}) { }) {
_setSelectionPosition(null, isEnd: isEnd); _setSelectionPosition(null, isEnd: isEnd);
final Matrix4 transform = paragraph.getTransformTo(null); final Matrix4 transform = paragraph.getTransformTo(null)..invert();
transform.invert();
final Offset localPosition = MatrixUtils.transformPoint( final Offset localPosition = MatrixUtils.transformPoint(
transform, transform,
globalPosition, globalPosition,
@@ -2318,8 +2369,8 @@ class _SelectableFragment
existingSelectionEnd, existingSelectionEnd,
); );
} }
final Matrix4 originTransform = originParagraph.getTransformTo(null); final Matrix4 originTransform = originParagraph.getTransformTo(null)
originTransform.invert(); ..invert();
final Offset originParagraphLocalPosition = MatrixUtils.transformPoint( final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
originTransform, originTransform,
globalPosition, globalPosition,
@@ -2623,8 +2674,8 @@ class _SelectableFragment
existingSelectionEnd, existingSelectionEnd,
); );
} }
final Matrix4 originTransform = originParagraph.getTransformTo(null); final Matrix4 originTransform = originParagraph.getTransformTo(null)
originTransform.invert(); ..invert();
final Offset originParagraphLocalPosition = MatrixUtils.transformPoint( final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
originTransform, originTransform,
globalPosition, globalPosition,
@@ -3086,8 +3137,7 @@ class _SelectableFragment
RenderObject? current = paragraph; RenderObject? current = paragraph;
while (current != null) { while (current != null) {
if (current is RenderParagraph) { if (current is RenderParagraph) {
final Matrix4 currentTransform = current.getTransformTo(null); final Matrix4 currentTransform = current.getTransformTo(null)..invert();
currentTransform.invert();
final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint( final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint(
currentTransform, currentTransform,
globalPosition, globalPosition,
@@ -3779,13 +3829,14 @@ class _SelectableFragment
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add( properties
DiagnosticsProperty<String>( ..add(
'textInsideRange', DiagnosticsProperty<String>(
range.textInside(fullText), 'textInsideRange',
), range.textInside(fullText),
); ),
properties.add(DiagnosticsProperty<TextRange>('range', range)); )
properties.add(DiagnosticsProperty<String>('fullText', fullText)); ..add(DiagnosticsProperty<TextRange>('range', range))
..add(DiagnosticsProperty<String>('fullText', fullText));
} }
} }

View File

@@ -1,6 +1,10 @@
import 'dart:ui' as ui; // Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:PiliPlus/common/widgets/text/paragraph.dart'; import 'dart:ui' as ui show TextHeightBehavior;
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' hide RenderParagraph; import 'package:flutter/rendering.dart' hide RenderParagraph;
@@ -114,6 +118,8 @@ class RichText extends MultiChildRenderObjectWidget {
this.textHeightBehavior, this.textHeightBehavior,
this.selectionRegistrar, this.selectionRegistrar,
this.selectionColor, this.selectionColor,
this.onShowMore,
required this.primary,
}) : assert(maxLines == null || maxLines > 0), }) : assert(maxLines == null || maxLines > 0),
assert(selectionRegistrar == null || selectionColor != null), assert(selectionRegistrar == null || selectionColor != null),
assert( assert(
@@ -228,6 +234,10 @@ class RichText extends MultiChildRenderObjectWidget {
/// widgets. /// widgets.
final Color? selectionColor; final Color? selectionColor;
final Color primary;
final VoidCallback? onShowMore;
@override @override
RenderParagraph createRenderObject(BuildContext context) { RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context)); assert(textDirection != null || debugCheckHasDirectionality(context));
@@ -245,7 +255,8 @@ class RichText extends MultiChildRenderObjectWidget {
locale: locale ?? Localizations.maybeLocaleOf(context), locale: locale ?? Localizations.maybeLocaleOf(context),
registrar: selectionRegistrar, registrar: selectionRegistrar,
selectionColor: selectionColor, selectionColor: selectionColor,
primary: Theme.of(context).colorScheme.primary, primary: primary,
onShowMore: onShowMore,
); );
} }
@@ -253,7 +264,7 @@ class RichText extends MultiChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderParagraph renderObject) { void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
assert(textDirection != null || debugCheckHasDirectionality(context)); assert(textDirection != null || debugCheckHasDirectionality(context));
renderObject renderObject
..text = text ..text = (text: text, primary: primary)
..textAlign = textAlign ..textAlign = textAlign
..textDirection = textDirection ?? Directionality.of(context) ..textDirection = textDirection ?? Directionality.of(context)
..softWrap = softWrap ..softWrap = softWrap
@@ -266,74 +277,75 @@ class RichText extends MultiChildRenderObjectWidget {
..locale = locale ?? Localizations.maybeLocaleOf(context) ..locale = locale ?? Localizations.maybeLocaleOf(context)
..registrar = selectionRegistrar ..registrar = selectionRegistrar
..selectionColor = selectionColor ..selectionColor = selectionColor
..primary = Theme.of(context).colorScheme.primary; ..onShowMore = onShowMore;
} }
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add( properties
EnumProperty<TextAlign>( ..add(
'textAlign', EnumProperty<TextAlign>(
textAlign, 'textAlign',
defaultValue: TextAlign.start, textAlign,
), defaultValue: TextAlign.start,
); ),
properties.add( )
EnumProperty<TextDirection>( ..add(
'textDirection', EnumProperty<TextDirection>(
textDirection, 'textDirection',
defaultValue: null, textDirection,
), defaultValue: null,
); ),
properties.add( )
FlagProperty( ..add(
'softWrap', FlagProperty(
value: softWrap, 'softWrap',
ifTrue: 'wrapping at box width', value: softWrap,
ifFalse: 'no wrapping except at line break characters', ifTrue: 'wrapping at box width',
showName: true, ifFalse: 'no wrapping except at line break characters',
), showName: true,
); ),
properties.add( )
EnumProperty<TextOverflow>( ..add(
'overflow', EnumProperty<TextOverflow>(
overflow, 'overflow',
defaultValue: TextOverflow.clip, overflow,
), defaultValue: TextOverflow.clip,
); ),
properties.add( )
DiagnosticsProperty<TextScaler>( ..add(
'textScaler', DiagnosticsProperty<TextScaler>(
textScaler, 'textScaler',
defaultValue: TextScaler.noScaling, textScaler,
), defaultValue: TextScaler.noScaling,
); ),
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); )
properties.add( ..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'))
EnumProperty<TextWidthBasis>( ..add(
'textWidthBasis', EnumProperty<TextWidthBasis>(
textWidthBasis, 'textWidthBasis',
defaultValue: TextWidthBasis.parent, textWidthBasis,
), defaultValue: TextWidthBasis.parent,
); ),
properties.add(StringProperty('text', text.toPlainText())); )
properties.add( ..add(StringProperty('text', text.toPlainText()))
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null), ..add(
); DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
properties.add( )
DiagnosticsProperty<StrutStyle>( ..add(
'strutStyle', DiagnosticsProperty<StrutStyle>(
strutStyle, 'strutStyle',
defaultValue: null, strutStyle,
), defaultValue: null,
); ),
properties.add( )
DiagnosticsProperty<TextHeightBehavior>( ..add(
'textHeightBehavior', DiagnosticsProperty<TextHeightBehavior>(
textHeightBehavior, 'textHeightBehavior',
defaultValue: null, textHeightBehavior,
), defaultValue: null,
); ),
);
} }
} }

View File

@@ -17,8 +17,8 @@ library;
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui show TextHeightBehavior; import 'dart:ui' as ui show TextHeightBehavior;
import 'package:PiliPlus/common/widgets/text/paragraph.dart'; import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
import 'package:PiliPlus/common/widgets/text/rich_text.dart'; import 'package:PiliPlus/common/widgets/flutter/text/rich_text.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide RichText; import 'package:flutter/material.dart' hide RichText;
import 'package:flutter/rendering.dart' hide RenderParagraph; import 'package:flutter/rendering.dart' hide RenderParagraph;
@@ -47,7 +47,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
/// Container( /// Container(
/// width: 100, /// width: 100,
/// decoration: BoxDecoration(border: Border.all()), /// decoration: BoxDecoration(border: Border.all()),
/// child: Text(overflow: TextOverflow.ellipsis, 'Hello $_name, how are you?')) /// child: const Text(
/// 'Hello, how are you?',
/// overflow: TextOverflow.ellipsis,
/// ),
/// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
@@ -60,10 +64,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
/// ![If a second line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_max_lines.png) /// ![If a second line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_max_lines.png)
/// ///
/// ```dart /// ```dart
/// Text( /// const Text(
/// 'Hello, how are you?',
/// overflow: TextOverflow.fade, /// overflow: TextOverflow.fade,
/// maxLines: 1, /// maxLines: 1,
/// 'Hello $_name, how are you?') /// )
/// ``` /// ```
/// ///
/// Here soft wrapping is enabled and the [Text] widget tries to wrap the words /// Here soft wrapping is enabled and the [Text] widget tries to wrap the words
@@ -74,10 +79,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
/// ![If a single line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_soft_wrap.png) /// ![If a single line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_soft_wrap.png)
/// ///
/// ```dart /// ```dart
/// Text( /// const Text(
/// 'Hello, how are you?',
/// overflow: TextOverflow.fade, /// overflow: TextOverflow.fade,
/// softWrap: false, /// softWrap: false,
/// 'Hello $_name, how are you?') /// )
/// ``` /// ```
/// ///
/// Here soft wrapping is disabled with `softWrap: false` and the [Text] widget /// Here soft wrapping is disabled with `softWrap: false` and the [Text] widget
@@ -174,6 +180,8 @@ class Text extends StatelessWidget {
this.textWidthBasis, this.textWidthBasis,
this.textHeightBehavior, this.textHeightBehavior,
this.selectionColor, this.selectionColor,
this.onShowMore,
required this.primary,
}) : textSpan = null, }) : textSpan = null,
assert( assert(
textScaler == null || textScaleFactor == null, textScaler == null || textScaleFactor == null,
@@ -211,6 +219,8 @@ class Text extends StatelessWidget {
this.textWidthBasis, this.textWidthBasis,
this.textHeightBehavior, this.textHeightBehavior,
this.selectionColor, this.selectionColor,
this.onShowMore,
required this.primary,
}) : data = null, }) : data = null,
assert( assert(
textScaler == null || textScaleFactor == null, textScaler == null || textScaleFactor == null,
@@ -349,6 +359,10 @@ class Text extends StatelessWidget {
/// (semi-transparent grey). /// (semi-transparent grey).
final Color? selectionColor; final Color? selectionColor;
final Color primary;
final VoidCallback? onShowMore;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
@@ -402,8 +416,10 @@ class Text extends StatelessWidget {
text: TextSpan( text: TextSpan(
style: effectiveTextStyle, style: effectiveTextStyle,
text: data, text: data,
locale: locale,
children: textSpan != null ? <InlineSpan>[textSpan!] : null, children: textSpan != null ? <InlineSpan>[textSpan!] : null,
), ),
primary: primary,
), ),
); );
} else { } else {
@@ -433,8 +449,11 @@ class Text extends StatelessWidget {
text: TextSpan( text: TextSpan(
style: effectiveTextStyle, style: effectiveTextStyle,
text: data, text: data,
locale: locale,
children: textSpan != null ? <InlineSpan>[textSpan!] : null, children: textSpan != null ? <InlineSpan>[textSpan!] : null,
), ),
onShowMore: onShowMore,
primary: primary,
); );
} }
if (semanticsLabel != null || semanticsIdentifier != null) { if (semanticsLabel != null || semanticsIdentifier != null) {
@@ -532,6 +551,7 @@ class _SelectableTextContainer extends StatefulWidget {
required this.textWidthBasis, required this.textWidthBasis,
this.textHeightBehavior, this.textHeightBehavior,
required this.selectionColor, required this.selectionColor,
required this.primary,
}); });
final TextSpan text; final TextSpan text;
@@ -546,6 +566,7 @@ class _SelectableTextContainer extends StatefulWidget {
final TextWidthBasis textWidthBasis; final TextWidthBasis textWidthBasis;
final ui.TextHeightBehavior? textHeightBehavior; final ui.TextHeightBehavior? textHeightBehavior;
final Color selectionColor; final Color selectionColor;
final Color primary;
@override @override
State<_SelectableTextContainer> createState() => State<_SelectableTextContainer> createState() =>
@@ -588,6 +609,7 @@ class _SelectableTextContainerState extends State<_SelectableTextContainer> {
textHeightBehavior: widget.textHeightBehavior, textHeightBehavior: widget.textHeightBehavior,
selectionColor: widget.selectionColor, selectionColor: widget.selectionColor,
text: widget.text, text: widget.text,
primary: widget.primary,
), ),
); );
} }
@@ -608,6 +630,7 @@ class _RichText extends StatelessWidget {
required this.textWidthBasis, required this.textWidthBasis,
this.textHeightBehavior, this.textHeightBehavior,
required this.selectionColor, required this.selectionColor,
required this.primary,
}); });
final GlobalKey? textKey; final GlobalKey? textKey;
@@ -623,6 +646,7 @@ class _RichText extends StatelessWidget {
final TextWidthBasis textWidthBasis; final TextWidthBasis textWidthBasis;
final ui.TextHeightBehavior? textHeightBehavior; final ui.TextHeightBehavior? textHeightBehavior;
final Color selectionColor; final Color selectionColor;
final Color primary;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -642,6 +666,7 @@ class _RichText extends StatelessWidget {
selectionRegistrar: registrar, selectionRegistrar: registrar,
selectionColor: selectionColor, selectionColor: selectionColor,
text: text, text: text,
primary: primary,
); );
} }
} }
@@ -1088,7 +1113,7 @@ class _SelectableTextContainerDelegate
bool forwardSelection = bool forwardSelection =
currentSelectionEndIndex >= currentSelectionStartIndex; currentSelectionEndIndex >= currentSelectionStartIndex;
if (currentSelectionEndIndex == currentSelectionStartIndex) { if (currentSelectionEndIndex == currentSelectionStartIndex) {
// Determining selection direction is innacurate if currentSelectionStartIndex == currentSelectionEndIndex. // Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex.
// Use the range from the selectable within the selection as the source of truth for selection direction. // Use the range from the selectable within the selection as the source of truth for selection direction.
final SelectedContentRange rangeAtSelectableInSelection = final SelectedContentRange rangeAtSelectableInSelection =
selectables[currentSelectionStartIndex].getSelection()!; selectables[currentSelectionStartIndex].getSelection()!;

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