Compare commits

..

285 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
bc8907b3ef mod: show lv6_s
Closes #687

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 18:18:53 +08:00
bggRGjQaUbCoE
14f8ec37c5 opt: dyn tab
opt: reload

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 16:19:51 +08:00
bggRGjQaUbCoE
2b567e7cb3 fix: set pageTransition
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 13:46:46 +08:00
bggRGjQaUbCoE
b58a3ec044 Revert "mod: show hot label"
This reverts commit 2d0d578bb4.
2025-04-15 13:32:47 +08:00
bggRGjQaUbCoE
2d0d578bb4 mod: show hot label
Closes #683

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 13:11:47 +08:00
bggRGjQaUbCoE
54ba05c4aa Revert "opt: video cover"
This reverts commit 7cc0c83df1.
2025-04-15 13:09:46 +08:00
bggRGjQaUbCoE
27b251b06e opt: member page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 11:48:09 +08:00
bggRGjQaUbCoE
5643ebfe48 feat: custom page transition
Closes #682

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 11:48:09 +08:00
bggRGjQaUbCoE
d9c2f6bf91 revert: dm color panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 11:18:47 +08:00
bggRGjQaUbCoE
3eb404a9e2 opt: search trending page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-15 11:18:41 +08:00
bggRGjQaUbCoE
bc9c20c509 feat: search trending page
Closes #684

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 23:43:47 +08:00
bggRGjQaUbCoE
7cc0c83df1 opt: video cover
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 23:35:22 +08:00
bggRGjQaUbCoE
41daefa6c4 fix: MsgType.share_v2
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 18:04:02 +08:00
bggRGjQaUbCoE
38fa8a10b7 opt: emote panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 15:41:41 +08:00
bggRGjQaUbCoE
07d37a1209 feat: search trending
Closes #678

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 15:25:45 +08:00
bggRGjQaUbCoE
509f0d1266 fix: #680
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 15:25:45 +08:00
bggRGjQaUbCoE
7966bab62d opt: episode panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 13:17:04 +08:00
bggRGjQaUbCoE
a136c150ad opt: download dialog
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 11:16:56 +08:00
bggRGjQaUbCoE
a89fe6b026 mod: remove add live dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 11:01:49 +08:00
bggRGjQaUbCoE
56460c937d mod: send vip colorful dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-14 10:48:40 +08:00
bggRGjQaUbCoE
f2080bfb7b opt: view later from dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 21:41:48 +08:00
bggRGjQaUbCoE
012d55452e opt: timeline tabbar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 21:19:26 +08:00
bggRGjQaUbCoE
6ac482ed5e opt: pgc timeline
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 21:08:22 +08:00
bggRGjQaUbCoE
68df173558 feat: pgc timeline
Closes #653

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 20:38:24 +08:00
bggRGjQaUbCoE
d9c6c31a4d mod: remove refresh fav folder
Closes #675

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 17:18:26 +08:00
bggRGjQaUbCoE
d3d2715418 opt: add live emoticonUnique
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 15:36:30 +08:00
bggRGjQaUbCoE
a93fbd4444 opt: live send dm panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 15:01:26 +08:00
bggRGjQaUbCoE
9fee9a4cf1 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 14:49:31 +08:00
bggRGjQaUbCoE
4bbc008788 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 14:44:35 +08:00
bggRGjQaUbCoE
671b6e1ef7 feat: send live emote
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 14:40:19 +08:00
bggRGjQaUbCoE
634bae915a mod: save dm config
Closes #673

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-13 11:22:41 +08:00
bggRGjQaUbCoE
a7bbfc983e mod: show pgc renewalTime
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 21:38:13 +08:00
bggRGjQaUbCoE
17548e935e mod: show pgc renewalTime
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 21:26:50 +08:00
bggRGjQaUbCoE
15f84712cd fix: parse reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 20:53:59 +08:00
bggRGjQaUbCoE
2f34ae7d45 opt: show video fav menu
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 20:17:33 +08:00
bggRGjQaUbCoE
16cbe7e43c fix: query reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 20:12:34 +08:00
bggRGjQaUbCoE
8d633377ae feat: sort fav folder
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 18:52:29 +08:00
bggRGjQaUbCoE
0b867c254f fix: log date
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 17:00:08 +08:00
bggRGjQaUbCoE
08a47e6c1d mod: clear outdated logs
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 16:42:35 +08:00
bggRGjQaUbCoE
6c9cd8b120 fix: play all later
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 16:23:02 +08:00
bggRGjQaUbCoE
71e7219084 mod: enable autoClearCache by def
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 15:55:09 +08:00
bggRGjQaUbCoE
c13063b230 opt: FavVideoCardH
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 15:50:16 +08:00
bggRGjQaUbCoE
26ca69cb83 opt: view later tabbar
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 15:36:53 +08:00
bggRGjQaUbCoE
afc8c5f873 refa: later view page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 15:06:44 +08:00
bggRGjQaUbCoE
4d3f739a0c mod: disable preload reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 09:40:29 +08:00
bggRGjQaUbCoE
1781fdb7ca fix: search arc
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 09:24:33 +08:00
bggRGjQaUbCoE
32aa37505c Update README.md
Closes #669

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 09:24:33 +08:00
bggRGjQaUbCoE
9f9ed7dd4b fix: #668
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-12 00:11:51 +08:00
bggRGjQaUbCoE
03e3b897cf opt: check coin
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 21:49:43 +08:00
bggRGjQaUbCoE
3bc20ce1d4 opt: DynamicCardSkeleton
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 21:44:25 +08:00
bggRGjQaUbCoE
9ce9940306 mod: specify list type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 21:35:47 +08:00
bggRGjQaUbCoE
da35cf471e Revert "opt: DynamicCardSkeleton"
This reverts commit 3a52c1199c.
2025-04-11 20:54:53 +08:00
bggRGjQaUbCoE
c517df2c09 opt: keep member search page
fix: search arc

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 20:52:16 +08:00
My-Responsitories
02dee71670 mod: use cached coin (#667) 2025-04-11 12:15:09 +00:00
bggRGjQaUbCoE
1eadcd41f6 fix: live follow list
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 18:42:57 +08:00
bggRGjQaUbCoE
e8185535b0 opt: coin pic
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 18:20:19 +08:00
bggRGjQaUbCoE
b68bebfa2e fix: #665
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 17:56:34 +08:00
bggRGjQaUbCoE
3801bdf9d7 feat: get/check coin
Closes #661

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 17:28:58 +08:00
bggRGjQaUbCoE
9a6ba82467 fix: save panel: pgc uri
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 13:01:16 +08:00
bggRGjQaUbCoE
3a52c1199c opt: DynamicCardSkeleton
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 12:25:47 +08:00
bggRGjQaUbCoE
ea5c0584cc fix: footer
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 11:06:23 +08:00
bggRGjQaUbCoE
01b30d942b fix: state
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-11 10:53:59 +08:00
My-Responsitories
5aa5308a50 feat: relative slide (#662) 2025-04-11 01:57:22 +00:00
allllllllllla
de029b7043 Adapt to 16:10 tablet (#660)
* Update view.dart

* Update view_v.dart
2025-04-11 00:18:24 +08:00
bggRGjQaUbCoE
a45da453ce Revert "mod: revert success"
This reverts commit 68f03f2311.
2025-04-10 22:14:11 +08:00
dom
e1b73f4766 refa: query data (#659)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 21:43:01 +08:00
bggRGjQaUbCoE
99b19e7b03 opt: anim save panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 21:15:41 +08:00
bggRGjQaUbCoE
37bd849a86 fix: #658
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 20:41:19 +08:00
bggRGjQaUbCoE
4eb6f78a38 opt: reply item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 14:19:38 +08:00
bggRGjQaUbCoE
68f03f2311 mod: revert success
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 14:18:20 +08:00
My-Responsitories
2a60a9b393 mod: save panel use absolute time (#657) 2025-04-10 04:34:40 +00:00
My-Responsitories
1d4b08672b fix: init before speed test 2025-04-10 12:27:22 +08:00
bggRGjQaUbCoE
b0d9a1dada fix: #656
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 11:23:43 +08:00
bggRGjQaUbCoE
796494e53f mod: replace stream
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 09:38:33 +08:00
bggRGjQaUbCoE
cef7bfd534 opt: query data
fix: webdav backup

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-10 00:08:19 +08:00
bggRGjQaUbCoE
36ff4a0ed3 Revert "mod: tweak for xiaomi"
This reverts commit 44fa2a8c3e.
2025-04-09 20:19:39 +08:00
bggRGjQaUbCoE
6a6894030b fix: save reply type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 18:39:19 +08:00
bggRGjQaUbCoE
497d31ddf7 opt: nav stream
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 18:22:09 +08:00
bggRGjQaUbCoE
783218429c opt: nav/searchbar stream
Closes #648

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 18:04:27 +08:00
bggRGjQaUbCoE
0ccd15047b opt: bottom sheet
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 17:06:35 +08:00
bggRGjQaUbCoE
fe2a6ec006 opt: more panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 15:39:39 +08:00
bggRGjQaUbCoE
a3ecf59fae opt: medialist page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 15:24:47 +08:00
bggRGjQaUbCoE
4f4f89a1d7 opt: reply header
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 14:42:38 +08:00
bggRGjQaUbCoE
ece3bdd2e8 opt: reply page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 14:30:08 +08:00
bggRGjQaUbCoE
f403ed1a21 opt: webdav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 14:04:53 +08:00
bggRGjQaUbCoE
17e3a0206a opt: video play
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 13:30:52 +08:00
My-Responsitories
5da86d85de fix: memberArchive challenge (#646) 2025-04-09 05:20:39 +00:00
My-Responsitories
d3cbc95235 feat: scroll spring in history & fav (#645) 2025-04-09 05:16:29 +00:00
bggRGjQaUbCoE
a7eebcc209 opt: showEpisodes
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 12:28:02 +08:00
bggRGjQaUbCoE
fca22eb592 opt: slide padding
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 12:27:56 +08:00
bggRGjQaUbCoE
1202e5ec0f opt: search dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 11:50:06 +08:00
bggRGjQaUbCoE
03830533eb opt: pages
Closes #644

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-09 00:28:44 +08:00
bggRGjQaUbCoE
850e5a199e fix: slide padding
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 22:33:04 +08:00
bggRGjQaUbCoE
2d11158ecd refa: search dyn
Closes #641

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 22:09:47 +08:00
bggRGjQaUbCoE
a34c18b262 feat: webdav
Closes #432

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 20:59:21 +08:00
bggRGjQaUbCoE
560b1e40cc fix: #639
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 19:09:34 +08:00
My-Responsitories
3cd512857c mod: dyn search (#638) 2025-04-08 08:38:05 +00:00
bggRGjQaUbCoE
356adbef5c opt: reply ctr tag
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 11:26:11 +08:00
bggRGjQaUbCoE
42d7445d83 opt: skip segment
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 10:50:01 +08:00
bggRGjQaUbCoE
3a0f32fce7 chore: update dm dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-08 00:22:26 +08:00
bggRGjQaUbCoE
6bc128cfda opt: save reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 22:49:04 +08:00
bggRGjQaUbCoE
6f2d697748 opt: query sponsor block
Closes #633

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 22:32:26 +08:00
bggRGjQaUbCoE
4de180c23a opt: save panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 22:22:10 +08:00
My-Responsitories
af289c533f some modifitions (#636)
* opt: MsgUnReadTypeV2

* mod: recoard history in anonymity mode
2025-04-07 13:19:24 +00:00
bggRGjQaUbCoE
82d615fbbf feat: save dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 17:52:04 +08:00
dom
457f2ea6c7 Update bug-反馈.yml 2025-04-07 14:04:57 +08:00
bggRGjQaUbCoE
41ad5c45ed fix: #634
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 14:04:14 +08:00
bggRGjQaUbCoE
e9da2e8d6b opt: save reply page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 13:48:32 +08:00
bggRGjQaUbCoE
a8cfbb12fd fix: block page state
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 13:39:57 +08:00
bggRGjQaUbCoE
6d89b7769e chore: rename imageview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 12:40:48 +08:00
bggRGjQaUbCoE
2d86daec83 fix: #630
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 12:39:01 +08:00
bggRGjQaUbCoE
a5e8594611 feat: del msg feed
opt: msg feed

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 12:24:14 +08:00
bggRGjQaUbCoE
99810ef512 fix: dyn pubtime
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 11:30:56 +08:00
bggRGjQaUbCoE
2317b831db opt: save reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 11:22:44 +08:00
bggRGjQaUbCoE
e073086cf4 opt: reply item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 10:09:34 +08:00
bggRGjQaUbCoE
b14844f459 fix: save reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 09:50:19 +08:00
dom
8719c8f639 feat: save reply (#629)
Closes #614

opt: more panel

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 09:22:33 +08:00
My-Responsitories
d3cec0ec72 fix #624 (#625) 2025-04-07 09:17:19 +08:00
bggRGjQaUbCoE
a8daf02610 opt: video sheet
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-06 15:49:40 +08:00
bggRGjQaUbCoE
f9b844fb1a mod: open link verify settings btn
Closes #622

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-06 12:12:17 +08:00
bggRGjQaUbCoE
6d1d6b575a opt: send dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-06 11:35:04 +08:00
bggRGjQaUbCoE
0a5a094e54 chore: update dm dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-06 11:28:05 +08:00
bggRGjQaUbCoE
754da4777a opt: live: send danmaku
Closes #618

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-06 11:22:19 +08:00
bggRGjQaUbCoE
216e3e606e mod: live: double tap
Closes #619

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-06 10:52:05 +08:00
bggRGjQaUbCoE
bb013a8fe6 mod: hide special dm by def
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 20:48:50 +08:00
bggRGjQaUbCoE
6b6449f023 mod: article: get user mid
Closes #615

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 20:28:56 +08:00
bggRGjQaUbCoE
fcf3348371 fix: add special dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 20:28:44 +08:00
bggRGjQaUbCoE
f90f759667 opt: jump to reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 16:42:34 +08:00
bggRGjQaUbCoE
b02e6c04b9 feat: special danmaku by @My-Responsitories
Closes #91
Closes #219
Closes #394
Closes #602
Closes #613

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 16:32:30 +08:00
bggRGjQaUbCoE
08dc04f874 opt: show dyn blocked item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 14:11:01 +08:00
bggRGjQaUbCoE
4776b84c7c mod: sys msg: push dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 14:10:52 +08:00
bggRGjQaUbCoE
78d13b586a fix: sys msg: push video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 11:49:50 +08:00
bggRGjQaUbCoE
f522ecd42d chore: remove unused pack
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 11:18:15 +08:00
bggRGjQaUbCoE
44fa2a8c3e mod: tweak for xiaomi
Closes #608

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 11:18:02 +08:00
bggRGjQaUbCoE
ff30c8c2bf opt: push dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-05 10:54:09 +08:00
bggRGjQaUbCoE
4aaaffbcea fix: dyn comment type
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 23:40:11 +08:00
bggRGjQaUbCoE
21da122902 opt: push opus
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 23:22:33 +08:00
bggRGjQaUbCoE
849904ad45 revert: push opus
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 23:16:42 +08:00
bggRGjQaUbCoE
1c0bae600f mod: add nav/search debounce option
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 21:53:30 +08:00
bggRGjQaUbCoE
f1433c6e9b mod: show reply rootText
Closes #605

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 21:17:22 +08:00
bggRGjQaUbCoE
2dc106adcb fix: reply error widget
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 20:56:37 +08:00
bggRGjQaUbCoE
df6738f607 mod: remove webview msg btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 18:44:26 +08:00
bggRGjQaUbCoE
ee64f1e7f1 mod: note: check login
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 18:39:36 +08:00
bggRGjQaUbCoE
d921f6176b opt: get cacheSize
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 18:31:19 +08:00
bggRGjQaUbCoE
7009c3400a mod: remove ios ua
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 18:22:47 +08:00
bggRGjQaUbCoE
7bd481b090 fix: ios message webview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 18:00:14 +08:00
bggRGjQaUbCoE
7fafa88eb7 opt: load previous
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 17:53:11 +08:00
bggRGjQaUbCoE
cb3e57feec fix: load previous
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 16:04:59 +08:00
bggRGjQaUbCoE
9a7d73cb6b opt: load previous data
Closes #597

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 15:26:27 +08:00
bggRGjQaUbCoE
f5c2bd47d5 fix: fav: play all
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 12:14:55 +08:00
bggRGjQaUbCoE
c154d25f7a opt: episode item
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 12:03:22 +08:00
bggRGjQaUbCoE
8c259205f5 feat: record search history option
Closes #592

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 12:00:13 +08:00
bggRGjQaUbCoE
849329b66b opt: bottomnav/searchbar stream debounce
Closes #590

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 11:26:53 +08:00
bggRGjQaUbCoE
f542565dc5 mod: filter advanced dm
Closes #602

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 10:59:57 +08:00
bggRGjQaUbCoE
08aedbf0b0 feat: custom for/backward duration
Closes #366
Closes #601

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-04 10:43:03 +08:00
bggRGjQaUbCoE
09c8a41c52 feat: drag subtitle
Closes #588

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-03 20:04:56 +08:00
bggRGjQaUbCoE
6a7d14a3f8 fix: view reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-03 18:04:41 +08:00
bggRGjQaUbCoE
5b171ec044 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-03 15:23:09 +08:00
bggRGjQaUbCoE
978d634cb3 feat: set top reply
Closes #589

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-03 14:50:42 +08:00
bggRGjQaUbCoE
7437d8c592 feat: manual check dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-03 13:18:24 +08:00
My-Responsitories
e190ca5868 mod: account (#591)
* fix: onLoginMain

* fix: account override

* opt: sponsor block url
2025-04-03 04:18:30 +00:00
bggRGjQaUbCoE
64fc995f6b fix: report video
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-03 12:02:25 +08:00
bggRGjQaUbCoE
2d0e801a1a opt: override note title
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 22:21:18 +08:00
bggRGjQaUbCoE
d409424871 opt: webview: add note
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 22:06:17 +08:00
bggRGjQaUbCoE
b855ef9865 mod: webview: intercept refresh url
Closes #587

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 21:25:39 +08:00
bggRGjQaUbCoE
86abf006d0 mod: live schedule
Closes #581

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 20:47:11 +08:00
My-Responsitories
d1a6798f2e fix: wbiSign 2025-04-02 18:52:13 +08:00
bggRGjQaUbCoE
f64d543ec7 mod: show live media notification
Closes #584

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 17:34:17 +08:00
bggRGjQaUbCoE
2abf01362c opt: medialist page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 16:37:01 +08:00
bggRGjQaUbCoE
9bbd934f8e fix: episode panel
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 15:52:29 +08:00
bggRGjQaUbCoE
6ab72b65aa fix: video gesture
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 15:12:24 +08:00
bggRGjQaUbCoE
c39de1e245 opt: download img
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 15:05:14 +08:00
bggRGjQaUbCoE
d112843a8a fix: mid
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 13:59:52 +08:00
bggRGjQaUbCoE
89df091542 opt: video gesture
Closes #586

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 12:42:00 +08:00
bggRGjQaUbCoE
d870c36a96 mod: custom slide fs
Closes #585

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 12:33:31 +08:00
bggRGjQaUbCoE
fc55bf33d0 fix: episode cid
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-02 12:33:11 +08:00
bggRGjQaUbCoE
f99740ef2d refa: list sheet
Closes #369

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-01 23:53:32 +08:00
bggRGjQaUbCoE
f9f30a5f13 mod: tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-01 17:19:35 +08:00
bggRGjQaUbCoE
f70cf05870 opt: member page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-01 16:22:27 +08:00
bggRGjQaUbCoE
22866012ca mod: remove unused pack
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-01 14:06:02 +08:00
bggRGjQaUbCoE
cdb2718aeb chore: deprecate prev reply, videopage
Closes #579

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-01 12:48:59 +08:00
bggRGjQaUbCoE
e4f3203351 opt: show pubTs
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 21:41:15 +08:00
bggRGjQaUbCoE
d6b388ad5c opt: duration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 18:20:50 +08:00
bggRGjQaUbCoE
61819d9f27 opt: image preview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 17:53:35 +08:00
bggRGjQaUbCoE
3ccc7ef69d fix: bangumi mid
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 17:37:42 +08:00
bggRGjQaUbCoE
e834311664 mod: set buffer value
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 17:37:42 +08:00
bggRGjQaUbCoE
fd8dff327c fix: view staff from intro
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 16:44:41 +08:00
bggRGjQaUbCoE
a9df8cd883 opt: push following detail
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 16:38:58 +08:00
bggRGjQaUbCoE
909394965e fix: push bilibili://following/detail
Closes #576

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 16:29:49 +08:00
bggRGjQaUbCoE
2d5991e0c5 opt: query history
Closes #578

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 16:02:49 +08:00
bggRGjQaUbCoE
e7ae66a3dc fix: push bilibili://comment/msg_fold
Closes #576

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 15:34:11 +08:00
bggRGjQaUbCoE
5929150047 fix: push bilibili://following/detail
related #576

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 14:54:53 +08:00
bggRGjQaUbCoE
acb6bc569e opt: btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-31 14:54:53 +08:00
bggRGjQaUbCoE
678db34c81 opt: member page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 21:32:36 +08:00
bggRGjQaUbCoE
134bfd43ff opt: video: charged badge
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 21:14:07 +08:00
bggRGjQaUbCoE
8712248ef2 opt: member video card
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 21:00:37 +08:00
bggRGjQaUbCoE
c97227e807 opt: hmember page 2025-03-30 20:24:32 +08:00
bggRGjQaUbCoE
40429021be feat: locate last viewed video
Closes #453

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 19:54:57 +08:00
bggRGjQaUbCoE
e89bd2fedf opt: member prInfo
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 17:16:10 +08:00
bggRGjQaUbCoE
9da3a538fb refa: horizontal member page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 16:20:40 +08:00
bggRGjQaUbCoE
a904414f3d mod: prinfo
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 13:47:59 +08:00
bggRGjQaUbCoE
f003e8bf35 mod: member card: show prInfo
Closes #571

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 12:17:54 +08:00
bggRGjQaUbCoE
7399915357 mod: later: show progress
Closes #569

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-30 11:31:46 +08:00
My-Responsitories
7aa0289c1f fix: audioNormalization 2025-03-29 23:51:32 +08:00
bggRGjQaUbCoE
99d0b1c468 mod: btn to view all fav pgc
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 21:03:19 +08:00
bggRGjQaUbCoE
84a342a0e0 mod: enable retry by def
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 20:39:20 +08:00
bggRGjQaUbCoE
db1c836a3e opt: share/save video cover
Closes #563

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 20:39:20 +08:00
My-Responsitories
6539457f83 fix: select group sheet 2025-03-29 20:30:19 +08:00
My-Responsitories
82f9f48a8e opt: select dialog & feat: select subtitle if muted (#564)
* opt: select dialog

* opt: subtitle

* feat: select subtitle if muted
2025-03-29 09:52:06 +00:00
bggRGjQaUbCoE
2ddfea5cf3 fix: webview params
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 15:37:19 +08:00
bggRGjQaUbCoE
79aee2fdd9 opt: view note
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 15:13:11 +08:00
bggRGjQaUbCoE
5dc8b8e54f feat: fav article page
Closes #402

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 14:41:13 +08:00
bggRGjQaUbCoE
72fa9c51f0 feat: fav pgc page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 13:45:14 +08:00
bggRGjQaUbCoE
6ea8ffea7a opt: note
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 11:24:39 +08:00
bggRGjQaUbCoE
eea5257da2 fix: only play audio
related #558

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 10:05:43 +08:00
bggRGjQaUbCoE
385cffefb8 fix(dep): downgrade media-kit
Closes #543
Closes #546
Closes #551
Closes #558

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 10:00:30 +08:00
bggRGjQaUbCoE
347420c531 feat: del note
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-29 09:53:02 +08:00
bggRGjQaUbCoE
cc774015f9 feat: fav note page
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 22:35:38 +08:00
bggRGjQaUbCoE
da3f64feab refa: fav page [wip]
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 18:38:31 +08:00
My-Responsitories
76d031e8d1 feat: parallel upload & download image (#556)
* feat: parallel upload file

* feat: parallel download file
2025-03-28 10:30:15 +00:00
bggRGjQaUbCoE
25995b0ed6 fix: #555
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 17:46:45 +08:00
bggRGjQaUbCoE
3cdd40a710 feat: create note
related #554

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 17:32:27 +08:00
bggRGjQaUbCoE
f36f8d69fc refa: subtitle
Closes #553

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 16:13:21 +08:00
bggRGjQaUbCoE
5655e6ccdf opt: article report
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 11:06:00 +08:00
bggRGjQaUbCoE
49fff821b1 feat: live/article report
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 10:57:54 +08:00
bggRGjQaUbCoE
3b34cecdcd mod: set pos
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-28 10:09:11 +08:00
My-Responsitories
3693d6c350 change status api (#552) 2025-03-28 01:57:50 +00:00
bggRGjQaUbCoE
54cb1a6fc0 fix: #547
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 21:54:45 +08:00
bggRGjQaUbCoE
ab7b1524b6 opt: show dm
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 21:18:17 +08:00
bggRGjQaUbCoE
2b4a27076c fix: #344
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 20:59:35 +08:00
bggRGjQaUbCoE
63a7fa95f5 fix: pip aspectRatio
related #543

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 20:44:18 +08:00
bggRGjQaUbCoE
a15b932a69 opt: query dm
Closes #544

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 20:03:41 +08:00
bggRGjQaUbCoE
3a6b6614a4 opt: video tool bar
Closes #528

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 15:43:11 +08:00
bggRGjQaUbCoE
6ff83e34f3 opt: sort fav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 14:47:40 +08:00
bggRGjQaUbCoE
e4cadc5a40 chore: update dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 13:50:33 +08:00
bggRGjQaUbCoE
907d37dd4c mod: whisper: show article_card
ref orz12/main

Closes #540

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 11:10:40 +08:00
bggRGjQaUbCoE
00d30313af opt: video widget
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-27 10:59:16 +08:00
bggRGjQaUbCoE
950dd82e3c fix: sort fav
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 23:40:03 +08:00
bggRGjQaUbCoE
c53c3a387c fix: #539
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 23:32:57 +08:00
bggRGjQaUbCoE
5d0b2dc8e3 feat: custom app font weight
Closes #533

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 21:42:57 +08:00
bggRGjQaUbCoE
f5e9375917 mod: update qa when switching
Closes #437

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 20:40:38 +08:00
bggRGjQaUbCoE
cdfab7a7db chore: update dep
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 19:02:24 +08:00
bggRGjQaUbCoE
f3e6a59e4f chore: upgrade deps
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 17:21:41 +08:00
bggRGjQaUbCoE
239c9ca2a7 mod: top or refresh debounce
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 16:39:52 +08:00
bggRGjQaUbCoE
becb566ca8 feat: sort fav
Closes #530

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 16:21:28 +08:00
bggRGjQaUbCoE
aa378d924b opt: post segments
Closes #531

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 12:10:55 +08:00
bggRGjQaUbCoE
812f351ddd opt: intro: queryUserStat
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-26 12:10:55 +08:00
My-Responsitories
b9adf26ee0 opt: model & feat: filter play (#529)
* opt: model

* opt: model

* feat: filter play
2025-03-25 14:45:30 +00:00
bggRGjQaUbCoE
018cd058ca opt: set playback speed
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 21:19:11 +08:00
bggRGjQaUbCoE
cd3385be63 opt: only play audio
Closes #519

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 18:25:12 +08:00
bggRGjQaUbCoE
d4e4813c78 feat: static dm duration
Closes #479

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 17:47:11 +08:00
My-Responsitories
8030912087 Revert "fix: mixinKeyEncTab" (#527)
This reverts commit 0f78669faf.
2025-03-25 17:03:15 +08:00
bggRGjQaUbCoE
4879701008 opt: up panel
Closes #513

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 16:49:27 +08:00
bggRGjQaUbCoE
6ab8e5925e mod: remove maxConnectionsPerHost
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 15:36:52 +08:00
bggRGjQaUbCoE
52dda9964c opt: queryFollowUp
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 15:15:19 +08:00
bggRGjQaUbCoE
1825329236 fix: SetSwitchItem
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 14:52:11 +08:00
bggRGjQaUbCoE
60a650f798 fix: #526
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 14:38:12 +08:00
bggRGjQaUbCoE
0f78669faf fix: mixinKeyEncTab
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 14:13:09 +08:00
bggRGjQaUbCoE
015309b3dc opt: main: anim to top
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 14:06:19 +08:00
My-Responsitories
a74edd22c1 feat: refresh on top (#524)
* feat: refresh on top

* check ctr client

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

---------

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-25 13:46:38 +08:00
My-Responsitories
7a6085e923 refa: video model (#523) 2025-03-25 02:12:44 +00:00
bggRGjQaUbCoE
bf464994df opt: video progressbar
Closes #507
Closes #514

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 21:29:32 +08:00
bggRGjQaUbCoE
a611a88f69 opt: post segment
Closes #483

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 21:10:10 +08:00
bggRGjQaUbCoE
025b5c8e6d mod: search history
Closes #488

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 21:10:10 +08:00
bggRGjQaUbCoE
d37685f7cf opt: member page: show uname
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 21:10:10 +08:00
My-Responsitories
10a22b5186 opt: stat widget (#520) 2025-03-24 13:09:11 +00:00
bggRGjQaUbCoE
d9a74c43dc fix: del history
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 20:06:59 +08:00
bggRGjQaUbCoE
90c8aeb05d mod: history: full type
Closes #473

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 19:10:02 +08:00
bggRGjQaUbCoE
34f63612a4 fix: #517
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 14:29:54 +08:00
bggRGjQaUbCoE
edfa9a8dd1 mod: live: only play audio
Closes #465

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 14:07:42 +08:00
bggRGjQaUbCoE
95fa19f121 opt: dm widget
Closes #160
Closes #515

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 13:44:54 +08:00
bggRGjQaUbCoE
79d0d314f5 fix: #516
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 13:12:36 +08:00
bggRGjQaUbCoE
ddbf168c87 chore: update dm dep
Closes #511

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 11:37:02 +08:00
bggRGjQaUbCoE
0eee8bbac2 mod: def hwdec
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 10:50:51 +08:00
bggRGjQaUbCoE
92f02b5943 fix: search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 10:20:43 +08:00
bggRGjQaUbCoE
f110c2a55f fix: fav search
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 10:16:01 +08:00
bggRGjQaUbCoE
8ddf42fff1 opt: switch btn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-24 00:19:25 +08:00
My-Responsitories
d2c34d64c3 fix #510 2025-03-23 23:17:18 +08:00
bggRGjQaUbCoE
2341027972 mod: show switch btn when playall
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-23 22:19:13 +08:00
bggRGjQaUbCoE
006c9301d9 fix: follow staff
related #481

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-23 21:53:26 +08:00
bggRGjQaUbCoE
1f8955d0b3 opt: prev/next play
Closes #506

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-23 21:47:12 +08:00
bggRGjQaUbCoE
5a758ebb3a opt: play all
Closes #503

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-23 20:49:29 +08:00
bggRGjQaUbCoE
97bef56006 mod: live qa
Closes #464

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-23 20:35:16 +08:00
bggRGjQaUbCoE
96ba36ed67 fix: def subtitle stroke
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-03-23 20:34:59 +08:00
My-Responsitories
edf84fcc8f opt: blacklist (#501) 2025-03-23 11:13:07 +00:00
375 changed files with 19314 additions and 15157 deletions

View File

@@ -9,7 +9,11 @@ body:
attributes:
label: 检查清单
options:
- label: 之前没有人提交过类似或相同的 bug report。
- label: 之前没有人提交过类似或相同的 bug report。1
required: true
- label: 之前没有人提交过类似或相同的 bug report。2
required: true
- label: 之前没有人提交过类似或相同的 bug report。3
required: true
- label: 正在使用最新版本。
required: true

View File

@@ -47,13 +47,22 @@
## feat
- [x] 直播弹幕发送表情
- [x] 收藏夹排序
- [x] 稍后再看`未看`/`未看完`/`已看完`分类
- [x] WebDAV 备份/恢复设置
- [x] 保存评论/动态
- [x] 高级弹幕 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] 取消/置顶评论
- [x] 记笔记
- [x] 多账号支持 by [@My-Responsitories](https://github.com/My-Responsitories)
- [x] 屏蔽带货动态/评论
- [x] 互动视频
- [x] 发评/动态反诈
- [x] 高能进度条
- [x] 滑动跳转预览视频缩略图
- [x] Live Photo
- [x] 复制/移动收藏夹/稍后再看视频
- [x] 复制/移动/排序收藏夹/稍后再看视频
- [x] 超分辨率
- [x] 合并弹幕
- [x] 会员彩色弹幕
@@ -71,7 +80,6 @@
- [x] 评论楼中楼定位点击查看的评论
- [x] 评论楼中楼按热度/时间排序
- [x] 评论点踩
- [x] 显示ops专栏
- [x] 私信发图
- [x] 投币动画
- [x] 取消/追番,更新追番状态
@@ -156,7 +164,6 @@
- [x] 音质选择(视视频而定)
- [x] 解码格式选择(视视频而定)
- [x] 弹幕
- [ ] 直播弹幕
- [x] 字幕
- [x] 记忆播放
- [x] 视频比例:高度/宽度适应、填充、包含等

View File

@@ -7,8 +7,10 @@ import com.ryanheise.audioservice.AudioServiceActivity
import android.content.ComponentName
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.WindowManager.LayoutParams
import kotlin.system.exitProcess
@@ -55,6 +57,22 @@ class MainActivity : AudioServiceActivity() {
}
startActivity(intent)
} catch (e: Exception) {}
} else if (call.method == "linkVerifySettings") {
try {
val intent = Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:" + context.packageName))
context.startActivity(intent)
} catch (t: Throwable) {
try {
val intent = Intent("android.intent.action.MAIN", Uri.parse("package:" + context.packageName))
intent.setClassName("com.android.settings", "com.android.settings.applications.InstalledAppOpenByDefaultActivity")
context.startActivity(intent)
} catch (t2: Throwable) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + context.packageName))
context.startActivity(intent)
}
}
} else {
result.notImplemented()
}

View File

@@ -18,19 +18,33 @@ subprojects {
afterEvaluate { project ->
if (project.extensions.findByName("android") != null) {
Integer pluginCompileSdk = project.android.compileSdk
if (pluginCompileSdk != null && pluginCompileSdk < 31) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 31 (to work around https://issuetracker.google.com/issues/199180389)."
+ "\nIf there is not a new version of " + project.name + ", consider filing an issue against "
+ project.name
+ " to increase their compileSdk to the latest (otherwise try updating to the latest version)."
)
project.android {
compileSdk 31
if (pluginCompileSdk != null) {
if (pluginCompileSdk < 31) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 31 (to work around https://issuetracker.google.com/issues/199180389)."
+ "\nIf there is not a new version of " + project.name + ", consider filing an issue against "
+ project.name
+ " to increase their compileSdk to the latest (otherwise try updating to the latest version)."
)
project.android {
compileSdk 31
}
}
if (pluginCompileSdk > 34) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 34"
)
project.android {
compileSdk 34
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 912 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/images/lv/lv6_s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -87,6 +87,7 @@ class DynamicCardSkeleton extends StatelessWidget {
],
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/common/widgets/no_splash_factory.dart';
import 'package:PiliPlus/common/widgets/overlay_pop.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:flutter/material.dart';
class AnimatedDialog extends StatefulWidget {
@@ -9,7 +10,7 @@ class AnimatedDialog extends StatefulWidget {
required this.closeFn,
});
final dynamic videoItem;
final BaseVideoItemModel videoItem;
final Function closeFn;
@override

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart';
class PBadge extends StatelessWidget {
@@ -13,10 +14,11 @@ class PBadge extends StatelessWidget {
final String? semanticsLabel;
final bool bold;
final double? textScaleFactor;
final EdgeInsets? padding;
const PBadge({
super.key,
this.text,
required this.text,
this.top,
this.right,
this.bottom,
@@ -28,10 +30,15 @@ class PBadge extends StatelessWidget {
this.semanticsLabel,
this.bold = true,
this.textScaleFactor,
this.padding,
});
@override
Widget build(BuildContext context) {
if (text.isNullOrEmpty) {
return const SizedBox.shrink();
}
ColorScheme t = Theme.of(context).colorScheme;
// 背景色
Color bgColor = t.primary;
@@ -40,20 +47,21 @@ class PBadge extends StatelessWidget {
// 边框色
Color borderColor = Colors.transparent;
if (type == 'gray') {
bgColor = Colors.black54.withOpacity(0.4);
bgColor = Colors.black45;
color = Colors.white;
}
if (type == 'color') {
} else if (type == 'color') {
bgColor = t.secondaryContainer.withOpacity(0.5);
color = t.onSecondaryContainer;
}
if (type == 'line') {
} else if (type == 'line') {
bgColor = Colors.transparent;
color = t.primary;
borderColor = t.primary;
} else if (type == 'error') {
bgColor = t.error;
color = t.onError;
}
EdgeInsets paddingStyle =
late EdgeInsets paddingStyle =
const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
double fontSize = 11;
BorderRadius br = BorderRadius.circular(4);
@@ -65,14 +73,14 @@ class PBadge extends StatelessWidget {
}
Widget content = Container(
padding: paddingStyle,
padding: padding ?? paddingStyle,
decoration: BoxDecoration(
borderRadius: br,
color: bgColor,
border: Border.all(color: borderColor),
),
child: Text(
text ?? "",
text!,
textScaler: textScaleFactor != null
? TextScaler.linear(textScaleFactor!)
: null,

View File

@@ -33,3 +33,70 @@ void showConfirmDialog({
},
);
}
void showPgcFollowDialog({
required BuildContext context,
required String type,
required int followStatus,
required ValueChanged<int> onUpdateStatus,
}) {
Widget statusItem({
required bool enabled,
required String text,
required VoidCallback onTap,
}) {
return ListTile(
dense: true,
enabled: enabled,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
'标记为 $text',
style: const TextStyle(fontSize: 14),
),
),
trailing: !enabled ? const Icon(size: 22, Icons.check) : null,
onTap: onTap,
);
}
showDialog(
context: context,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
...[
{'followStatus': 3, 'title': '看过'},
{'followStatus': 2, 'title': '在看'},
{'followStatus': 1, 'title': '想看'},
].map(
(Map item) => statusItem(
enabled: followStatus != item['followStatus'],
text: item['title'],
onTap: () {
Get.back();
onUpdateStatus(item['followStatus']);
},
),
),
ListTile(
dense: true,
title: Padding(
padding: EdgeInsets.only(left: 10),
child: Text(
'取消$type',
style: TextStyle(fontSize: 14),
),
),
onTap: () {
Get.back();
onUpdateStatus(-1);
},
)
],
),
));
}

View File

@@ -35,7 +35,7 @@ class DynamicSliverAppBar extends StatefulWidget {
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
this.toolbarHeight = kToolbarHeight + 20,
this.toolbarHeight = kToolbarHeight,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
@@ -43,8 +43,10 @@ class DynamicSliverAppBar extends StatefulWidget {
this.forceMaterialTransparency = false,
this.clipBehavior,
this.appBarClipper,
this.hasTabBar = false,
});
final bool hasTabBar;
final Widget? flexibleSpace;
final Widget? leading;
final bool automaticallyImplyLeading;
@@ -95,7 +97,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
// 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;
Orientation? _orientation;
@override
void initState() {
@@ -103,13 +104,6 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
_updateHeight();
}
@override
void didUpdateWidget(covariant DynamicSliverAppBar oldWidget) {
super.didUpdateWidget(oldWidget);
_updateHeight();
}
void _updateHeight() {
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
// otherwise this will throw an error
@@ -123,23 +117,27 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
});
}
@override
void didChangeDependencies() {
_height = 0;
_updateHeight();
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
Orientation orientation = MediaQuery.orientationOf(context);
if (_orientation != orientation) {
_orientation = orientation;
_height = 0;
}
if (_height == 0) {
return SliverToBoxAdapter(
child: Container(
child: SizedBox(
key: _childKey,
child: widget.flexibleSpace ?? SizedBox(height: kToolbarHeight),
),
);
}
MediaQuery.orientationOf(context);
return SliverAppBar(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
@@ -168,7 +166,7 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: widget.toolbarHeight,
expandedHeight: _height,
expandedHeight: _height + (widget.hasTabBar ? 48 : 0),
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,

View File

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

View File

@@ -1,6 +1,8 @@
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
show SourceModel;
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'network_img_layer.dart';
@@ -32,7 +34,7 @@ Widget htmlRender({
final bool isEmote = imgUrl.contains('/emote/');
final bool isMall = imgUrl.contains('/mall/');
if (isMall) {
return const SizedBox();
return const SizedBox.shrink();
}
// bool inTable =
// extensionContext.element!.previousElementSibling == null ||
@@ -43,6 +45,18 @@ Widget htmlRender({
// width: isEmote ? 22 : null,
// height: isEmote ? 22 : null,
// );
String? clazz = attributes['class'];
String? height = RegExp(r'max-height:(\d+)px')
.firstMatch('${attributes['style']}')
?.group(1);
if (clazz?.contains('cut-off') == true || height != null) {
return CachedNetworkImage(
width: constrainedWidth,
height: height != null ? double.parse(height) : null,
imageUrl: Utils.thumbnailImgUrl(imgUrl),
fit: BoxFit.contain,
);
}
return Hero(
tag: imgUrl,
child: GestureDetector(

View File

@@ -22,7 +22,7 @@ Widget iconButton({
color: iconColor ?? Theme.of(context).colorScheme.onSecondaryContainer,
),
style: IconButton.styleFrom(
padding: EdgeInsets.all(0),
padding: EdgeInsets.zero,
backgroundColor:
bgColor ?? Theme.of(context).colorScheme.secondaryContainer,
),

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/utils/download.dart';
import 'package:flutter/material.dart';
@@ -71,25 +72,36 @@ void imageSaveDialog({
style: Theme.of(context).textTheme.titleSmall,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: '保存封面图',
onPressed: () async {
bool saveStatus = await DownloadUtils.downloadImg(
context,
[cover ?? ''],
);
// 保存成功,自动关闭弹窗
if (saveStatus) {
if (cover?.isNotEmpty == true) ...[
const SizedBox(width: 4),
iconButton(
context: context,
tooltip: '分享',
onPressed: () {
SmartDialog.dismiss();
}
},
icon: Icon(
Icons.download,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
DownloadUtils.onShareImg(cover!);
},
iconSize: 20,
icon: Icons.share,
bgColor: Colors.transparent,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
iconButton(
context: context,
tooltip: '保存封面图',
onPressed: () async {
bool saveStatus =
await DownloadUtils.downloadImg(context, [cover!]);
if (saveStatus) {
SmartDialog.dismiss();
}
},
iconSize: 20,
icon: Icons.download,
bgColor: Colors.transparent,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
],
),
),

View File

@@ -31,7 +31,7 @@ class ImageModel {
bool get isLivePhoto => _isLivePhoto ??= liveUrl?.isNotEmpty == true;
}
Widget imageview(
Widget imageView(
double maxWidth,
List<ImageModel> picArr, {
VoidCallback? onViewImage,

View File

@@ -1,21 +1,16 @@
import 'dart:io';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/utils/download.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'interactive_viewer_boundary.dart';
import 'interactive_viewer.dart' as custom;
@@ -380,7 +375,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
itemBuilder: (context) {
return [
PopupMenuItem(
onTap: () => onShareImg(
onTap: () => DownloadUtils.onShareImg(
widget.sources[currentIndex.value].url),
child: const Text("分享图片"),
),
@@ -394,7 +389,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
PopupMenuItem(
onTap: () {
DownloadUtils.downloadImg(
context,
this.context,
[widget.sources[currentIndex.value].url],
);
},
@@ -404,7 +399,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
PopupMenuItem(
onTap: () {
DownloadUtils.downloadImg(
context,
this.context,
widget.sources
.map((item) => item.url)
.toList(),
@@ -417,7 +412,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
PopupMenuItem(
onTap: () {
DownloadUtils.downloadLivePhoto(
context: context,
context: this.context,
url: widget.sources[currentIndex.value].url,
liveUrl: widget
.sources[currentIndex.value].liveUrl!,
@@ -442,34 +437,6 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
);
}
// 图片分享
void onShareImg(String imgUrl) async {
try {
SmartDialog.showLoading();
var response = await Request()
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
final temp = await getTemporaryDirectory();
SmartDialog.dismiss();
String imgName =
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
var path = '${temp.path}/$imgName';
File(path).writeAsBytesSync(response.data);
Rect? sharePositionOrigin;
if (Platform.isIOS && (await Utils.isIpad())) {
sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2);
}
Share.shareXFiles(
[XFile(path)],
subject: imgUrl,
sharePositionOrigin: sharePositionOrigin,
);
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
Widget _itemBuilder(index) {
return Center(
child: Hero(
@@ -480,19 +447,27 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
image: FileImage(File(widget.sources[index].url)),
),
SourceType.networkImage => CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
fadeOutDuration: const Duration(milliseconds: 0),
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: _getActualUrl(index),
// fit: BoxFit.contain,
progressIndicatorBuilder: (context, url, progress) {
return Center(
child: SizedBox(
width: 150.0,
child:
LinearProgressIndicator(value: progress.progress ?? 0),
),
placeholderFadeInDuration: Duration.zero,
placeholder: (context, url) {
return CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: Utils.thumbnailImgUrl(widget.sources[index].url),
);
},
// fit: BoxFit.contain,
// progressIndicatorBuilder: (context, url, progress) {
// return Center(
// child: SizedBox(
// width: 150.0,
// child:
// LinearProgressIndicator(value: progress.progress ?? 0),
// ),
// );
// },
// errorListener: (value) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// setState(() {
@@ -573,7 +548,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
children: [
ListTile(
onTap: () {
onShareImg(widget.sources[currentIndex.value].url);
DownloadUtils.onShareImg(
widget.sources[currentIndex.value].url);
Get.back();
},
dense: true,
@@ -591,7 +567,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
onTap: () {
Get.back();
DownloadUtils.downloadImg(
context,
this.context,
[widget.sources[currentIndex.value].url],
);
},
@@ -603,7 +579,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
onTap: () {
Get.back();
DownloadUtils.downloadImg(
context,
this.context,
widget.sources.map((item) => item.url).toList(),
);
},
@@ -616,7 +592,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
onTap: () {
Get.back();
DownloadUtils.downloadLivePhoto(
context: context,
context: this.context,
url: widget.sources[currentIndex.value].url,
liveUrl: widget.sources[currentIndex.value].liveUrl!,
width: widget.sources[currentIndex.value].width!,

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class KeepAliveWrapper extends StatefulWidget {
const KeepAliveWrapper({
super.key,
required this.builder,
this.wantKeepAlive = true,
});
final WidgetBuilder builder;
final bool wantKeepAlive;
@override
State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.builder(context);
}
@override
bool get wantKeepAlive => widget.wantKeepAlive;
}

View File

@@ -1,537 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/bangumi/info.dart' as bangumi;
import 'package:PiliPlus/models/video_detail_res.dart' as video;
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../../utils/storage.dart';
import '../../utils/utils.dart';
import 'package:PiliPlus/common/widgets/spring_physics.dart';
class ListSheetContent extends CommonSlidePage {
const ListSheetContent({
super.key,
this.index, // tab index
this.season,
this.episodes,
this.bvid,
this.aid,
required this.currentCid,
required this.changeFucCall,
this.onClose,
this.onReverse,
this.showTitle,
this.isSupportReverse,
this.isReversed,
super.enableSlide,
});
final dynamic index;
final dynamic season;
final dynamic episodes;
final String? bvid;
final int? aid;
final int currentCid;
final Function changeFucCall;
final VoidCallback? onClose;
final VoidCallback? onReverse;
final bool? showTitle;
final bool? isSupportReverse;
final bool? isReversed;
@override
State<ListSheetContent> createState() => _ListSheetContentState();
}
class _ListSheetContentState extends CommonSlidePageState<ListSheetContent>
with TickerProviderStateMixin {
late List<ItemScrollController> itemScrollController = [];
late int currentIndex = _currentIndex;
late List<bool> reverse;
int get _index => widget.index ?? 0;
late final bool _isList = widget.season != null &&
widget.season?.sections is List &&
widget.season.sections.length > 1;
dynamic get episodes =>
widget.episodes ?? widget.season?.sections[_index].episodes;
TabController? _ctr;
StreamController? _indexStream;
int? _seasonFav;
StreamController? _favStream;
@override
void didUpdateWidget(ListSheetContent oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.showTitle != false) {
return;
}
int currentIndex = _currentIndex;
void jumpToCurrent() {
if (this.currentIndex != currentIndex) {
this.currentIndex = currentIndex;
try {
itemScrollController[_index].jumpTo(
index: currentIndex,
);
} catch (_) {}
}
}
// jump to current
if (_ctr != null && widget.index != _ctr?.index) {
_ctr?.animateTo(_index, duration: const Duration(milliseconds: 200));
Future.delayed(const Duration(milliseconds: 300)).then((_) {
jumpToCurrent();
});
} else {
jumpToCurrent();
}
}
int get _currentIndex => max(
0,
_isList
? widget.season.sections[_index].episodes
.indexWhere((e) => e.cid == widget.currentCid)
: episodes.indexWhere((e) => e.cid == widget.currentCid));
void listener() {
_indexStream?.add(_ctr?.index);
}
late bool _isInit = true;
@override
void initState() {
super.initState();
if (_isList) {
_indexStream ??= StreamController<int>.broadcast();
_ctr = TabController(
vsync: this,
length: widget.season.sections.length,
initialIndex: _index,
)..addListener(listener);
}
itemScrollController = _isList
? List.generate(
widget.season.sections.length, (_) => ItemScrollController())
: [ItemScrollController()];
reverse = _isList
? List.generate(widget.season.sections.length, (_) => false)
: [false];
if (Accounts.main.isLogin && widget.bvid != null && widget.season != null) {
_favStream ??= StreamController<int>();
() async {
dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid);
if (result['status']) {
_seasonFav = result['data']['season_fav'] ? 1 : 0;
_favStream?.add(_seasonFav);
}
}();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (enableSlide && GStorage.collapsibleVideoPage) {
if (mounted) {
setState(() {
_isInit = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
itemScrollController[_index].jumpTo(index: currentIndex);
} catch (_) {}
});
}
} else {
try {
itemScrollController[_index].jumpTo(index: currentIndex);
} catch (_) {}
}
});
}
@override
void dispose() {
_favStream?.close();
_favStream = null;
_indexStream?.close();
_indexStream = null;
_ctr?.removeListener(listener);
_ctr?.dispose();
super.dispose();
}
Widget buildEpisodeListItem(
dynamic episode,
int index,
int length,
bool isCurrentIndex,
) {
Color primary = Theme.of(context).colorScheme.primary;
late String title;
if (episode.runtimeType.toString() == "EpisodeItem") {
if (episode.longTitle != null && episode.longTitle != "") {
dynamic leading = episode.title ?? index + 1;
title =
"${Utils.isStringNumeric(leading) ? '$leading话' : leading} ${episode.longTitle!}";
} else {
title = episode.title!;
}
} else if (episode.runtimeType.toString() == "PageItem") {
title = episode.pagePart!;
} else if (episode.runtimeType.toString() == "Part") {
title = episode.pagePart!;
// debugPrint("未知类型:${episode.runtimeType}");
}
return ListTile(
onTap: () {
if (episode.badge != null && episode.badge == "会员") {
dynamic userInfo = GStorage.userInfo.get('userInfoCache');
int vipStatus = 0;
if (userInfo != null) {
vipStatus = userInfo.vipStatus;
}
if (vipStatus != 1) {
SmartDialog.showToast('需要大会员');
// return;
}
}
SmartDialog.showToast('切换到:$title');
widget.onClose?.call();
currentIndex = index;
widget.changeFucCall(
episode is bangumi.EpisodeItem ? episode.epId : null,
episode.runtimeType.toString() == "EpisodeItem"
? episode.bvid
: widget.bvid,
episode.cid,
episode.runtimeType.toString() == "EpisodeItem"
? episode.aid
: widget.aid,
episode is video.EpisodeItem
? episode.arc?.pic
: episode is bangumi.EpisodeItem
? episode.cover
: null,
);
},
dense: false,
leading: (episode is video.EpisodeItem && episode.arc?.pic != null) ||
(episode is video.Part && episode.firstFrame != null) ||
(episode is bangumi.EpisodeItem && episode.cover != null)
? Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: isCurrentIndex
? BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
width: 1.8,
strokeAlign: BorderSide.strokeAlignOutside,
color: Theme.of(context).colorScheme.primary,
),
)
: null,
child: LayoutBuilder(
builder: (context, constraints) => NetworkImgLayer(
radius: 6,
src: episode is video.EpisodeItem
? episode.arc?.pic
: episode is bangumi.EpisodeItem
? episode.cover
: episode.firstFrame,
width: constraints.maxHeight * StyleString.aspectRatio,
height: constraints.maxHeight,
),
),
)
: isCurrentIndex
? Image.asset(
'assets/images/live.png',
color: primary,
height: 12,
semanticLabel: "正在播放:",
)
: null,
title: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: isCurrentIndex ? FontWeight.bold : null,
color: isCurrentIndex
? primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (episode.badge != null) ...[
if (episode.badge == '会员')
Image.asset(
'assets/images/big-vip.png',
height: 20,
semanticLabel: "大会员",
)
else
Text(episode.badge),
const SizedBox(width: 10),
],
if (episode is! bangumi.EpisodeItem) Text('${index + 1}/$length'),
],
),
);
}
@override
Widget build(BuildContext context) {
if (enableSlide && GStorage.collapsibleVideoPage && _isInit) {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
);
}
return enableSlide
? Padding(
padding: EdgeInsets.only(top: padding),
child: buildPage,
)
: buildPage;
}
@override
Widget get buildPage => Material(
color: widget.showTitle == false
? Colors.transparent
: Theme.of(context).colorScheme.surface,
child: Column(
children: [
Container(
height: 45,
padding: EdgeInsets.symmetric(
horizontal: widget.showTitle != false ? 14 : 6),
child: Row(
children: [
if (widget.showTitle != false)
Text(
'合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})',
style: Theme.of(context).textTheme.titleMedium,
),
StreamBuilder(
stream: _favStream?.stream,
builder: (context, snapshot) => snapshot.hasData
? mediumButton(
tooltip: _seasonFav == 1 ? '取消订阅' : '订阅',
icon: _seasonFav == 1
? Icons.notifications_off_outlined
: Icons.notifications_active_outlined,
onPressed: () async {
dynamic result = await VideoHttp.seasonFav(
isFav: _seasonFav == 1,
seasonId: widget.season.id,
);
if (result['status']) {
SmartDialog.showToast(
'${_seasonFav == 1 ? '取消' : ''}订阅成功');
_seasonFav = _seasonFav == 1 ? 0 : 1;
_favStream?.add(_seasonFav);
} else {
SmartDialog.showToast(result['msg']);
}
},
)
: const SizedBox.shrink(),
),
mediumButton(
tooltip: '跳至顶部',
icon: Icons.vertical_align_top,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? 0
: _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至底部',
icon: Icons.vertical_align_bottom,
onPressed: () {
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse[_ctr?.index ?? 0]
? _isList
? widget.season.sections[_ctr?.index].episodes
.length -
1
: episodes.length - 1
: 0,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
mediumButton(
tooltip: '跳至当前',
icon: Icons.my_location,
onPressed: () async {
if (_ctr != null && _ctr?.index != (_index)) {
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
},
),
if (widget.isSupportReverse == true)
if (!_isList)
_reverseButton
else
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) {
return snapshot.data == _index
? _reverseButton
: const SizedBox.shrink();
},
),
const Spacer(),
StreamBuilder(
stream: _indexStream?.stream,
initialData: _index,
builder: (context, snapshot) => mediumButton(
tooltip: reverse[snapshot.data] ? '顺序' : '倒序',
icon: !reverse[snapshot.data]
? MdiIcons.sortNumericAscending
: MdiIcons.sortNumericDescending,
onPressed: () {
setState(() {
reverse[_ctr?.index ?? 0] =
!reverse[_ctr?.index ?? 0];
});
},
),
),
if (widget.onClose != null)
mediumButton(
tooltip: '关闭',
icon: Icons.close,
onPressed: widget.onClose,
),
],
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
if (_isList)
TabBar(
controller: _ctr,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
tabs: (widget.season.sections as List)
.map((item) => Tab(text: item.title))
.toList(),
dividerHeight: 1,
dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
),
Expanded(
child: _isList
? Material(
color: Colors.transparent,
child: tabBarView(
controller: _ctr,
children: List.generate(
widget.season.sections.length,
(index) => _buildBody(
index, widget.season.sections[index].episodes),
),
),
)
: enableSlide
? slideList()
: buildList,
),
],
),
);
@override
Widget get buildList => Material(
color: Colors.transparent,
child: _buildBody(null, episodes),
);
Widget get _reverseButton => mediumButton(
tooltip: widget.isReversed == true ? '正序播放' : '倒序播放',
icon: widget.isReversed == true
? MdiIcons.sortDescending
: MdiIcons.sortAscending,
onPressed: () async {
if (widget.showTitle == false) {
// jump to current
if (_ctr != null && _ctr?.index != (_index)) {
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
try {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
duration: const Duration(milliseconds: 200),
);
} catch (_) {}
}
widget.onReverse?.call();
},
);
Widget _buildBody(i, episodes) => ScrollablePositionedList.separated(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 80,
),
reverse: reverse[i ?? 0],
itemCount: episodes.length,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return buildEpisodeListItem(
episodes[index],
index,
episodes.length,
i != null
? i == (_index)
? currentIndex == index
: false
: currentIndex == index,
);
},
itemScrollController: itemScrollController[i ?? 0],
separatorBuilder: (context, index) => Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
);
}

View File

@@ -1,5 +1,4 @@
import 'package:PiliPlus/common/widgets/http_error.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:flutter/material.dart';
Widget get loadingWidget => Center(child: CircularProgressIndicator());
@@ -11,7 +10,6 @@ Widget errorWidget({errMsg, callback}) => HttpError(
);
Widget scrollErrorWidget({errMsg, callback}) => CustomScrollView(
controller: ScrollController(),
slivers: [
HttpError(
errMsg: errMsg,
@@ -19,27 +17,3 @@ Widget scrollErrorWidget({errMsg, callback}) => CustomScrollView(
)
],
);
Widget replyErrorWidget(context, isSliver, errMsg, onReload) => HttpError(
isSliver: isSliver,
errMsg:
'${errMsg.startsWith('gRPC Error') ? '如无法加载评论:\n关闭代理\n或设置中关闭使用gRPC加载评论\n\n' : ''}$errMsg',
callback: onReload,
extraWidget: errMsg.startsWith('gRPC Error') && GlobalData().grpcReply
? FilledButton.tonal(
onPressed: () {
GlobalData().grpcReply = false;
onReload();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
return Theme.of(context).colorScheme.primary.withAlpha(20);
}),
),
child: Text(
'暂时关闭gRPC加载评论',
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
)
: null,
);

View File

@@ -22,6 +22,7 @@ class NetworkImgLayer extends StatelessWidget {
this.isLongPic,
this.callback,
this.getPlaceHolder,
this.boxFit,
});
final String? src;
@@ -38,6 +39,7 @@ class NetworkImgLayer extends StatelessWidget {
final Function? isLongPic;
final Function? callback;
final Function? getPlaceHolder;
final BoxFit? boxFit;
@override
Widget build(BuildContext context) {
@@ -57,7 +59,7 @@ class NetworkImgLayer extends StatelessWidget {
Widget _buildImage(context) {
int? memCacheWidth, memCacheHeight;
if (callback?.call() == true || width <= height) {
if (ignoreHeight == true || callback?.call() == true || width <= height) {
memCacheWidth = width.cacheSize(context);
} else {
memCacheHeight = height.cacheSize(context);
@@ -68,7 +70,7 @@ class NetworkImgLayer extends StatelessWidget {
height: ignoreHeight == null || ignoreHeight == false ? height : null,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: BoxFit.cover,
fit: boxFit ?? BoxFit.cover,
alignment:
isLongPic?.call() == true ? Alignment.topCenter : Alignment.center,
fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 120),
@@ -81,7 +83,6 @@ class NetworkImgLayer extends StatelessWidget {
}
Widget placeholder(BuildContext context) {
int cacheWidth = width.cacheSize(context);
return Container(
width: width,
height: height,
@@ -104,7 +105,7 @@ class NetworkImgLayer extends StatelessWidget {
: 'assets/images/loading.png',
width: width,
height: height,
cacheWidth: cacheWidth == 0 ? null : cacheWidth,
cacheWidth: width.cacheSize(context),
// cacheHeight: height.cacheSize(context),
),
),

View File

@@ -6,3 +6,14 @@ class Pair<T, R> {
T first;
R second;
}
class Triple<T, R, S> {
Triple({
required this.first,
required this.second,
required this.third,
});
T first;
R second;
S third;
}

View File

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

View File

@@ -0,0 +1,97 @@
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
Widget videoTabBarView({
required List<Widget> children,
TabController? controller,
}) =>
TabBarView(
physics: const CustomTabBarViewClampingScrollPhysics(),
controller: controller,
children: children,
);
Widget tabBarView({
required List<Widget> children,
TabController? controller,
}) =>
TabBarView(
physics: const CustomTabBarViewScrollPhysics(),
controller: controller,
children: children,
);
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
const CustomTabBarViewScrollPhysics({super.parent});
@override
CustomTabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => CustomSpringDescription();
}
class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
const CustomTabBarViewClampingScrollPhysics({super.parent});
@override
CustomTabBarViewClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return CustomTabBarViewClampingScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => CustomSpringDescription();
}
class PositionRetainedScrollPhysics extends AlwaysScrollableScrollPhysics {
const PositionRetainedScrollPhysics({super.parent, this.shouldRetain = true});
final bool shouldRetain;
@override
PositionRetainedScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PositionRetainedScrollPhysics(parent: buildParent(ancestor));
}
@override
double adjustPositionForNewDimensions({
required ScrollMetrics oldPosition,
required ScrollMetrics newPosition,
required bool isScrolling,
required double velocity,
}) {
final position = super.adjustPositionForNewDimensions(
oldPosition: oldPosition,
newPosition: newPosition,
isScrolling: isScrolling,
velocity: velocity,
);
late final diff = newPosition.maxScrollExtent - oldPosition.maxScrollExtent;
if (shouldRetain && oldPosition.pixels == 0 && diff > 0) {
return position + diff;
} else {
return position;
}
}
}
class CustomSpringDescription implements SpringDescription {
@override
final mass = GStorage.springDescription[0];
@override
final stiffness = GStorage.springDescription[1];
@override
final damping = GStorage.springDescription[2];
CustomSpringDescription._();
static final _instance = CustomSpringDescription._();
factory CustomSpringDescription() => _instance;
}

View File

@@ -1,54 +0,0 @@
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
Widget videoTabBarView({
required List<Widget> children,
TabController? controller,
}) =>
TabBarView(
physics: const CustomTabBarViewClampingScrollPhysics(),
controller: controller,
children: children,
);
Widget tabBarView({
required List<Widget> children,
TabController? controller,
}) =>
TabBarView(
physics: const CustomTabBarViewScrollPhysics(),
controller: controller,
children: children,
);
class CustomTabBarViewScrollPhysics extends ScrollPhysics {
const CustomTabBarViewScrollPhysics({super.parent});
@override
CustomTabBarViewScrollPhysics applyTo(ScrollPhysics? ancestor) {
return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => SpringDescription(
mass: GStorage.springDescription[0],
stiffness: GStorage.springDescription[1],
damping: GStorage.springDescription[2],
);
}
class CustomTabBarViewClampingScrollPhysics extends ClampingScrollPhysics {
const CustomTabBarViewClampingScrollPhysics({super.parent});
@override
CustomTabBarViewClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return CustomTabBarViewClampingScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => SpringDescription(
mass: GStorage.springDescription[0],
stiffness: GStorage.springDescription[1],
damping: GStorage.springDescription[2],
);
}

View File

@@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/utils.dart';
Widget statDanMu({
required BuildContext context,
String? theme,
dynamic danmu,
String? size,
Color? textColor,
}) {
Map<String, Color> colorObject = {
'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline.withOpacity(0.8),
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
};
Color color = textColor ?? colorObject[theme]!;
return Row(
children: [
Icon(
Icons.subtitles_outlined,
size: 14,
color: color,
),
const SizedBox(width: 2),
Text(
Utils.numFormat(danmu!),
style: TextStyle(
fontSize: size == 'medium' ? 12 : 11,
color: color,
),
overflow: TextOverflow.clip,
semanticsLabel: '${Utils.numFormat(danmu!)}条弹幕',
)
],
);
}

View File

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

View File

@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/utils.dart';
Widget statView({
required BuildContext context,
String? theme,
dynamic view,
String? size,
String? goto,
Color? textColor,
}) {
Map<String, Color> colorObject = {
'white': Colors.white,
'gray': Theme.of(context).colorScheme.outline.withOpacity(0.8),
'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
};
Color color = textColor ?? colorObject[theme]!;
return Row(
children: [
Icon(
goto == 'picture'
? Icons.remove_red_eye_outlined
: Icons.play_circle_outlined,
size: 13,
color: color,
),
const SizedBox(width: 2),
Text(
Utils.numFormat(view!),
style: TextStyle(
fontSize: size == 'medium' ? 12 : 11,
color: color,
),
overflow: TextOverflow.clip,
semanticsLabel:
'${Utils.numFormat(view!)}${goto == "picture" ? "浏览" : "播放"}',
),
],
);
}

View File

@@ -1,5 +1,8 @@
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/video_progress_indicator.dart';
import 'package:PiliPlus/models/model_hot_video_item.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/models/search/result.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../../http/search.dart';
@@ -7,8 +10,7 @@ import '../../utils/utils.dart';
import '../constants.dart';
import 'badge.dart';
import 'network_img_layer.dart';
import 'stat/danmu.dart';
import 'stat/view.dart';
import 'stat/stat.dart';
import 'video_popup_menu.dart';
// 视频卡片 - 水平布局
@@ -25,7 +27,7 @@ class VideoCardH extends StatelessWidget {
this.onLongPress,
this.onViewLater,
});
final dynamic videoItem;
final BaseVideoItemModel videoItem;
final String source;
final bool showOwner;
final bool showView;
@@ -37,15 +39,22 @@ class VideoCardH extends StatelessWidget {
@override
Widget build(BuildContext context) {
final int aid = videoItem.aid;
final String bvid = videoItem.bvid;
final int aid = videoItem.aid!;
final String bvid = videoItem.bvid!;
String type = 'video';
try {
type = videoItem.type;
} catch (_) {}
// try {
// type = videoItem.type;
// } catch (_) {}
if (videoItem is SearchVideoItemModel) {
var typeOrNull = (videoItem as SearchVideoItemModel).type;
if (typeOrNull?.isNotEmpty == true) {
type = typeOrNull!;
}
}
return Material(
color: Colors.transparent,
child: Stack(
clipBehavior: Clip.none,
children: [
Semantics(
label: Utils.videoItemSemantics(videoItem),
@@ -62,13 +71,7 @@ class VideoCardH extends StatelessWidget {
} else {
imageSaveDialog(
context: context,
title: videoItem.title is String
? videoItem.title
: videoItem.title is List
? (videoItem.title as List)
.map((item) => item['text'])
.join()
: '',
title: videoItem.title,
cover: videoItem.pic,
);
}
@@ -82,9 +85,11 @@ class VideoCardH extends StatelessWidget {
SmartDialog.showToast('课堂视频暂不支持播放');
return;
}
if (videoItem is HotVideoItemModel &&
videoItem.redirectUrl?.isNotEmpty == true) {
if (Utils.viewPgcFromUri(videoItem.redirectUrl!)) {
if ((videoItem is HotVideoItemModel) &&
(videoItem as HotVideoItemModel).redirectUrl?.isNotEmpty ==
true) {
if (Utils.viewPgcFromUri(
(videoItem as HotVideoItemModel).redirectUrl!)) {
return;
}
}
@@ -122,23 +127,49 @@ class VideoCardH extends StatelessWidget {
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
num? progress;
if (videoItem is HotVideoItemModel) {
progress =
(videoItem as HotVideoItemModel).progress;
}
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: videoItem.pic as String,
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
if (videoItem is HotVideoItemModel &&
videoItem.pgcLabel?.isNotEmpty == true)
if (videoItem is HotVideoItemModel)
PBadge(
text: videoItem.pgcLabel,
text:
(videoItem as HotVideoItemModel).pgcLabel,
top: 6.0,
right: 6.0,
),
if (videoItem.duration != 0)
if (progress != null && progress != 0) ...[
PBadge(
text: Utils.timeFormat(videoItem.duration!),
text: progress == -1
? '已看完'
: '${Utils.timeFormat(progress)}/${Utils.timeFormat(videoItem.duration)}',
right: 6,
bottom: 8,
type: 'gray',
),
Positioned(
left: 0,
bottom: 0,
right: 0,
child: videoProgressIndicator(
progress == -1
? 1
: progress / videoItem.duration,
),
)
] else if (videoItem.duration > 0)
PBadge(
text: Utils.timeFormat(videoItem.duration),
right: 6.0,
bottom: 6.0,
type: 'gray',
@@ -181,7 +212,7 @@ class VideoCardH extends StatelessWidget {
);
}
Widget videoContent(context) {
Widget videoContent(BuildContext context) {
String pubdate = showPubdate
? Utils.dateFormat(videoItem.pubdate!, formatType: 'day')
: '';
@@ -190,7 +221,33 @@ class VideoCardH extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (videoItem.title is String)
if ((videoItem is SearchVideoItemModel) &&
(videoItem as SearchVideoItemModel).titleList?.isNotEmpty == true)
Expanded(
child: Text.rich(
overflow: TextOverflow.ellipsis,
maxLines: 2,
TextSpan(
children: [
for (var i
in (videoItem as SearchVideoItemModel).titleList!)
TextSpan(
text: i['text'],
style: TextStyle(
fontSize:
Theme.of(context).textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
color: i['type'] == 'em'
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
)
else
Expanded(
child: Text(
videoItem.title,
@@ -203,31 +260,6 @@ class VideoCardH extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
)
else
Expanded(
child: Text.rich(
overflow: TextOverflow.ellipsis,
maxLines: 2,
TextSpan(
children: [
for (final i in videoItem.title) ...[
TextSpan(
text: i['text'] as String,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
color: i['type'] == 'em'
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
]
],
),
),
),
// const Spacer(),
// if (videoItem.rcmdReason != null &&
@@ -265,18 +297,18 @@ class VideoCardH extends StatelessWidget {
Row(
children: [
if (showView) ...[
statView(
StatView(
context: context,
theme: 'gray',
view: videoItem.stat.view as int,
value: videoItem.stat.viewStr,
),
const SizedBox(width: 8),
],
if (showDanmaku)
statDanMu(
StatDanMu(
context: context,
theme: 'gray',
danmu: videoItem.stat.danmu as int,
value: videoItem.stat.danmuStr,
),
const Spacer(),
if (source == 'normal') const SizedBox(width: 24),

View File

@@ -1,6 +1,5 @@
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/stat/danmu.dart';
import 'package:PiliPlus/common/widgets/stat/view.dart';
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/common/widgets/video_popup_menu.dart';
import 'package:PiliPlus/common/widgets/video_progress_indicator.dart';
import 'package:PiliPlus/models/space_archive/item.dart';
@@ -18,10 +17,12 @@ class VideoCardHMemberVideo extends StatelessWidget {
required this.videoItem,
this.onTap,
this.bvid,
this.fromViewAid,
});
final Item videoItem;
final VoidCallback? onTap;
final dynamic bvid;
final String? fromViewAid;
@override
Widget build(BuildContext context) {
@@ -43,12 +44,12 @@ class VideoCardHMemberVideo extends StatelessWidget {
return;
}
}
if (videoItem.bvid == null || videoItem.firstCid == null) {
if (videoItem.bvid == null || videoItem.cid == null) {
return;
}
try {
Utils.toViewPage(
'bvid=${videoItem.bvid}&cid=${videoItem.firstCid}',
'bvid=${videoItem.bvid}&cid=${videoItem.cid}',
arguments: {
'heroTag': Utils.makeHeroTag(videoItem.bvid),
},
@@ -83,6 +84,25 @@ class VideoCardHMemberVideo extends StatelessWidget {
width: maxWidth,
height: maxHeight,
),
if (fromViewAid == videoItem.param)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.black54,
),
child: Center(
child: const Text(
'上次观看',
style: TextStyle(
color: Colors.white,
fontSize: 15,
letterSpacing: 1.5,
),
),
),
),
),
if (videoItem.badges?.isNotEmpty == true)
PBadge(
text: videoItem.badges!
@@ -90,15 +110,11 @@ class VideoCardHMemberVideo extends StatelessWidget {
.join('|'),
right: 6.0,
top: 6.0,
type: videoItem.badges!.first.text == '充电专属'
? 'error'
: 'primary',
),
if (videoItem.duration != null)
PBadge(
text: Utils.timeFormat(videoItem.duration),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (videoItem.history != null)
if (videoItem.history != null) ...[
Builder(builder: (context) {
try {
return Positioned(
@@ -114,6 +130,34 @@ class VideoCardHMemberVideo extends StatelessWidget {
return const SizedBox.shrink();
}
}),
Builder(builder: (context) {
try {
return PBadge(
text: videoItem.history!['progress'] ==
videoItem.history!['duration']
? '已看完'
: '${Utils.timeFormat(videoItem.history!['progress'])}/${Utils.timeFormat(videoItem.history!['duration'])}',
right: 6.0,
bottom: 6.0,
type: 'gray',
);
} catch (_) {
return PBadge(
text:
Utils.timeFormat(videoItem.duration),
right: 6.0,
bottom: 6.0,
type: 'gray',
);
}
}),
] else if (videoItem.duration > 0)
PBadge(
text: Utils.timeFormat(videoItem.duration),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
@@ -148,7 +192,7 @@ class VideoCardHMemberVideo extends StatelessWidget {
Expanded(
child: Text(
// videoItem.season?['title'] ?? videoItem.title ?? '',
videoItem.title ?? '',
videoItem.title,
textAlign: TextAlign.start,
style: TextStyle(
fontWeight: videoItem.bvid != null && videoItem.bvid == bvid
@@ -180,19 +224,19 @@ class VideoCardHMemberVideo extends StatelessWidget {
const SizedBox(height: 3),
Row(
children: [
statView(
StatView(
context: context,
theme: 'gray',
// view: videoItem.season?['view_content'] ??
// videoItem.viewContent,
view: videoItem.viewContent,
value: videoItem.stat.viewStr,
),
const SizedBox(width: 8),
statDanMu(
StatDanMu(
context: context,
theme: 'gray',
// danmu: videoItem.season?['danmaku'] ?? videoItem.danmaku,
danmu: videoItem.danmaku,
value: videoItem.stat.danmuStr,
),
],
),

View File

@@ -5,9 +5,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import '../../models/home/rcmd/result.dart';
import '../../models/model_rec_video_item.dart';
import 'stat/danmu.dart';
import 'stat/view.dart';
import '../../http/dynamics.dart';
import 'stat/stat.dart';
import '../../utils/id_utils.dart';
import '../../utils/utils.dart';
import '../constants.dart';
@@ -17,7 +15,7 @@ import 'video_popup_menu.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
final dynamic videoItem;
final BaseRecVideoItemModel videoItem;
final VoidCallback? onRemove;
const VideoCardV({
@@ -32,14 +30,14 @@ class VideoCardV extends StatelessWidget {
}
void onPushDetail(heroTag) async {
String goto = videoItem.goto;
String goto = videoItem.goto!;
switch (goto) {
case 'bangumi':
Utils.viewBangumi(epId: videoItem.param);
Utils.viewBangumi(epId: videoItem.param!);
break;
case 'av':
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
int cid = videoItem.cid;
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid!);
int cid = videoItem.cid!;
if (cid == -1) {
cid = await SearchHttp.ab2c(aid: videoItem.aid, bvid: bvid);
}
@@ -56,32 +54,21 @@ class VideoCardV extends StatelessWidget {
case 'picture':
try {
String dynamicType = 'picture';
String uri = videoItem.uri;
String uri = videoItem.uri!;
String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) {
if (uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554
dynamicType = 'read';
RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!;
Match match = regex.firstMatch(uri)!;
String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
}
if (uri.startsWith('http')) {
String path = Uri.parse(uri).path;
if (isStringNumeric(path.split('/')[1])) {
// 请求接口
var res =
await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
if (res['status']) {
Get.toNamed('/dynamicDetail', arguments: {
'item': res['data'],
'floor': 1,
'action': 'detail'
});
} else {
SmartDialog.showToast(res['msg']);
}
String id = Uri.parse(uri).path.split('/')[1];
if (isStringNumeric(id)) {
Utils.pushDynFromId(id);
return;
}
}
@@ -96,8 +83,8 @@ class VideoCardV extends StatelessWidget {
}
break;
default:
SmartDialog.showToast(videoItem.goto);
Utils.handleWebview(videoItem.uri);
SmartDialog.showToast(goto);
Utils.handleWebview(videoItem.uri!);
}
}
@@ -115,7 +102,7 @@ class VideoCardV extends StatelessWidget {
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.id)),
onTap: () => onPushDetail(Utils.makeHeroTag(videoItem.aid)),
onLongPress: () => imageSaveDialog(
context: context,
title: videoItem.title,
@@ -180,7 +167,7 @@ class VideoCardV extends StatelessWidget {
Row(
children: [
Expanded(
child: Text(videoItem.title + "\n",
child: Text("${videoItem.title}\n",
// semanticsLabel: "${videoItem.title}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -224,7 +211,7 @@ class VideoCardV extends StatelessWidget {
),
const SizedBox(width: 2),
],
if (videoItem.isFollowed == 1) ...[
if (videoItem.isFollowed) ...[
const PBadge(
text: '已关注',
stack: 'normal',
@@ -260,18 +247,18 @@ class VideoCardV extends StatelessWidget {
Widget videoStat(context) {
return Row(
children: [
statView(
StatView(
context: context,
theme: 'gray',
view: videoItem.stat.view,
value: videoItem.stat.viewStr,
goto: videoItem.goto,
),
const SizedBox(width: 4),
if (videoItem.goto != 'picture')
statDanMu(
StatDanMu(
context: context,
theme: 'gray',
danmu: videoItem.stat.danmu,
value: videoItem.stat.danmuStr,
),
if (videoItem is RecVideoItemModel) ...<Widget>[
const Spacer(),
@@ -295,7 +282,7 @@ class VideoCardV extends StatelessWidget {
],
if (videoItem is RecVideoItemAppModel &&
videoItem.desc != null &&
videoItem.desc.contains(' · ')) ...<Widget>[
videoItem.desc!.contains(' · ')) ...<Widget>[
const Spacer(),
Expanded(
flex: 0,
@@ -311,7 +298,7 @@ class VideoCardV extends StatelessWidget {
.withOpacity(0.8),
),
text: Utils.shortenChineseDateString(
videoItem.desc.split(' · ').last)),
videoItem.desc!.split(' · ').last)),
)),
const SizedBox(width: 2),
]

View File

@@ -1,5 +1,7 @@
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models/space/item.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../../utils/utils.dart';
@@ -28,9 +30,15 @@ class VideoCardVMemberHome extends StatelessWidget {
return;
}
}
String bvid = videoItem.bvid ?? '';
String? aid = videoItem.param;
String? bvid = videoItem.bvid;
if (aid == null && bvid == null) {
return;
}
int? cid = videoItem.firstCid;
cid ??= await SearchHttp.ab2c(aid: aid, bvid: bvid);
Utils.toViewPage(
'bvid=$bvid&cid=${videoItem.firstCid}',
'bvid=${bvid ?? IdUtils.av2bv(int.parse(aid!))}&cid=$cid',
arguments: {
// 'videoItem': videoItem,
'pic': videoItem.cover,

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
@@ -21,16 +22,16 @@ class VideoCustomAction {
}
class VideoCustomActions {
dynamic videoItem;
BaseSimpleVideoItemModel videoItem;
BuildContext context;
late List<VideoCustomAction> actions;
VoidCallback? onRemove;
VideoCustomActions(this.videoItem, this.context, [this.onRemove]) {
actions = [
if ((videoItem.bvid as String?)?.isNotEmpty == true) ...[
if (videoItem.bvid?.isNotEmpty == true) ...[
VideoCustomAction(
videoItem.bvid,
videoItem.bvid!,
'copy',
Stack(
children: [
@@ -39,7 +40,7 @@ class VideoCustomActions {
],
),
() {
Utils.copyText(videoItem.bvid);
Utils.copyText(videoItem.bvid!);
},
),
VideoCustomAction(
@@ -84,7 +85,7 @@ class VideoCustomActions {
SmartDialog.showToast("未能获取dislikeReasons或feedbacks");
return;
}
Widget actionButton(DislikeReason? r, FeedbackReason? f) {
Widget actionButton(Reason? r, Reason? f) {
return SearchText(
text: r?.name ?? f?.name ?? '未知',
onTap: (_) async {
@@ -258,13 +259,11 @@ class VideoCustomActions {
TextButton(
onPressed: () async {
var res = await VideoHttp.relationMod(
mid: videoItem.owner.mid,
mid: videoItem.owner.mid!,
act: 5,
reSrc: 11,
);
List<int> blackMidsList = GStorage.blackMidsList;
blackMidsList.insert(0, videoItem.owner.mid);
GStorage.setBlackMidsList(blackMidsList);
GStorage.setBlackMid(videoItem.owner.mid!);
Get.back();
SmartDialog.showToast(res['msg'] ?? '成功');
},

View File

@@ -49,7 +49,7 @@ class Api {
/// aid num 稿件avid 必要(可选) avid与bvid任选一个
/// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
// https://api.bilibili.com/x/web-interface/archive/has/like
static const String hasLikeVideo = '/x/web-interface/archive/has/like';
// static const String hasLikeVideo = '/x/web-interface/archive/has/like';
static const String bangumiLikeCoinFav = '/pgc/season/episode/community';
@@ -79,7 +79,19 @@ class Api {
/// aid num 稿件avid 必要(可选) avid与bvid任选一个
/// bvid str 稿件bvid 必要(可选) avid与bvid任选一个
/// https://api.bilibili.com/x/web-interface/archive/coins
static const String hasCoinVideo = '/x/web-interface/archive/coins';
// static const String hasCoinVideo = '/x/web-interface/archive/coins';
/// 收藏夹 详情
/// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
/// pn int 当前页
/// ps int pageSize
/// keyword String 搜索词
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
/// tid int 分区id
/// platform web
/// type 0 当前收藏夹 1 全部收藏夹
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
static const String favResourceList = '/x/v3/fav/resource/list';
// 收藏视频双端POST
// access_key str APP登录Token APP方式必要
@@ -96,10 +108,20 @@ class Api {
static const String delFav = '/x/v3/fav/resource/batch-del';
static const String copyFav = '/x/v3/fav/resource/copy';
static const String moveFav = '/x/v3/fav/resource/move';
static const String cleanFav = '/x/v3/fav/resource/clean';
static const String sortFav = '/x/v3/fav/resource/sort';
static const String sortFavFolder = '/x/v3/fav/folder/sort';
// 判断视频是否被收藏双端GET
/// aid
// https://api.bilibili.com/x/v2/fav/video/favoured
static const String hasFavVideo = '/x/v2/fav/video/favoured';
// static const String hasFavVideo = '/x/v2/fav/video/favoured';
// 分享视频 Web端 POST
// https://api.bilibili.com/x/web-interface/share/add
@@ -123,10 +145,6 @@ class Api {
// rid num 目标 视频稿件avid
static const String favFolder = '/x/v3/fav/folder/created/list-all';
static const String copyFav = '/x/v3/fav/resource/copy';
static const String moveFav = '/x/v3/fav/resource/move';
static const String copyToview = '/x/v2/history/toview/copy';
static const String moveToview = '/x/v2/history/toview/move';
@@ -135,7 +153,9 @@ class Api {
static const String relatedList = '/x/web-interface/archive/related';
// 查询用户与自己关系_仅查关注
static const String hasFollow = '/x/relation';
static const String relation = '/x/relation';
static const String relations = '/x/relation/relations';
// 操作用户关系
static const String relationMod = '/x/relation/modify';
@@ -190,20 +210,6 @@ class Api {
static const String deleteFolder = '/x/v3/fav/folder/del';
static const String cleanFav = '/x/v3/fav/resource/clean';
/// 收藏夹 详情
/// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
/// pn int 当前页
/// ps int pageSize
/// keyword String 搜索词
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
/// tid int 分区id
/// platform web
/// type 0 当前收藏夹 1 全部收藏夹
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
// 正在直播的up & 关注的up
// https://api.bilibili.com/x/polymer/web-dynamic/v1/portal
static const String followUp = '/x/polymer/web-dynamic/v1/portal';
@@ -219,7 +225,7 @@ class Api {
'${HttpString.tUrl}/dynamic_like/v1/dynamic_like/thumb';
// 获取稍后再看
static const String seeYouLater = '/x/v2/history/toview';
static const String seeYouLater = '/x/v2/history/toview/web';
// 获取历史记录
static const String historyList = '/x/web-interface/history/cursor';
@@ -257,6 +263,11 @@ class Api {
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
static const String heartBeat = '/x/click-interface/web/heartbeat';
static const String historyReport = '/x/v2/history/report';
static const String roomEntryAction =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction';
static const String mediaListHistory = '/x/v1/medialist/history';
// 查询视频分P列表 (avid/bvid转cid)
@@ -277,10 +288,6 @@ class Api {
// https://api.bilibili.com/x/relation/tag?mid=17340771&tagid=-10&pn=1&ps=20
static const String tagFollowings = '/x/relation/tag';
// 关注分类
// https://api.bilibili.com/x/relation/tags
static const String followingsClass = '/x/relation/tags';
// 搜索follow
static const followSearch = '/x/relation/followings/search';
@@ -324,6 +331,9 @@ class Api {
static const String spaceArchive =
'${HttpString.appBaseUrl}/x/v2/space/archive/cursor';
static const String spaceStory =
'${HttpString.appBaseUrl}/x/v2/feed/index/space/story/cursor';
static const String spaceChargingArchive =
'${HttpString.appBaseUrl}/x/v2/space/archive/charging';
@@ -362,7 +372,9 @@ class Api {
static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索
static const String memberDynamicSearch = '/x/space/dynamic/search';
// static const String memberDynamicSearch = '/x/space/dynamic/search';
static const String memberDynamicSearch =
'/x/polymer/web-dynamic/v1/feed/space/search';
// 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@@ -371,7 +383,7 @@ class Api {
static const String toViewLater = '/x/v2/history/toview/add';
// 移除已观看
static const String toViewDel = '/x/v2/history/toview/del';
static const String toViewDel = '/x/v2/history/toview/v2/dels';
// 清空稍后再看
static const String toViewClear = '/x/v2/history/toview/clear';
@@ -406,7 +418,7 @@ class Api {
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
// 我的追番/追剧 ?type=1&pn=1&ps=15
static const String bangumiFollow = '/x/space/bangumi/follow/list';
static const String bangumiFollowList = '/x/space/bangumi/follow/list';
// 黑名单
static const String blackLst = '/x/relation/blacks';
@@ -638,9 +650,6 @@ class Api {
/// 最近点赞的视频
static const getRecentLikeVideoApi = '/x/space/like/video';
/// 最近追番
static const getRecentBangumiApi = '/x/space/bangumi/follow/list';
/// 用户专栏
static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series';
@@ -672,9 +681,6 @@ class Api {
/// 我的订阅-合集详情
static const favSeasonList = '/x/space/fav/season/list';
/// 我的订阅-播单详情
static const favResourceList = '/x/v3/fav/resource/list';
/// 发送私信
static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg';
@@ -693,7 +699,9 @@ class Api {
static const String removeMsg = '/session_svr/v1/session_svr/remove_session';
static const String removeSysMsg = '/x/sys-msg/del_notify_list';
static const String delSysMsg = '/x/sys-msg/del_notify_list';
static const String delMsgfeed = '/x/msgfeed/del';
static const String setTop = '/session_svr/v1/session_svr/set_top';
@@ -707,6 +715,7 @@ class Api {
static const String uploadImage = '/x/upload/web/image';
// 点赞投币收藏关注
static const String videoRelation = '/x/web-interface/archive/relation';
static const String seasonFav = '/x/v3/fav/season/'; // + fav unfav
@@ -722,5 +731,37 @@ class Api {
static const String pgcIndexResult = '/pgc/season/index/result';
static const String noteList = '/x/note/publish/list/archive';
static const String archiveNoteList = '/x/note/publish/list/archive';
static const String noteList = '/x/note/list';
static const String userNoteList = '/x/note/publish/list/user';
static const String addNote = '/x/note/add';
static const String delNote = '/x/note/del';
static const String delPublishNote = '/x/note/publish/del';
static const String archiveNote = '/x/note/list/archive';
static const String favArticle = '/x/polymer/web-dynamic/v1/opus/feed/fav';
static const String communityAction =
'/x/community/cosmo/interface/simple_action';
static const String delFavArticle = '/x/article/favorites/del';
static const String addFavArticle = '/x/article/favorites/add';
static const String replyTop = '/x/v2/reply/top';
static const String getCoin = '${HttpString.accountBaseUrl}/site/getCoin';
static const String getLiveEmoticons =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/v2/emoticon/GetEmoticons';
static const String pgcTimeline = '/pgc/web/timeline';
static const String searchTrending = '/x/v2/search/trending/ranking';
}

View File

@@ -1,4 +1,6 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/pgc_timeline/pgc_timeline.dart';
import 'package:PiliPlus/models/bangumi/pgc_timeline/result.dart';
import '../models/bangumi/list.dart';
import '../models/bangumi/pgc_index/condition.dart';
@@ -50,7 +52,7 @@ class BangumiHttp {
}
}
static Future<LoadingState> bangumiList({
static Future<LoadingState<List<BangumiListItemModel>?>> bangumiList({
int? page,
int? indexType,
}) async {
@@ -67,20 +69,41 @@ class BangumiHttp {
}
}
static Future<LoadingState> bangumiFollow({
dynamic mid,
static Future<LoadingState<BangumiListDataModel>> bangumiFollowList({
required dynamic mid,
required int type,
required int pn,
int? followStatus,
}) async {
var res = await Request().get(Api.bangumiFollow, queryParameters: {
var res = await Request().get(Api.bangumiFollowList, queryParameters: {
'vmid': mid,
'type': type,
if (followStatus != null) 'follow_status': followStatus,
'pn': pn,
});
if (res.data['code'] == 0) {
BangumiListDataModel data =
BangumiListDataModel.fromJson(res.data['data']);
return LoadingState.success(data);
return LoadingState.success(
BangumiListDataModel.fromJson(res.data['data']));
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState<List<Result>?>> pgcTimeline({
int types = 1, // 1`番剧`<br />3`电影`<br />4`国创` |
required int before,
required int after,
}) async {
var res = await Request().get(
Api.pgcTimeline,
queryParameters: {
'types': types,
'before': before,
'after': after,
},
);
if (res.data['code'] == 0) {
return LoadingState.success(PgcTimeline.fromJson(res.data).result);
} else {
return LoadingState.error(res.data['message']);
}

View File

@@ -4,7 +4,8 @@ import '../models/user/black.dart';
import 'index.dart';
class BlackHttp {
static Future<LoadingState> blackList({required int pn, int? ps}) async {
static Future<LoadingState<BlackListDataModel>> blackList(
{required int pn, int? ps}) async {
var res = await Request().get(Api.blackLst, queryParameters: {
'pn': pn,
'ps': ps ?? 50,

View File

@@ -8,5 +8,7 @@ class HttpString {
static const String messageBaseUrl = 'https://message.bilibili.com';
static const String dynamicShareBaseUrl = 'https://t.bilibili.com';
static const String spaceBaseUrl = 'https://space.bilibili.com';
static const String accountBaseUrl = 'https://account.bilibili.com';
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
}

View File

@@ -16,7 +16,7 @@ class DanmakuHttp {
await GrpcRepo.dmSegMobile(cid: cid, segmentIndex: segmentIndex);
if (!response['status']) {
if (queryCount >= 3) {
return DmSegMobileReply();
return {'status': false};
} else {
await Future.delayed(const Duration(seconds: 1));
return await queryDanmaku(
@@ -43,7 +43,7 @@ class DanmakuHttp {
}
}
}
return data;
return {'status': true, 'data': data};
}
static Future shootDanmaku({
@@ -60,7 +60,7 @@ class DanmakuHttp {
int? fontsize, // 弹幕字号默认25
int? pool, // 弹幕池选择0普通池 1字幕池 2特殊池代码/BAS弹幕默认普通池0
//int? rnd,// 当前时间戳*1000000若无此项则发送弹幕冷却时间限制为90s若有此项则发送弹幕冷却时间限制为5s
int? colorful, //60001专属渐变彩色需要会员
bool? colorful, //60001专属渐变彩色需要会员
int? checkbox_type, //是否带 UP 身份标识0普通4带有标识
// String? csrf,//CSRF Token位于 Cookie Cookie 方式必要
// String? access_key,// APP 登录 Token APP 方式必要
@@ -77,11 +77,11 @@ class DanmakuHttp {
//'aid': aid,
'bvid': bvid,
'progress': progress,
'color': color,
'color': colorful == true ? null : color,
'fontsize': fontsize,
'pool': pool,
'rnd': DateTime.now().microsecondsSinceEpoch,
'colorful': colorful,
'colorful': colorful == true ? 60001 : null,
'checkbox_type': checkbox_type,
'csrf': await Request.getCsrf(),
// 'access_key': access_key,
@@ -97,7 +97,6 @@ class DanmakuHttp {
if (response.statusCode != 200) {
return {
'status': false,
'data': [],
'msg': '弹幕发送失败,状态码:${response.statusCode}',
};
}
@@ -109,7 +108,6 @@ class DanmakuHttp {
} else {
return {
'status': false,
'data': [],
'msg': "${response.data['code']}: ${response.data['message']}",
};
}

View File

@@ -12,7 +12,6 @@ class DanmakuFilterHttp {
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}

View File

@@ -8,7 +8,7 @@ import '../models/dynamics/up.dart';
import 'index.dart';
class DynamicsHttp {
static Future<LoadingState> followDynamic({
static Future<LoadingState<DynamicsDataModel>> followDynamic({
String? type,
String? offset,
int? mid,
@@ -53,11 +53,7 @@ class DynamicsHttp {
'data': FollowUpModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -80,11 +76,7 @@ class DynamicsHttp {
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -102,7 +94,7 @@ class DynamicsHttp {
if (id != null) 'id': id,
if (rid != null) 'rid': rid,
if (type != null) 'type': type,
'features': 'itemOpusStyle',
'features': 'itemOpusStyle,listOnlyfans',
},
options:
clearCookie ? Options(extra: {'account': AnonymousAccount()}) : null,
@@ -116,14 +108,12 @@ class DynamicsHttp {
} catch (err) {
return {
'status': false,
'data': [],
'msg': err.toString(),
};
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}

View File

@@ -4,7 +4,7 @@ import '../models/fans/result.dart';
import 'index.dart';
class FanHttp {
static Future<LoadingState> fans(
static Future<LoadingState<FansDataModel>> fans(
{int? vmid, int? pn, int? ps, String? orderType}) async {
var res = await Request().get(Api.fans, queryParameters: {
'vmid': vmid,

View File

@@ -17,11 +17,7 @@ class FollowHttp {
'data': FollowDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@@ -14,12 +14,21 @@ class HtmlHttp {
var response = await Request().get(
"https://www.bilibili.com/opus/$id",
extra: {'ua': 'pc'},
options: Options(
followRedirects: false,
validateStatus: (status) => true,
),
);
if (response.data is! String && response.data is! List<int>) {
return;
}
try {
if (response.data.contains('Redirecting to')) {
String? cvid = RegExp(r'cv\d+').firstMatch(response.data)?.group(0);
if (cvid != null) {
return await reqReadHtml(cvid, dynamicType, false);
}
RegExp regex = RegExp(r'//([\w\.]+)/(\w+)/(\w+)');
Match match = regex.firstMatch(response.data)!;
String matchedString = match.group(0)!;
@@ -55,19 +64,32 @@ class HtmlHttp {
.innerHtml;
} catch (_) {}
String commentId = opusDetail
List comment = opusDetail
.querySelector('.bili-comment-container')!
.className
.split(' ')[1]
.split('-')[2];
.split('-');
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
dynamic mid;
try {
final regex = RegExp(r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\});');
final match = regex.firstMatch(response.data);
if (match != null) {
final json = jsonDecode(match.group(1)!);
mid = json['detail']['basic']['uid'];
}
} catch (_) {}
return {
'status': true,
'mid': mid,
'avatar': avatar,
'uname': uname,
'updateTime': updateTime,
'content': (test ?? '') + opusContent,
'commentId': int.parse(commentId)
'commentType': int.parse(comment[1]),
'commentId': int.parse(comment[2]),
};
} catch (err) {
debugPrint('err: $err');
@@ -75,11 +97,13 @@ class HtmlHttp {
}
// read
static Future reqReadHtml(id, dynamicType) async {
String? redirectUrl = await UrlUtils.parseRedirectUrl(
'https://www.bilibili.com/$dynamicType/$id/');
if (redirectUrl != null) {
return await reqHtml(redirectUrl.split('/').last, dynamicType);
static Future reqReadHtml(id, dynamicType, [bool redirect = true]) async {
if (redirect) {
String? redirectUrl = await UrlUtils.parseRedirectUrl(
'https://www.bilibili.com/$dynamicType/$id/');
if (redirectUrl != null) {
return await reqHtml(redirectUrl.split('/').last, dynamicType);
}
}
var response = await Request().get(
@@ -103,12 +127,11 @@ class HtmlHttp {
// String avatar =
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
// 正则寻找形如"author":{"mid":\d+,"name":".*","face":"xxxx"的匹配项
String avatar =
RegExp(r'"author":\{"mid":\d+?,"name":".+?","face":"(.+?)"')
.firstMatch(response.data)!
.group(1)!
.replaceAll(r'\u002F', '/')
.split('@')[0];
final match =
RegExp(r'"author":\{"mid":(\d+)?,"name":".+?","face":"(.+?)"')
.firstMatch(response.data)!;
String mid = match.group(1)!;
String avatar = match.group(2)!.replaceAll(r'\u002F', '/').split('@')[0];
// debugPrint(avatar);
String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情
@@ -148,11 +171,10 @@ class HtmlHttp {
}
}
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!;
String number = RegExp(r'\d+').firstMatch(id)!.group(0)!;
return {
'status': true,
'mid': mid,
'avatar': avatar,
'uname': uname,
'updateTime': '',

View File

@@ -1,40 +1,34 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:math' show Random;
import 'package:PiliPlus/build_config.dart';
import 'package:PiliPlus/http/retry_interceptor.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/accounts/account_manager/account_mgr.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:archive/archive.dart';
import 'package:brotli/brotli.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:flutter/material.dart';
import '../utils/storage.dart';
import 'api.dart';
import 'constants.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as web;
class Request {
static const gzipDecoder = GZipDecoder();
static const brotilDecoder = BrotliDecoder();
static const _gzipDecoder = GZipDecoder();
static const _brotilDecoder = BrotliDecoder();
static final Request _instance = Request._internal();
static late AccountManager accountManager;
static late final Dio dio;
factory Request() => _instance;
late bool enableSystemProxy;
late String systemProxyHost;
late String systemProxyPort;
static final _rand = Random();
static final RegExp spmPrefixExp =
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
// static final _rand = Random();
// static final RegExp _spmPrefixExp = RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
/// 设置cookie
static setCookie() async {
static Future<void> setCookie() async {
accountManager = AccountManager();
dio.interceptors.add(accountManager);
await Accounts.refresh();
@@ -49,6 +43,22 @@ class Request {
isSecure: item.secure,
isHttpOnly: item.httpOnly,
)));
if (Accounts.main.isLogin) {
final coin = GStorage.userInfo.get('userInfoCache')?.money;
if (coin == null) {
setCoin();
} else {
GlobalData().coins = coin;
}
}
}
static Future<void> setCoin() async {
final res = await UserHttp.getCoin();
if (res['status']) {
GlobalData().coins = res['data'];
}
}
// 从cookie中获取 csrf token
@@ -56,37 +66,36 @@ class Request {
return Accounts.main.csrf;
}
static Future<void> buvidActive(Account account) async {
// 这样线程不安全, 但仍按预期进行
if (account.activited) return;
account.activited = true;
try {
final html = await Request().get(Api.dynamicSpmPrefix,
options: Options(extra: {'account': account}));
final String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;
final String randPngEnd = base64.encode(
List<int>.generate(32, (_) => _rand.nextInt(256)) +
List<int>.filled(4, 0) +
[73, 69, 78, 68] +
List<int>.generate(4, (_) => _rand.nextInt(256)));
// static Future<void> buvidActive(Account account) async {
// // 这样线程不安全, 但仍按预期进行
// if (account.activited) return;
// account.activited = true;
// try {
// final html = await Request().get(Api.dynamicSpmPrefix,
// options: Options(extra: {'account': account}));
// final String spmPrefix = _spmPrefixExp.firstMatch(html.data)!.group(1)!;
// final String randPngEnd = base64.encode(
// List<int>.generate(32, (_) => _rand.nextInt(256)) +
// List<int>.filled(4, 0) +
// [73, 69, 78, 68] +
// List<int>.generate(4, (_) => _rand.nextInt(256)));
String jsonData = json.encode({
'3064': 1,
'39c8': '$spmPrefix.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': randPngEnd.substring(randPngEnd.length - 50),
},
});
// String jsonData = json.encode({
// '3064': 1,
// '39c8': '$spmPrefix.fp.risk',
// '3c43': {
// 'adca': 'Linux',
// 'bfe9': randPngEnd.substring(randPngEnd.length - 50),
// },
// });
await Request().post(Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: Headers.jsonContentType));
;
} catch (e) {
log("setCookie, $e");
}
}
// await Request().post(Api.activateBuvidApi,
// data: {'payload': jsonData},
// options: Options(contentType: Headers.jsonContentType));
// } catch (e) {
// log("setCookie, $e");
// }
// }
/*
* config it and create
@@ -97,9 +106,9 @@ class Request {
//请求基地址,可以包含子路径
baseUrl: HttpString.apiBaseUrl,
//连接服务器超时时间,单位是毫秒.
connectTimeout: const Duration(milliseconds: 4000),
connectTimeout: const Duration(milliseconds: 10000),
//响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 4000),
receiveTimeout: const Duration(milliseconds: 10000),
//Http请求头.
headers: {
'connection': 'keep-alive',
@@ -113,12 +122,10 @@ class Request {
responseDecoder: responseDecoder, // Http2Adapter没有自动解压
persistentConnection: true);
enableSystemProxy = GStorage.setting
.get(SettingBoxKey.enableSystemProxy, defaultValue: false) as bool;
systemProxyHost =
GStorage.setting.get(SettingBoxKey.systemProxyHost, defaultValue: '');
systemProxyPort =
GStorage.setting.get(SettingBoxKey.systemProxyPort, defaultValue: '');
final bool enableSystemProxy = GStorage.setting
.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
final String systemProxyHost = GStorage.defaultSystemProxyHost;
final String systemProxyPort = GStorage.defaultSystemProxyPort;
final http11Adapter = IOHttpClientAdapter(createHttpClient: () {
final client = HttpClient()
@@ -248,20 +255,28 @@ class Request {
/*
* 下载文件
*/
downloadFile(urlPath, savePath) async {
Response response;
Future<Response> downloadFile(urlPath, savePath, {cancelToken}) async {
try {
response = await dio.download(urlPath, savePath,
onReceiveProgress: (int count, int total) {
Response response = await dio.download(
urlPath,
savePath,
cancelToken: cancelToken,
// onReceiveProgress: (int count, int total) {
//进度
// debugPrint("$count $total");
});
debugPrint('downloadFile success: ${response.data}');
return response.data;
// },
);
// debugPrint('downloadFile success: ${response.data}');
return response;
} on DioException catch (e) {
debugPrint('downloadFile error: $e');
return Future.error(AccountManager.dioError(e));
// debugPrint('downloadFile error: $e');
return Response(
data: {
'message': await AccountManager.dioError(e),
},
statusCode: -1,
requestOptions: RequestOptions(),
);
}
}
@@ -276,21 +291,22 @@ class Request {
}
static String headerUa({type = 'mob'}) {
return type == 'mob'
? Platform.isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1'
: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36'
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
return switch (type) {
'mob' =>
'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36',
_ =>
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15'
};
}
static String responseDecoder(List<int> responseBytes, RequestOptions options,
ResponseBody responseBody) {
switch (responseBody.headers['content-encoding']?.firstOrNull) {
case 'gzip':
return utf8.decode(gzipDecoder.decodeBytes(responseBytes),
return utf8.decode(_gzipDecoder.decodeBytes(responseBytes),
allowMalformed: true);
case 'br':
return utf8.decode(brotilDecoder.convert(responseBytes),
return utf8.decode(_brotilDecoder.convert(responseBytes),
allowMalformed: true);
default:
return utf8.decode(responseBytes, allowMalformed: true);

View File

@@ -2,6 +2,9 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/live/danmu_info.dart';
import 'package:PiliPlus/models/live/follow.dart';
import 'package:PiliPlus/models/live/live_emoticons/data.dart';
import 'package:PiliPlus/models/live/live_emoticons/datum.dart';
import 'package:PiliPlus/utils/wbi_sign.dart';
import 'package:dio/dio.dart';
import '../models/live/item.dart';
import '../models/live/room_info.dart';
@@ -10,13 +13,27 @@ import 'api.dart';
import 'init.dart';
class LiveHttp {
static Future<LoadingState> liveList(
static Future<LoadingState<List<LiveItemModel>?>> liveList(
{int? vmid, int? pn, int? ps, String? orderType}) async {
var res = await Request().get(Api.liveList,
queryParameters: {'page': pn, 'page_size': 30, 'platform': 'web'});
var res = await Request().get(
Api.liveList,
queryParameters: await WbiSign.makSign({
'page': pn,
'page_size': 30,
'platform': 'web',
'web_location': 0.0,
}),
options: Options(
headers: {
'origin': 'https://live.bilibili.com',
'referer': 'https://live.bilibili.com/',
'user-agent': Request.headerUa(type: 'pc'),
},
),
);
if (res.data['code'] == 0) {
List<LiveItemModel> list = res.data['data']['list']
.map<LiveItemModel>((e) => LiveItemModel.fromJson(e))
List<LiveItemModel>? list = (res.data['data']?['list'] as List?)
?.map<LiveItemModel>((e) => LiveItemModel.fromJson(e))
.toList();
return LoadingState.success(list);
} else {
@@ -24,35 +41,34 @@ class LiveHttp {
}
}
static Future sendLiveMsg({
roomId,
msg,
}) async {
static Future sendLiveMsg({roomId, msg, dmType, emoticonOptions}) async {
dynamic csrf = await Request.getCsrf();
var res = await Request().post(
Api.sendLiveMsg,
data: {
data: FormData.fromMap({
'bubble': 0,
'msg': msg,
'color': 16777215,
'mode': 1,
'room_type': 0,
'jumpfrom': 71000,
'reply_mid': 0,
'reply_attr': 0,
'replay_dmid': '',
'statistics': Constants.statistics,
'reply_type': 0,
'reply_uname': '',
if (dmType != null) 'dm_type': dmType,
if (emoticonOptions != null)
'emoticonOptions': emoticonOptions
else ...{
'room_type': 0,
'jumpfrom': 0,
'reply_mid': 0,
'reply_attr': 0,
'replay_dmid': '',
'statistics': Constants.statistics,
'reply_type': 0,
'reply_uname': '',
},
'fontsize': 25,
'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'roomid': roomId,
'csrf': csrf,
'csrf_token': csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
}),
);
if (res.data['code'] == 0) {
return {
@@ -82,11 +98,7 @@ class LiveHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': RoomInfoModel.fromJson(res.data['data'])};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -100,11 +112,7 @@ class LiveHttp {
'data': RoomInfoH5Model.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -115,11 +123,7 @@ class LiveHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']['room']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -130,11 +134,7 @@ class LiveHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': LiveDanmakuInfo.fromJson(res.data)};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -162,4 +162,21 @@ class LiveHttp {
};
}
}
static Future<LoadingState<List<LiveEmoteDatum>?>> getLiveEmoticons(
{required int roomId}) async {
var res = await Request().get(
Api.getLiveEmoticons,
queryParameters: {
'platform': 'pc',
'room_id': roomId,
},
);
if (res.data['code'] == 0) {
return LoadingState.success(
LiveEmoteData.fromJson(res.data['data']).data);
} else {
return LoadingState.error(res.data['message']);
}
}
}

View File

@@ -2,25 +2,52 @@ abstract class LoadingState<T> {
const LoadingState();
factory LoadingState.loading() = Loading;
// factory LoadingState.empty() = Empty;
factory LoadingState.success(T response) = Success<T>;
factory LoadingState.error(String errMsg) = Error;
}
class Loading extends LoadingState<Never> {
const Loading();
}
Loading._internal();
// class Empty extends LoadingState<Never> {
// const Empty();
// }
static final Loading _instance = Loading._internal();
factory Loading() => _instance;
}
class Success<T> extends LoadingState<T> {
final T response;
const Success(this.response);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is Success) {
return response == other.response;
}
return false;
}
@override
int get hashCode => response.hashCode;
}
class Error extends LoadingState<Never> {
final String errMsg;
const Error(this.errMsg);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is Error) {
return errMsg == other.errMsg;
}
return false;
}
@override
int get hashCode => errMsg.hashCode;
}

View File

@@ -1,10 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/grpc/grpc_repo.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space/data.dart';
import 'package:PiliPlus/models/space_archive/data.dart' as space_archive;
import 'package:PiliPlus/models/space_article/data.dart' as space_article;
import 'package:PiliPlus/models/space/data.dart' as space_;
import 'package:PiliPlus/models/space_fav/space_fav.dart';
import 'package:PiliPlus/pages/member/new/content/member_contribute/member_contribute.dart'
show ContributeType;
@@ -61,7 +65,7 @@ class MemberHttp {
}
}
static Future<LoadingState> spaceArticle({
static Future<LoadingState<space_article.Data>> spaceArticle({
required int mid,
required int page,
}) async {
@@ -137,13 +141,13 @@ class MemberHttp {
},
);
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']['items_lists']);
return LoadingState.success(res.data['data']?['items_lists']);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState> spaceArchive({
static Future<LoadingState<space_archive.Data>> spaceArchive({
required ContributeType type,
required int? mid,
String? aid,
@@ -153,6 +157,7 @@ class MemberHttp {
int? next,
int? seasonId,
int? seriesId,
includeCursor,
}) async {
Map<String, String> data = {
if (aid != null) 'aid': aid.toString(),
@@ -170,6 +175,7 @@ class MemberHttp {
'qn': type == ContributeType.video ? '80' : '32',
if (order != null) 'order': order,
if (sort != null) 'sort': sort,
if (includeCursor != null) 'include_cursor': includeCursor.toString(),
'statistics': Constants.statistics,
'vmid': mid.toString(),
};
@@ -196,8 +202,51 @@ class MemberHttp {
}
}
static Future<LoadingState> space({
static Future<LoadingState> spaceStory({
required mid,
required aid,
required beforeSize,
required afterSize,
required cid,
required contain,
required index,
}) async {
Map<String, String> data = {
'aid': aid.toString(),
'before_size': beforeSize.toString(),
'after_size': afterSize.toString(),
'cid': cid.toString(),
'contain': contain.toString(),
'index': index.toString(),
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
'mobi_app': 'android_hd',
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'vmid': mid.toString(),
};
dynamic res = await Request().get(
Api.spaceStory,
queryParameters: data,
options: Options(
headers: {
'bili-http-engine': 'cronet',
'user-agent': Constants.userAgent,
},
),
);
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState<space_.Data>> space({
int? mid,
dynamic fromViewAid,
}) async {
Map<String, String> data = {
'build': '1462100',
@@ -206,6 +255,7 @@ class MemberHttp {
'mobi_app': 'android_hd',
'platform': 'android',
's_locale': 'zh_CN',
if (fromViewAid != null) 'from_view_aid': fromViewAid,
'statistics': Constants.statistics,
'vmid': mid.toString(),
};
@@ -250,11 +300,7 @@ class MemberHttp {
'data': MemberInfoModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -263,11 +309,7 @@ class MemberHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -282,23 +324,19 @@ class MemberHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
static Future memberArchive({
int? mid,
int ps = 40,
required int mid,
int ps = 25,
int tid = 0,
int? pn,
String? keyword,
String order = 'pubdate',
bool orderAvoided = true,
dynamic wwebid,
String? wwebid,
}) async {
String dmImgStr = Utils.base64EncodeRandomString(16, 64);
String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);
@@ -310,7 +348,7 @@ class MemberHttp {
'keyword': keyword ?? '',
'order': order,
'platform': 'web',
'web_location': 1550101,
'web_location': '333.1387',
'order_avoided': orderAvoided,
'dm_img_list': '[]',
'dm_img_str': dmImgStr,
@@ -321,7 +359,11 @@ class MemberHttp {
var res = await Request().get(
Api.memberArchive,
queryParameters: params,
extra: {'ua': 'Mozilla/5.0'},
options: Options(headers: {
HttpHeaders.userAgentHeader: Request.headerUa(type: 'pc'),
HttpHeaders.refererHeader: HttpString.spaceBaseUrl,
'origin': HttpString.spaceBaseUrl,
}),
);
if (res.data['code'] == 0) {
return {
@@ -340,7 +382,7 @@ class MemberHttp {
}
// 用户动态
static Future<LoadingState> memberDynamic({
static Future<LoadingState<DynamicsDataModel>> memberDynamic({
String? offset,
int? mid,
}) async {
@@ -382,23 +424,26 @@ class MemberHttp {
// 搜索用户动态
static Future memberDynamicSearch({
int? pn,
int? ps,
int? mid,
required int pn,
required dynamic mid,
required dynamic offset,
required String keyword,
}) async {
var res = await Request().get(Api.memberDynamicSearch, queryParameters: {
'keyword': keyword,
'mid': mid,
'pn': pn,
'ps': ps,
'platform': 'web'
});
var res = await Request().get(
Api.memberDynamicSearch,
queryParameters: {
'host_mid': mid,
'page': pn,
'offset': offset,
'keyword': keyword,
'features': 'itemOpusStyle,listOnlyfans',
'web_location': 333.1387,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['cards'],
'count': res.data['data']['total']
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} else {
return {
@@ -419,11 +464,7 @@ class MemberHttp {
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -466,13 +507,9 @@ class MemberHttp {
),
);
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
return {'status': true, 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -498,11 +535,7 @@ class MemberHttp {
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -517,11 +550,7 @@ class MemberHttp {
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -538,16 +567,13 @@ class MemberHttp {
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
// 最近投币
static Future<LoadingState> getRecentCoinVideo({required int mid}) async {
static Future<LoadingState<List<MemberCoinsDataModel>?>> getRecentCoinVideo(
{required int mid}) async {
Map params = await WbiSign.makSign({
'mid': mid,
'gaia_source': 'main_web',
@@ -564,16 +590,18 @@ class MemberHttp {
},
);
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']
.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList());
List<MemberCoinsDataModel>? list = (res.data['data'] as List?)
?.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList();
return LoadingState.success(list);
} else {
return LoadingState.error(res.data['message']);
}
}
// 最近点赞
static Future<LoadingState> getRecentLikeVideo({required int mid}) async {
static Future<LoadingState<List<MemberCoinsDataModel>?>> getRecentLikeVideo(
{required int mid}) async {
Map params = await WbiSign.makSign({
'mid': mid,
'gaia_source': 'main_web',
@@ -590,9 +618,10 @@ class MemberHttp {
},
);
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']['list']
.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList());
List<MemberCoinsDataModel>? list = (res.data['data']?['list'] as List?)
?.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList();
return LoadingState.success(list);
} else {
return LoadingState.error(res.data['message']);
}
@@ -626,11 +655,7 @@ class MemberHttp {
debugPrint(err.toString());
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -641,11 +666,7 @@ class MemberHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}

View File

@@ -17,7 +17,7 @@ import 'api.dart';
import 'init.dart';
class MsgHttp {
static Future<LoadingState> msgFeedReplyMe(
static Future<LoadingState<MsgFeedReplyMe>> msgFeedReplyMe(
{int cursor = -1, int cursorTime = -1}) async {
var res = await Request().get(Api.msgFeedReply, queryParameters: {
'id': cursor == -1 ? null : cursor,
@@ -34,7 +34,7 @@ class MsgHttp {
}
}
static Future<LoadingState> msgFeedAtMe(
static Future<LoadingState<MsgFeedAtMe>> msgFeedAtMe(
{int cursor = -1, int cursorTime = -1}) async {
var res = await Request().get(Api.msgFeedAt, queryParameters: {
'id': cursor == -1 ? null : cursor,
@@ -51,7 +51,7 @@ class MsgHttp {
}
}
static Future<LoadingState> msgFeedLikeMe(
static Future<LoadingState<MsgFeedLikeMe>> msgFeedLikeMe(
{int cursor = -1, int cursorTime = -1}) async {
var res = await Request().get(Api.msgFeedLike, queryParameters: {
'id': cursor == -1 ? null : cursor,
@@ -68,7 +68,7 @@ class MsgHttp {
}
}
static Future<LoadingState> msgFeedNotify(
static Future<LoadingState<List<SystemNotifyList>?>> msgFeedNotify(
{int cursor = -1, int pageSize = 20}) async {
var res = await Request().get(Api.msgSysNotify, queryParameters: {
'cursor': cursor == -1 ? null : cursor,
@@ -110,11 +110,7 @@ class MsgHttp {
'data': res.data['data'],
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -231,22 +227,24 @@ class MsgHttp {
dynamic path,
String? category,
String? biz,
CancelToken? cancelToken,
}) async {
String csrf = await Request.getCsrf();
Map<String, dynamic> data = await WbiSign.makSign({
'file_up': await MultipartFile.fromFile(path),
final file = await MultipartFile.fromFile(path);
Map<String, dynamic> data = {
'file_up': file,
if (category != null) 'category': category,
if (biz != null) 'biz': biz,
'csrf': csrf,
});
'csrf': await Request.getCsrf(),
};
var res = await Request().post(
Api.uploadBfs,
data: FormData.fromMap(data),
cancelToken: cancelToken,
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
'data': res.data['data']..['img_size'] = file.length,
};
} else {
return {
@@ -329,12 +327,41 @@ class MsgHttp {
}
}
static Future removeSysMsg(
static Future delMsgfeed(
int tp,
dynamic id,
) async {
String csrf = await Request.getCsrf();
var res = await Request().post(
HttpString.messageBaseUrl + Api.removeSysMsg,
Api.delMsgfeed,
data: {
'tp': tp,
'id': id,
'build': 0,
'mobi_app': 'web',
'csrf_token': csrf,
'csrf': csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
static Future delSysMsg(
dynamic id,
) async {
String csrf = await Request.getCsrf();
var res = await Request().post(
HttpString.messageBaseUrl + Api.delSysMsg,
queryParameters: {
'mobi_app': 'android',
'csrf': csrf,
@@ -403,21 +430,13 @@ class MsgHttp {
try {
return {
'status': true,
'data': SessionDataModel.fromJson(res.data['data']),
'data': SessionDataModel.fromJson(res.data['data']).sessionList,
};
} catch (err) {
return {
'status': false,
'date': [],
'msg': err.toString(),
};
return {'status': false, 'msg': err.toString()};
}
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -439,11 +458,7 @@ class MsgHttp {
debugPrint('err🔟: $err');
}
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -469,11 +484,7 @@ class MsgHttp {
debugPrint(err.toString());
}
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -501,7 +512,6 @@ class MsgHttp {
} else {
return {
'status': false,
'date': [],
'msg': "message: ${res.data['message']},"
" msg: ${res.data['msg']},"
" code: ${res.data['code']}",
@@ -550,11 +560,7 @@ class MsgHttp {
'data': res.data['data'],
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'] ?? res.data['msg'],
};
return {'status': false, 'msg': res.data['message']};
}
}

View File

@@ -20,6 +20,7 @@ class ReplyHttp {
static RegExp replyRegExp =
RegExp(GStorage.banWordForReply, caseSensitive: false);
@Deprecated('Use replyListGrpc instead')
static Future<LoadingState> replyList({
required bool isLogin,
required int oid,
@@ -123,7 +124,7 @@ class ReplyHttp {
}
}
static Future<LoadingState> replyListGrpc({
static Future<LoadingState<MainListReply>> replyListGrpc({
int type = 1,
required int oid,
required CursorReq cursor,
@@ -214,6 +215,7 @@ class ReplyHttp {
return false;
}
@Deprecated('Use replyReplyListGrpc instead')
static Future<LoadingState> replyReplyList({
required bool isLogin,
required int oid,
@@ -349,11 +351,7 @@ class ReplyHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -377,15 +375,12 @@ class ReplyHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
static Future<LoadingState> getEmoteList({String? business}) async {
static Future<LoadingState<List<Packages>?>> getEmoteList(
{String? business}) async {
var res = await Request().get(Api.myEmote, queryParameters: {
'business': business ?? 'reply',
'web_location': '333.1245',
@@ -397,4 +392,30 @@ class ReplyHttp {
return LoadingState.error(res.data['message']);
}
}
static Future replyTop({
required oid,
required type,
required rpid,
required bool isUpTop,
}) async {
var res = await Request().post(
Api.replyTop,
data: {
'oid': oid,
'type': type,
'rpid': rpid,
'action': isUpTop ? 0 : 1,
'csrf': await Request.getCsrf(),
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@@ -15,6 +15,7 @@ class RetryInterceptor extends Interceptor {
case DioExceptionType.connectionError:
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.unknown:
if ((err.requestOptions.extra['_rt'] ??= 0) < _count) {
Future.delayed(
Duration(

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:PiliPlus/models/search/search_trending/trending_data.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -29,11 +30,7 @@ class SearchHttp {
};
}
return {
'status': false,
'data': [],
'msg': '请求错误',
};
return {'status': false, 'msg': '请求错误'};
}
// 获取搜索建议
@@ -50,19 +47,17 @@ class SearchHttp {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
: null,
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
@@ -106,11 +101,11 @@ class SearchHttp {
try {
switch (searchType) {
case SearchType.video:
List<int> blackMidsList = GStorage.blackMidsList;
Set<int> blackMids = GStorage.blackMids;
if (res.data['data']['result'] != null) {
for (var i in res.data['data']['result']) {
// 屏蔽推广和拉黑用户
i['available'] = !blackMidsList.contains(i['mid']);
i['available'] = !blackMids.contains(i['mid']);
}
}
data = SearchVideoModel.fromJson(res.data['data']);
@@ -148,8 +143,7 @@ class SearchHttp {
} else if (bvid != null) {
data['bvid'] = bvid;
}
final dynamic res = await Request()
.get(Api.ab2c, queryParameters: <String, dynamic>{...data});
final dynamic res = await Request().get(Api.ab2c, queryParameters: data);
if (res.data['code'] == 0) {
return part != null
? ((res.data['data'] as List).getOrNull(part - 1)?['cid'] ??
@@ -161,7 +155,8 @@ class SearchHttp {
}
}
static Future<LoadingState> bangumiInfoNew({int? seasonId, int? epId}) async {
static Future<LoadingState<BangumiInfoModel>> bangumiInfoNew(
{int? seasonId, int? epId}) async {
final dynamic res = await Request().get(
Api.bangumiInfo,
queryParameters: {
@@ -201,8 +196,8 @@ class SearchHttp {
} else if (epId != null) {
data['ep_id'] = epId;
}
final dynamic res = await Request()
.get(Api.bangumiInfo, queryParameters: <String, dynamic>{...data});
final dynamic res =
await Request().get(Api.bangumiInfo, queryParameters: data);
if (res.data['code'] == 0) {
return {
@@ -210,11 +205,22 @@ class SearchHttp {
'data': BangumiInfoModel.fromJson(res.data['result']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
return {'status': false, 'msg': res.data['message']};
}
}
static Future<LoadingState<TrendingData>> searchTrending(
{int limit = 30}) async {
final dynamic res = await Request().get(
Api.searchTrending,
queryParameters: {
'limit': limit,
},
);
if (res.data['code'] == 0) {
return LoadingState.success(TrendingData.fromJson(res.data['data']));
} else {
return LoadingState.error(res.data['message']);
}
}
}

View File

@@ -1,5 +1,8 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/video/later.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/wbi_sign.dart';
import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../common/constants.dart';
@@ -28,6 +31,7 @@ class UserHttp {
var res = await Request().get(Api.userInfo);
if (res.data['code'] == 0) {
UserInfoData data = UserInfoData.fromJson(res.data['data']);
GlobalData().coins = data.money;
return {'status': true, 'data': data};
} else {
return {'status': false, 'msg': res.data['message']};
@@ -40,12 +44,12 @@ class UserHttp {
UserStat data = UserStat.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
// 收藏夹
static Future<LoadingState> userfavFolder({
static Future<LoadingState<FavFolderData>> userfavFolder({
required int pn,
required int ps,
required dynamic mid,
@@ -62,6 +66,52 @@ class UserHttp {
}
}
static Future sortFavFolder({
required List<int?> sort,
}) async {
Map<String, dynamic> data = {
'sort': sort.join(','),
'csrf': await Request.getCsrf(),
};
Utils.appSign(data);
var res = await Request().post(
Api.sortFavFolder,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future sortFav({
required dynamic mediaId,
required List<String> sort,
}) async {
Map<String, dynamic> data = {
'media_id': mediaId,
'sort': sort.join(','),
'csrf': await Request.getCsrf(),
};
Utils.appSign(data);
var res = await Request().post(
Api.sortFav,
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future cleanFav({
required dynamic mediaId,
}) async {
@@ -145,14 +195,14 @@ class UserHttp {
}
}
static Future<LoadingState> userFavFolderDetail(
static Future<LoadingState<FavDetailData>> userFavFolderDetail(
{required int mediaId,
required int pn,
required int ps,
String keyword = '',
String order = 'mtime',
int type = 0}) async {
var res = await Request().get(Api.userFavFolderDetail, queryParameters: {
var res = await Request().get(Api.favResourceList, queryParameters: {
'media_id': mediaId,
'pn': pn,
'ps': ps,
@@ -170,16 +220,29 @@ class UserHttp {
}
// 稍后再看
static Future<LoadingState> seeYouLater() async {
var res = await Request().get(Api.seeYouLater);
static Future<LoadingState<Map>> seeYouLater({
required int page,
int viewed = 0,
String keyword = '',
bool asc = false,
}) async {
var res = await Request().get(
Api.seeYouLater,
queryParameters: await WbiSign.makSign({
'pn': page,
'ps': 20,
'viewed': viewed,
'key': keyword,
'asc': asc,
'need_split': true,
'web_location': 333.881,
}),
);
if (res.data['code'] == 0) {
if (res.data['data']['count'] == 0) {
return LoadingState.success({
'list': [],
'count': 0,
});
return LoadingState.success({'count': 0});
}
List<HotVideoItemModel> list = [];
List<HotVideoItemModel> list = <HotVideoItemModel>[];
if (res.data['data']?['list'] != null) {
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
@@ -195,12 +258,13 @@ class UserHttp {
}
// 观看历史
static Future<LoadingState> historyList({
static Future<LoadingState<HistoryData>> historyList({
required String type,
int? max,
int? viewAt,
}) async {
var res = await Request().get(Api.historyList, queryParameters: {
'type': 'all',
'type': type,
'ps': 20,
'max': max ?? 0,
'view_at': viewAt ?? 0,
@@ -232,7 +296,7 @@ class UserHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -268,13 +332,10 @@ class UserHttp {
}
// 移除已观看
static Future toViewDel({
List? aids,
}) async {
static Future toViewDel({required List<int?> aids}) async {
final Map<String, dynamic> params = {
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
if (aids != null) 'aid': aids.join(',') else 'viewed': true
'resources': aids.join(',')
};
dynamic res = await Request().post(
Api.toViewDel,
@@ -307,12 +368,12 @@ class UserHttp {
}
}
// 清空稍后再看
static Future toViewClear() async {
// 清空稍后再看 // clean_type: null->all, 1->invalid, 2->viewed
static Future toViewClear([int? cleanType]) async {
var res = await Request().post(
Api.toViewClear,
queryParameters: {
'jsonp': 'jsonp',
if (cleanType != null) 'clean_type': cleanType,
'csrf': await Request.getCsrf(),
},
);
@@ -324,7 +385,7 @@ class UserHttp {
}
// 删除历史记录
static Future delHistory(List kidList) async {
static Future delHistory(List<String> kidList) async {
var res = await Request().post(
Api.delHistory,
data: {
@@ -345,7 +406,7 @@ class UserHttp {
static Future hasFollow(int mid) async {
var res = await Request().get(
Api.hasFollow,
Api.relation,
queryParameters: {
'fid': mid,
},
@@ -400,7 +461,7 @@ class UserHttp {
}
// 我的订阅
static Future<LoadingState> userSubFolder({
static Future<LoadingState<List<SubFolderItemData>?>> userSubFolder({
required int mid,
required int pn,
required int ps,
@@ -442,6 +503,84 @@ class UserHttp {
}
}
static Future<LoadingState> favArticle({
required int page,
}) async {
var res = await Request().get(Api.favArticle, queryParameters: {
'page_size': 20,
'page': page,
});
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']?['items']);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future addFavArticle({
required int id,
}) async {
var res = await Request().post(
Api.addFavArticle,
data: {
'id': id,
'csrf': await Request.getCsrf(),
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future delFavArticle({
required int id,
}) async {
var res = await Request().post(
Api.delFavArticle,
data: {
'id': id,
'csrf': await Request.getCsrf(),
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future communityAction({
required dynamic opusId,
required dynamic action,
}) async {
var res = await Request().post(
Api.communityAction,
queryParameters: {
'csrf': await Request.getCsrf(),
},
data: {
"entity": {
"object_id_str": opusId,
"type": {"biz": 2}
},
"action": action, // 3 fav, 4 unfav
},
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
static Future favResourceList({
required int id,
required int pn,
@@ -500,41 +639,6 @@ class UserHttp {
}
}
// 稍后再看播放全部
// static Future toViewPlayAll({required int oid, required String bvid}) async {
// var res = await Request().get(
// Api.watchLaterHtml,
// data: {
// 'oid': oid,
// 'bvid': bvid,
// },
// );
// String scriptContent =
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
// int startIndex = scriptContent.indexOf('{');
// int endIndex = scriptContent.lastIndexOf('};');
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// // 解析JSON字符串为Map
// Map<String, dynamic> jsonData = json.decode(jsonContent);
// // 输出解析后的数据
// return {
// 'status': true,
// 'data': jsonData['resourceList']
// .map((e) => MediaVideoItemModel.fromJson(e))
// .toList()
// };
// }
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 稍后再看列表
static Future getMediaList({
required dynamic type,
@@ -571,38 +675,19 @@ class UserHttp {
.map<MediaVideoItemModel>(
(e) => MediaVideoItemModel.fromJson(e))
.toList()
: []
: <MediaVideoItemModel>[]
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 解析收藏夹视频
// static Future parseFavVideo({
// required int mediaId,
// required int oid,
// required String bvid,
// }) async {
// var res = await Request().get(
// 'https://www.bilibili.com/list/ml$mediaId',
// queryParameters: {
// 'oid': mediaId,
// 'bvid': bvid,
// },
// );
// String scriptContent =
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
// int startIndex = scriptContent.indexOf('{');
// int endIndex = scriptContent.lastIndexOf('};');
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// // 解析JSON字符串为Map
// Map<String, dynamic> jsonData = json.decode(jsonContent);
// return {
// 'status': true,
// 'data': jsonData['resourceList']
// .map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
// .toList()
// };
// }
static Future getCoin() async {
final res = await Request().get(Api.getCoin);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']?['money']};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card;
import 'package:PiliPlus/grpc/grpc_repo.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/member/article.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
@@ -49,13 +50,12 @@ class VideoHttp {
},
);
if (res.data['code'] == 0) {
List<RecVideoItemModel> list = [];
List<int> blackMidsList = GStorage.blackMidsList;
List<RecVideoItemModel> list = <RecVideoItemModel>[];
Set<int> blackMids = GStorage.blackMids;
for (var i in res.data['data']['item']) {
//过滤掉live与ad以及拉黑用户
if (i['goto'] == 'av' &&
(i['owner'] != null &&
!blackMidsList.contains(i['owner']['mid']))) {
(i['owner'] != null && !blackMids.contains(i['owner']['mid']))) {
RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
@@ -119,16 +119,15 @@ class VideoHttp {
}),
);
if (res.data['code'] == 0) {
List<RecVideoItemAppModel> list = [];
List<int> blackMidsList = GStorage.blackMidsList;
List<RecVideoItemAppModel> list = <RecVideoItemAppModel>[];
Set<int> blackMids = GStorage.blackMids;
for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
i['card_goto'] != 'ad_web_s' &&
i['ad_info'] == null &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_id']))) {
(i['args'] != null && !blackMids.contains(i['args']['up_id']))) {
// if (zoneRegExp.pattern.isNotEmpty &&
// i['args']?['rname'] != null &&
// zoneRegExp.hasMatch(i['args']['rname'])) {
@@ -147,17 +146,17 @@ class VideoHttp {
}
// 最热视频
static Future<LoadingState> hotVideoList(
static Future<LoadingState<List<HotVideoItemModel>>> hotVideoList(
{required int pn, required int ps}) async {
var res = await Request().get(
Api.hotList,
queryParameters: {'pn': pn, 'ps': ps},
);
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
List<int> blackMidsList = GStorage.blackMidsList;
List<HotVideoItemModel> list = <HotVideoItemModel>[];
Set<int> blackMids = GStorage.blackMids;
for (var i in res.data['data']['list']) {
if (!blackMidsList.contains(i['owner']['mid']) &&
if (!blackMids.contains(i['owner']['mid']) &&
!RecommendFilter.filterTitle(i['title']) &&
!RecommendFilter.filterLikeRatio(
i['stat']['like'], i['stat']['view'])) {
@@ -178,10 +177,10 @@ class VideoHttp {
static Future<LoadingState> hotVideoListGrpc({required int idx}) async {
dynamic res = await GrpcRepo.popular(idx);
if (res['status']) {
List<card.Card> list = [];
List<int> blackMidsList = GStorage.blackMidsList;
List<card.Card> list = <card.Card>[];
Set<int> blackMids = GStorage.blackMids;
for (card.Card item in res['data']) {
if (!blackMidsList.contains(item.smallCoverV5.up.id.toInt())) {
if (!blackMids.contains(item.smallCoverV5.up.id.toInt())) {
list.add(item);
}
}
@@ -217,7 +216,7 @@ class VideoHttp {
};
// 免登录查看1080p
if ((Accounts.get(AccountType.video).isLogin) &&
if (!Accounts.get(AccountType.video).isLogin &&
GStorage.setting.get(SettingBoxKey.p1080, defaultValue: true)) {
data['try_look'] = 1;
}
@@ -260,13 +259,12 @@ class VideoHttp {
}
return {
'status': false,
'data': [],
'code': res.data['code'],
'msg': res.data['message'],
};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err};
return {'status': false, 'msg': err};
}
}
@@ -347,17 +345,16 @@ class VideoHttp {
}
// 相关视频
static Future<LoadingState> relatedVideoList({required String bvid}) async {
static Future<LoadingState<List<HotVideoItemModel>?>> relatedVideoList(
{required String bvid}) async {
var res =
await Request().get(Api.relatedList, queryParameters: {'bvid': bvid});
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
for (var i in res.data['data']) {
HotVideoItemModel videoItem = HotVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem, relatedVideos: true)) {
list.add(videoItem);
}
}
final items = (res.data['data'] as List?)
?.map((i) => HotVideoItemModel.fromJson(i));
final list = RecommendFilter.applyFilterToRelatedVideos
? items?.where((i) => !RecommendFilter.filterAll(i)).toList()
: items?.toList();
return LoadingState.success(list);
} else {
return LoadingState.error(res.data['message']);
@@ -378,27 +375,26 @@ class VideoHttp {
}
// 获取点赞状态
static Future hasLikeVideo({required String bvid}) async {
var res =
await Request().get(Api.hasLikeVideo, queryParameters: {'bvid': bvid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// static Future hasLikeVideo({required String bvid}) async {
// var res =
// await Request().get(Api.hasLikeVideo, queryParameters: {'bvid': bvid});
// if (res.data['code'] == 0) {
// return {'status': true, 'data': res.data['data']};
// } else {
// return {'status': false, 'msg': res.data['message']};
// }
// }
// 获取投币状态
static Future hasCoinVideo({required String bvid}) async {
var res =
await Request().get(Api.hasCoinVideo, queryParameters: {'bvid': bvid});
debugPrint('res: $res');
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// static Future hasCoinVideo({required String bvid}) async {
// var res =
// await Request().get(Api.hasCoinVideo, queryParameters: {'bvid': bvid});
// if (res.data['code'] == 0) {
// return {'status': true, 'data': res.data['data']};
// } else {
// return {'status': false, 'msg': res.data['message']};
// }
// }
// 投币
static Future coinVideo({
@@ -415,24 +411,25 @@ class VideoHttp {
'select_like': selectLike.toString(),
// 'csrf': Accounts.main.csrf,
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
// 获取收藏状态
static Future hasFavVideo({required int aid}) async {
var res =
await Request().get(Api.hasFavVideo, queryParameters: {'aid': aid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// static Future hasFavVideo({required int aid}) async {
// var res =
// await Request().get(Api.hasFavVideo, queryParameters: {'aid': aid});
// if (res.data['code'] == 0) {
// return {'status': true, 'data': res.data['data']};
// } else {
// return {'status': false, 'msg': res.data['message']};
// }
// }
// 一键三连 bangumi
static Future triple({dynamic epId, required dynamic seasonId}) async {
@@ -454,7 +451,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -482,7 +479,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -504,7 +501,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -721,7 +718,7 @@ class VideoHttp {
FavFolderData data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
} else {
return {'status': false, 'data': []};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -743,7 +740,7 @@ class VideoHttp {
bool? syncToDynamic,
}) async {
if (message == '') {
return {'status': false, 'data': [], 'msg': '请输入评论内容'};
return {'status': false, 'msg': '请输入评论内容'};
}
Map<String, dynamic> data = {
'type': type.index,
@@ -763,7 +760,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
@@ -786,16 +783,6 @@ class VideoHttp {
}
}
// 查询是否关注up
static Future hasFollow({required int mid}) async {
var res = await Request().get(Api.hasFollow, queryParameters: {'fid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
}
}
// 操作用户关系
static Future relationMod(
{required int mid, required int act, required int reSrc}) async {
@@ -830,6 +817,32 @@ class VideoHttp {
}
}
static Future roomEntryAction({
roomId,
}) async {
await Request().post(
Api.roomEntryAction,
queryParameters: {
'csrf': await Request.getCsrf(),
},
data: {
'room_id': roomId,
'platform': 'pc',
},
);
}
static Future historyReport({
aid,
type,
}) async {
await Request().post(Api.historyReport, queryParameters: {
if (aid != null) 'aid': aid,
if (type != null) 'type': type,
'csrf': Accounts.main.csrf,
});
}
// 视频播放进度
static Future heartBeat({
bvid,
@@ -908,13 +921,13 @@ class VideoHttp {
}
static Future bangumiUpdate({
dynamic seasonId,
dynamic status,
required List seasonId,
required dynamic status,
}) async {
var res = await Request().post(
Api.bangumiUpdate,
data: {
'season_id': seasonId,
'season_id': seasonId.join(','),
'status': status,
'csrf': Accounts.main.csrf,
},
@@ -959,11 +972,11 @@ class VideoHttp {
'data': AiConclusionModel.fromJson(res.data['data']),
};
} else {
return {'status': false, 'data': []};
return {'status': false, 'msg': res.data['message']};
}
}
static Future subtitlesJson(
static Future<Map<String, dynamic>> subtitlesJson(
{String? aid, String? bvid, required int cid}) async {
assert(aid != null || bvid != null);
var res = await Request().get(
@@ -993,101 +1006,56 @@ class VideoHttp {
*/
return {
'status': true,
'data': data['subtitle']['subtitles'],
'subtitles': data['subtitle']['subtitles'],
'view_points': data['view_points'],
// 'last_play_time': data['last_play_time'],
'last_play_cid': data['last_play_cid'],
'interaction': data['interaction'],
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
return {'status': false, 'msg': res.data['message']};
}
}
static Future vttSubtitles(List subtitlesJson) async {
if (subtitlesJson.isEmpty) {
return [];
}
List<Map<String, String>> subtitlesVtt = [];
static Future vttSubtitles(Map<String, dynamic> subtile) async {
String subtitleTimecode(num seconds) {
int h = (seconds / 3600).floor();
int m = ((seconds % 3600) / 60).floor();
int s = (seconds % 60).floor();
int ms = ((seconds * 1000) % 1000).floor();
if (h == 0) {
return "${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}.${ms.toString().padLeft(3, '0')}";
}
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}.${ms.toString().padLeft(3, '0')}";
int h = seconds ~/ 3600;
seconds %= 3600;
int m = seconds ~/ 60;
seconds %= 60;
String sms = seconds.toStringAsFixed(3).padLeft(6, '0');
return h == 0
? "${m.toString().padLeft(2, '0')}:$sms"
: "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:$sms";
}
String processList(List list) {
return list.fold('WEBVTT\n\n', (previous, item) {
return '$previous${item?['sid'] ?? 0}\n${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n${item['content'].trim()}\n\n';
});
final sb = StringBuffer('WEBVTT\n\n');
sb.writeAll(
list.map((item) =>
'${item?['sid'] ?? 0}\n${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n${item['content'].trim()}'),
'\n\n');
return sb.toString();
}
for (var i in subtitlesJson) {
var res =
await Request().get("https://${i['subtitle_url'].split('//')[1]}");
/*
{
"font_size": 0.4,
"font_color": "#FFFFFF",
"background_alpha": 0.5,
"background_color": "#9C27B0",
"Stroke": "none",
"type": "AIsubtitle",
"lang": "zh",
"version": "v1.6.0.4",
"body": [
{
"from": 0.5,
"to": 1.58,
"sid": 1,
"location": 2,
"content": "很多人可能不知道",
"music": 0.0
},
……,
{
"from": 558.629,
"to": 560.22,
"sid": 280,
"location": 2,
"content": "我们下期再见",
"music": 0.0
}
]
}
*/
if (res.data != null && res.data?['body'] is List) {
String vttData = await compute(processList, res.data['body'] as List);
subtitlesVtt.add({
'language': i['lan'],
'title': i['lan_doc'],
'text': vttData,
});
} else {
// SmartDialog.showToast("字幕${i['lan_doc']}加载失败, ${res.data['message']}");
debugPrint('字幕${i['lan_doc']}加载失败, ${res.data['message']}');
}
var res = await Request().get("https:${subtile['subtitle_url']}");
if (res.data?['body'] is List) {
return await compute(processList, res.data['body'] as List);
}
if (subtitlesVtt.isNotEmpty) {
subtitlesVtt.insert(0, {'language': '', 'title': '关闭字幕', 'text': ""});
}
return subtitlesVtt;
return null;
}
// 视频排行
static Future<LoadingState> getRankVideoList(int rid) async {
static Future<LoadingState<List<HotVideoItemModel>>> getRankVideoList(
int rid) async {
var rankApi = "${Api.getRankApi}?rid=$rid&type=all";
var res = await Request().get(rankApi);
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
List<int> blackMidsList = GStorage.blackMidsList;
List<HotVideoItemModel> list = <HotVideoItemModel>[];
Set<int> blackMids = GStorage.blackMids;
for (var i in res.data['data']['list']) {
if (!blackMidsList.contains(i['owner']['mid']) &&
if (!blackMids.contains(i['owner']['mid']) &&
!RecommendFilter.filterTitle(i['title']) &&
!RecommendFilter.filterLikeRatio(
i['stat']['like'], i['stat']['view'])) {
@@ -1111,7 +1079,7 @@ class VideoHttp {
required int page,
}) async {
var res = await Request().get(
Api.noteList,
Api.archiveNoteList,
queryParameters: {
'csrf': Accounts.main.csrf,
'oid': oid,
@@ -1127,4 +1095,115 @@ class VideoHttp {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState<List<FavArticleModel>?>> noteList({
required int page,
}) async {
var res = await Request().get(
Api.noteList,
queryParameters: {
'pn': page,
'ps': 10,
'csrf': Accounts.main.csrf,
},
);
if (res.data['code'] == 0) {
List<FavArticleModel>? list = (res.data['data']?['list'] as List?)
?.map((e) => FavArticleModel.fromJson(e))
.toList();
return LoadingState.success(list);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState<List<FavArticleModel>?>> userNoteList({
required int page,
}) async {
var res = await Request().get(
Api.userNoteList,
queryParameters: {
'pn': page,
'ps': 10,
'csrf': Accounts.main.csrf,
},
);
if (res.data['code'] == 0) {
List<FavArticleModel>? list = (res.data['data']?['list'] as List?)
?.map((e) => FavArticleModel.fromJson(e))
.toList();
return LoadingState.success(list);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future<LoadingState> addNote({
required oid,
required String title,
required String summary,
}) async {
String noteId = '';
try {
final res = await Request().get(Api.archiveNote, queryParameters: {
'oid': oid,
'oid_type': 0,
'csrf': Accounts.main.csrf,
});
if (res.data['code'] == 0) {
if (res.data['data']['noteIds'] != null) {
noteId = res.data['data']['noteIds'].first;
}
}
} catch (_) {}
final res = await Request().post(
Api.addNote,
data: {
'cont_len': summary.length,
'note_id': noteId,
'oid': oid,
'oid_type': 0,
'platform': 'web',
'title': title,
'summary': summary,
'content': jsonEncode([
{"insert": summary},
]),
'from': 'close',
'hash': DateTime.now().millisecondsSinceEpoch,
'tags': '',
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return LoadingState.success(res.data['data']?['list']);
} else {
return LoadingState.error(res.data['message']);
}
}
static Future delNote({
required bool isPublish,
required List noteIds,
}) async {
final res = await Request().post(
isPublish ? Api.delPublishNote : Api.delNote,
data: {
isPublish ? 'cvids' : 'note_ids': noteIds.join(','),
'csrf': Accounts.main.csrf,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (res.data['code'] == 0) {
return {'status': true};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@@ -16,7 +16,6 @@ import 'package:hive/hive.dart';
import 'package:PiliPlus/common/widgets/custom_toast.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/models/common/color_type.dart';
import 'package:PiliPlus/pages/video/detail/index.dart';
import 'package:PiliPlus/router/app_pages.dart';
import 'package:PiliPlus/pages/main/view.dart';
import 'package:PiliPlus/services/service_locator.dart';
@@ -32,7 +31,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
await GStorage.init();
if (GStorage.setting.get(SettingBoxKey.autoClearCache, defaultValue: false)) {
if (GStorage.setting.get(SettingBoxKey.autoClearCache, defaultValue: true)) {
await CacheManage.clearLibraryCache();
}
if (GStorage.setting
@@ -218,7 +217,6 @@ class MyApp extends StatelessWidget {
),
navigatorObservers: [
FlutterSmartDialog.observer,
VideoDetailPage.routeObserver,
VideoDetailPageV.routeObserver,
MainApp.routeObserver,
],
@@ -235,7 +233,7 @@ class _CustomHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context)
..maxConnectionsPerHost = 32
// ..maxConnectionsPerHost = 32
..idleTimeout = const Duration(seconds: 15);
if (badCertificateCallback) {
client.badCertificateCallback =

View File

@@ -1,3 +1,6 @@
import 'package:PiliPlus/pages/common/multi_select_controller.dart'
show MultiSelectData;
class BangumiListDataModel {
BangumiListDataModel({
this.hasNext,
@@ -8,7 +11,7 @@ class BangumiListDataModel {
});
int? hasNext;
List? list;
List<BangumiListItemModel>? list;
int? num;
int? size;
int? total;
@@ -24,7 +27,7 @@ class BangumiListDataModel {
}
}
class BangumiListItemModel {
class BangumiListItemModel with MultiSelectData {
BangumiListItemModel({
this.badge,
this.badgeType,
@@ -44,6 +47,7 @@ class BangumiListItemModel {
this.title,
this.titleIcon,
this.progress,
this.newEp,
});
String? badge;
@@ -62,10 +66,12 @@ class BangumiListItemModel {
String? subTitle;
String? title;
String? titleIcon;
Map? newEp;
String? progress;
String? renewalTime;
BangumiListItemModel.fromJson(Map<String, dynamic> json) {
renewalTime = json['renewal_time'];
badge = json['badge'] == '' ? null : json['badge'];
badgeType = json['badge_type'];
cover = json['cover'];
@@ -82,7 +88,7 @@ class BangumiListItemModel {
subTitle = json['sub_title'];
title = json['title'];
titleIcon = json['title_icon'];
newEp = json['new_ep'];
progress = json['progress'];
}
}

View File

@@ -0,0 +1,91 @@
import 'icon_font.dart';
class Episode {
String? cover;
int? delay;
int? delayId;
String? delayIndex;
String? delayReason;
bool? enableVt;
String? epCover;
int? episodeId;
int? follow;
String? follows;
IconFont? iconFont;
String? plays;
String? pubIndex;
String? pubTime;
int? pubTs;
int? published;
int? seasonId;
String? squareCover;
String? title;
Episode({
this.cover,
this.delay,
this.delayId,
this.delayIndex,
this.delayReason,
this.enableVt,
this.epCover,
this.episodeId,
this.follow,
this.follows,
this.iconFont,
this.plays,
this.pubIndex,
this.pubTime,
this.pubTs,
this.published,
this.seasonId,
this.squareCover,
this.title,
});
factory Episode.fromJson(Map<String, dynamic> json) => Episode(
cover: json['cover'] as String?,
delay: json['delay'] as int?,
delayId: json['delay_id'] as int?,
delayIndex: json['delay_index'] as String?,
delayReason: json['delay_reason'] as String?,
enableVt: json['enable_vt'] as bool?,
epCover: json['ep_cover'] as String?,
episodeId: json['episode_id'] as int?,
follow: json['follow'] as int?,
follows: json['follows'] as String?,
iconFont: json['icon_font'] == null
? null
: IconFont.fromJson(json['icon_font'] as Map<String, dynamic>),
plays: json['plays'] as String?,
pubIndex: json['pub_index'] as String?,
pubTime: json['pub_time'] as String?,
pubTs: json['pub_ts'] as int?,
published: json['published'] as int?,
seasonId: json['season_id'] as int?,
squareCover: json['square_cover'] as String?,
title: json['title'] as String?,
);
Map<String, dynamic> toJson() => {
'cover': cover,
'delay': delay,
'delay_id': delayId,
'delay_index': delayIndex,
'delay_reason': delayReason,
'enable_vt': enableVt,
'ep_cover': epCover,
'episode_id': episodeId,
'follow': follow,
'follows': follows,
'icon_font': iconFont?.toJson(),
'plays': plays,
'pub_index': pubIndex,
'pub_time': pubTime,
'pub_ts': pubTs,
'published': published,
'season_id': seasonId,
'square_cover': squareCover,
'title': title,
};
}

View File

@@ -0,0 +1,16 @@
class IconFont {
String? name;
String? text;
IconFont({this.name, this.text});
factory IconFont.fromJson(Map<String, dynamic> json) => IconFont(
name: json['name'] as String?,
text: json['text'] as String?,
);
Map<String, dynamic> toJson() => {
'name': name,
'text': text,
};
}

View File

@@ -0,0 +1,23 @@
import 'result.dart';
class PgcTimeline {
int? code;
String? message;
List<Result>? result;
PgcTimeline({this.code, this.message, this.result});
factory PgcTimeline.fromJson(Map<String, dynamic> json) => PgcTimeline(
code: json['code'] as int?,
message: json['message'] as String?,
result: (json['result'] as List<dynamic>?)
?.map((e) => Result.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> toJson() => {
'code': code,
'message': message,
'result': result?.map((e) => e.toJson()).toList(),
};
}

View File

@@ -0,0 +1,35 @@
import 'episode.dart';
class Result {
String? date;
int? dateTs;
int? dayOfWeek;
List<Episode>? episodes;
int? isToday;
Result({
this.date,
this.dateTs,
this.dayOfWeek,
this.episodes,
this.isToday,
});
factory Result.fromJson(Map<String, dynamic> json) => Result(
date: json['date'] as String?,
dateTs: json['date_ts'] as int?,
dayOfWeek: json['day_of_week'] as int?,
episodes: (json['episodes'] as List<dynamic>?)
?.map((e) => Episode.fromJson(e as Map<String, dynamic>))
.toList(),
isToday: json['is_today'] as int?,
);
Map<String, dynamic> toJson() => {
'date': date,
'date_ts': dateTs,
'day_of_week': dayOfWeek,
'episodes': episodes?.map((e) => e.toJson()).toList(),
'is_today': isToday,
};
}

View File

@@ -8,7 +8,7 @@ class PostSegmentModel {
required this.category,
required this.actionType,
});
Pair<int, int> segment;
Pair<double, double> segment;
SegmentType category;
ActionType actionType;
}

View File

@@ -7,10 +7,12 @@ class DynamicsDataModel {
this.hasMore,
this.items,
this.offset,
this.total,
});
bool? hasMore;
List<DynamicItemModel>? items;
String? offset;
int? total;
DynamicsDataModel.fromJson(Map<String, dynamic> json) {
hasMore = json['has_more'];
@@ -18,6 +20,7 @@ class DynamicsDataModel {
?.map<DynamicItemModel>((e) => DynamicItemModel.fromJson(e))
.toList();
offset = json['offset'];
total = json['total'];
}
}

View File

@@ -10,8 +10,9 @@ class FollowDataModel {
FollowDataModel.fromJson(Map<String, dynamic> json) {
total = json['total'] ?? 0;
list = (json['list'] as List?)
?.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList();
?.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList() ??
[];
}
}

View File

@@ -1,60 +1,20 @@
import 'package:PiliPlus/models/model_rec_video_item.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
class RecVideoItemAppModel {
RecVideoItemAppModel({
this.id,
this.aid,
this.bvid,
this.cid,
this.pic,
this.stat,
this.duration,
this.title,
this.isFollowed,
this.owner,
this.rcmdReason,
this.goto,
this.param,
this.uri,
this.talkBack,
this.bangumiView,
this.bangumiFollow,
this.bangumiBadge,
this.cardType,
this.adInfo,
this.threePoint,
this.desc,
});
class RecVideoItemAppModel extends BaseRecVideoItemModel {
int? id;
int? aid;
String? bvid;
int? cid;
String? pic;
RcmdStat? stat;
int? duration;
String? title;
int? isFollowed;
RcmdOwner? owner;
String? rcmdReason;
String? goto;
int? param;
String? uri;
String? talkBack;
// 番剧
String? bangumiView;
String? bangumiFollow;
String? bangumiBadge;
String? cardType;
Map? adInfo;
ThreePoint? threePoint;
String? desc;
RecVideoItemAppModel.fromJson(Map<String, dynamic> json) {
id = json['player_args'] != null
? json['player_args']['aid']
: int.parse(json['param'] ?? '-1');
: int.tryParse(json['param'] ?? '-1');
aid = id;
bvid = json['bvid'] ??
(json['player_args'] != null
@@ -70,21 +30,22 @@ class RecVideoItemAppModel {
title = json['title'];
owner = RcmdOwner.fromJson(json);
rcmdReason = json['bottom_rcmd_reason'] ?? json['top_rcmd_reason'];
if (rcmdReason != null && rcmdReason!.contains('')) {
// 有时能在推荐原因里获得点赞数
(stat as RcmdStat).like = Utils.parseNum(rcmdReason!);
}
// 由于app端api并不会直接返回与owner的关注状态
// 所以借用推荐原因是否为“已关注”、“新关注”判别关注状态从而与web端接口等效
isFollowed = (rcmdReason == '关注') || (rcmdReason == '新关注') ? 1 : 0;
isFollowed = const {'已关注', '关注'}.contains(rcmdReason);
// 如果是就无需再显示推荐原因交由view统一处理即可
if (isFollowed == 1) {
rcmdReason = null;
}
if (isFollowed) rcmdReason = null;
goto = json['goto'];
param = int.parse(json['param']);
uri = json['uri'];
talkBack = json['talk_back'];
if (json['goto'] == 'bangumi') {
bangumiView = json['cover_left_text_1'];
bangumiFollow = json['cover_left_text_2'];
bangumiBadge = json['cover_right_text'];
}
@@ -95,30 +56,37 @@ class RecVideoItemAppModel {
: null;
desc = json['desc'];
}
// @override
// int? get pubdate => null;
}
class RcmdStat {
RcmdStat({
this.view,
this.like,
this.danmu,
});
String? view;
String? like;
String? danmu;
class RcmdStat implements BaseStat {
@override
int? like;
@override
int? get view => Utils.parseNum(viewStr);
@override
int? get danmu => Utils.parseNum(danmuStr);
@override
late String viewStr;
@override
late String danmuStr;
RcmdStat.fromJson(Map<String, dynamic> json) {
view = json["cover_left_text_1"];
danmu = json['cover_left_text_2'] ?? '-';
viewStr = json["cover_left_text_1"];
danmuStr = json['cover_left_text_2'];
}
@override
set danmu(_) {}
@override
set view(_) {}
}
class RcmdOwner {
RcmdOwner({this.name, this.mid});
String? name;
int? mid;
class RcmdOwner extends BaseOwner {
RcmdOwner.fromJson(Map<String, dynamic> json) {
name = json['goto'] == 'av'
? json['args']['up_name']
@@ -130,63 +98,26 @@ class RcmdOwner {
}
class ThreePoint {
ThreePoint({
this.dislikeReasons,
this.feedbacks,
this.watchLater,
});
List<DislikeReason>? dislikeReasons;
List<FeedbackReason>? feedbacks;
List<Reason>? dislikeReasons;
List<Reason>? feedbacks;
int? watchLater;
ThreePoint.fromJson(Map<String, dynamic> json) {
if (json['dislike_reasons'] != null) {
dislikeReasons = [];
json['dislike_reasons'].forEach((v) {
dislikeReasons!.add(DislikeReason.fromJson(v));
});
}
if (json['feedbacks'] != null) {
feedbacks = [];
json['feedbacks'].forEach((v) {
feedbacks!.add(FeedbackReason.fromJson(v));
});
}
dislikeReasons = (json['dislike_reasons'] as List?)
?.map((v) => Reason.fromJson(v))
.toList();
feedbacks =
(json['feedbacks'] as List?)?.map((v) => Reason.fromJson(v)).toList();
watchLater = json['watch_later'];
}
}
class DislikeReason {
DislikeReason({
this.id,
this.name,
this.toast,
});
class Reason {
int? id;
String? name;
String? toast;
DislikeReason.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
toast = json['toast'];
}
}
class FeedbackReason {
FeedbackReason({
this.id,
this.name,
this.toast,
});
int? id;
String? name;
String? toast;
FeedbackReason.fromJson(Map<String, dynamic> json) {
Reason.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
toast = json['toast'];

View File

@@ -22,9 +22,8 @@ class LiveFollowingModel {
LiveFollowingModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
list = (json['list'] as List?)
?.map((item) => LiveFollowingItemModel.fromJson(item))
.toList() ??
<LiveFollowingItemModel>[];
?.map((item) => LiveFollowingItemModel.fromJson(item))
.toList();
liveCount = json['live_count'];
neverLivedCount = json['never_lived_count'];
neverLivedFaces = json['never_lived_faces'];

View File

@@ -0,0 +1,23 @@
import 'datum.dart';
class LiveEmoteData {
int? fansBrand;
List<LiveEmoteDatum>? data;
dynamic purchaseUrl;
LiveEmoteData({this.fansBrand, this.data, this.purchaseUrl});
factory LiveEmoteData.fromJson(Map<String, dynamic> json) => LiveEmoteData(
fansBrand: json['fans_brand'] as int?,
data: (json['data'] as List<dynamic>?)
?.map((e) => LiveEmoteDatum.fromJson(e as Map<String, dynamic>))
.toList(),
purchaseUrl: json['purchase_url'] as dynamic,
);
Map<String, dynamic> toJson() => {
'fans_brand': fansBrand,
'data': data?.map((e) => e.toJson()).toList(),
'purchase_url': purchaseUrl,
};
}

View File

@@ -0,0 +1,71 @@
import 'emoticon.dart';
import 'top_show.dart';
import 'top_show_recent.dart';
class LiveEmoteDatum {
List<LiveEmoticon>? emoticons;
int? pkgId;
String? pkgName;
int? pkgType;
String? pkgDescript;
int? pkgPerm;
int? unlockIdentity;
int? unlockNeedGift;
String? currentCover;
List<dynamic>? recentlyUsedEmoticons;
TopShow? topShow;
TopShowRecent? topShowRecent;
LiveEmoteDatum({
this.emoticons,
this.pkgId,
this.pkgName,
this.pkgType,
this.pkgDescript,
this.pkgPerm,
this.unlockIdentity,
this.unlockNeedGift,
this.currentCover,
this.recentlyUsedEmoticons,
this.topShow,
this.topShowRecent,
});
factory LiveEmoteDatum.fromJson(Map<String, dynamic> json) => LiveEmoteDatum(
emoticons: (json['emoticons'] as List<dynamic>?)
?.map((e) => LiveEmoticon.fromJson(e as Map<String, dynamic>))
.toList(),
pkgId: json['pkg_id'] as int?,
pkgName: json['pkg_name'] as String?,
pkgType: json['pkg_type'] as int?,
pkgDescript: json['pkg_descript'] as String?,
pkgPerm: json['pkg_perm'] as int?,
unlockIdentity: json['unlock_identity'] as int?,
unlockNeedGift: json['unlock_need_gift'] as int?,
currentCover: json['current_cover'] as String?,
recentlyUsedEmoticons:
json['recently_used_emoticons'] as List<dynamic>?,
topShow: json['top_show'] == null
? null
: TopShow.fromJson(json['top_show'] as Map<String, dynamic>),
topShowRecent: json['top_show_recent'] == null
? null
: TopShowRecent.fromJson(
json['top_show_recent'] as Map<String, dynamic>),
);
Map<String, dynamic> toJson() => {
'emoticons': emoticons?.map((e) => e.toJson()).toList(),
'pkg_id': pkgId,
'pkg_name': pkgName,
'pkg_type': pkgType,
'pkg_descript': pkgDescript,
'pkg_perm': pkgPerm,
'unlock_identity': unlockIdentity,
'unlock_need_gift': unlockNeedGift,
'current_cover': currentCover,
'recently_used_emoticons': recentlyUsedEmoticons,
'top_show': topShow?.toJson(),
'top_show_recent': topShowRecent?.toJson(),
};
}

View File

@@ -0,0 +1,83 @@
class LiveEmoticon {
String? emoji;
String? descript;
String? url;
int? isDynamic;
int? inPlayerArea;
int? width;
int? height;
int? identity;
int? unlockNeedGift;
int? perm;
int? unlockNeedLevel;
int? emoticonValueType;
int? bulgeDisplay;
String? unlockShowText;
String? unlockShowColor;
String? emoticonUnique;
String? unlockShowImage;
int? emoticonId;
LiveEmoticon({
this.emoji,
this.descript,
this.url,
this.isDynamic,
this.inPlayerArea,
this.width,
this.height,
this.identity,
this.unlockNeedGift,
this.perm,
this.unlockNeedLevel,
this.emoticonValueType,
this.bulgeDisplay,
this.unlockShowText,
this.unlockShowColor,
this.emoticonUnique,
this.unlockShowImage,
this.emoticonId,
});
factory LiveEmoticon.fromJson(Map<String, dynamic> json) => LiveEmoticon(
emoji: json['emoji'] as String?,
descript: json['descript'] as String?,
url: json['url'] as String?,
isDynamic: json['is_dynamic'] as int?,
inPlayerArea: json['in_player_area'] as int?,
width: json['width'] as int? ?? 0,
height: json['height'] as int? ?? 0,
identity: json['identity'] as int?,
unlockNeedGift: json['unlock_need_gift'] as int?,
perm: json['perm'] as int?,
unlockNeedLevel: json['unlock_need_level'] as int?,
emoticonValueType: json['emoticon_value_type'] as int?,
bulgeDisplay: json['bulge_display'] as int?,
unlockShowText: json['unlock_show_text'] as String?,
unlockShowColor: json['unlock_show_color'] as String?,
emoticonUnique: json['emoticon_unique'] as String?,
unlockShowImage: json['unlock_show_image'] as String?,
emoticonId: json['emoticon_id'] as int?,
);
Map<String, dynamic> toJson() => {
'emoji': emoji,
'descript': descript,
'url': url,
'is_dynamic': isDynamic,
'in_player_area': inPlayerArea,
'width': width,
'height': height,
'identity': identity,
'unlock_need_gift': unlockNeedGift,
'perm': perm,
'unlock_need_level': unlockNeedLevel,
'emoticon_value_type': emoticonValueType,
'bulge_display': bulgeDisplay,
'unlock_show_text': unlockShowText,
'unlock_show_color': unlockShowColor,
'emoticon_unique': emoticonUnique,
'unlock_show_image': unlockShowImage,
'emoticon_id': emoticonId,
};
}

View File

@@ -0,0 +1,26 @@
import 'data.dart';
class LiveEmoticons {
int? code;
String? message;
int? ttl;
LiveEmoteData? data;
LiveEmoticons({this.code, this.message, this.ttl, this.data});
factory LiveEmoticons.fromJson(Map<String, dynamic> json) => LiveEmoticons(
code: json['code'] as int?,
message: json['message'] as String?,
ttl: json['ttl'] as int?,
data: json['data'] == null
? null
: LiveEmoteData.fromJson(json['data'] as Map<String, dynamic>),
);
Map<String, dynamic> toJson() => {
'code': code,
'message': message,
'ttl': ttl,
'data': data?.toJson(),
};
}

View File

@@ -0,0 +1,16 @@
class TopLeft {
String? image;
String? text;
TopLeft({this.image, this.text});
factory TopLeft.fromJson(Map<String, dynamic> json) => TopLeft(
image: json['image'] as String?,
text: json['text'] as String?,
);
Map<String, dynamic> toJson() => {
'image': image,
'text': text,
};
}

View File

@@ -0,0 +1,16 @@
class TopRight {
String? image;
String? text;
TopRight({this.image, this.text});
factory TopRight.fromJson(Map<String, dynamic> json) => TopRight(
image: json['image'] as String?,
text: json['text'] as String?,
);
Map<String, dynamic> toJson() => {
'image': image,
'text': text,
};
}

View File

@@ -0,0 +1,23 @@
import 'top_left.dart';
import 'top_right.dart';
class TopShow {
TopLeft? topLeft;
TopRight? topRight;
TopShow({this.topLeft, this.topRight});
factory TopShow.fromJson(Map<String, dynamic> json) => TopShow(
topLeft: json['top_left'] == null
? null
: TopLeft.fromJson(json['top_left'] as Map<String, dynamic>),
topRight: json['top_right'] == null
? null
: TopRight.fromJson(json['top_right'] as Map<String, dynamic>),
);
Map<String, dynamic> toJson() => {
'top_left': topLeft?.toJson(),
'top_right': topRight?.toJson(),
};
}

View File

@@ -0,0 +1,23 @@
import 'top_left.dart';
import 'top_right.dart';
class TopShowRecent {
TopLeft? topLeft;
TopRight? topRight;
TopShowRecent({this.topLeft, this.topRight});
factory TopShowRecent.fromJson(Map<String, dynamic> json) => TopShowRecent(
topLeft: json['top_left'] == null
? null
: TopLeft.fromJson(json['top_left'] as Map<String, dynamic>),
topRight: json['top_right'] == null
? null
: TopRight.fromJson(json['top_right'] as Map<String, dynamic>),
);
Map<String, dynamic> toJson() => {
'top_left': topLeft?.toJson(),
'top_right': topRight?.toJson(),
};
}

View File

@@ -2,7 +2,6 @@ enum LiveQuality {
dolby,
super4K,
origin,
veryHigh,
bluRay,
superHD,
smooth,
@@ -37,7 +36,8 @@ extension VideoQualityDesc on LiveQuality {
'原画',
'蓝光',
'超清',
'高清',
'流畅',
];
get description => _descList[index];
String get description => _descList[index];
}

View File

@@ -1,3 +1,7 @@
import 'package:PiliPlus/utils/utils.dart';
import '../model_video.dart';
class MemberArchiveDataModel {
MemberArchiveDataModel({
this.list,
@@ -51,114 +55,60 @@ class TListItemModel {
}
}
class VListItemModel {
VListItemModel({
this.comment,
this.typeid,
this.play,
this.pic,
this.subtitle,
this.description,
this.copyright,
this.title,
this.review,
this.author,
this.mid,
this.created,
this.pubdate,
this.length,
this.duration,
this.videoReview,
this.aid,
this.bvid,
this.cid,
this.hideClick,
this.isChargingSrc,
this.rcmdReason,
this.owner,
});
class VListItemModel extends BaseVideoItemModel {
int? comment;
int? typeid;
int? play;
String? pic;
String? subtitle;
String? description;
String? copyright;
String? title;
int? review;
String? author;
int? mid;
int? created;
int? pubdate;
String? length;
String? duration;
int? videoReview;
int? aid;
String? bvid;
int? cid;
bool? hideClick;
bool? isChargingSrc;
Stat? stat;
String? rcmdReason;
Owner? owner;
VListItemModel.fromJson(Map<String, dynamic> json) {
comment = json['comment'];
typeid = json['typeid'];
play = json['play'];
pic = json['pic'];
subtitle = json['subtitle'];
description = json['description'];
desc = json['description'];
copyright = json['copyright'];
title = json['title'];
review = json['review'];
author = json['author'];
mid = json['mid'];
created = json['created'];
pubdate = json['created'];
length = json['length'];
duration = json['length'];
videoReview = json['video_review'];
if (json['length'] != null) duration = Utils.duration(json['length']);
aid = json['aid'];
bvid = json['bvid'];
cid = null;
hideClick = json['hide_click'];
isChargingSrc = json['is_charging_arc'];
stat = Stat.fromJson(json);
rcmdReason = null;
owner = Owner.fromJson(json);
stat = VListStat.fromJson(json);
owner = VListOwner.fromJson(json);
}
// @override
// int? cid = null;
// @override
// String? rcmdReason = null;
// @override
// String? goto;
// @override
// bool isFollowed;
// @override
// String? uri;
}
class VListOwner extends BaseOwner {
VListOwner.fromJson(Map<String, dynamic> json) {
mid = json["mid"];
name = json["author"];
}
}
class Stat {
Stat({
this.view,
this.danmu,
});
int? view;
int? danmu;
Stat.fromJson(Map<String, dynamic> json) {
class VListStat extends BaseStat {
VListStat.fromJson(Map<String, dynamic> json) {
view = json["play"];
danmu = json['video_review'];
}
}
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Owner.fromJson(Map<String, dynamic> json) {
mid = json["mid"];
name = json["author"];
face = '';
}
}

View File

@@ -0,0 +1,31 @@
import 'package:PiliPlus/pages/common/multi_select_controller.dart';
class FavArticleModel with MultiSelectData {
FavArticleModel({
this.webUrl,
this.title,
this.summary,
this.message,
this.pic,
this.cvid,
this.noteId,
});
String? webUrl;
String? title;
String? summary;
String? message;
String? pic;
dynamic cvid;
dynamic noteId;
FavArticleModel.fromJson(Map json) {
webUrl = json['web_url'];
title = json['title'];
summary = json['summary'];
message = json['message'];
pic = json['arc']?['pic'];
cvid = json['cvid'];
noteId = json['note_id'];
}
}

View File

@@ -1,89 +1,26 @@
class MemberCoinsDataModel {
MemberCoinsDataModel({
this.aid,
this.bvid,
this.cid,
this.coins,
this.copyright,
this.ctime,
this.desc,
this.duration,
this.owner,
this.pic,
this.pubLocation,
this.pubdate,
this.resourceType,
this.state,
this.subtitle,
this.time,
this.title,
this.tname,
this.videos,
this.view,
this.danmaku,
});
import '../model_hot_video_item.dart';
int? aid;
String? bvid;
int? cid;
int? coins;
int? copyright;
int? ctime;
String? desc;
int? duration;
Owner? owner;
String? pic;
String? pubLocation;
int? pubdate;
String? resourceType;
int? state;
class MemberCoinsDataModel extends HotVideoItemModel {
String? subtitle;
int? coins;
int? time;
String? title;
String? tname;
int? videos;
int? view;
int? danmaku;
// int? get view => stat.view;
// int? get danmaku => stat.danmu;
MemberCoinsDataModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
cid = json['cid'];
MemberCoinsDataModel.fromJson(Map<String, dynamic> json)
: super.fromJson(json) {
coins = json['coins'];
copyright = json['copyright'];
ctime = json['ctime'];
desc = json['desc'];
duration = json['duration'];
owner = Owner.fromJson(json['owner']);
pic = json['pic'];
pubLocation = json['pub_location'];
pubdate = json['pubdate'];
resourceType = json['resource_type'];
state = json['state'];
subtitle = json['subtitle'];
time = json['time'];
title = json['title'];
tname = json['tname'];
videos = json['videos'];
view = json['stat']['view'];
danmaku = json['stat']['danmaku'];
}
}
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Owner.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
// view = json['stat']['view'];
// danmaku = json['stat']['danmaku'];
}
// @override
// String? goto;
// @override
// bool isFollowed;
// @override
// String? rcmdReason;
// @override
// String? uri;
}

View File

@@ -11,6 +11,7 @@ class MemberInfoModel {
this.official,
this.vip,
this.liveRoom,
this.isSeniorMember,
});
int? mid;
@@ -24,6 +25,7 @@ class MemberInfoModel {
Map? official;
Vip? vip;
LiveRoom? liveRoom;
int? isSeniorMember;
MemberInfoModel.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
@@ -38,6 +40,7 @@ class MemberInfoModel {
vip = Vip.fromJson(json['vip']);
liveRoom =
json['live_room'] != null ? LiveRoom.fromJson(json['live_room']) : null;
isSeniorMember = json['is_senior_member'];
}
}

View File

@@ -1,23 +1,23 @@
class MemberTagItemModel {
import 'package:PiliPlus/pages/common/multi_select_controller.dart'
show MultiSelectData;
class MemberTagItemModel with MultiSelectData {
MemberTagItemModel({
this.count,
this.name,
this.tagid,
this.tip,
this.checked,
});
int? count;
String? name;
int? tagid;
String? tip;
bool? checked;
MemberTagItemModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
name = json['name'];
tagid = json['tagid'];
tip = json['tip'];
checked = false;
}
}

View File

@@ -1,66 +1,25 @@
import './model_owner.dart';
import 'package:PiliPlus/pages/common/multi_select_controller.dart'
show MultiSelectData;
import 'model_owner.dart';
import 'model_rec_video_item.dart';
import 'model_video.dart';
class HotVideoItemModel {
HotVideoItemModel({
this.aid,
this.cid,
this.bvid,
this.videos,
this.tid,
this.tname,
this.copyright,
this.pic,
this.title,
this.pubdate,
this.ctime,
this.desc,
this.state,
this.duration,
this.middionId,
this.owner,
this.stat,
this.vDynamic,
this.dimension,
this.shortLinkV2,
this.firstFrame,
this.pubLocation,
this.seasontype,
this.isOgv,
this.rcmdReason,
this.checked,
this.pgcLabel,
this.redirectUrl,
});
int? aid;
int? cid;
String? bvid;
// 稍后再看, 排行榜等网页返回也使用该类
class HotVideoItemModel extends BaseRecVideoItemModel with MultiSelectData {
int? videos;
int? tid;
String? tname;
int? copyright;
String? pic;
String? title;
int? pubdate;
int? ctime;
String? desc;
int? state;
int? duration;
int? middionId;
Owner? owner;
Stat? stat;
String? vDynamic;
Dimension? dimension;
String? shortLinkV2;
String? firstFrame;
String? pubLocation;
int? seasontype;
bool? isOgv;
RcmdReason? rcmdReason;
bool? checked;
String? pgcLabel;
String? redirectUrl;
num? progress;
HotVideoItemModel.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
cid = json["cid"];
@@ -76,97 +35,63 @@ class HotVideoItemModel {
desc = json["desc"];
state = json["state"];
duration = json["duration"];
middionId = json["middion_id"];
owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json['stat']);
vDynamic = json["vDynamic"];
dimension = Dimension.fromMap(json['dimension']);
shortLinkV2 = json["short_link_v2"];
stat = HotStat.fromJson(json['stat']);
dimension = Dimension.fromJson(json['dimension']);
firstFrame = json["first_frame"];
pubLocation = json["pub_location"];
seasontype = json["seasontype"];
isOgv = json["isOgv"];
rcmdReason = json['rcmd_reason'] != '' && json['rcmd_reason'] != null
? RcmdReason.fromJson(json['rcmd_reason'])
: null;
dynamic rcmd = json['rcmd_reason'];
rcmdReason = rcmd is Map ? rcmd['content'] : rcmd; // 相关视频里rcmd为String,
if (rcmdReason?.isEmpty == true) rcmdReason = null;
pgcLabel = json['pgc_label'];
redirectUrl = json['redirect_url'];
// uri = json['uri']; // 仅在稍后再看存在
progress = json['progress'];
}
// @override
// get isFollowed => false;
// @override
// get goto => 'av';
// @override
// get uri => 'bilibili://video/$aid';
}
class Stat {
Stat({
this.aid,
this.view,
this.danmu,
this.reply,
this.favorite,
this.coin,
this.share,
this.nowRank,
this.hisRank,
this.like,
this.dislike,
this.vt,
this.vv,
});
int? aid;
int? view;
int? danmu;
class HotStat extends Stat {
int? reply;
int? favorite;
int? coin;
int? share;
int? nowRank;
int? hisRank;
int? like;
int? dislike;
int? vt;
int? vv;
Stat.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
view = json["view"];
danmu = json['danmaku'];
HotStat.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
reply = json["reply"];
favorite = json["favorite"];
coin = json['coin'];
share = json["share"];
nowRank = json["now_rank"];
hisRank = json['his_rank'];
like = json["like"];
dislike = json["dislike"];
vt = json['vt'];
vv = json["vv"];
}
}
class Dimension {
Dimension({this.width, this.height, this.rotate});
// class RcmdReason {
// RcmdReason({
// this.rcornerMark,
// this.content,
// });
int? width;
int? height;
int? rotate;
// int? rcornerMark;
// String? content = '';
Dimension.fromMap(Map<String, dynamic> json) {
width = json["width"];
height = json["height"];
rotate = json["rotate"];
}
}
class RcmdReason {
RcmdReason({
this.rcornerMark,
this.content,
});
int? rcornerMark;
String? content = '';
RcmdReason.fromJson(Map<String, dynamic> json) {
rcornerMark = json["corner_mark"];
content = json["content"] ?? '';
}
}
// RcmdReason.fromJson(Map<String, dynamic> json) {
// rcornerMark = json["corner_mark"];
// content = json["content"] ?? '';
// }
// }

View File

@@ -1,17 +1,21 @@
import 'package:hive/hive.dart';
import 'model_video.dart';
part 'model_owner.g.dart';
@HiveType(typeId: 3)
class Owner {
class Owner implements BaseOwner {
Owner({
this.mid,
this.name,
this.face,
});
@HiveField(0)
@override
int? mid;
@HiveField(1)
@override
String? name;
@HiveField(2)
String? face;

View File

@@ -1,55 +1,19 @@
import './model_owner.dart';
import 'package:hive/hive.dart';
import 'model_video.dart';
part 'model_rec_video_item.g.dart';
@HiveType(typeId: 0)
class RecVideoItemModel {
RecVideoItemModel({
this.id,
this.bvid,
this.cid,
this.goto,
this.uri,
this.pic,
this.title,
this.duration,
this.pubdate,
this.owner,
this.stat,
this.isFollowed,
this.rcmdReason,
});
@HiveField(0)
int? id = -1;
@HiveField(1)
String? bvid = '';
@HiveField(2)
int? cid = -1;
@HiveField(3)
String? goto = '';
@HiveField(4)
String? uri = '';
@HiveField(5)
String? pic = '';
@HiveField(6)
String? title = '';
@HiveField(7)
int? duration = -1;
@HiveField(8)
int? pubdate = -1;
@HiveField(9)
Owner? owner;
@HiveField(10)
Stat? stat;
@HiveField(11)
int? isFollowed;
@HiveField(12)
abstract class BaseRecVideoItemModel extends BaseVideoItemModel {
String? goto;
String? uri;
String? rcmdReason;
// app推荐专属
int? param;
String? bangumiBadge;
}
class RecVideoItemModel extends BaseRecVideoItemModel {
RecVideoItemModel.fromJson(Map<String, dynamic> json) {
id = json["id"];
aid = json["id"];
bvid = json["bvid"];
cid = json["cid"];
goto = json["goto"];
@@ -60,34 +24,15 @@ class RecVideoItemModel {
pubdate = json["pubdate"];
owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]);
isFollowed = json["is_followed"] ?? 0;
isFollowed = json["is_followed"] == 1;
// rcmdReason = json["rcmd_reason"] != null
// ? RcmdReason.fromJson(json["rcmd_reason"])
// : RcmdReason(content: '');
rcmdReason = json["rcmd_reason"]?['content'];
}
}
@HiveType(typeId: 1)
class Stat {
Stat({
this.view,
this.like,
this.danmu,
});
@HiveField(0)
int? view;
@HiveField(1)
int? like;
@HiveField(2)
int? danmu;
Stat.fromJson(Map<String, dynamic> json) {
// 无需在model中转换以保留原始数据在view层处理即可
view = json["view"];
like = json["like"];
danmu = json['danmaku'];
}
// @override
// String? get desc => null;
}
// @HiveType(typeId: 2)
@@ -96,10 +41,8 @@ class Stat {
// this.reasonType,
// this.content,
// });
// @HiveField(0)
// int? reasonType;
// @HiveField(1)
// String? content = '';
// // int? reasonType;
// // String? content;
//
// RcmdReason.fromJson(Map<String, dynamic> json) {
// reasonType = json["reason_type"];

View File

@@ -1,154 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'model_rec_video_item.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
@override
final int typeId = 0;
@override
RecVideoItemModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemModel(
id: fields[0] as int?,
bvid: fields[1] as String?,
cid: fields[2] as int?,
goto: fields[3] as String?,
uri: fields[4] as String?,
pic: fields[5] as String?,
title: fields[6] as String?,
duration: fields[7] as int?,
pubdate: fields[8] as int?,
owner: fields[9] as Owner?,
stat: fields[10] as Stat?,
isFollowed: fields[11] as int?,
rcmdReason: fields[12] as String?,
);
}
@override
void write(BinaryWriter writer, RecVideoItemModel obj) {
writer
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.bvid)
..writeByte(2)
..write(obj.cid)
..writeByte(3)
..write(obj.goto)
..writeByte(4)
..write(obj.uri)
..writeByte(5)
..write(obj.pic)
..writeByte(6)
..write(obj.title)
..writeByte(7)
..write(obj.duration)
..writeByte(8)
..write(obj.pubdate)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.stat)
..writeByte(11)
..write(obj.isFollowed)
..writeByte(12)
..write(obj.rcmdReason);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class StatAdapter extends TypeAdapter<Stat> {
@override
final int typeId = 1;
@override
Stat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Stat(
view: fields[0] as int?,
like: fields[1] as int?,
danmu: fields[2] as int?,
);
}
@override
void write(BinaryWriter writer, Stat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
//
// class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
// @override
// final int typeId = 2;
//
// @override
// RcmdReason read(BinaryReader reader) {
// final numOfFields = reader.readByte();
// final fields = <int, dynamic>{
// for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
// };
// return RcmdReason(
// reasonType: fields[0] as int?,
// content: fields[1] as String?,
// );
// }
//
// @override
// void write(BinaryWriter writer, RcmdReason obj) {
// writer
// ..writeByte(2)
// ..writeByte(0)
// ..write(obj.reasonType)
// ..writeByte(1)
// ..write(obj.content);
// }
//
// @override
// int get hashCode => typeId.hashCode;
//
// @override
// bool operator ==(Object other) =>
// identical(this, other) ||
// other is RcmdReasonAdapter &&
// runtimeType == other.runtimeType &&
// typeId == other.typeId;
// }

View File

@@ -0,0 +1,59 @@
import 'package:PiliPlus/utils/utils.dart';
abstract class BaseSimpleVideoItemModel {
late String title;
String? bvid;
int? cid;
String? pic;
int duration = -1;
late BaseOwner owner;
late BaseStat stat;
}
abstract class BaseVideoItemModel extends BaseSimpleVideoItemModel {
int? aid;
String? desc;
int? pubdate;
bool isFollowed = false;
}
abstract class BaseOwner {
int? mid;
String? name;
}
abstract class BaseStat {
int? view;
int? like;
int? danmu;
String get viewStr => Utils.numFormat(view);
String get danmuStr => Utils.numFormat(danmu);
}
class Stat extends BaseStat {
Stat.fromJson(Map<String, dynamic> json) {
view = json["view"];
like = json["like"];
danmu = json['danmaku'];
}
}
class PlayStat extends BaseStat {
PlayStat.fromJson(Map<String, dynamic> json) {
view = json['play'];
danmu = json['danmaku'];
}
}
class Dimension {
int? width;
int? height;
int? rotate;
Dimension.fromJson(Map<String, dynamic> json) {
width = json["width"];
height = json["height"];
rotate = json["rotate"];
}
}

View File

@@ -6,8 +6,7 @@ class MsgFeedReplyMe {
MsgFeedReplyMe({this.cursor, this.items, this.lastViewAt});
MsgFeedReplyMe.fromJson(Map<String, dynamic> json) {
cursor =
json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
if (json['items'] != null) {
items = <ReplyMeItems>[];
json['items'].forEach((v) {
@@ -56,13 +55,14 @@ class ReplyMeItems {
int? isMulti;
int? replyTime;
ReplyMeItems(
{this.id,
this.user,
this.item,
this.counts,
this.isMulti,
this.replyTime});
ReplyMeItems({
this.id,
this.user,
this.item,
this.counts,
this.isMulti,
this.replyTime,
});
ReplyMeItems.fromJson(Map<String, dynamic> json) {
id = json['id'];
@@ -99,11 +99,11 @@ class User {
User(
{this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow});
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow});
User.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
@@ -153,28 +153,28 @@ class Item {
Item(
{this.subjectId,
this.rootId,
this.sourceId,
this.targetId,
this.type,
this.businessId,
this.business,
this.title,
this.desc,
this.image,
this.uri,
this.nativeUri,
this.detailTitle,
this.rootReplyContent,
this.sourceContent,
this.targetReplyContent,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
this.hideLikeButton,
this.likeState,
this.danmu,
this.message});
this.rootId,
this.sourceId,
this.targetId,
this.type,
this.businessId,
this.business,
this.title,
this.desc,
this.image,
this.uri,
this.nativeUri,
this.detailTitle,
this.rootReplyContent,
this.sourceContent,
this.targetReplyContent,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
this.hideLikeButton,
this.likeState,
this.danmu,
this.message});
Item.fromJson(Map<String, dynamic> json) {
subjectId = json['subject_id'];
@@ -246,11 +246,11 @@ class AtDetails {
AtDetails(
{this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow});
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow});
AtDetails.fromJson(Map<String, dynamic> json) {
mid = json['mid'];

View File

@@ -1,12 +1,10 @@
import 'package:PiliPlus/utils/em.dart';
import 'package:PiliPlus/utils/utils.dart';
class SearchVideoModel {
SearchVideoModel({
this.numResults,
this.list,
});
import '../model_owner.dart';
import '../model_video.dart';
class SearchVideoModel {
int? numResults;
List<SearchVideoItemModel>? list;
@@ -19,68 +17,25 @@ class SearchVideoModel {
}
}
class SearchVideoItemModel {
SearchVideoItemModel({
this.type,
this.id,
this.cid,
// this.author,
this.mid,
// this.typeid,
// this.typename,
this.arcurl,
this.aid,
this.bvid,
this.title,
this.description,
this.pic,
// this.play,
this.videoReview,
// this.favorites,
this.tag,
// this.review,
this.pubdate,
this.senddate,
this.duration,
// this.viewType,
// this.like,
// this.upic,
// this.danmaku,
this.owner,
this.stat,
this.rcmdReason,
});
class SearchVideoItemModel extends BaseVideoItemModel {
String? type;
int? id;
int? cid;
// String? author;
int? mid;
// String? typeid;
// String? typename;
String? arcurl;
int? aid;
String? bvid;
List? title;
// List? titleList;
String? description;
String? pic;
// String? play;
int? videoReview;
// int? videoReview;
// String? favorites;
String? tag;
// String? review;
int? pubdate;
int? senddate;
int? duration;
int? ctime;
// String? duration;
// String? viewType;
// String? like;
// String? upic;
// String? danmaku;
Owner? owner;
Stat? stat;
String? rcmdReason;
List<Map<String, String>>? titleList;
SearchVideoItemModel.fromJson(Map<String, dynamic> json) {
type = json['type'];
@@ -88,43 +43,35 @@ class SearchVideoItemModel {
arcurl = json['arcurl'];
aid = json['aid'];
bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
description = json['description'];
titleList = Em.regTitle(json['title']);
title = titleList!.map((i) => i['text']!).join();
desc = json['description'];
pic = json['pic'] != null && json['pic'].startsWith('//')
? 'https:${json['pic']}'
: json['pic'] ?? '';
videoReview = json['video_review'];
pubdate = json['pubdate'];
senddate = json['senddate'];
ctime = json['senddate'];
duration = Utils.duration(json['duration']);
owner = Owner.fromJson(json);
stat = Stat.fromJson(json);
owner = SearchOwner.fromJson(json);
stat = SearchStat.fromJson(json);
}
// @override
// String? goto;
// @override
// bool isFollowed;
// @override
// String? uri;
}
class Stat {
Stat({
this.view,
this.danmu,
this.favorite,
this.reply,
this.like,
});
// 播放量
int? view;
// 弹幕数
int? danmu;
class SearchStat extends BaseStat {
// 收藏数
int? favorite;
// 评论数
int? reply;
// 喜欢
int? like;
Stat.fromJson(Map<String, dynamic> json) {
SearchStat.fromJson(Map<String, dynamic> json) {
view = json['play'];
danmu = json['danmaku'];
favorite = json['favorite'];
@@ -133,17 +80,8 @@ class Stat {
}
}
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Owner.fromJson(Map<String, dynamic> json) {
class SearchOwner extends Owner {
SearchOwner.fromJson(Map<String, dynamic> json) {
mid = json["mid"];
name = json["author"];
face = json['upic'];
@@ -185,6 +123,7 @@ class SearchUserItemModel {
this.isLive,
this.roomId,
this.officialVerify,
this.isSeniorMember,
});
String? type;
@@ -203,6 +142,7 @@ class SearchUserItemModel {
int? isLive;
int? roomId;
Map? officialVerify;
int? isSeniorMember;
SearchUserItemModel.fromJson(Map<String, dynamic> json) {
type = json['type'];
@@ -221,6 +161,7 @@ class SearchUserItemModel {
isLive = json['is_live'];
roomId = json['room_id'];
officialVerify = json['official_verify'];
isSeniorMember = json['is_senior_member'];
}
}
@@ -301,7 +242,7 @@ class SearchLiveItemModel {
rankScore = json['rank_score'];
roomid = json['roomid'];
attentions = json['attentions'];
cateName = Em.regCate(json['cate_name']) ?? '';
cateName = Em.regCate(json['cate_name']);
}
}

View File

@@ -0,0 +1,28 @@
import 'trending_data.dart';
class SearchTrending {
int? code;
String? message;
int? ttl;
TrendingData? data;
SearchTrending({this.code, this.message, this.ttl, this.data});
factory SearchTrending.fromJson(Map<String, dynamic> json) {
return SearchTrending(
code: json['code'] as int?,
message: json['message'] as String?,
ttl: json['ttl'] as int?,
data: json['data'] == null
? null
: TrendingData.fromJson(json['data'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'code': code,
'message': message,
'ttl': ttl,
'data': data?.toJson(),
};
}

View File

@@ -0,0 +1,14 @@
class StatDatas {
StatDatas();
factory StatDatas.fromJson(Map<String, dynamic> json) {
// TODO: implement fromJson
throw UnimplementedError('StatDatas.fromJson($json) is not implemented');
}
Map<String, dynamic> toJson() {
// TODO: implement toJson
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,95 @@
import 'stat_datas.dart';
class TopList {
int? hotId;
String? keyword;
String? showName;
int? score;
int? wordType;
int? gotoType;
String? gotoValue;
String? icon;
List<dynamic>? liveId;
int? callReason;
String? heatLayer;
int? pos;
int? id;
String? status;
String? nameType;
int? resourceId;
int? setGray;
List<dynamic>? cardValues;
int? heatScore;
StatDatas? statDatas;
TopList({
this.hotId,
this.keyword,
this.showName,
this.score,
this.wordType,
this.gotoType,
this.gotoValue,
this.icon,
this.liveId,
this.callReason,
this.heatLayer,
this.pos,
this.id,
this.status,
this.nameType,
this.resourceId,
this.setGray,
this.cardValues,
this.heatScore,
this.statDatas,
});
factory TopList.fromJson(Map<String, dynamic> json) => TopList(
hotId: json['hot_id'] as int?,
keyword: json['keyword'] as String?,
showName: json['show_name'] as String?,
score: json['score'] as int?,
wordType: json['word_type'] as int?,
gotoType: json['goto_type'] as int?,
gotoValue: json['goto_value'] as String?,
icon: json['icon'] as String?,
liveId: json['live_id'] as List<dynamic>?,
callReason: json['call_reason'] as int?,
heatLayer: json['heat_layer'] as String?,
pos: json['pos'] as int?,
id: json['id'] as int?,
status: json['status'] as String?,
nameType: json['name_type'] as String?,
resourceId: json['resource_id'] as int?,
setGray: json['set_gray'] as int?,
cardValues: json['card_values'] as List<dynamic>?,
heatScore: json['heat_score'] as int?,
statDatas: json['stat_datas'] == null
? null
: StatDatas.fromJson(json['stat_datas'] as Map<String, dynamic>),
);
Map<String, dynamic> toJson() => {
'hot_id': hotId,
'keyword': keyword,
'show_name': showName,
'score': score,
'word_type': wordType,
'goto_type': gotoType,
'goto_value': gotoValue,
'icon': icon,
'live_id': liveId,
'call_reason': callReason,
'heat_layer': heatLayer,
'pos': pos,
'id': id,
'status': status,
'name_type': nameType,
'resource_id': resourceId,
'set_gray': setGray,
'card_values': cardValues,
'heat_score': heatScore,
'stat_datas': statDatas?.toJson(),
};
}

View File

@@ -0,0 +1,28 @@
import 'package:PiliPlus/models/search/search_trending/trending_list.dart';
class TrendingData {
String? trackid;
List<TrendingList>? list;
List<TrendingList>? topList;
String? hotwordEggInfo;
TrendingData({this.trackid, this.list, this.topList, this.hotwordEggInfo});
factory TrendingData.fromJson(Map<String, dynamic> json) => TrendingData(
trackid: json['trackid'] as String?,
list: (json['list'] as List<dynamic>?)
?.map((e) => TrendingList.fromJson(e as Map<String, dynamic>))
.toList(),
topList: (json['top_list'] as List<dynamic>?)
?.map((e) => TrendingList.fromJson(e as Map<String, dynamic>))
.toList(),
hotwordEggInfo: json['hotword_egg_info'] as String?,
);
Map<String, dynamic> toJson() => {
'trackid': trackid,
'list': list?.map((e) => e.toJson()).toList(),
'top_list': topList?.map((e) => e.toJson()).toList(),
'hotword_egg_info': hotwordEggInfo,
};
}

View File

@@ -0,0 +1,47 @@
class TrendingList {
int? position;
String? keyword;
String? showName;
int? wordType;
String? icon;
int? hotId;
String? isCommercial;
int? resourceId;
bool? showLiveIcon;
TrendingList({
this.position,
this.keyword,
this.showName,
this.wordType,
this.icon,
this.hotId,
this.isCommercial,
this.resourceId,
this.showLiveIcon,
});
factory TrendingList.fromJson(Map<String, dynamic> json) => TrendingList(
position: json['position'] as int?,
keyword: json['keyword'] as String?,
showName: json['show_name'] as String?,
wordType: json['word_type'] as int?,
icon: json['icon'] as String?,
hotId: json['hot_id'] as int?,
isCommercial: json['is_commercial'] as String?,
resourceId: json['resource_id'] as int?,
showLiveIcon: json['show_live_icon'] as bool?,
);
Map<String, dynamic> toJson() => {
'position': position,
'keyword': keyword,
'show_name': showName,
'word_type': wordType,
'icon': icon,
'hot_id': hotId,
'is_commercial': isCommercial,
'resource_id': resourceId,
'show_live_icon': showLiveIcon,
};
}

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/models/space/pr_info.dart';
import 'package:PiliPlus/models/space/space_tag_bottom.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -58,8 +59,8 @@ class Card {
String? pendantUrl;
@JsonKey(name: 'pendant_title')
String? pendantTitle;
// @JsonKey(name: 'pr_info')
// PrInfo? prInfo;
@JsonKey(name: 'pr_info')
PrInfo? prInfo;
Relation? relation;
@JsonKey(name: 'is_deleted')
int? isDeleted;
@@ -120,7 +121,7 @@ class Card {
this.achieve,
this.pendantUrl,
this.pendantTitle,
// this.prInfo,
this.prInfo,
this.relation,
this.isDeleted,
this.honours,

View File

@@ -58,9 +58,9 @@ Card _$CardFromJson(Map<String, dynamic> json) => Card(
: Achieve.fromJson(json['achieve'] as Map<String, dynamic>),
pendantUrl: json['pendant_url'] as String?,
pendantTitle: json['pendant_title'] as String?,
// prInfo: json['pr_info'] == null
// ? null
// : PrInfo.fromJson(json['pr_info'] as Map<String, dynamic>),
prInfo: json['pr_info'] == null
? null
: PrInfo.fromJson(json['pr_info'] as Map<String, dynamic>),
relation: json['relation'] == null
? null
: Relation.fromJson(json['relation'] as Map<String, dynamic>),
@@ -131,7 +131,7 @@ Map<String, dynamic> _$CardToJson(Card instance) => <String, dynamic>{
'achieve': instance.achieve,
'pendant_url': instance.pendantUrl,
'pendant_title': instance.pendantTitle,
// 'pr_info': instance.prInfo,
'pr_info': instance.prInfo,
'relation': instance.relation,
'is_deleted': instance.isDeleted,
'honours': instance.honours,

View File

@@ -1,17 +1,18 @@
import 'package:PiliPlus/models/space/item.dart';
import 'package:json_annotation/json_annotation.dart';
part 'coin_archive.g.dart';
@JsonSerializable()
class CoinArchive {
int? count;
List<dynamic>? item;
int? count;
List<Item>? item;
CoinArchive({this.count, this.item});
CoinArchive({this.count, this.item});
factory CoinArchive.fromJson(Map<String, dynamic> json) {
return _$CoinArchiveFromJson(json);
}
factory CoinArchive.fromJson(Map<String, dynamic> json) {
return _$CoinArchiveFromJson(json);
}
Map<String, dynamic> toJson() => _$CoinArchiveToJson(this);
Map<String, dynamic> toJson() => _$CoinArchiveToJson(this);
}

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