From 9d94c72e950f9b3315465a462c3c077fa3d4095e Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:51:41 +0000 Subject: [PATCH] tweaks (#2426) * opt: danmaku weight * opt: cache clean * opt: level img * opt: play icon * opt: svg big-vip * opt: webview ua * opt: simple dialog * feat: export vtt * tweak * opt: mapIndexed * feat: more subtitle * refa: settings page * feat: codec list options * drawPath Signed-off-by: dom * custom dialog option Signed-off-by: dom * update Signed-off-by: dom * Revert "drawPath" This reverts commit e8a4b19f0f64b5d0c381c8f7989fd2c8fa1fce27. * opt: _initStreamIndex * fix: avoid gap * fix: scale [skip ci] * fix: hide repost menu not login * tweaks Signed-off-by: dom --------- Co-authored-by: dom --- assets/images/big-vip.png | Bin 2622 -> 0 bytes assets/images/big-vip.svg | 1 + assets/images/lv/lv0.png | Bin 915 -> 0 bytes assets/images/lv/lv1.png | Bin 876 -> 0 bytes assets/images/lv/lv2.png | Bin 1049 -> 0 bytes assets/images/lv/lv3.png | Bin 991 -> 0 bytes assets/images/lv/lv4.png | Bin 912 -> 0 bytes assets/images/lv/lv5.png | Bin 1077 -> 0 bytes assets/images/lv/lv6.png | Bin 1092 -> 0 bytes assets/images/lv/lv6_s.png | Bin 1426 -> 0 bytes assets/images/play.png | Bin 2367 -> 0 bytes lib/common/assets.dart | 3 +- lib/common/widgets/avatars.dart | 11 +- lib/common/widgets/dialog/dialog.dart | 57 ++-- lib/common/widgets/dialog/export_import.dart | 38 +-- .../widgets/dialog/simple_dialog_option.dart | 29 ++ .../widgets/image_viewer/gallery_viewer.dart | 114 +++---- lib/common/widgets/pendant_avatar.dart | 7 +- .../progress_bar/segment_progress_bar.dart | 8 +- .../widgets/self_sized_horizontal_list.dart | 3 + lib/common/widgets/svg/level_icon.dart | 293 +++++++++++++++++ lib/common/widgets/svg/play_icon.dart | 218 ++++++++++++ lib/common/widgets/video_popup_menu.dart | 215 ++++++------ lib/grpc/dm.dart | 8 + lib/grpc/url.dart | 1 + lib/http/browser_ua.dart | 2 +- lib/http/download.dart | 49 +-- lib/http/sponsor_block.dart | 2 +- lib/main.dart | 2 - lib/models/common/setting_type.dart | 18 + .../common/video/video_decode_type.dart | 3 - .../video/video_play_info/subtitle.dart | 13 +- .../video/video_play_info/subtitle_info.dart | 8 +- lib/pages/about/view.dart | 15 +- lib/pages/article/widgets/opus_content.dart | 7 +- lib/pages/audio/controller.dart | 147 ++++----- lib/pages/bubble/view.dart | 49 ++- lib/pages/danmaku/controller.dart | 5 +- lib/pages/danmaku/view.dart | 2 + lib/pages/download/detail/widgets/item.dart | 70 ++-- lib/pages/download/view.dart | 90 +++-- lib/pages/dynamics/widgets/author_panel.dart | 99 +++--- lib/pages/dynamics/widgets/video_panel.dart | 10 +- lib/pages/dynamics/widgets/vote.dart | 88 +++-- lib/pages/fav_create/view.dart | 50 ++- lib/pages/fav_detail/view.dart | 145 ++++---- lib/pages/follow/view.dart | 93 +++--- lib/pages/live_dm_block/view.dart | 10 +- lib/pages/live_room/controller.dart | 25 +- .../live_room/widgets/header_control.dart | 45 ++- lib/pages/member/widget/user_info_card.dart | 10 +- lib/pages/member_opus/view.dart | 51 ++- lib/pages/member_profile/view.dart | 15 +- lib/pages/mine/view.dart | 15 +- lib/pages/msg_feed_top/like_me/view.dart | 95 +++--- lib/pages/pgc_review/child/view.dart | 112 +++---- lib/pages/pgc_review/view.dart | 74 ++--- lib/pages/search_panel/user/widgets/item.dart | 11 +- lib/pages/setting/common_setting.dart | 65 ++++ lib/pages/setting/extra_setting.dart | 34 -- lib/pages/setting/models/extra_settings.dart | 88 +++-- .../setting/models/privacy_settings.dart | 4 +- .../setting/models/recommend_settings.dart | 4 +- lib/pages/setting/models/video_settings.dart | 56 +--- lib/pages/setting/pages/bar_set.dart | 14 +- lib/pages/setting/pages/color_select.dart | 7 +- lib/pages/setting/play_setting.dart | 34 -- lib/pages/setting/privacy_setting.dart | 33 -- lib/pages/setting/recommend_setting.dart | 51 --- lib/pages/setting/style_setting.dart | 34 -- lib/pages/setting/video_setting.dart | 34 -- lib/pages/setting/view.dart | 58 ++-- lib/pages/sponsor_block/block_mixin.dart | 309 +++++++++--------- lib/pages/video/controller.dart | 200 ++++++------ .../video/introduction/pgc/controller.dart | 111 +++---- .../introduction/pgc/widgets/pgc_panel.dart | 6 +- .../video/introduction/ugc/controller.dart | 89 +++-- lib/pages/video/member/view.dart | 10 +- lib/pages/video/note/view.dart | 10 +- .../video/reply/widgets/reply_item_grpc.dart | 9 +- lib/pages/video/view.dart | 9 +- lib/pages/video/view_point/view.dart | 24 +- lib/pages/video/widgets/header_control.dart | 188 ++++++----- lib/pages/whisper/widgets/item.dart | 70 ++-- lib/pages/whisper_settings/view.dart | 75 ++--- lib/plugin/pl_player/view/view.dart | 13 +- lib/router/app_pages.dart | 23 -- lib/utils/bili_utils.dart | 13 +- lib/utils/cache_manager.dart | 26 +- lib/utils/id_utils.dart | 19 +- lib/utils/request_utils.dart | 168 +++++----- lib/utils/storage_key.dart | 4 +- lib/utils/storage_pref.dart | 40 ++- lib/utils/video_utils.dart | 25 ++ pubspec.lock | 10 +- pubspec.yaml | 8 +- 96 files changed, 2268 insertions(+), 2143 deletions(-) delete mode 100644 assets/images/big-vip.png create mode 100644 assets/images/big-vip.svg delete mode 100644 assets/images/lv/lv0.png delete mode 100644 assets/images/lv/lv1.png delete mode 100644 assets/images/lv/lv2.png delete mode 100644 assets/images/lv/lv3.png delete mode 100644 assets/images/lv/lv4.png delete mode 100644 assets/images/lv/lv5.png delete mode 100644 assets/images/lv/lv6.png delete mode 100644 assets/images/lv/lv6_s.png delete mode 100644 assets/images/play.png create mode 100644 lib/common/widgets/dialog/simple_dialog_option.dart create mode 100644 lib/common/widgets/svg/level_icon.dart create mode 100644 lib/common/widgets/svg/play_icon.dart create mode 100644 lib/pages/setting/common_setting.dart delete mode 100644 lib/pages/setting/extra_setting.dart delete mode 100644 lib/pages/setting/play_setting.dart delete mode 100644 lib/pages/setting/privacy_setting.dart delete mode 100644 lib/pages/setting/recommend_setting.dart delete mode 100644 lib/pages/setting/style_setting.dart delete mode 100644 lib/pages/setting/video_setting.dart diff --git a/assets/images/big-vip.png b/assets/images/big-vip.png deleted file mode 100644 index bb009154668fb7ca30e2f7a9531f621cfb22f264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2622 zcmV-E3c>Y>P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H13Cl@D zK~#90&6|60R96|mf4e5x&6|V}2+&G{2@oQM@Rmw=6oK++skGKIf;1LqECY#Z zopF@ffp#3NwzjkmDne1FU@aDTlt6Y(k z^FVDATx*4<8_>}OzMImP5D&>N$V`F317KJNq$Y+lp2NT@;8Hj@L1a_l31kkYno4OJ zoUesXE<u0KCL z4IZ7QH*5;;f#R{z_1)o>F?y5>{|w-p{V(=cz(C;Vx?45it^M{ac= z|MfQ6nbiD(ehypzk!(Pe5X`YED6fuwZ~%$%#%<=tjdhNH3Z)lSErWn{ffoW5kOn-X zyT#*!ogdjVFo0Vo7HUGAeb3o`RHm(-bp-e^feQFGDu4dngu4o&-+#6);bCpF}X9RGUA1&RAGD!Txz04h~>u0W6thE~nnY#{pSR zBx#}lek*IuJ1y4h?5-87tWjNc05hFP;v$$f4_plq!yAxn-hL->$D73jpsv|y zHyG;#M(HlsHih=gYMR0rVT@_*lr7r`4X9I&H{Xf4zQJ5;4gJtvWzo5>cJlYK&@xw* z@09NZvUHalEjT^rYGCtj_^biCEd^lhrK=k*U4>Wo!eI->n%a>Y6o)G4;jw6toU4Hs z-mxZIG6$x7!MOdW+haSex|DuFsuM`j3)^gwWv9cWk;d)S^|4=5yvlb39phZHxgoM|qz_f%hib@VV;Ggo4Je$M?D-rNr@QM%-6C#>BGH0Ul5G|zil!s&q3 zv*j;oM4JZ=ob4T8Je151etCc~!{H~(VO*5qGW9!McLEo5mj|au_b`xoUwDtX%#LGy zi&O4kNqMPAal?1!!;*=Sk7tN7Qe5Rk3X*~umKk|f4~YBu;5c)c3w2N$p!-nqKnJjR zBCL48d_gt3H%D2#Qt3phv4TlWgdq`ByEuSlQ$l-ZULR~Z5Nt_aylNgV^fR*nRu+f% z2g=EWRApMd!ikik>5W>w|7?yc9!loIg7Kkc|9D8k*MYQj!aJqb%{wtU;!ukX5&4M2-gUKewj-P*5z{duaEdM$Uzxk)Noa5|5_)3Ur=12W5 zr+@~(A+%T5JZdOp=#9i6TygN&H2C2oMvF`^<<+q5h`G$k|G>|-TigGaO_4;48JQ`N zm!oRg3w=RRedg5RfEoG59VaEg^N+}4XC*3BSqrc4vzGg7>4eRD;KLA3j?)QG&o$p_ zXN)ommi!6&6i|j#WCk;*(AX+mG!ceps@wW=>WZY(Z-$Xk?&*;f*X|SMa%qY3C8%)4 z!7OFIu>vTE;Jm;VomjFPW*QWI77RBZn4cq6@Z0ypd-*_il}XW+8Af9?F^Ieo==Ky}TqahD`k>_F3C%R%EX{c%N&(&N)0QE6OlBB0K=|LV0FxURcpY918j z248#sq&O;pboWRu@r4~wt?wo(I=t}O2U3m~$faxgkl3V=Fj*OVsYhyzZ=b7mAd$B$%8l&drqu_~>Hr&XS!DKVD|!{jCDt z3cR3FfCI4(XXtLPYmraeECPEsNpZvTOQqge&p}`o7D{9^L{s`OYR&*yyFj*=|2JH5 zGB)+gw;<&)%7Q2rmw<06TXM7EyGx9mi@QUz3)U}^$}=@3z-3i~nF+TYDLmFQVh}vL zR0^K%HreU${fA-XU?YiN0*0Y$E2Xk;AX7q**C(Exk#iGkCX9fUv&>=%TY+y7MX4-E zvGh*hAw!AM3$VQu+H5SPF(W-moG|rP=jUDEQNT3DA5OXa8>k8?{CIq@r(9xucIOrR zxZ|ak@S*YMl_u~uGVenUB%A{i4qzkjtf^Fo7xq<1Wk!=tHbG`e-+_r_lPs{T@*9C4 zU@>zU$&s1mh%Id%In^|&%qXvx{Cr)r^<=6831f{OD)q#9L#!N`X+a9Tc3OOjJSxIN zQzMac1P@rgqum>DXsR7Le0@{J6%WY?keLcY(qU+(m7_6%97Ohc \ No newline at end of file diff --git a/assets/images/lv/lv0.png b/assets/images/lv/lv0.png deleted file mode 100644 index f9ed49b620d04f29610aa42834b6946d2eafb3b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 915 zcmV;E18n?>P)Lda*%`NsvxmkA-?$%4ewVr{|tby3&#_k@sduh7}daLzaUF-j?;lsXBt zh~qdpe_Rv<;RomZUD7096HU|5Xfz0=R8DfgX_{!YTAmmI@J|dL04kLVD5Z(-FO^CV zMR715d+Td}2T`1V%_f9Ado=wGDwPUWS62Z5mSw>(jF~!+Bng|Fo5^7Gc#mh|$wR4T!;ELfHmTd-QKVq@d)+NCH84i68fwkL#OcXt;nD=Pp1 z+qQ9ZbOhIRWA%4+bp_kD194fFQLEKJNccnUF@O-l0u5$-?EG^y>i&E_|7Y+3psFeg zg#vDGZzKD9czD41`T5lAs;XjreLYko^EE&agjlgjI8OL(80RU<+<>fE8jy7YS+g`C z>jbi9X+YKqWX;lmMRy>IV(1Aj?n}?2NZkN`jZu;$G#ZVGCMb%6dc7WqN4?>tN$Lie zrU}NpM~dHt437z#reSYy5BYrF)AsW668HD_X{R-n)D0L6252^$p(d2eW$f(iAeZx2 zs0_ov#l=PJzM_gFaf&CAM?Fo`LWOMG#^d9oC)V%x(QG!cy}j)@j%u2Q{r!C?iW1Oi zx7#>7JBv(soI;_1TCFy*-XO~|*4CoSoRJ1ptJTo!uIu99;2r zFN(RR0HqYNJh?{sVuF78x9z7xm!dLvCCYS)e`~q&S9+Jos3s?XE002ovPDHLkV1nAwr^WyP diff --git a/assets/images/lv/lv1.png b/assets/images/lv/lv1.png deleted file mode 100644 index ad8b70ce0ad220d08090a6aef98cd19513865534..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 876 zcmV-y1C#uTP)?f?J)8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10{KZq zK~z|U?U_$*+CUV>zZs?`Muf^rB}haPK@_r!L@v;KRJ}&GRj<$^^d$MSV}V#yjQ~rj zvM4b^q{NIA3Ghr8QE1E$=eO!T{_iq%ToUPEKH&rV{V!y!253yt?2eC8QrZnav?J?*1r zvx#oEt5wW2O_a;!>1WHb(C_!>8cL_rs8*}fz7;~CR;yt&8ikJIc^)|D(}%V!v*tM= zB=HeDB$SmNXfgoxk0npj8jr5%9i%I*tQJ-*xA|f z#VeHx2q6$c1R8f72kmw{ZaJYunm0{zx<8*qN(t9>mFaHVHV7fg6h1sW46Nfg4qC02 zu4Vua=Kzc`W!Fh5g=JZgQerq9!t=bjH6qRD^ZC#%qwBh8G#X0$@mq}Q;bn~P?d_pd zDg|CM3E(lL;8pUk3++L1a#=)xvl@#@5!ue-O8~x462xT2Sg=CX65+?8Xywnp@>GK#%O$PNPNab7sj0%KY$CiM)xLu0bP=4 zQWK5OM1w|R41p+$i3>G6N`z8CD=pA=dhc~XK!)jKT4quj{Ox+?%sJ=&+jDO3C7g3? zR6?PUIMzPb4bCqBkPQwWC!Alm7#!44!fmlmaB|tC@NQ1X4Z)<~hQLL##pD#ka(n3R zAtvA^II2xfRtkQz6Id8QH#nNjPGkfgs~e{qPQWcWi+-1JhVOHu=#31h>=vLh77qX& zzE(6igC)OzaLXRNkA6&K7F<`DxV-HgJXSXV;M<=OJP!9{^&Oo33WvoGm(CgBvAPhn=if+F z0?0&~lSQj-wJjifbtPNIzs5gc^X2t#bk(mc9st@rO|XkH7HLZ3n`R2$O!n!nW$DYv zFF@I603q7$$YwK(vI|gd{tHm<0?N&XJ`AlTAbWAXu@g>AgkO!BwJwl~nO5y3BeaH;7mT#`d& z=bR%=jX(9}!f*k;#(v;!q+es-8)(PL`ePs<@W~!rYC5A?Jmm~e#$O|voHe?voR~5L z2BwD~5&=hR52)gx$3?7GjFdQE@qokY^6rjV`iVk0E&gWB$Gq^we zOmkV$7ySf);AriBm3^nTMH53|3LcKWM0kEuXW2_gf`qQ!S9SaUm>a{Ru~!`UH+k3H z0=4#P1nt=`l>4HCcsKPS@B1m5MrhUW$1zs^){tyfee_9aRz}Dr^~#ukoWo0Ob}J=kWdy78hMV{R7`)KO#VED zP*?^T##ARMa%t8>(Rlm)pYC8m?Vn&V2Mew8Vv5nz)M697z?yNWUr6##M#n^S}daMJY`Fn%I( TxZEDJ00000NkvXXu0mjfCQjCw diff --git a/assets/images/lv/lv3.png b/assets/images/lv/lv3.png deleted file mode 100644 index 80a0db3fad5977e2f421f7b4b7a68cf786a55f66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 991 zcmV<510ei~P)41$G6fkp^g`jFbRdDMyRdl*U~PMpMP?KV+ADa-Nkx#xU7=eySqF*8~f z7oI}dJ1{rQ;Ijb2K8Mc|<2JQ20XN^xpCZU-0WL8s0u1g0>}0?Tkb3Lg%rSuO0Mrxk zVoigX5+J8d6UZxqipz;tv^XvSwcMrxVi>@{va<{l6~*t4tGF}}iwZ(?IpHQ3Y~1^t zmCX3p`2ww4ybJncDg-t5tA$M+3PMD=a$AdQKp=3azFK*;OB0-mDiQz;n`w0363y-t z{V`QWL&TDSbk-7v%`13r|1o7qKf*ZR6a*sU+P+R*S4dg1y%<}->q+mLbEsRxx$TjE zo%JJU;r>XjrvC;_;Od?jHfjP4;A6(bqtU#p?}wQZG9`)5kT*Ve#8ezj);~Tt2+$Rp z*A>#sZOIM=%^;~e{d*hSKhUXSqqfKw0XWbVLPRALEcd?qk+VGKqInREHP-;Q9DSJ= zuj{YkS}lonPe9c!(CX;b70)7AsYh;TD+2a+g>Z3a)K`sYs}V>=6yF81tq2GLZAHMZ zf(^gFUbK}4e9D-(`!S2SM-ki=58?QxCQgDufd2k!J-KFQdfKotZFuwDpfzrA-$$Ub zP+cLM-rC_>Xpb&%VvAm7_p*KY1>XsnTCl5ZN0Q+x5_)ujE4yRp3^`Mf3>59v`%dXQ z0q67{&)MFGdo-wG z@eQ!DcvCVSrwjNpXV#6~A5#(YD1xbo4=2Hc2@MAmTFvX@1se}Wawth>+rFAuRH{l6 z23=y6BxCq{9;4a%ueyT?4d?qKjZ$t3D;|Nqh=Nqqo#|H-240TWmu$mk41H}}JOCth z|Bo%cj6l7#YIOnt-j|=Pbw- zY!D^Muo)fYHoEzC))6wHi*&ChetHc$Q{v52hP}s&Z&DXu6Jar7O47#i*1n?TxYu=- zu-rdJR6#|-Eayy4m6r$8zia>i N002ovPDHLkV1g`kwR8Xg diff --git a/assets/images/lv/lv4.png b/assets/images/lv/lv4.png deleted file mode 100644 index 5441967b204259d9699d593bd3c7165ba28e76f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 912 zcmV;B18@9^P)$ zzd%TPWM*s@!^naN3z>C5@kKPu45l>)fIXi=)s^*@;#>wgso_@e7tI|=&(S1eBmo#7 zLSNo2te5zqB_wN zP;ygKW)jYR&tCzci3URU9>nXig9R&&>^SJJTc1X@?f1%;Z>J+f=mhAmQLnJA$4f$e z8tFamO=nfG=fe)GAi~dp@gel>J+Hj22ibWjUlR3A{*!ld=N9ye{~0{-K9Z0Mp)>)l-AHS9;qmEj=##6ETX4&V2T|~n(Awp- zky}7t+<>1Wr6Ud*m6t%nru^BPABG3fcMpm`Z)eeG167$kgjE8?UUh{s0bqU(eSST< znh)^T%bfu^JApdoJIc-$ANR|I>;o0jmp>1~6Xj mD5{7tL*NvvlY}qgaQZhB^5AsYu5vm60000bZ_c9w#f%qi=v4Fkj2;4`DS%A@<9nTT-40v&_lqq4vi%Wq(vs?;*E_$J(ScdIh_=p0_=|wgn?hf|Nr`lZSxQ)g=L8#hnpe_6K}oQ(qLq|gfJn@!?>il* zy7W|^l};al@#6{4F{0fsBDwv*RA*UNe?#ujYqQ#mObgN-Ot4w@}It0wRj+$;TX&jaCuQ_oH+^rj!pmo&W$4X*;|mqmjX${69~5J z$kqXDmgA6?KZnfV`~SL{%+v{L><)5AcK9mZy7DWaq`&+sAa7p=mrJQijz-^Zw_V(# z>CtOiOLo|+Q+4-Ni~MJ3gBM$^IeSGktsG&?h|24Kw6T$|_Znvk^ZR3D8I-JS*D;3x vHHI+(BKQ9NAA^890t&loQUu1|4BGo2Z<#nq*B25500000NkvXXu0mjfgA(nr diff --git a/assets/images/lv/lv6.png b/assets/images/lv/lv6.png deleted file mode 100644 index 399e585a70e9ba064cd0f9952a24fb120931e4a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1092 zcmV-K1iSl*P)_2k}W`Fo?kz9^l19Atok-2NNi= z{XwZnOf>Ot;MMp_`~yIv38=KTIt>d^DCkIR zbIyGC{+hcar9{(m?3lK&ymS@7Q2?o!qZcG7L(MF};^9MYNup~BrkH0529JZM5{^&$ z;*qxxNQqGZ-3ceF9b#QT*$8b*xuhGVdcw(KaVvz-gEn0PR{=a%cae#rzP!GF5ccq> z4NbG_iN>>8ES>&DK+69{K7(x5mk*=Av=`8{y8COpcLQyyrJ$|Z*MQg|Ny@O&%N@)& z-sJlJK@I@iaaifQZ*|nLQ;=oJw&N#!>Pky^9#+4!&h%^8&4!#cV{0aZpUn1u)-#J3&yGM;j&)VLXQTXtK z${jQf9bbP#ES+8(+fu?EItzdDcJ*YO!@F@kT47SI`FdV^(5#LC!zh7BF_C3OOYq3Q zUG=ZlpDY_JoemfXY*#N3K>+EkqIl(UP4$)2a%(_2{|nIQ0vb(d7uZxV4f?);Xug7j z&(1=+u43NQ0+4CK9-oNpFCNaJaOxugAp$4C7J~Kc1evLVVrx_}%|goyu&LJoxw?wt z_*lhueP95o_uk(yM)CrfCa7~8GfP*$;)s6=aIQR(X~Lek z4C>q#a@F>{0Q=&la17szy{t;aE`ayr523wQ>+1`Npue&Y_QdC)En5{|k;}pR`KP$c zsz~et@IPkYUKoO0Qtlm~_4T3Siz}e6E*P&KfIU14YEwSUARf-aJ%0`|zy5Sdf~sqP z*5424mEHKAI}m^WrP>?^?&vV=v2l?7`1XoDy9*s(eg)Ru4KP)Ee)lfi%z4Q9`N+i+ z(_p-IuyXBtp066CssPniQFtB-AD@KCn zsLj816UC8CXwgg(Nm9A-Q3Q~sk`3RlJu2H+M?U>K+>s1q;qfutyKx=v_!#8MO5`;D z?cYM%Y%TC?7Fa2D1Z|W*d`P+RWIa2xthe`GU_w(3@k&rECMecoNP&WI zffxd%7(xLz6`^P?Y}b~yyPcigotfjsBHigucV}j|bQk;F zo$q|#Id9&XIcMe!VT>Uc2-Va)trD#jK*eNhv4Niu)<7p)#eZmE+mN3|0)US((wsA9 zGQx@`TSJD-$r&?2Sng=C2ju}^pt^c4N2za8r4ZuPwK)!4X(>Rhd8`Hg0UH|3SPq>r z6J*vbsK5Pcx`qtZtlY?eBLGOo3bKf6Qv;mKS6HrNl7!%f^SED-8<>?Zry@<>!z(aXvu7dAAael|4_5d?@E>Ocz%0RZatUm%}8Wjtv`1%x+N0UU*LwG)xH zGik?^9`! zVH$_STnqsCgg7ApZ(*JZfL~ex;muV^an0XnZJcZg%2&wr>2Pn}62GWpV6j+yS05W6 zk-&n(N%8-;nS{XIum!|B&9K{5_px!Esv@*$Bh=fsb>X}*Q(XT6#M*Uf^Oz(-YT66+ z#_wr!g;j5XU$WG)78Z$sg+fX3yfKpq0w4(K^9l;!c;UsgW^!d^5Z|k{r6qj)7_|QW zqw{nK@eBHUZI+1mJ`m)a5g@seTRqiJRu#JYOWIo3b?v^%|!j~^SiKW`rM z$k{e1omUL|gw<=nKeNcxICRELIA2?4U4WuMI?xOj4v(8Mp=J`JHI5Jv=Q!`5DbRX) zpj_)R>~pTHHb2flKSp-%0ylqtvNS<}@b((RI=S^^+I4tB&2;bBiILBCLHoPs{%kzl z4VytrW>4g_X;H3rLb=+R6z57y4b4=pbs=)*o3!I*Z_PBg{i&}Q1 zd*FDbQlF$KKs1&$8JR$elF!H4&=90U2LTg59hM;`PF_Z$DTd z06K4;zDQLO{_-f8JpMZLFrmINW-^BGkruFl0aNuU&7_4zaBtr+Zaw z(oEvC0$R|%&tIt@0!C18bdZ gA&&rmQjKl?7lU`QDV}MAX#fBK07*qoM6N<$f{H+!)c^nh diff --git a/assets/images/play.png b/assets/images/play.png deleted file mode 100644 index b5d0299c5861828310407147445d868a3cb1efd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2367 zcmV-F3BdM=P)3}vK~!ko?V5XRQ`a5GzvrCmR}v?w^AM#l1_Q0@LMR(hF;-}4M^&vw&D2e; z)OMTHLeZewG}xwTlP0lsLtFRmV_H#LQ2Ga}V4`fS57hL*x)!aSC{VfqfrJC!G{;+-T?>ctyNvt@Nb}a{-+Y8xK~#AnMnb%5_ii9rz`%H zR6%6Qc&zADvLeq+2{8N4zT7FKGnK5sGm`{#r{d0(aHg?~PM3d%7XV5CwhCf-fG!?) zCIbLFHnhq>R}H}K#>4IIKjW}Y4A;&F%_ zjj12n2h6>rTX(fUkd>&&9%L>Y#F4hoKE^`XKMXzJ^fWBkxmTeCvv>mhcP(mW0c;eHGTh^ z&>Xz{OE^EpJ++_z9`no**P)Rw1M-uV2k3o(ktO8)tm}2}&GoB~-U1E6dAW!J0^&Ct z{j)tzMD9N1NIU;;p+F`C^vccqVqoiE>pSax;JS^Q8lgdWFms;{wzuByz59(fx1Azi zf|0T5qL%<<9+R9OhRO!|aL1le&oYa~?Hi!w5IwJ@!L^Tg1rY69{|e$c6>`2t0c3$3 zMG@IiIM0HC!-@)#xT%eiA!_E|aH8|_xG%*I-`jj6#nV%FJ@@ZE48wGkDX&r#t3Zyd zh&kb|TI8M8=7y#9%?(X;tR9h}t5+^>YNk`0F=LQGbjJB{4%pbZws+G{PRbHZPTB9=ttK4ActtXd(o@A0-{mt+g`J>ItfzZ<@z1497I4u zaiesiWG>otjnDDa#FIECbK5$tzM6pk)`K^C z0tv?zWq&1FZmxEsK6kYzT681`Fz{A?-J)s(*0w{?;EU6oWBWDCS8YIFV!FQcRUoC34krlWaAMf0a#7rOaQzpLbX_lI8dGZ|J7HR#%0ThhFOQ=SaoCX1(9qkw?r%38`PHpUu4imY zh9(3K4y#a$0f?Z=Ko_I9fDxpThK?jI;y;LXMVIb*=*gb$DFTv&gf1~Ozfv5WKoSNl zn6O|XgCx%56#93bp7-=Go=*->7Epvrrk%ck%0TH67`Xu5f}MjLK@Ykyu_*_9-iN;vgW>42KhF^F!Q?H^ck;K8T*qmu$;B`OP=k|$0k9XuD&h6MvD z4B#{_>^!sJsTW_h3^hP4SaONgBxt8ef|xIj=OJ|@5yhF*!1_aXKcS{H+*h(T!?+S) z#!at1#qn&)V?xIey3zON-UZLR`eym#GFEblm5y~_8bwY#D5M1wW4MTJYv>CveQ>Po zbe^fzs7nKyg(G{a_t|}EA%i%2aH;d1vL!e{bxY~8OB)e-SR-K}je+j-QwLPMg8T|| zxJ3a8b6xpf%0sA4eh>OYiGgXQC;ygr-ZD8cc%`6Z`Gr>GB#w->A>-|?-`wMyoz#1c!>P;th=T2Eb6(gHvp@3zp+duM-H zEmt(s)NpwuXeou8!8%eGv&YpKImF@Xmp$U2qg*X4DM1*9A|QA0@_AUkbS!?_v}r@b zq-u<2#wlPDKncLM(n?xf2L=HN3GOT~2M2HW@4s`-1EO|vdTZ&_o@xc`D#=ZK?&LDB z{W-<>VCA(BX>%rTUo$#&pBnvO=qdx%&jy2*1Cv?#y|{4g#+oHlOPW$iswA9x()6fO znJ4nrD(6&0gZ5}^Z0jArpDG?O8mA!$-Gdk#RU7JaTlU2|j)Ftp8hrLkv+h<)x=Lo- zae54)QMW#pR2zzg?)q{3Sn{+#+_0i@h0j=w9&~#o7*D9X%5ZzQy|T5Lq2b*56CksD zQ0Dxw9god~;?ac|O(QsogK_TiOaSbA0|7H*e4uEvyQk{NtZ~_D@_!#yRUmwr7E6Ux3{? zxH>R@-gHY*+RyCW@HWn46eiVTK~kM8rU|wy+C02?fo~cQb>c%m>D>B0`VfOjbXdB8 z?%PHhI5_gcd!I&Y#2=Uj%QyVjt}pKWOCS0$fQ+;MiZ)f`+X4cJU=~6F-{yHMX3uM> zuMO7*Yl5Lb(C42zkY(s&V|qH3OvMwug9o~IM@<7sTtp1%T;~T0n<~7B3>-cL(S!&B zxelRhScJh}Eks6`}6E+MO z(2;_Ud^1In)=@UDctKkP;PeOPWs8D#JhyGMurVy(JSv@*9ojD8O3;P{5W`Z?^vFt< zeNfPm#ym+vvGzn(L~?XW=_#+saV5K=9oB5Z+SEj!%PTp_lAq-jK3`$*a`u;+DVJsK lQ^iTx6rsvRmH6Mi{tE~Vb{ph+t&IQx002ovPDHLkV1mbAf}8*V diff --git a/lib/common/assets.dart b/lib/common/assets.dart index 87589b600..ed429dc80 100644 --- a/lib/common/assets.dart +++ b/lib/common/assets.dart @@ -6,11 +6,10 @@ abstract final class Assets { static const logoIco = 'assets/images/logo/ico/app_icon.ico'; static const logoLarge = 'assets/images/logo/desktop/logo_large.png'; - static const vipIcon = 'assets/images/big-vip.png'; + static const vipIcon = 'assets/images/big-vip.svg'; static const avatarPlaceHolder = 'assets/images/noface.jpeg'; static const loading = 'assets/images/loading.png'; static const buffering = 'assets/images/loading.webp'; - static const play = 'assets/images/play.png'; static const topicHeader = 'assets/images/topic-header-bg.png'; static const trendingBanner = 'assets/images/trending_banner.png'; static const ai = 'assets/images/ai.png'; diff --git a/lib/common/widgets/avatars.dart b/lib/common/widgets/avatars.dart index c49db91af..4fc165d18 100644 --- a/lib/common/widgets/avatars.dart +++ b/lib/common/widgets/avatars.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/model_owner.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; Widget avatars({ @@ -28,19 +29,19 @@ Widget avatars({ width: offset * users.length + gap, child: Stack( clipBehavior: .none, - children: users.indexed - .map( - (e) => Positioned( + children: users + .mapIndexed( + (i, e) => Positioned( top: 0, bottom: 0, width: size, - left: e.$1 * offset, + left: i * offset, child: DecoratedBox( decoration: decoration, child: Padding( padding: const .all(padding), child: NetworkImgLayer( - src: e.$2.face, + src: e.face, width: imgSize, height: imgSize, type: .avatar, diff --git a/lib/common/widgets/dialog/dialog.dart b/lib/common/widgets/dialog/dialog.dart index 9d9ff7b22..db4b717f5 100644 --- a/lib/common/widgets/dialog/dialog.dart +++ b/lib/common/widgets/dialog/dialog.dart @@ -64,42 +64,39 @@ void showPgcFollowDialog({ showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...const [ - (followStatus: 3, title: '看过'), - (followStatus: 2, title: '在看'), - (followStatus: 1, title: '想看'), - ].map( - (item) => statusItem( - enabled: followStatus != item.followStatus, - text: item.title, - onTap: () { - Get.back(); - onUpdateStatus(item.followStatus); - }, - ), - ), - ListTile( - dense: true, - title: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - '取消$type', - style: const TextStyle(fontSize: 14), - ), - ), + children: [ + ...const [ + (followStatus: 3, title: '看过'), + (followStatus: 2, title: '在看'), + (followStatus: 1, title: '想看'), + ].map( + (item) => statusItem( + enabled: followStatus != item.followStatus, + text: item.title, onTap: () { Get.back(); - onUpdateStatus(-1); + onUpdateStatus(item.followStatus); }, ), - ], - ), + ), + ListTile( + dense: true, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + '取消$type', + style: const TextStyle(fontSize: 14), + ), + ), + onTap: () { + Get.back(); + onUpdateStatus(-1); + }, + ), + ], ), ); } diff --git a/lib/common/widgets/dialog/export_import.dart b/lib/common/widgets/dialog/export_import.dart index 26deeb8a0..4b8076686 100644 --- a/lib/common/widgets/dialog/export_import.dart +++ b/lib/common/widgets/dialog/export_import.dart @@ -2,6 +2,7 @@ import 'dart:async' show FutureOr; import 'dart:convert' show utf8, jsonDecode; import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/storage_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -214,21 +215,19 @@ Future showImportExportDialog( builder: (context) { const style = TextStyle(fontSize: 15); return SimpleDialog( - clipBehavior: Clip.hardEdge, + clipBehavior: .hardEdge, title: Text('导入/导出$title'), children: [ - ListTile( - dense: true, - title: const Text('导出至剪贴板', style: style), - onTap: () { + DialogOption( + child: const Text('导出至剪贴板', style: style), + onPressed: () { Get.back(); exportToClipBoard(onExport: onExport); }, ), - ListTile( - dense: true, - title: const Text('导出文件至本地', style: style), - onTap: () { + DialogOption( + child: const Text('导出文件至本地', style: style), + onPressed: () { Get.back(); exportToLocalFile(onExport: onExport, localFileName: localFileName); }, @@ -237,18 +236,16 @@ Future showImportExportDialog( height: 1, color: ColorScheme.of(context).outline.withValues(alpha: 0.1), ), - ListTile( - dense: true, - title: const Text('输入', style: style), - onTap: () { + DialogOption( + child: const Text('输入', style: style), + onPressed: () { Get.back(); importFromInput(context, title: title, onImport: onImport); }, ), - ListTile( - dense: true, - title: const Text('从剪贴板导入', style: style), - onTap: () { + DialogOption( + child: const Text('从剪贴板导入', style: style), + onPressed: () { Get.back(); importFromClipBoard( context, @@ -258,10 +255,9 @@ Future showImportExportDialog( ); }, ), - ListTile( - dense: true, - title: const Text('从本地文件导入', style: style), - onTap: () { + DialogOption( + child: const Text('从本地文件导入', style: style), + onPressed: () { Get.back(); importFromLocalFile(onImport: onImport); }, diff --git a/lib/common/widgets/dialog/simple_dialog_option.dart b/lib/common/widgets/dialog/simple_dialog_option.dart new file mode 100644 index 000000000..26a8d907e --- /dev/null +++ b/lib/common/widgets/dialog/simple_dialog_option.dart @@ -0,0 +1,29 @@ +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:flutter/material.dart'; + +final EdgeInsets _padding = PlatformUtils.isMobile + ? const .symmetric(horizontal: 16, vertical: 14) + : const .symmetric(horizontal: 16, vertical: 10); + +class DialogOption extends StatelessWidget { + const DialogOption({ + super.key, + this.onPressed, + this.child, + }); + + final VoidCallback? onPressed; + + final Widget? child; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: Padding( + padding: _padding, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/image_viewer/gallery_viewer.dart b/lib/common/widgets/image_viewer/gallery_viewer.dart index 30ba4be0a..77d77f5f6 100644 --- a/lib/common/widgets/image_viewer/gallery_viewer.dart +++ b/lib/common/widgets/image_viewer/gallery_viewer.dart @@ -18,6 +18,7 @@ import 'dart:io' show File, Platform; import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/image.dart'; @@ -534,76 +535,67 @@ class _GalleryViewerState extends State HapticFeedback.mediumImpact(); showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (PlatformUtils.isMobile) - ListTile( - onTap: () { - Get.back(); - ImageUtils.onShareImg(item.url); - }, - dense: true, - title: const Text('分享', style: TextStyle(fontSize: 14)), - ), - ListTile( - onTap: () { + children: [ + if (PlatformUtils.isMobile) + DialogOption( + onPressed: () { Get.back(); - Utils.copyText(item.url); + ImageUtils.onShareImg(item.url); }, - dense: true, - title: const Text('复制链接', style: TextStyle(fontSize: 14)), + child: const Text('分享', style: TextStyle(fontSize: 14)), ), - ListTile( - onTap: () { + DialogOption( + onPressed: () { + Get.back(); + Utils.copyText(item.url); + }, + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + ImageUtils.downloadImg([item.url]); + }, + child: const Text('保存图片', style: TextStyle(fontSize: 14)), + ), + if (PlatformUtils.isDesktop) + DialogOption( + onPressed: () { Get.back(); - ImageUtils.downloadImg([item.url]); + PageUtils.launchURL(item.url); }, - dense: true, - title: const Text('保存图片', style: TextStyle(fontSize: 14)), + child: const Text('网页打开', style: TextStyle(fontSize: 14)), + ) + else if (widget.sources.length > 1) + DialogOption( + onPressed: () { + Get.back(); + ImageUtils.downloadImg( + widget.sources.map((item) => item.url).toList(), + ); + }, + child: const Text('保存全部图片', style: TextStyle(fontSize: 14)), ), - if (PlatformUtils.isDesktop) - ListTile( - onTap: () { - Get.back(); - PageUtils.launchURL(item.url); - }, - dense: true, - title: const Text('网页打开', style: TextStyle(fontSize: 14)), - ) - else if (widget.sources.length > 1) - ListTile( - onTap: () { - Get.back(); - ImageUtils.downloadImg( - widget.sources.map((item) => item.url).toList(), - ); - }, - dense: true, - title: const Text('保存全部图片', style: TextStyle(fontSize: 14)), + if (item.sourceType == SourceType.livePhoto) + DialogOption( + onPressed: () { + Get.back(); + ImageUtils.downloadLivePhoto( + url: item.url, + liveUrl: item.liveUrl!, + width: item.width!, + height: item.height!, + ); + }, + child: Text( + '保存${Platform.isIOS ? ' Live Photo' : '视频'}', + style: const TextStyle(fontSize: 14), ), - if (item.sourceType == SourceType.livePhoto) - ListTile( - onTap: () { - Get.back(); - ImageUtils.downloadLivePhoto( - url: item.url, - liveUrl: item.liveUrl!, - width: item.width!, - height: item.height!, - ); - }, - dense: true, - title: Text( - '保存${Platform.isIOS ? ' Live Photo' : '视频'}', - style: const TextStyle(fontSize: 14), - ), - ), - ], - ), + ), + ], ), ); } diff --git a/lib/common/widgets/pendant_avatar.dart b/lib/common/widgets/pendant_avatar.dart index 9589e381c..5c88faa4f 100644 --- a/lib/common/widgets/pendant_avatar.dart +++ b/lib/common/widgets/pendant_avatar.dart @@ -4,10 +4,10 @@ import 'package:PiliPlus/common/widgets/extra_hittest_stack.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/avatar_badge_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; class PendantAvatar extends StatelessWidget { const PendantAvatar( @@ -142,12 +142,11 @@ class PendantAvatar extends StatelessWidget { Widget _buildBadge(BuildContext context, ColorScheme colorScheme) { final child = switch (badgeType) { - .vip => Image.asset( + .vip => SvgPicture.asset( Assets.vipIcon, width: badgeSize, height: badgeSize, - cacheWidth: badgeSize.cacheSize(context), - semanticLabel: badgeType.desc, + semanticsLabel: badgeType.desc, ), _ => Icon( Icons.offline_bolt, diff --git a/lib/common/widgets/progress_bar/segment_progress_bar.dart b/lib/common/widgets/progress_bar/segment_progress_bar.dart index e2cbae375..d83d3cc05 100644 --- a/lib/common/widgets/progress_bar/segment_progress_bar.dart +++ b/lib/common/widgets/progress_bar/segment_progress_bar.dart @@ -207,13 +207,7 @@ class RenderViewPointProgressBar ), ), ) - ..pushStyle( - ui.TextStyle( - color: Colors.white, - fontSize: size, - height: 1, - ), - ) + ..pushStyle(.new(color: Colors.white, fontSize: size, height: 1)) ..addText(title); return builder.build() ..layout(const ui.ParagraphConstraints(width: double.infinity)); diff --git a/lib/common/widgets/self_sized_horizontal_list.dart b/lib/common/widgets/self_sized_horizontal_list.dart index dea354ea8..777267adb 100644 --- a/lib/common/widgets/self_sized_horizontal_list.dart +++ b/lib/common/widgets/self_sized_horizontal_list.dart @@ -28,6 +28,9 @@ class _SelfSizedHorizontalListState extends State { @override Widget build(BuildContext context) { if (_height == null) { + if (widget.itemCount == 0) { + return const SizedBox.shrink(); + } return OnlyLayoutWidget( onPerformLayout: (Size size) { if (!mounted) return; diff --git a/lib/common/widgets/svg/level_icon.dart b/lib/common/widgets/svg/level_icon.dart new file mode 100644 index 000000000..71a17746a --- /dev/null +++ b/lib/common/widgets/svg/level_icon.dart @@ -0,0 +1,293 @@ +// dart format width=120 +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +class UserLevel extends LeafRenderObjectWidget { + const UserLevel( + this.level, { + super.key, + this.height = 11, + this.flash = false, + }); + + final double height; + final int level; + final bool flash; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderLevel(height, level, flash); + } + + @override + void updateRenderObject( + BuildContext context, + RenderLevel renderObject, + ) { + renderObject + ..height = height + ..level = level + ..flash = flash; + } +} + +class RenderLevel extends RenderBox { + RenderLevel(this._height, this._level, this._flash); + + double _height; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + int _level; + set level(int value) { + if (_level == value) return; + _level = value; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + + bool _flash; + set flash(bool value) { + if (_flash == value) return; + _flash = value; + markNeedsLayout(); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return constraints.constrainSizeAndAttemptToPreserveAspectRatio( + Size( + (_flash ? LevelCanvas._extendR : LevelCanvas._totalR) * _height / LevelCanvas._totalB, + _height, + ), + ); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + void paint(PaintingContext context, Offset offset) { + final paint = Paint()..color = lookupBackgroundColor(_level); + LevelCanvas(context.canvas) + ..save() + ..translate(offset.dx, offset.dy) + ..scale(size.height / LevelCanvas._totalB) + ..drawLevelBack(paint, bolt: _flash) + ..drawLevelLv() + ..drawLEDigit(_level, paint..color = Colors.white) + ..restore(); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.label = '${_flash ? "硬核" : ""}$_level级'; + } + + static Color lookupBackgroundColor(int level) { + return switch (level) { + 0 || 1 => const Color(0xFFC0C0C0), + 2 => const Color(0xFF8BD29B), + 3 => const Color(0xFF7BCDEF), + 4 => const Color(0xFFFEBB8B), + 5 => const Color(0xFFEE672A), + _ => const Color(0xFFF04C49), + }; + } +} + +extension type LevelCanvas(Canvas _) implements Canvas { + // ========== 布局常量 ========== + static const _r = Radius.circular(20); + + static const double _left = 629; + static const double _right = 877; + static const double _colW = 68; // 竖段宽度 + static const double _lColR = _left + _colW; // 697 + static const double _rColL = _right - _colW; // 810 + + // 三条横线的边界 + static const double _rowH = 68; + static const double _rowSp = 146; + static const double _topY = 55; + static const double _topYB = _topY + _rowH; // 123 + static const double _midY = _topY + _rowSp; // 201 + static const double _midYB = _midY + _rowH; // 269 + static const double _botY = _midY + _rowSp; // 347 + static const double _botYB = _botY + _rowH; // 415 + + // 竖段拼接用的中心线 + static const double _midMid = (_midY + _midYB) / 2; // 235 + + static final _boltIcon = + (ParagraphBuilder( + ParagraphStyle( + fontSize: 460, + fontFamily: Icons.bolt_rounded.fontFamily, + height: 1, + fontWeight: FontWeight.w900, + textDirection: TextDirection.ltr, + ), + )..addText(.fromCharCode(Icons.bolt_rounded.codePoint))).build() + ..layout(const ParagraphConstraints(width: double.infinity)); + void drawBolt() => drawParagraph(_boltIcon, const Offset(840, 5)); + + void _draw1(Paint paint) { + drawRRect(const .fromLTRBXY(673, _botY, 833, _botYB, 20, 20), paint); + drawRRect(.fromLTRBAndCorners(673, _topY, 787, _topYB, topLeft: _r, bottomLeft: _r, topRight: _r), paint); + drawRect(const .fromLTRB(719, _topYB, 787, _botY), paint); + } + + void drawLEDigit(int digit, Paint paint) { + if (digit == 1) return _draw1(paint); + final bits = switch (digit) { + 0 => 0x7E, + 2 => 0x6D, + 3 => 0x79, + 4 => 0x33, + 5 => 0x5B, + 6 => 0x5F, + 7 => 0x70, + 8 => 0x7F, + 9 => 0x7B, + // _ => throw ArgumentError('Unsupported digit: $digit'), + _ => 0x4F, // `E` + }; + + _drawSegments( + bits & 0x40 != 0, + bits & 0x20 != 0, + bits & 0x10 != 0, + bits & 0x08 != 0, + bits & 0x04 != 0, + bits & 0x02 != 0, + bits & 0x01 != 0, + paint, + ); + } + + void _drawSegments(bool a, bool b, bool c, bool d, bool e, bool f, bool g, Paint paint) { + // 横段 + if (a) { + _drawRRect(_left, _topY, _right, _topYB, _r, _r, f ? .zero : _r, b ? .zero : _r, paint); + } + if (g) { + _drawRRect(_left, _midY, _right, _midYB, f ? .zero : _r, b ? .zero : _r, e ? .zero : _r, c ? .zero : _r, paint); + } + if (d) { + _drawRRect(_left, _botY, _right, _botYB, e ? .zero : _r, c ? .zero : _r, _r, _r, paint); + } + + // 竖段 + // 左上竖段 f + if (f) { + final top = (a ? _topYB : _topY) - 1; // 有上横则齐底,否则到顶 + final bottom = (g ? _midY : (e ? _midMid : _midYB)) + 1; + final rTop = a ? Radius.zero : _r; + final rBot = g || e ? Radius.zero : _r; + _drawRRect(_left, top, _lColR, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 右上竖段 b + if (b) { + final top = (a ? _topYB : _topY) - 1; + final bottom = (g ? _midY : (c ? _midMid : _midYB)) + 1; + final rTop = a ? Radius.zero : _r; + final rBot = g || c ? Radius.zero : _r; + _drawRRect(_rColL, top, _right, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 左下竖段 e + if (e) { + final top = (g ? _midYB : (f ? _midMid : _midY)) - 1; + final bottom = (d ? _botY : _botYB) + 1; + final rTop = g || f ? Radius.zero : _r; + final rBot = d ? Radius.zero : _r; + _drawRRect(_left, top, _lColR, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 右下竖段 c + if (c) { + final top = (g ? _midYB : (b ? _midMid : _midY)) - 1; + final bottom = (d ? _botY : _botYB) + 1; + final rTop = g || b ? Radius.zero : _r; + final rBot = d ? Radius.zero : _r; + _drawRRect(_rColL, top, _right, bottom, rTop, rTop, rBot, rBot, paint); + } + } + + /// 绘制圆角矩形,四角全零时退化为矩形 + void _drawRRect(double l, double t, double r, double b, Radius tl, Radius tr, Radius bl, Radius br, Paint paint) { + if (tl == .zero && tr == .zero && bl == .zero && br == .zero) { + drawRect(.fromLTRB(l, t, r, b), paint); + } else { + drawRRect(.fromLTRBAndCorners(l, t, r, b, topLeft: tl, topRight: tr, bottomLeft: bl, bottomRight: br), paint); + } + } + + static final _lvPicture = () { + final recorder = PictureRecorder(); + final paint = Paint()..color = Colors.white; + final canvas = Canvas(recorder); + + const double vLeft = 296; + const double lvTop = 106; + const double llr = 123; + const double vtb = 282; + + canvas + // L + ..drawRRect(.fromLTRBAndCorners(56, lvTop, llr, _botYB, topLeft: _r, topRight: _r, bottomLeft: _r), paint) + ..drawRRect(.fromLTRBAndCorners(llr - 1, _botY, 256, _botYB, topRight: _r, bottomRight: _r), paint) + // V + ..drawRRect(.fromLTRBAndCorners(vLeft, lvTop, 363, vtb + 1, topLeft: _r, topRight: _r), paint) + ..drawRRect(.fromLTRBAndCorners(476, lvTop, 543, vtb + 1, topLeft: _r, topRight: _r), paint) + ..drawPath( + Path() + ..moveTo(vLeft, vtb) + ..lineTo(vLeft, 292) + ..arcToPoint(const Offset(300, 313), radius: const .circular(50), clockwise: false) + ..lineTo(395, 408) + ..arcToPoint(const Offset(444, 408), radius: const .circular(50), clockwise: false) + ..lineTo(539, 313) + ..arcToPoint(const Offset(543, 292), radius: const .circular(50), clockwise: false) + ..lineTo(543, vtb) + ..lineTo(476, vtb) + ..lineTo(419.5, 340) + ..lineTo(363, vtb) + ..close(), + paint, + ); + return recorder.endRecording(); + }(); + + void drawLevelLv() => drawPicture(_lvPicture); + + static const double _totalR = 930; + static const double _extendR = 1250; + static const double _totalB = 466; + + void drawLevelBack(Paint paint, {bool bolt = false}) { + const radius = Radius.circular(27); + final double right = bolt ? _extendR : _totalR; + const double blockTop = 48; + drawRRect( + RRect.fromLTRBAndCorners(0, blockTop, right, _totalB, topLeft: radius, bottomLeft: radius, bottomRight: radius), + paint, + ); + drawRRect( + RRect.fromLTRBAndCorners(576, 0, right, blockTop + 1, topLeft: radius, topRight: radius), + paint, + ); + + if (bolt) drawBolt(); + } +} diff --git a/lib/common/widgets/svg/play_icon.dart b/lib/common/widgets/svg/play_icon.dart new file mode 100644 index 000000000..2b64a0958 --- /dev/null +++ b/lib/common/widgets/svg/play_icon.dart @@ -0,0 +1,218 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +class PlayIcon extends LeafRenderObjectWidget { + const PlayIcon({super.key, this.size = 60}); + + final double size; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderPlay(size); + } + + @override + void updateRenderObject(BuildContext context, RenderPlay renderObject) { + renderObject.imgSize = size; + } +} + +class RenderPlay extends RenderBox { + RenderPlay(this._imgSize); + + double _imgSize; + set imgSize(double value) { + if (_imgSize == value) return; + _imgSize = value; + markNeedsLayout(); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return constraints.constrainDimensions(_imgSize, _imgSize); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas; + final size = this.size.shortestSide; + if (offset != .zero || size != 60) { + canvas.save(); + if (offset != .zero) canvas.translate(offset.dx, offset.dy); + if (size != 60) { + canvas.scale(size / 60); + } + } + canvas.drawPicture(_picture); + if (offset != .zero || size != 60) { + canvas.restore(); + } + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.label = '播放'; + } + + /// [SvgPicture] can not parse mask filter + /// fom i0.hdslb.com/bfs/static/player/img/play.svg + /// scale size from 80 to 60 + static final _picture = () { + final rec = PictureRecorder(); + final canvas = Canvas(rec); + final path = Path() + ..moveTo(41.576, 7.318) + ..cubicTo(41.244, 5.892, 39.91, 4.886, 38.41, 5.011) + ..cubicTo(38.068, 5.039, 37.813, 5.13, 37.59, 5.245) + ..cubicTo(37.37, 5.361, 37.187, 5.506, 37.034, 5.672) + ..cubicTo(36.957, 5.754, 36.891, 5.844, 36.824, 5.934) + ..lineTo(36.622, 6.203) + ..lineTo(36.222, 6.743) + ..cubicTo(35.694, 7.467, 35.178, 8.2, 34.678, 8.945) + ..cubicTo(34.179, 9.69, 33.694, 10.445, 33.231, 11.217) + ..cubicTo(33.092, 11.449, 32.954, 11.683, 32.819, 11.917) + ..cubicTo(32.258, 11.909, 31.697, 11.902, 31.137, 11.898) + ..cubicTo(29.094, 11.884, 27.051, 11.891, 25.008, 11.926) + ..cubicTo(24.871, 11.688, 24.732, 11.452, 24.591, 11.217) + ..cubicTo(24.128, 10.445, 23.643, 9.69, 23.144, 8.945) + ..cubicTo(22.645, 8.2, 22.129, 7.467, 21.6, 6.743) + ..lineTo(21.2, 6.203) + ..lineTo(20.998, 5.934) + ..cubicTo(20.931, 5.844, 20.865, 5.754, 20.788, 5.672) + ..cubicTo(20.635, 5.506, 20.452, 5.361, 20.232, 5.245) + ..cubicTo(20.009, 5.13, 19.754, 5.039, 19.412, 5.011) + ..cubicTo(17.956, 4.888, 16.59, 5.85, 16.246, 7.318) + ..cubicTo(16.168, 7.652, 16.176, 7.924, 16.217, 8.172) + ..cubicTo(16.26, 8.418, 16.34, 8.636, 16.451, 8.833) + ..cubicTo(16.506, 8.931, 16.571, 9.023, 16.635, 9.114) + ..lineTo(16.829, 9.389) + ..lineTo(17.219, 9.936) + ..cubicTo(17.743, 10.663, 18.281, 11.381, 18.834, 12.086) + ..cubicTo(18.845, 12.099, 18.855, 12.112, 18.865, 12.124) + ..cubicTo(18.025, 12.164, 17.184, 12.209, 16.344, 12.26) + ..cubicTo(15.523, 12.311, 14.701, 12.365, 13.88, 12.428) + ..lineTo(12.648, 12.525) + ..lineTo(12.032, 12.577) + ..lineTo(11.68, 12.616) + ..cubicTo(11.562, 12.63, 11.445, 12.651, 11.328, 12.668) + ..cubicTo(10.39, 12.827, 9.477, 13.141, 8.641, 13.595) + ..cubicTo(7.804, 14.049, 7.043, 14.641, 6.399, 15.34) + ..cubicTo(5.754, 16.04, 5.224, 16.845, 4.837, 17.716) + ..cubicTo(4.45, 18.586, 4.208, 19.521, 4.12, 20.467) + ..cubicTo(3.808, 23.756, 3.603, 27.055, 3.529, 30.365) + ..cubicTo(3.453, 33.676, 3.53, 36.99, 3.722, 40.289) + ..cubicTo(3.77, 41.114, 3.825, 41.939, 3.887, 42.763) + ..lineTo(3.986, 43.998) + ..lineTo(4.039, 44.616) + ..lineTo(4.046, 44.693) + ..lineTo(4.056, 44.782) + ..lineTo(4.075, 44.961) + ..cubicTo(4.087, 45.08, 4.107, 45.198, 4.126, 45.317) + ..cubicTo(4.278, 46.264, 4.586, 47.189, 5.037, 48.037) + ..cubicTo(5.486, 48.887, 6.078, 49.66, 6.777, 50.319) + ..cubicTo(7.475, 50.978, 8.283, 51.522, 9.16, 51.921) + ..cubicTo(10.035, 52.319, 10.978, 52.575, 11.935, 52.664) + ..cubicTo(11.998, 52.672, 12.047, 52.675, 12.098, 52.68) + ..lineTo(12.252, 52.693) + ..lineTo(12.56, 52.72) + ..lineTo(13.176, 52.771) + ..lineTo(14.408, 52.868) + ..cubicTo(15.23, 52.927, 16.052, 52.985, 16.874, 53.033) + ..cubicTo(23.449, 53.424, 30.03, 53.502, 36.609, 53.259) + ..cubicTo(38.254, 53.199, 39.898, 53.118, 41.542, 53.016) + ..cubicTo(42.364, 52.963, 43.186, 52.908, 44.008, 52.843) + ..lineTo(45.241, 52.743) + ..lineTo(45.857, 52.689) + ..lineTo(46.214, 52.65) + ..cubicTo(46.334, 52.635, 46.452, 52.614, 46.571, 52.596) + ..cubicTo(47.52, 52.432, 48.443, 52.112, 49.288, 51.649) + ..cubicTo(50.134, 51.188, 50.902, 50.586, 51.553, 49.878) + ..cubicTo(52.204, 49.17, 52.739, 48.353, 53.127, 47.471) + ..cubicTo(53.321, 47.03, 53.479, 46.573, 53.598, 46.107) + ..cubicTo(53.631, 45.991, 53.656, 45.873, 53.681, 45.755) + ..lineTo(53.719, 45.579) + ..lineTo(53.749, 45.401) + ..cubicTo(53.77, 45.283, 53.79, 45.164, 53.803, 45.045) + ..cubicTo(53.818, 44.927, 53.834, 44.8, 53.843, 44.704) + ..cubicTo(54.179, 41.414, 54.402, 38.111, 54.476, 34.794) + ..cubicTo(54.553, 31.475, 54.442, 28.153, 54.205, 24.853) + ..cubicTo(54.145, 24.028, 54.078, 23.204, 54.002, 22.38) + ..lineTo(53.884, 21.145) + ..lineTo(53.82, 20.528) + ..lineTo(53.804, 20.374) + ..lineTo(53.794, 20.29) + ..lineTo(53.782, 20.201) + ..cubicTo(53.766, 20.083, 53.754, 19.964, 53.731, 19.846) + ..cubicTo(53.578, 18.901, 53.266, 17.979, 52.813, 17.136) + ..cubicTo(52.362, 16.291, 51.771, 15.522, 51.073, 14.869) + ..cubicTo(50.375, 14.215, 49.57, 13.677, 48.698, 13.284) + ..cubicTo(47.827, 12.89, 46.89, 12.64, 45.94, 12.552) + ..lineTo(45.854, 12.544) + ..lineTo(45.777, 12.537) + ..lineTo(45.623, 12.524) + ..lineTo(45.315, 12.499) + ..lineTo(44.698, 12.449) + ..lineTo(43.466, 12.357) + ..cubicTo(42.644, 12.3, 41.822, 12.247, 41.0, 12.202) + ..cubicTo(40.326, 12.164, 39.651, 12.131, 38.977, 12.1) + ..cubicTo(38.98, 12.096, 38.984, 12.091, 38.988, 12.086) + ..cubicTo(39.542, 11.381, 40.079, 10.663, 40.603, 9.936) + ..lineTo(40.994, 9.389) + ..lineTo(41.187, 9.114) + ..cubicTo(41.252, 9.023, 41.316, 8.931, 41.371, 8.833) + ..cubicTo(41.482, 8.636, 41.563, 8.418, 41.605, 8.172) + ..cubicTo(41.646, 7.924, 41.654, 7.652, 41.576, 7.318) + ..close() + ..moveTo(21.283, 26.038) + ..cubicTo(21.321, 25.666, 21.427, 25.305, 21.597, 24.973) + ..cubicTo(22.351, 23.498, 24.158, 22.913, 25.634, 23.667) + ..lineTo(26.683, 24.211) + ..cubicTo(28.428, 25.126, 30.148, 26.088, 31.842, 27.097) + ..cubicTo(34.726, 28.814, 34.726, 28.814, 37.376, 30.628) + ..cubicTo(37.694, 30.846, 37.967, 31.123, 38.18, 31.444) + ..cubicTo(39.096, 32.824, 38.72, 34.686, 37.34, 35.603) + ..lineTo(36.265, 36.309) + ..cubicTo(34.823, 37.245, 33.349, 38.161, 31.842, 39.058) + ..cubicTo(28.87, 40.828, 28.87, 40.828, 25.698, 42.513) + ..cubicTo(25.352, 42.697, 24.973, 42.811, 24.583, 42.849) + ..cubicTo(22.934, 43.01, 21.466, 41.805, 21.305, 40.156) + ..lineTo(21.221, 39.247) + ..cubicTo(21.04, 37.126, 20.949, 35.005, 20.949, 32.884) + ..cubicTo(20.949, 29.361, 20.949, 29.361, 21.283, 26.038) + ..close(); + + final paint = Paint() + ..color = Colors.black.withValues(alpha: 0.3 * 0.8) + ..maskFilter = const .blur(.normal, 1.0); + + // feOffset dy="2" + canvas + ..save() + ..translate(0, 2) + ..drawPath(path, paint) + ..restore(); + + // dy=0, blur=3.5 + paint + ..color = Colors.black.withValues(alpha: 0.2 * 0.8) + ..maskFilter = const .blur(.normal, 3.5); + + canvas.drawPath(path, paint); + + paint + ..color = Colors.white.withValues(alpha: 0.8) + ..maskFilter = null; + + canvas.drawPath(path, paint); + + return rec.endRecording(); + }(); +} diff --git a/lib/common/widgets/video_popup_menu.dart b/lib/common/widgets/video_popup_menu.dart index cde21a56f..165037467 100644 --- a/lib/common/widgets/video_popup_menu.dart +++ b/lib/common/widgets/video_popup_menu.dart @@ -147,134 +147,115 @@ class VideoPopupMenu extends StatelessWidget { showDialog( context: context, builder: (context) { - return AlertDialog( - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: .start, - children: [ - if (tp.dislikeReasons != null) ...[ - const Text('我不想看'), - const SizedBox(height: 5), - Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: tp.dislikeReasons!.map(( - item, - ) { - return actionButton(item, null); - }).toList(), - ), - ], - if (tp.feedbacks != null) ...[ - const SizedBox(height: 5), - const Text('反馈'), - const SizedBox(height: 5), - Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: tp.feedbacks!.map((item) { - return actionButton(null, item); - }).toList(), - ), - ], - const Divider(), - Center( - child: FilledButton.tonal( - onPressed: () async { - SmartDialog.showLoading( - msg: '正在提交', + return SimpleDialog( + contentPadding: const .fromLTRB(24, 16, 24, 24), + children: [ + if (tp.dislikeReasons != null) ...[ + const Text('我不想看'), + const SizedBox(height: 5), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: tp.dislikeReasons! + .map((item) => actionButton(item, null)) + .toList(), + ), + ], + if (tp.feedbacks != null) ...[ + const SizedBox(height: 5), + const Text('反馈'), + const SizedBox(height: 5), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: tp.feedbacks! + .map((item) => actionButton(null, item)) + .toList(), + ), + ], + const Divider(), + Center( + child: FilledButton.tonal( + onPressed: () async { + SmartDialog.showLoading( + msg: '正在提交', + ); + final res = + await VideoHttp.feedDislikeCancel( + id: item.param!, + goto: item.goto!, ); - final res = - await VideoHttp.feedDislikeCancel( - id: item.param!, - goto: item.goto!, - ); - SmartDialog.dismiss(); - SmartDialog.showToast( - res.isSuccess - ? "成功" - : res.toString(), - ); - Get.back(); - }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - child: const Text("撤销"), - ), + SmartDialog.dismiss(); + SmartDialog.showToast( + res.isSuccess ? "成功" : res.toString(), + ); + Get.back(); + }, + style: FilledButton.styleFrom( + visualDensity: VisualDensity.compact, ), - ], + child: const Text("撤销"), + ), ), - ), + ], ); }, ); } else { showDialog( context: context, - builder: (context) => AlertDialog( - content: SingleChildScrollView( - child: Column( + builder: (context) => SimpleDialog( + contentPadding: const .all(24), + children: [ + const Center(child: Text("web端暂不支持精细选择")), + const SizedBox(height: 5), + Wrap( + spacing: 5.0, + runSpacing: 2.0, + alignment: .center, children: [ - const SizedBox(height: 5), - const Text("web端暂不支持精细选择"), - const SizedBox(height: 5), - Wrap( - spacing: 5.0, - runSpacing: 2.0, - children: [ - FilledButton.tonal( - onPressed: () async { - Get.back(); - SmartDialog.showLoading( - msg: '正在提交', - ); - final res = - await VideoHttp.dislikeVideo( - bvid: videoItem.bvid!, - type: true, - ); - SmartDialog.dismiss(); - if (res.isSuccess) { - SmartDialog.showToast('点踩成功'); - onRemove?.call(); - } else { - res.toast(); - } - }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - child: const Text("点踩"), - ), - FilledButton.tonal( - onPressed: () async { - Get.back(); - SmartDialog.showLoading( - msg: '正在提交', - ); - final res = - await VideoHttp.dislikeVideo( - bvid: videoItem.bvid!, - type: false, - ); - SmartDialog.dismiss(); - SmartDialog.showToast( - res.isSuccess - ? '取消踩' - : res.toString(), - ); - }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - child: const Text("撤销"), - ), - ], + FilledButton.tonal( + onPressed: () async { + Get.back(); + SmartDialog.showLoading(msg: '正在提交'); + final res = await VideoHttp.dislikeVideo( + bvid: videoItem.bvid!, + type: true, + ); + SmartDialog.dismiss(); + if (res.isSuccess) { + SmartDialog.showToast('点踩成功'); + onRemove?.call(); + } else { + res.toast(); + } + }, + style: FilledButton.styleFrom( + visualDensity: .compact, + ), + child: const Text("点踩"), + ), + FilledButton.tonal( + onPressed: () async { + Get.back(); + SmartDialog.showLoading(msg: '正在提交'); + final res = await VideoHttp.dislikeVideo( + bvid: videoItem.bvid!, + type: false, + ); + SmartDialog.dismiss(); + SmartDialog.showToast( + res.isSuccess ? '取消踩' : res.toString(), + ); + }, + style: FilledButton.styleFrom( + visualDensity: .compact, + ), + child: const Text("撤销"), ), ], ), - ), + ], ), ); } @@ -298,9 +279,7 @@ class VideoPopupMenu extends StatelessWidget { child: Text( '点错了', style: TextStyle( - color: Theme.of( - context, - ).colorScheme.outline, + color: ColorScheme.of(context).outline, ), ), ), diff --git a/lib/grpc/dm.dart b/lib/grpc/dm.dart index 5c5c1f447..74ffb7bdc 100644 --- a/lib/grpc/dm.dart +++ b/lib/grpc/dm.dart @@ -21,4 +21,12 @@ abstract final class DmGrpc { isolate: true, ); } + + static Future> dmView(int aid, int cid) { + return GrpcReq.request( + GrpcUrl.dmView, + DmViewReq(pid: Int64(aid), oid: Int64(cid), type: 1), + DmViewReply.fromBuffer, + ); + } } diff --git a/lib/grpc/url.dart b/lib/grpc/url.dart index 6ace7c1cc..78238ef02 100644 --- a/lib/grpc/url.dart +++ b/lib/grpc/url.dart @@ -14,6 +14,7 @@ abstract final class GrpcUrl { // danmaku static const dmSegMobile = '/bilibili.community.service.dm.v1.DM/DmSegMobile'; + static const dmView = '/bilibili.community.service.dm.v1.DM/DmView'; // reply static const reply = '/bilibili.main.community.reply.v1.Reply'; diff --git a/lib/http/browser_ua.dart b/lib/http/browser_ua.dart index 9414fbd1c..8211c2ebc 100644 --- a/lib/http/browser_ua.dart +++ b/lib/http/browser_ua.dart @@ -7,5 +7,5 @@ abstract final class BrowserUa { '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 const 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 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36 os/android build/8430300 osVer/10 sdkInt/29 network/2 BiliApp/8430300 mobi_app/android_q channel/master innerVer/8430300'; } diff --git a/lib/http/download.dart b/lib/http/download.dart index 66244d8f5..9c14f50f2 100644 --- a/lib/http/download.dart +++ b/lib/http/download.dart @@ -2,7 +2,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; -import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/video/play/url.dart'; @@ -40,9 +39,9 @@ abstract final class DownloadHttp { }, ); if (res case Success(:final response)) { - final Dash? dash = response.dash; + final dash = response.dash; if (dash != null) { - final List videoList = dash.video!; + final videoList = dash.video!; final curHighestVideoQa = videoList.first.quality.code; final preferVideoQa = entry.preferedVideoQuality; int targetVideoQa = curHighestVideoQa; @@ -55,19 +54,18 @@ abstract final class DownloadHttp { ); } - /// 取出符合当前画质的videoList - final List videosList = videoList - .where((e) => e.quality.code == targetVideoQa) - .toList(); - /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 - final List supportFormats = response.supportFormats!; + final supportFormats = response.supportFormats!; // 根据画质选编码格式 - final FormatItem targetSupportFormats = supportFormats.firstWhere( + final targetSupportFormats = supportFormats.firstWhere( (e) => e.quality == targetVideoQa, orElse: () => supportFormats.first, ); - final List supportDecodeFormats = targetSupportFormats.codecs!; + + final currentDecodeFormats = VideoUtils.selectCodec( + targetSupportFormats.codecs!, + Pref.preferCodecs, + ); entry ..typeTag = targetVideoQa.toString() @@ -77,31 +75,10 @@ abstract final class DownloadHttp { targetSupportFormats.newDesc ?? VideoQuality.fromCode(targetVideoQa).desc; - String preferDecode = Pref.defaultDecode; // def avc - String preferSecondDecode = Pref.secondDecode; // def av1 - - // 默认从设置中取AV1 - VideoDecodeFormatType currentDecodeFormats = - VideoDecodeFormatType.fromString(preferDecode); - VideoDecodeFormatType secondDecodeFormats = - VideoDecodeFormatType.fromString(preferSecondDecode); - // 当前视频没有对应格式返回第一个 - int flag = 0; - for (final e in supportDecodeFormats) { - if (currentDecodeFormats.codes.any(e.startsWith)) { - flag = 1; - break; - } else if (secondDecodeFormats.codes.any(e.startsWith)) { - flag = 2; - } - } - if (flag == 2) { - currentDecodeFormats = secondDecodeFormats; - } else if (flag == 0) { - currentDecodeFormats = VideoDecodeFormatType.fromString( - supportDecodeFormats.first, - ); - } + /// 取出符合当前画质的videoList + final videosList = videoList + .where((e) => e.quality.code == targetVideoQa) + .toList(); /// 取出符合当前解码格式的videoItem final videoDash = videosList.firstWhere( diff --git a/lib/http/sponsor_block.dart b/lib/http/sponsor_block.dart index 553f2f051..0dfaa69b6 100644 --- a/lib/http/sponsor_block.dart +++ b/lib/http/sponsor_block.dart @@ -79,7 +79,7 @@ abstract final class SponsorBlock { int? type, SegmentType? category, }) async { - assert((type == null) == (category == null)); + assert((type == null) != (category == null)); final res = await Request().post( _api(SponsorBlockApi.voteOnSponsorTime), queryParameters: { diff --git a/lib/main.dart b/lib/main.dart index c3fe0d64f..05b649822 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -110,8 +110,6 @@ void main() async { ..lazyPut(DownloadService.new); HttpOverrides.global = _CustomHttpOverrides(); - CacheManager.autoClearCache(); - if (PlatformUtils.isMobile) { if (Platform.isAndroid) MaxScreenSize.init(); await Future.wait([ diff --git a/lib/models/common/setting_type.dart b/lib/models/common/setting_type.dart index 366623f60..64abaa232 100644 --- a/lib/models/common/setting_type.dart +++ b/lib/models/common/setting_type.dart @@ -1,3 +1,11 @@ +import 'package:PiliPlus/pages/setting/models/extra_settings.dart'; +import 'package:PiliPlus/pages/setting/models/model.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart'; +import 'package:PiliPlus/pages/setting/models/privacy_settings.dart'; +import 'package:PiliPlus/pages/setting/models/recommend_settings.dart'; +import 'package:PiliPlus/pages/setting/models/style_settings.dart'; +import 'package:PiliPlus/pages/setting/models/video_settings.dart'; + enum SettingType { privacySetting('隐私设置'), recommendSetting('推荐流设置'), @@ -11,4 +19,14 @@ enum SettingType { final String title; const SettingType(this.title); + + List get settings => switch (this) { + .privacySetting => privacySettings, + .recommendSetting => recommendSettings, + .videoSetting => videoSettings, + .playSetting => playSettings, + .styleSetting => styleSettings, + .extraSetting => extraSettings, + _ => throw UnimplementedError(), + }; } diff --git a/lib/models/common/video/video_decode_type.dart b/lib/models/common/video/video_decode_type.dart index f17ec8c67..4020de3f2 100644 --- a/lib/models/common/video/video_decode_type.dart +++ b/lib/models/common/video/video_decode_type.dart @@ -12,9 +12,6 @@ enum VideoDecodeFormatType { const VideoDecodeFormatType(this.codes); - static VideoDecodeFormatType fromCode(String code) => - values.firstWhere((i) => i.codes.contains(code)); - static VideoDecodeFormatType fromString(String val) => values.firstWhere((i) => i.codes.any(val.startsWith)); } diff --git a/lib/models_new/video/video_play_info/subtitle.dart b/lib/models_new/video/video_play_info/subtitle.dart index 797adb963..f3bf28306 100644 --- a/lib/models_new/video/video_play_info/subtitle.dart +++ b/lib/models_new/video/video_play_info/subtitle.dart @@ -1,4 +1,4 @@ -class Subtitle { +class Subtitle implements Comparable { late String lan; String? lanDoc; String? subtitleUrl; @@ -8,6 +8,8 @@ class Subtitle { Subtitle({ required this.lan, this.lanDoc, + this.subtitleUrl, + this.isAi = false, }); Subtitle.fromJson(Map json) { @@ -17,4 +19,13 @@ class Subtitle { subtitleUrl = json["subtitle_url"]; subtitleUrlV2 = json["subtitle_url_v2"]; } + + @override + int compareTo(Subtitle other) { + final thisHasZh = lan.contains('zh'); + final otherHasZh = other.lan.contains('zh'); + if (thisHasZh != otherHasZh) return thisHasZh ? -1 : 1; + if (isAi != other.isAi) return isAi ? 1 : -1; + return 0; + } } diff --git a/lib/models_new/video/video_play_info/subtitle_info.dart b/lib/models_new/video/video_play_info/subtitle_info.dart index d559c1697..33e886186 100644 --- a/lib/models_new/video/video_play_info/subtitle_info.dart +++ b/lib/models_new/video/video_play_info/subtitle_info.dart @@ -14,12 +14,6 @@ class SubtitleInfo { (json['subtitles'] as List?) ?.map((e) => Subtitle.fromJson(e as Map)) .toList() - ?..sort((a, b) { - final aHasZh = a.lan.contains('zh'); - final bHasZh = b.lan.contains('zh'); - if (aHasZh != bHasZh) return aHasZh ? -1 : 1; - if (a.isAi != b.isAi) return a.isAi ? 1 : -1; - return 0; - }), + ?..sort(), ); } diff --git a/lib/pages/about/view.dart b/lib/pages/about/view.dart index 3a232286d..a07c4b685 100644 --- a/lib/pages/about/view.dart +++ b/lib/pages/about/view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/export_import.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/services/logger.dart'; @@ -276,9 +277,8 @@ Commit Hash: ${BuildConfig.commitHash}''', clipBehavior: Clip.hardEdge, title: const Text('是否重置所有设置?'), children: [ - ListTile( - dense: true, - onTap: () async { + DialogOption( + onPressed: () async { Get.back(); await Future.wait([ GStorage.setting.clear(), @@ -286,16 +286,15 @@ Commit Hash: ${BuildConfig.commitHash}''', ]); SmartDialog.showToast('重置成功'); }, - title: const Text('重置可导出的设置', style: style), + child: const Text('重置可导出的设置', style: style), ), - ListTile( - dense: true, - onTap: () async { + DialogOption( + onPressed: () async { Get.back(); await GStorage.clear(); SmartDialog.showToast('重置成功'); }, - title: const Text('重置所有数据(含登录信息)', style: style), + child: const Text('重置所有数据(含登录信息)', style: style), ), ], ); diff --git a/lib/pages/article/widgets/opus_content.dart b/lib/pages/article/widgets/opus_content.dart index 869882c8a..e6257447b 100644 --- a/lib/pages/article/widgets/opus_content.dart +++ b/lib/pages/article/widgets/opus_content.dart @@ -19,6 +19,7 @@ import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:get/get_core/src/get_main.dart'; @@ -273,14 +274,14 @@ class OpusContent extends StatelessWidget { case 5 when (element.list != null): return SelectableText.rich( TextSpan( - children: element.list!.items?.indexed.map((entry) { + children: element.list!.items?.mapIndexed((i, entry) { return TextSpan( children: [ const WidgetSpan( child: Icon(MdiIcons.circleMedium), alignment: .middle, ), - ...entry.$2.nodes!.map((item) { + ...entry.nodes!.map((item) { if (item.word != null) { return _getSpan( item.word, @@ -307,7 +308,7 @@ class OpusContent extends StatelessWidget { } return const TextSpan(); }), - if (entry.$1 < element.list!.items!.length - 1) + if (i < element.list!.items!.length - 1) const TextSpan(text: '\n'), ], ); diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index 8544b4082..148fb2d42 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/grpc/audio.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' show @@ -533,62 +534,45 @@ class AudioController extends GetxController : '${HttpString.baseUrl}/audio/au$oid'; showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '复制链接', - style: TextStyle(fontSize: 14), - ), - onTap: () { + children: [ + DialogOption( + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + Utils.copyText(audioUrl); + }, + ), + DialogOption( + child: const Text('其它app打开', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + PageUtils.launchURL(audioUrl); + }, + ), + if (PlatformUtils.isMobile) + DialogOption( + child: const Text('分享视频', style: TextStyle(fontSize: 14)), + onPressed: () { Get.back(); - Utils.copyText(audioUrl); + if (audioItem.value case DetailItem( + :final arc, + :final owner, + )) { + ShareUtils.shareText( + '${arc.title} ' + 'UP主: ${owner.name}' + ' - $audioUrl', + ); + } }, ), - ListTile( - dense: true, - title: const Text( - '其它app打开', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - PageUtils.launchURL(audioUrl); - }, - ), - if (PlatformUtils.isMobile) - ListTile( - dense: true, - title: const Text( - '分享视频', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - if (audioItem.value case DetailItem( - :final arc, - :final owner, - )) { - ShareUtils.shareText( - '${arc.title} ' - 'UP主: ${owner.name}' - ' - $audioUrl', - ); - } - }, - ), - ListTile( - dense: true, - title: const Text( - '分享至动态', - style: TextStyle(fontSize: 14), - ), - onTap: () { + if (isLogin) + DialogOption( + child: const Text('分享至动态', style: TextStyle(fontSize: 14)), + onPressed: () { Get.back(); if (audioItem.value case DetailItem( :final arc, @@ -609,40 +593,35 @@ class AudioController extends GetxController } }, ), - if (isUgc) - ListTile( - dense: true, - title: const Text( - '分享至消息', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - if (audioItem.value case DetailItem( - :final arc, - :final owner, - )) { - try { - PageUtils.pmShare( - context, - content: { - "id": oid.toString(), - "title": arc.title, - "headline": arc.title, - "source": 5, - "thumb": arc.cover, - "author": owner.name, - "author_id": owner.mid.toString(), - }, - ); - } catch (e) { - SmartDialog.showToast(e.toString()); - } + if (isUgc && isLogin) + DialogOption( + child: const Text('分享至消息', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + if (audioItem.value case DetailItem( + :final arc, + :final owner, + )) { + try { + PageUtils.pmShare( + context, + content: { + "id": oid.toString(), + "title": arc.title, + "headline": arc.title, + "source": 5, + "thumb": arc.cover, + "author": owner.name, + "author_id": owner.mid.toString(), + }, + ); + } catch (e) { + SmartDialog.showToast(e.toString()); } - }, - ), - ], - ), + } + }, + ), + ], ), ); } diff --git a/lib/pages/bubble/view.dart b/lib/pages/bubble/view.dart index 159ece5ca..d3940c341 100644 --- a/lib/pages/bubble/view.dart +++ b/lib/pages/bubble/view.dart @@ -91,34 +91,31 @@ class _BubblePageState extends State tooltip: '排序', onPressed: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: .hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: .min, - children: sortInfo.sortItems!.map( - (e) { - final isSelected = item.sortType == e.sortType; - return ListTile( - dense: true, - enabled: !isSelected, - onTap: () { - Get.back(); - if (!isSelected) { - _controller.onSort(e.sortType); - } - }, - title: Text( - e.text!, - style: const TextStyle(fontSize: 14), - ), - trailing: isSelected - ? const Icon(size: 22, Icons.check) - : null, - ); - }, - ).toList(), - ), + children: sortInfo.sortItems!.map( + (e) { + final isSelected = item.sortType == e.sortType; + return ListTile( + dense: true, + enabled: !isSelected, + onTap: () { + Get.back(); + if (!isSelected) { + _controller.onSort(e.sortType); + } + }, + title: Text( + e.text!, + style: const TextStyle(fontSize: 14), + ), + trailing: isSelected + ? const Icon(size: 22, Icons.check) + : null, + ); + }, + ).toList(), ), ), icon: const Icon(Icons.sort, size: 20), diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart index 6206c27f0..254d36d8c 100644 --- a/lib/pages/danmaku/controller.dart +++ b/lib/pages/danmaku/controller.dart @@ -6,7 +6,6 @@ import 'package:PiliPlus/grpc/dm.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; -import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -69,7 +68,6 @@ class PlDanmakuController { final uniques = HashMap(); final filters = _plPlayerController.filters; - final danmakuWeight = DanmakuOptions.danmakuWeight; final shouldFilter = filters.count != 0; for (final element in elems) { if (_isLogin) { @@ -87,8 +85,7 @@ class PlDanmakuController { } } - if (element.weight < danmakuWeight || - (shouldFilter && filters.remove(element))) { + if (shouldFilter && filters.remove(element)) { continue; } } diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 40e57639e..233080bee 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -114,7 +114,9 @@ class _PlDanmakuState extends State { .getCurrentDanmaku(currentPosition); if (currentDanmakuList != null) { final blockColorful = DanmakuOptions.blockColorful; + final danmakuWeight = DanmakuOptions.danmakuWeight; for (DanmakuElem e in currentDanmakuList) { + if (e.weight < danmakuWeight) return; if (e.mode == 7) { try { _controller!.addDanmaku( diff --git a/lib/pages/download/detail/widgets/item.dart b/lib/pages/download/detail/widgets/item.dart index ac1c9e813..685a997f0 100644 --- a/lib/pages/download/detail/widgets/item.dart +++ b/lib/pages/download/detail/widgets/item.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; @@ -61,48 +62,37 @@ class DetailItem extends StatelessWidget { void onLongPress() => canDel && !enableMultiSelect ? showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('确定删除该视频?'), - onConfirm: onDelete, - ); - }, - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () async { - Get.back(); - final res = await downloadService.downloadDanmaku( - entry: entry, - isUpdate: true, - ); - if (res) { - SmartDialog.showToast('更新成功'); - } else { - SmartDialog.showToast('更新失败'); - } - }, - dense: true, - title: const Text( - '更新弹幕', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('确定删除该视频?'), + onConfirm: onDelete, + ); + }, + child: const Text('删除', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final res = await downloadService.downloadDanmaku( + entry: entry, + isUpdate: true, + ); + if (res) { + SmartDialog.showToast('更新成功'); + } else { + SmartDialog.showToast('更新失败'); + } + }, + child: const Text('更新弹幕', style: TextStyle(fontSize: 14)), + ), + ], ), ) : null; diff --git a/lib/pages/download/view.dart b/lib/pages/download/view.dart index eba4574a5..150e158bb 100644 --- a/lib/pages/download/view.dart +++ b/lib/pages/download/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -232,59 +233,48 @@ class _DownloadPageState extends State { ? null : showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('确定删除?'), - onConfirm: () async { - await GStorage.watchProgress.deleteAll( - pageInfo.entries.map((e) => e.cid.toString()), - ); - _downloadService.deletePage( - pageDirPath: pageInfo.dirPath, - ); - }, - ); - }, - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () async { - Get.back(); - final res = await Future.wait( - pageInfo.entries.map( - (e) => _downloadService.downloadDanmaku( - entry: e, - isUpdate: true, - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('确定删除?'), + onConfirm: () async { + await GStorage.watchProgress.deleteAll( + pageInfo.entries.map((e) => e.cid.toString()), + ); + _downloadService.deletePage( + pageDirPath: pageInfo.dirPath, + ); + }, + ); + }, + child: const Text('删除', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final res = await Future.wait( + pageInfo.entries.map( + (e) => _downloadService.downloadDanmaku( + entry: e, + isUpdate: true, ), - ); - if (res.every((e) => e)) { - SmartDialog.showToast('更新成功'); - } else { - SmartDialog.showToast('更新失败'); - } - }, - dense: true, - title: const Text( - '更新弹幕', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + ), + ); + if (res.every((e) => e)) { + SmartDialog.showToast('更新成功'); + } else { + SmartDialog.showToast('更新失败'); + } + }, + child: const Text('更新弹幕', style: TextStyle(fontSize: 14)), + ), + ], ), ); final first = pageInfo.entries.first; diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index 99ed4664d..008cb5bb8 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -431,41 +431,37 @@ class AuthorPanel extends StatelessWidget { final reply = response.upReply; final enableReply = reply.status == 1; - return AlertDialog( + return SimpleDialog( clipBehavior: .hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - ListTile( - dense: true, - enabled: selection.canModify, - title: Text( - '${enableSelection ? '停止' : '开启'}评论精选', - style: const TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - onSetReplySubject!( - enableSelection ? 2 : 1, - ); - }, + children: [ + ListTile( + dense: true, + enabled: selection.canModify, + title: Text( + '${enableSelection ? '停止' : '开启'}评论精选', + style: const TextStyle(fontSize: 14), ), - ListTile( - dense: true, - enabled: reply.canModify, - title: Text( - '${enableReply ? '关闭' : '恢复'}评论', - style: const TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - onSetReplySubject!(enableReply ? 3 : 4); - }, + onTap: () { + Get.back(); + onSetReplySubject!( + enableSelection ? 2 : 1, + ); + }, + ), + ListTile( + dense: true, + enabled: reply.canModify, + title: Text( + '${enableReply ? '关闭' : '恢复'}评论', + style: const TextStyle(fontSize: 14), ), - ], - ), + onTap: () { + Get.back(); + onSetReplySubject!(enableReply ? 3 : 4); + }, + ), + ], ); }, ); @@ -504,32 +500,29 @@ class AuthorPanel extends StatelessWidget { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: .min, - children: [ - ListTile( - dense: true, - enabled: isPrivate, - title: const Text( - '所有用户可见', - style: TextStyle(fontSize: 14), - ), - onTap: onTap, + children: [ + ListTile( + dense: true, + enabled: isPrivate, + title: const Text( + '所有用户可见', + style: TextStyle(fontSize: 14), ), - ListTile( - dense: true, - enabled: !isPrivate, - title: const Text( - '仅自己可见', - style: TextStyle(fontSize: 14), - ), - onTap: onTap, + onTap: onTap, + ), + ListTile( + dense: true, + enabled: !isPrivate, + title: const Text( + '仅自己可见', + style: TextStyle(fontSize: 14), ), - ], - ), + onTap: onTap, + ), + ], ), ); }, diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart index 8dc62f3ce..18c4157d2 100644 --- a/lib/pages/dynamics/widgets/video_panel.dart +++ b/lib/pages/dynamics/widgets/video_panel.dart @@ -1,11 +1,10 @@ // 视频or合集 -import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/svg/play_icon.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:flutter/material.dart'; @@ -113,12 +112,7 @@ Widget videoSeasonWidget( Text('${NumUtils.numFormat(stat.danmu)}弹幕'), ], const Spacer(), - Image.asset( - Assets.play, - width: 50, - height: 50, - cacheHeight: 50.cacheSize(context), - ), + const PlayIcon(size: 50), ], ), ), diff --git a/lib/pages/dynamics/widgets/vote.dart b/lib/pages/dynamics/widgets/vote.dart index 4ef0daced..be9574212 100644 --- a/lib/pages/dynamics/widgets/vote.dart +++ b/lib/pages/dynamics/widgets/vote.dart @@ -159,57 +159,53 @@ class _VotePanelState extends State { context: context, builder: (context) { final colorScheme = ColorScheme.of(context); - return AlertDialog( + return SimpleDialog( clipBehavior: .hardEdge, title: const Text('关注的人的投票'), - contentPadding: const .only(top: 10, bottom: 12), - content: SingleChildScrollView( - child: Column( - mainAxisSize: .min, - children: list - .map( - (e) => ListTile( - dense: true, - onTap: () => - Get.toNamed('/member?mid=${e.mid}'), - leading: NetworkImgLayer( - src: e.face, - width: 40, - height: 40, - type: .avatar, - ), - title: Text.rich( - style: const TextStyle(fontSize: 13), + contentPadding: const .only(bottom: 12), + titlePadding: const .fromLTRB(20, 20, 20, 10), + children: list + .map( + (e) => ListTile( + dense: true, + onTap: () => + Get.toNamed('/member?mid=${e.mid}'), + leading: NetworkImgLayer( + src: e.face, + width: 40, + height: 40, + type: .avatar, + ), + title: Text.rich( + style: const TextStyle(fontSize: 13), + TextSpan( + children: [ + TextSpan(text: e.name), TextSpan( - children: [ - TextSpan(text: e.name), - TextSpan( - text: ' 投给了', - style: TextStyle( - fontSize: 12, - color: colorScheme.outline, - ), - ), - ], + text: ' 投给了', + style: TextStyle( + fontSize: 12, + color: colorScheme.outline, + ), ), - ), - subtitle: Text( - style: const TextStyle(fontSize: 13), - e.votes - .map( - (vote) => _voteInfo.options - .firstWhereOrNull( - (e) => e.optIdx == vote, - ) - ?.optDesc, - ) - .join('、'), - ), + ], ), - ) - .toList(), - ), - ), + ), + subtitle: Text( + style: const TextStyle(fontSize: 13), + e.votes + .map( + (vote) => _voteInfo.options + .firstWhereOrNull( + (e) => e.optIdx == vote, + ) + ?.optDesc, + ) + .join('、'), + ), + ), + ) + .toList(), ); }, ); diff --git a/lib/pages/fav_create/view.dart b/lib/pages/fav_create/view.dart index 4b9faba17..3ee894682 100644 --- a/lib/pages/fav_create/view.dart +++ b/lib/pages/fav_create/view.dart @@ -1,5 +1,6 @@ import 'dart:io' show File; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/fav.dart'; @@ -195,37 +196,32 @@ class _CreateFavPageState extends State { if (_cover?.isNotEmpty == true) { showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () { - Get.back(); - _pickImg(context, theme); - }, - title: const Text( - '替换封面', - style: TextStyle(fontSize: 14), - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + _pickImg(context, theme); + }, + child: const Text( + '替换封面', + style: TextStyle(fontSize: 14), ), - ListTile( - dense: true, - onTap: () { - Get.back(); - _cover = null; - (context as Element).markNeedsBuild(); - }, - title: const Text( - '移除封面', - style: TextStyle(fontSize: 14), - ), + ), + DialogOption( + onPressed: () { + Get.back(); + _cover = null; + (context as Element).markNeedsBuild(); + }, + child: const Text( + '移除封面', + style: TextStyle(fontSize: 14), ), - ], - ), + ), + ], ), ); } else { diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index 58119d060..c541a4bf6 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -226,84 +226,85 @@ class _FavDetailPageState extends State with GridMixin { ); }, ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - final isOwner = _favDetailController.isOwner; - final folderInfo = _favDetailController.folderInfo.value; - return [ - if (isOwner) ...[ - PopupMenuItem( - onTap: _favDetailController.onSort, - child: const Text('排序'), - ), - PopupMenuItem( - onTap: () => - Get.toNamed( - '/createFav', - parameters: {'mediaId': mediaId}, - )?.then((res) { - if (res is FavFolderInfo) { - _favDetailController.folderInfo.value = res; - } - }), - child: const Text('编辑信息'), - ), - ] else - PopupMenuItem( - onTap: () => - _favDetailController.onFav(folderInfo.favState == 1), - child: Text('${folderInfo.favState == 1 ? '取消' : ''}收藏'), - ), - if (BiliUtils.isPublicFav(folderInfo.attr)) - PopupMenuItem( - onTap: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) => RepostPanel( - rid: _favDetailController.mediaId, - dynType: 4300, - pic: folderInfo.cover, - title: folderInfo.title, - uname: folderInfo.upper?.name, - ), - ), - child: const Text('分享至动态'), - ), - if (isOwner) ...[ - PopupMenuItem( - onTap: _favDetailController.cleanFav, - child: const Text('清除失效内容'), - ), - if (!BiliUtils.isDefaultFav(folderInfo.attr)) ...[ - const PopupMenuDivider(height: 12), + if (_favDetailController.account.isLogin) + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) { + final isOwner = _favDetailController.isOwner; + final folderInfo = _favDetailController.folderInfo.value; + return [ + if (isOwner) ...[ PopupMenuItem( - onTap: () => showConfirmDialog( + onTap: _favDetailController.onSort, + child: const Text('排序'), + ), + PopupMenuItem( + onTap: () => + Get.toNamed( + '/createFav', + parameters: {'mediaId': mediaId}, + )?.then((res) { + if (res is FavFolderInfo) { + _favDetailController.folderInfo.value = res; + } + }), + child: const Text('编辑信息'), + ), + ] else + PopupMenuItem( + onTap: () => + _favDetailController.onFav(folderInfo.favState == 1), + child: Text('${folderInfo.favState == 1 ? '取消' : ''}收藏'), + ), + if (BiliUtils.isPublicFav(folderInfo.attr)) + PopupMenuItem( + onTap: () => showModalBottomSheet( context: context, - title: const Text('确定删除该收藏夹?'), - onConfirm: () => - FavHttp.deleteFolder(mediaIds: mediaId).then((res) { - if (res.isSuccess) { - SmartDialog.showToast('删除成功'); - Get.back(result: true); - } else { - res.toast(); - } - }), - ), - child: Text( - '删除', - style: TextStyle( - color: theme.colorScheme.error, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => RepostPanel( + rid: _favDetailController.mediaId, + dynType: 4300, + pic: folderInfo.cover, + title: folderInfo.title, + uname: folderInfo.upper?.name, ), ), + child: const Text('分享至动态'), ), + if (isOwner) ...[ + PopupMenuItem( + onTap: _favDetailController.cleanFav, + child: const Text('清除失效内容'), + ), + if (!BiliUtils.isDefaultFav(folderInfo.attr)) ...[ + const PopupMenuDivider(height: 12), + PopupMenuItem( + onTap: () => showConfirmDialog( + context: context, + title: const Text('确定删除该收藏夹?'), + onConfirm: () => + FavHttp.deleteFolder(mediaIds: mediaId).then((res) { + if (res.isSuccess) { + SmartDialog.showToast('删除成功'); + Get.back(result: true); + } else { + res.toast(); + } + }), + ), + child: Text( + '删除', + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + ), + ], ], - ], - ]; - }, - ), + ]; + }, + ), const SizedBox(width: 10), ]; } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index 13688820e..2b0fefa8f 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; @@ -194,62 +195,48 @@ class _FollowPageState extends State { void _onHandleTag(int index, MemberTagItemModel item) { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - String tagName = item.name!; - showConfirmDialog( - context: context, - title: const Text('编辑分组名称'), - content: TextFormField( - autofocus: true, - initialValue: tagName, - onChanged: (value) => tagName = value, - inputFormatters: [ - LengthLimitingTextInputFormatter(16), - ], - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + String tagName = item.name!; + showConfirmDialog( + context: context, + title: const Text('编辑分组名称'), + content: TextFormField( + autofocus: true, + initialValue: tagName, + onChanged: (value) => tagName = value, + inputFormatters: [LengthLimitingTextInputFormatter(16)], + decoration: const InputDecoration( + border: OutlineInputBorder(), ), - onConfirm: () { - if (tagName.isNotEmpty) { - _followController.onUpdateTag(item, tagName); - } - }, - ); - }, - dense: true, - title: const Text( - '修改名称', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('删除分组'), - content: const Text('删除后,该分组下的用户依旧保留?'), - onConfirm: () => - _followController.onDelTag(index, item.tagid!), - ); - }, - dense: true, - title: const Text( - '删除分组', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + ), + onConfirm: () { + if (tagName.isNotEmpty) { + _followController.onUpdateTag(item, tagName); + } + }, + ); + }, + child: const Text('修改名称', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('删除分组'), + content: const Text('删除后,该分组下的用户依旧保留?'), + onConfirm: () => _followController.onDelTag(index, item.tagid!), + ); + }, + child: const Text('删除分组', style: TextStyle(fontSize: 14)), + ), + ], ), ); } diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart index f0dfbbc4b..d8a4d519f 100644 --- a/lib/pages/live_dm_block/view.dart +++ b/lib/pages/live_dm_block/view.dart @@ -9,6 +9,7 @@ import 'package:PiliPlus/pages/live_dm_block/controller.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:collection/collection.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show FilteringTextInputFormatter; @@ -168,15 +169,14 @@ class _LiveDmBlockPageState extends State { child: Wrap( spacing: 12, runSpacing: 12, - children: list.indexed.map( - (e) { - final item = e.$2; + children: list.mapIndexed( + (i, e) { return SearchText( - text: item is ShieldUserList ? item.uname! : item as String, + text: e is ShieldUserList ? e.uname! : e as String, onTap: (value) => showConfirmDialog( context: context, title: const Text('确定删除该规则?'), - onConfirm: () => _controller.onRemove(e.$1, item), + onConfirm: () => _controller.onRemove(i, e), ), ); }, diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 79dff6092..2cd59efe6 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -232,18 +232,19 @@ class LiveRoomController extends GetxController { final pref = Pref.liveStream; if (pref != null) { try { - final protocolName = pref[0]; - final formatName = pref[1]; - final codecName = pref[2]; - for (var i in stream.indexed) { - if (i.$2.protocolName == protocolName) { - streamIndex = i.$1; - for (var j in i.$2.format.indexed) { - if (j.$2.formatName == formatName) { - formatIndex = j.$1; - for (var k in j.$2.codec.indexed) { - if (k.$2.codecName == codecName) { - codecIndex = k.$1; + final String protocolName = pref[0]; + final String formatName = pref[1]; + final String codecName = pref[2]; + for (var (i, s) in stream.indexed) { + if (s.protocolName == protocolName) { + streamIndex = i; + for (var (j, f) in s.format.indexed) { + if (f.formatName == formatName) { + formatIndex = j; + for (var (k, c) in f.codec.indexed) { + if (c.codecName == codecName) { + codecIndex = k; + return; } } } diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index e19817126..911758fc1 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -20,6 +20,7 @@ import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -357,9 +358,8 @@ class _LiveHeaderControlState extends State padding: .only( bottom: MediaQuery.viewPaddingOf(context).bottom + 100, ), - children: controller.stream.indexed.map((stream) { - final isCurrStream = - stream.$1 == controller.streamIndex; + children: controller.stream.mapIndexed((si, stream) { + final isCurrStream = si == controller.streamIndex; final streamColor = isCurrStream ? secondary : onSurfaceVariant; @@ -368,15 +368,14 @@ class _LiveHeaderControlState extends State iconColor: streamColor, collapsedIconColor: streamColor, title: Text( - stream.$2.protocolName ?? stream.$1.toString(), + stream.protocolName ?? si.toString(), style: isCurrStream ? currStyle : const TextStyle(fontSize: 14), ), - children: stream.$2.format.indexed.map((format) { + children: stream.format.mapIndexed((fi, format) { final isCurrFormat = - isCurrStream && - format.$1 == controller.formatIndex; + isCurrStream && fi == controller.formatIndex; final formatColor = isCurrFormat ? secondary : onSurfaceVariant; @@ -385,16 +384,14 @@ class _LiveHeaderControlState extends State iconColor: formatColor, collapsedIconColor: formatColor, title: Text( - format.$2.formatName ?? format.$1.toString(), + format.formatName ?? fi.toString(), style: isCurrFormat ? currStyle : const TextStyle(fontSize: 14), ), - children: format.$2.codec.indexed.map((codec) { - final e = codec.$2; + children: format.codec.mapIndexed((ci, codec) { final isCurrCodec = - isCurrFormat && - codec.$1 == controller.codecIndex; + isCurrFormat && ci == controller.codecIndex; final codecColor = isCurrCodec ? secondary : onSurfaceVariant; @@ -403,19 +400,19 @@ class _LiveHeaderControlState extends State iconColor: codecColor, collapsedIconColor: codecColor, title: Text( - '${e.codecName ?? codec.$1.toString()} (${LiveQuality.fromCode(e.currentQn)?.desc ?? e.currentQn})', + '${codec.codecName ?? ci.toString()} (${LiveQuality.fromCode(codec.currentQn)?.desc ?? codec.currentQn})', style: isCurrCodec ? currStyle : const TextStyle(fontSize: 14), ), - children: e.urlInfo.indexed.map((url) { + children: codec.urlInfo.mapIndexed((ui, url) { final isCurrUrl = - (isCurrCodec && - url.$1 == controller.liveUrlIndex); + isCurrCodec && + ui == controller.liveUrlIndex; return ListTile( dense: true, title: Text( - '${url.$2.host}${e.baseUrl}...', + '${url.host}...', style: isCurrUrl ? const TextStyle(fontSize: 14) : TextStyle( @@ -429,17 +426,17 @@ class _LiveHeaderControlState extends State : () { Get.back(); controller.initLiveUrl( - streamIndex: stream.$1, - formatIndex: format.$1, - codecIndex: codec.$1, - liveUrlIndex: url.$1, + streamIndex: si, + formatIndex: fi, + codecIndex: ci, + liveUrlIndex: ui, ); GStorage.setting.put( SettingBoxKey.liveStream, [ - stream.$2.protocolName, - format.$2.formatName, - codec.$2.codecName, + stream.protocolName!, + format.formatName!, + codec.codecName!, ], ); }, diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 4e064ce27..9a887b204 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -217,14 +217,10 @@ class UserInfoCard extends StatelessWidget { ), ), ), - Image.asset( - BiliUtils.levelName( - card.levelInfo!.currentLevel!, - isSeniorMember: card.levelInfo?.identity == 2, - ), + BiliUtils.levelPicture( + card.levelInfo!.currentLevel!, + isSeniorMember: card.levelInfo?.identity == 2, height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${card.levelInfo?.currentLevel}', ), if (card.vip?.status == 1) Container( diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart index 22e4152b5..b4e0b1c77 100644 --- a/lib/pages/member_opus/view.dart +++ b/lib/pages/member_opus/view.dart @@ -98,37 +98,32 @@ class _MemberOpusState extends State child: FloatingActionButton.extended( onPressed: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: _controller.filter! - .map( - (e) => ListTile( - onTap: () { - if (e == _controller.type.value) { - return; - } - Get.back(); - _controller - ..type.value = e - ..onReload(); - }, - tileColor: e == _controller.type.value - ? Theme.of( - context, - ).colorScheme.onInverseSurface - : null, - dense: true, - title: Text( - e.text ?? e.tabName!, - style: const TextStyle(fontSize: 14), - ), + children: _controller.filter! + .map( + (e) => ListTile( + dense: true, + onTap: () { + if (e == _controller.type.value) { + return; + } + Get.back(); + _controller + ..type.value = e + ..onReload(); + }, + tileColor: e == _controller.type.value + ? ColorScheme.of(context).onInverseSurface + : null, + title: Text( + e.text ?? e.tabName!, + style: const TextStyle(fontSize: 14), ), - ) - .toList(), - ), + ), + ) + .toList(), ), ), icon: const Icon(size: 20, Icons.sort), diff --git a/lib/pages/member_profile/view.dart b/lib/pages/member_profile/view.dart index 0fe5f38e7..debf999a1 100644 --- a/lib/pages/member_profile/view.dart +++ b/lib/pages/member_profile/view.dart @@ -247,17 +247,14 @@ class _EditProfilePageState extends State { } Widget _sexDialog(int current) { - return AlertDialog( + return SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _sexDialogItem(1, current, '男'), - _sexDialogItem(0, current, '保密'), - _sexDialogItem(2, current, '女'), - ], - ), + children: [ + _sexDialogItem(1, current, '男'), + _sexDialogItem(0, current, '保密'), + _sexDialogItem(2, current, '女'), + ], ); } diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 4a86bceed..8f5c92b94 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -22,6 +22,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide ListTile; +import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -274,11 +275,10 @@ class _MediaPageState extends CommonPageState Positioned( right: -1, bottom: -2, - child: Image.asset( + child: SvgPicture.asset( Assets.vipIcon, height: 19, - cacheHeight: 19.cacheSize(context), - semanticLabel: "大会员", + semanticsLabel: "大会员", ), ), ], @@ -315,13 +315,10 @@ class _MediaPageState extends CommonPageState overflow: .ellipsis, ), ), - Image.asset( - BiliUtils.levelName( - levelInfo?.currentLevel ?? 0, - isSeniorMember: userInfo.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + levelInfo?.currentLevel ?? 0, + isSeniorMember: userInfo.isSeniorMember == 1, height: 10, - cacheHeight: 10.cacheSize(context), ), ], ), diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart index c5164002c..92ec062f4 100644 --- a/lib/pages/msg_feed_top/like_me/view.dart +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; @@ -97,13 +98,9 @@ class _LikeMePageState extends State { if (total.isEmpty && index == latest.length - 1) { _likeMeController.onLoadMore(); } - return _buildItem( - theme, - latest[index], - (id) { - _likeMeController.onRemove(id, index, true); - }, - ); + return _buildItem(theme, latest[index], (id) { + _likeMeController.onRemove(id, index, true); + }); }, itemCount: latest.length, separatorBuilder: (context, index) => divider, @@ -116,13 +113,9 @@ class _LikeMePageState extends State { if (index == total.length - 1) { _likeMeController.onLoadMore(); } - return _buildItem( - theme, - total[index], - (id) { - _likeMeController.onRemove(id, index, false); - }, - ); + return _buildItem(theme, total[index], (id) { + _likeMeController.onRemove(id, index, false); + }); }, itemCount: total.length, separatorBuilder: (context, index) => divider, @@ -200,51 +193,43 @@ class _LikeMePageState extends State { context: context, builder: (context) { final isNotice = item.noticeState == 0; - return AlertDialog( + return SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); + children: [ + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('删除'), + content: const Text('该条通知删除后,当有新点赞时会重新出现在列表,是否继续?'), + onConfirm: () => onRemove(item.id), + ); + }, + child: const Text('删除', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + if (isNotice) { showConfirmDialog( context: context, - title: const Text('删除'), - content: const Text('该条通知删除后,当有新点赞时会重新出现在列表,是否继续?'), - onConfirm: () => onRemove(item.id), + title: const Text('不再通知'), + content: const Text('这条内容的点赞将不再通知,但仍可在列表内查看,是否继续?'), + onConfirm: () => + _likeMeController.onSetNotice(item, isNotice), ); - }, - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), + } else { + _likeMeController.onSetNotice(item, isNotice); + } + }, + child: Text( + isNotice ? '不再通知' : '接收通知', + style: const TextStyle(fontSize: 14), ), - ListTile( - onTap: () { - Get.back(); - if (isNotice) { - showConfirmDialog( - context: context, - title: const Text('不再通知'), - content: const Text('这条内容的点赞将不再通知,但仍可在列表内查看,是否继续?'), - onConfirm: () => - _likeMeController.onSetNotice(item, isNotice), - ); - } else { - _likeMeController.onSetNotice(item, isNotice); - } - }, - dense: true, - title: Text( - isNotice ? '不再通知' : '接收通知', - style: const TextStyle(fontSize: 14), - ), - ), - ], - ), + ), + ], ); }, ); @@ -336,9 +321,7 @@ class _LikeMePageState extends State { width: 45, height: 45, src: item.item!.image, - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), + borderRadius: const BorderRadius.all(Radius.circular(8)), ), if (item.noticeState == 1) ...[ if (item.item?.image?.isNotEmpty == true) const SizedBox(width: 4), diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index a227d299b..70d5f7173 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -14,7 +15,6 @@ import 'package:PiliPlus/pages/pgc_review/child/controller.dart'; import 'package:PiliPlus/pages/pgc_review/post/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; @@ -121,71 +121,56 @@ class _PgcReviewChildPageState extends State Widget _itemWidget(ThemeData theme, int index, PgcReviewItemModel item) { void showMore() => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (item.author!.mid == Accounts.main.mid) ...[ - ListTile( - dense: true, - title: const Text( - '编辑', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - builder: (context) { - return PgcReviewPostPanel( - name: widget.name, - mediaId: widget.mediaId, - reviewId: item.reviewId, - content: item.content, - score: item.score, - ); - }, - ); - }, - ), - ListTile( - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('删除短评,同时删除评分?'), - onConfirm: () => _controller.onDel(index, item.reviewId!), - ); - }, - ), - ], - ListTile( - dense: true, - title: const Text( - '举报', - style: TextStyle(fontSize: 14), - ), - onTap: () => Get - ..back() - ..toNamed( - '/webview', - parameters: { - 'url': - 'https://www.bilibili.com/appeal/?reviewId=${item.reviewId}&type=shortComment&mediaId=${widget.mediaId}', + children: [ + if (item.author!.mid == Accounts.main.mid) ...[ + DialogOption( + child: const Text('编辑', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + builder: (context) { + return PgcReviewPostPanel( + name: widget.name, + mediaId: widget.mediaId, + reviewId: item.reviewId, + content: item.content, + score: item.score, + ); }, - ), + ); + }, + ), + DialogOption( + child: const Text('删除', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('删除短评,同时删除评分?'), + onConfirm: () => _controller.onDel(index, item.reviewId!), + ); + }, ), ], - ), + DialogOption( + child: const Text('举报', style: TextStyle(fontSize: 14)), + onPressed: () => Get + ..back() + ..toNamed( + '/webview', + parameters: { + 'url': + 'https://www.bilibili.com/appeal/?reviewId=${item.reviewId}&type=shortComment&mediaId=${widget.mediaId}', + }, + ), + ), + ], ), ); @@ -244,10 +229,9 @@ class _PgcReviewChildPageState extends State fontSize: 13, ), ), - Image.asset( - BiliUtils.levelName(item.author!.level!), + BiliUtils.levelPicture( + item.author!.level!, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/pgc_review/view.dart b/lib/pages/pgc_review/view.dart index 3d23873d2..62fd58ef4 100644 --- a/lib/pages/pgc_review/view.dart +++ b/lib/pages/pgc_review/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/models/common/pgc_review_type.dart'; import 'package:PiliPlus/pages/pgc_review/child/controller.dart'; import 'package:PiliPlus/pages/pgc_review/child/view.dart'; @@ -115,51 +116,40 @@ class _PgcReviewPageState extends State child: FloatingActionButton( onPressed: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '写短评', - style: TextStyle(fontSize: 14), + children: [ + DialogOption( + child: const Text('写短评', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + builder: (context) { + return PgcReviewPostPanel( + name: widget.name, + mediaId: widget.mediaId, + ); + }, + ); + }, + ), + DialogOption( + child: const Text('写长评', style: TextStyle(fontSize: 14)), + onPressed: () => Get + ..back() + ..toNamed( + '/webview', + parameters: { + 'url': + 'https://member.bilibili.com/article-text/mobile?theme=${theme.isDark ? 1 : 0}&media_id=${widget.mediaId}', + }, ), - onTap: () { - Get.back(); - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - builder: (context) { - return PgcReviewPostPanel( - name: widget.name, - mediaId: widget.mediaId, - ); - }, - ); - }, - ), - ListTile( - dense: true, - title: const Text( - '写长评', - style: TextStyle(fontSize: 14), - ), - onTap: () => Get - ..back() - ..toNamed( - '/webview', - parameters: { - 'url': - 'https://member.bilibili.com/article-text/mobile?theme=${theme.isDark ? 1 : 0}&media_id=${widget.mediaId}', - }, - ), - ), - ], - ), + ), + ], ), ), child: const Icon(Icons.edit), diff --git a/lib/pages/search_panel/user/widgets/item.dart b/lib/pages/search_panel/user/widgets/item.dart index fdb45feeb..4fc1d7ac9 100644 --- a/lib/pages/search_panel/user/widgets/item.dart +++ b/lib/pages/search_panel/user/widgets/item.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get_core/src/get_main.dart'; @@ -52,14 +51,10 @@ class SearchUserItem extends StatelessWidget { ), ), const SizedBox(width: 6), - Image.asset( - BiliUtils.levelName( - item.level!, - isSeniorMember: item.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + item.level!, + isSeniorMember: item.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${item.level}', ), ], ), diff --git a/lib/pages/setting/common_setting.dart b/lib/pages/setting/common_setting.dart new file mode 100644 index 000000000..abb605284 --- /dev/null +++ b/lib/pages/setting/common_setting.dart @@ -0,0 +1,65 @@ +import 'package:PiliPlus/models/common/setting_type.dart'; +import 'package:PiliPlus/pages/setting/models/model.dart'; +import 'package:flutter/material.dart'; + +class CommonSetting extends StatefulWidget { + const CommonSetting({ + super.key, + required this.settingType, + this.showAppBar = true, + }); + + final bool showAppBar; + final SettingType settingType; + + @override + State createState() => _CommonSettingState(); +} + +class _CommonSettingState extends State { + late EdgeInsets padding; + late List settings; + + void _initSetting() { + settings = widget.settingType.settings; + } + + @override + void initState() { + super.initState(); + _initSetting(); + } + + @override + void didUpdateWidget(CommonSetting oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.settingType != oldWidget.settingType) { + _initSetting(); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + padding = MediaQuery.viewPaddingOf(context); + } + + @override + Widget build(BuildContext context) { + final showAppBar = widget.showAppBar; + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: showAppBar ? AppBar(title: Text(widget.settingType.title)) : null, + body: ListView.builder( + key: ValueKey(widget.settingType), + padding: EdgeInsets.only( + left: showAppBar ? padding.left : 0, + right: showAppBar ? padding.right : 0, + bottom: padding.bottom + 100, + ), + itemCount: settings.length, + itemBuilder: (context, index) => settings[index].widget, + ), + ); + } +} diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart deleted file mode 100644 index 0c6343e0c..000000000 --- a/lib/pages/setting/extra_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/extra_settings.dart'; -import 'package:flutter/material.dart'; - -class ExtraSetting extends StatefulWidget { - const ExtraSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _ExtraSettingState(); -} - -class _ExtraSettingState extends State { - final settings = extraSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('其它设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index 7bc7d54a5..809c8bb59 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math' show max; import 'package:PiliPlus/common/widgets/custom_icon.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart' show deviceTouchSlop, touchSlopH; @@ -595,19 +596,10 @@ List get extraSettings => [ onTap: _showProxyDialog, ), ), - const SwitchModel( - title: '自动清除缓存', - subtitle: '每次启动时清除缓存', - leading: Icon(Icons.auto_delete_outlined), - setKey: SettingBoxKey.autoClearCache, - defaultVal: false, - ), NormalModel( title: '最大缓存大小', - getSubtitle: () { - final num = Pref.maxCacheSize; - return '当前最大缓存大小: 「${num == 0 ? '无限' : CacheManager.formatSize(Pref.maxCacheSize)}」'; - }, + getSubtitle: () => + '当前最大缓存大小: 「${CacheManager.formatSize(Pref.maxCacheSize)}」', leading: const Icon(Icons.delete_outlined), onTap: _showCacheDialog, ), @@ -721,48 +713,42 @@ Future audioNormalization( void _showDownPathDialog(BuildContext context, VoidCallback setState) { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - Utils.copyText(downloadPath); - }, - dense: true, - title: const Text('复制', style: TextStyle(fontSize: 14)), - ), - ListTile( - onTap: () { - Get.back(); - final defPath = defDownloadPath; - if (downloadPath == defPath) return; - downloadPath = defPath; - setState(); - Get.find().initDownloadList(); - GStorage.setting.delete(SettingBoxKey.downloadPath); - }, - dense: true, - title: const Text('重置', style: TextStyle(fontSize: 14)), - ), - ListTile( - onTap: () async { - Get.back(); - final path = await FilePicker.getDirectoryPath(); - if (path == null || path == downloadPath) return; - downloadPath = path; - setState(); - Get.find().initDownloadList(); - GStorage.setting.put(SettingBoxKey.downloadPath, path); - }, - dense: true, - title: const Text('设置新路径', style: TextStyle(fontSize: 14)), - ), - ], - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + Utils.copyText(downloadPath); + }, + child: const Text('复制', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + final defPath = defDownloadPath; + if (downloadPath == defPath) return; + downloadPath = defPath; + setState(); + Get.find().initDownloadList(); + GStorage.setting.delete(SettingBoxKey.downloadPath); + }, + child: const Text('重置', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final path = await FilePicker.getDirectoryPath(); + if (path == null || path == downloadPath) return; + downloadPath = path; + setState(); + Get.find().initDownloadList(); + GStorage.setting.put(SettingBoxKey.downloadPath, path); + }, + child: const Text('设置新路径', style: TextStyle(fontSize: 14)), + ), + ], ), ); } diff --git a/lib/pages/setting/models/privacy_settings.dart b/lib/pages/setting/models/privacy_settings.dart index 1a241d7ff..3734b72bf 100644 --- a/lib/pages/setting/models/privacy_settings.dart +++ b/lib/pages/setting/models/privacy_settings.dart @@ -25,9 +25,7 @@ List get privacySettings => [ context: context, builder: (context) => AlertDialog( title: const Text('账号模式详情'), - content: SingleChildScrollView( - child: _getAccountDetail(context), - ), + content: SingleChildScrollView(child: _getAccountDetail(context)), actions: [ TextButton( onPressed: Get.back, diff --git a/lib/pages/setting/models/recommend_settings.dart b/lib/pages/setting/models/recommend_settings.dart index 256d27468..84f4b584e 100644 --- a/lib/pages/setting/models/recommend_settings.dart +++ b/lib/pages/setting/models/recommend_settings.dart @@ -93,8 +93,8 @@ List get recommendSettings => [ onChanged: (value) => RecommendFilter.exemptFilterForFollowed = value, ), SwitchModel( - title: '过滤器也应用于相关视频', - subtitle: '视频详情页的相关视频也进行过滤¹', + title: '过滤器也应用于详情页相关视频', + subtitle: '其它(如热门视频、搜索等)均不受过滤器影响,无法豁免相关视频中的已关注UP', leading: const Icon(Icons.explore_outlined), setKey: SettingBoxKey.applyFilterToRelatedVideos, defaultVal: true, diff --git a/lib/pages/setting/models/video_settings.dart b/lib/pages/setting/models/video_settings.dart index 886e9d2c8..2136518ea 100644 --- a/lib/pages/setting/models/video_settings.dart +++ b/lib/pages/setting/models/video_settings.dart @@ -127,16 +127,11 @@ List get videoSettings => [ NormalModel( title: '首选解码格式', leading: const Icon(Icons.movie_creation_outlined), - getSubtitle: () => - '首选解码格式:${VideoDecodeFormatType.fromCode(Pref.defaultDecode).description},请根据设备支持情况与需求调整', - onTap: _showDecodeDialog, - ), - NormalModel( - title: '次选解码格式', - getSubtitle: () => - '非杜比视频次选:${VideoDecodeFormatType.fromCode(Pref.secondDecode).description},仍无则选择首个提供的解码格式', - leading: const Icon(Icons.swap_horizontal_circle_outlined), - onTap: _showSecondDecodeDialog, + getSubtitle: () { + final list = Pref.preferCodecs; + return '首选解码格式:${(list.isEmpty ? '第一个可用' : list.map((i) => i.name).join(","))},请根据设备支持情况与需求调整'; + }, + onTap: _showCodecsDialog, ), if (kDebugMode || Platform.isAndroid) NormalModel( @@ -349,42 +344,25 @@ Future _showLiveCellularQaDialog( } } -Future _showDecodeDialog( +Future _showCodecsDialog( BuildContext context, VoidCallback setState, ) async { - final res = await showDialog( + final res = await showDialog>( context: context, - builder: (context) => SelectDialog( - title: '默认解码格式', - value: Pref.defaultDecode, - values: VideoDecodeFormatType.values - .map((e) => (e.codes.first, e.description)) - .toList(), + builder: (context) => OrderedMultiSelectDialog( + title: '首选解码格式', + initValues: Pref.preferCodecs, + values: {for (final e in VideoDecodeFormatType.values) e: e.name}, ), ); if (res != null) { - await GStorage.setting.put(SettingBoxKey.defaultDecode, res); - setState(); - } -} - -Future _showSecondDecodeDialog( - BuildContext context, - VoidCallback setState, -) async { - final res = await showDialog( - context: context, - builder: (context) => SelectDialog( - title: '次选解码格式', - value: Pref.secondDecode, - values: VideoDecodeFormatType.values - .map((e) => (e.codes.first, e.description)) - .toList(), - ), - ); - if (res != null) { - await GStorage.setting.put(SettingBoxKey.secondDecode, res); + await (res.isEmpty + ? GStorage.setting.delete(SettingBoxKey.preferCodecs) + : GStorage.setting.put( + SettingBoxKey.preferCodecs, + res.map((i) => i.name).toList(), + )); setState(); } } diff --git a/lib/pages/setting/pages/bar_set.dart b/lib/pages/setting/pages/bar_set.dart index 7c2635ab2..422fdfb4f 100644 --- a/lib/pages/setting/pages/bar_set.dart +++ b/lib/pages/setting/pages/bar_set.dart @@ -17,6 +17,7 @@ class _BarSetPageState extends State with ReorderMixin { late final String key; late final String title; late final List> list; + late EdgeInsets padding; @override void initState() { @@ -29,7 +30,7 @@ class _BarSetPageState extends State with ReorderMixin { .map((e) => Pair(first: e, second: cache?.contains(e.index) ?? true)) .toList(); if (cache != null && cache.isNotEmpty) { - final cacheIndex = {for (final (k, v) in cache.indexed) v: k}; + final cacheIndex = {for (int i = 0; i < cache.length; i++) cache[i]: i}; list.sort((a, b) { final indexA = cacheIndex[a.first.index] ?? cacheIndex.length; final indexB = cacheIndex[b.first.index] ?? cacheIndex.length; @@ -38,6 +39,13 @@ class _BarSetPageState extends State with ReorderMixin { } } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final viewPad = MediaQuery.viewPaddingOf(context); + padding = .only(top: 10, right: viewPad.right + 34, bottom: viewPad.bottom); + } + void saveEdit() { GStorage.setting.put( key, @@ -73,9 +81,7 @@ class _BarSetPageState extends State with ReorderMixin { onReorderItem: onReorderItem, proxyDecorator: proxyDecorator, footer: Padding( - padding: - MediaQuery.viewPaddingOf(context).copyWith(top: 0, left: 0) + - const EdgeInsets.only(right: 34, top: 10), + padding: padding, child: const Align( alignment: Alignment.centerRight, child: Text('*长按拖动排序'), diff --git a/lib/pages/setting/pages/color_select.dart b/lib/pages/setting/pages/color_select.dart index 6aa14fe3b..b80766ea9 100644 --- a/lib/pages/setting/pages/color_select.dart +++ b/lib/pages/setting/pages/color_select.dart @@ -15,6 +15,7 @@ import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/theme_utils.dart'; +import 'package:collection/collection.dart'; import 'package:flex_seed_scheme/flex_seed_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -148,10 +149,8 @@ class _ColorSelectPageState extends State { alignment: WrapAlignment.center, spacing: 22, runSpacing: 18, - children: colorThemeTypes.indexed.map( - (e) { - final index = e.$1; - final item = e.$2; + children: colorThemeTypes.mapIndexed( + (index, item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart deleted file mode 100644 index 6c97855a5..000000000 --- a/lib/pages/setting/play_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/play_settings.dart'; -import 'package:flutter/material.dart'; - -class PlaySetting extends StatefulWidget { - const PlaySetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _PlaySettingState(); -} - -class _PlaySettingState extends State { - final settings = playSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('播放器设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/privacy_setting.dart b/lib/pages/setting/privacy_setting.dart deleted file mode 100644 index f2ff30bb3..000000000 --- a/lib/pages/setting/privacy_setting.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/privacy_settings.dart'; -import 'package:flutter/material.dart'; - -class PrivacySetting extends StatefulWidget { - const PrivacySetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _PrivacySettingState(); -} - -class _PrivacySettingState extends State { - final settings = privacySettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('隐私设置')) : null, - body: ListView( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - children: settings.map((item) => item.widget).toList(), - ), - ); - } -} diff --git a/lib/pages/setting/recommend_setting.dart b/lib/pages/setting/recommend_setting.dart deleted file mode 100644 index d77315fe3..000000000 --- a/lib/pages/setting/recommend_setting.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; -import 'package:PiliPlus/pages/setting/models/recommend_settings.dart'; -import 'package:flutter/material.dart' hide ListTile; - -class RecommendSetting extends StatefulWidget { - const RecommendSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _RecommendSettingState(); -} - -class _RecommendSettingState extends State { - final list = recommendSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - final theme = Theme.of(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: widget.showAppBar ? AppBar(title: const Text('推荐流设置')) : null, - body: ListView( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - children: [ - ...list.take(4).map((item) => item.widget), - const Divider(height: 1), - ...list.skip(4).map((item) => item.widget), - ListTile( - dense: true, - subtitle: Text( - '¹ 由于接口未提供关注信息,无法豁免相关视频中的已关注Up。\n\n' - '* 其它(如热门视频、手动搜索、链接跳转等)均不受过滤器影响。\n' - '* 设定较严苛的条件可导致推荐项数锐减或多次请求,请酌情选择。\n' - '* 后续可能会增加更多过滤条件,敬请期待。', - style: theme.textTheme.labelSmall!.copyWith( - color: theme.colorScheme.outline.withValues(alpha: 0.7), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart deleted file mode 100644 index 1b13350d7..000000000 --- a/lib/pages/setting/style_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/style_settings.dart'; -import 'package:flutter/material.dart'; - -class StyleSetting extends StatefulWidget { - const StyleSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _StyleSettingState(); -} - -class _StyleSettingState extends State { - final settings = styleSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('外观设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/video_setting.dart b/lib/pages/setting/video_setting.dart deleted file mode 100644 index 495720aa6..000000000 --- a/lib/pages/setting/video_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/video_settings.dart'; -import 'package:flutter/material.dart'; - -class VideoSetting extends StatefulWidget { - const VideoSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _VideoSettingState(); -} - -class _VideoSettingState extends State { - final settings = videoSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('音视频设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index 197652b57..633dd7317 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -4,12 +4,7 @@ import 'package:PiliPlus/http/login.dart'; import 'package:PiliPlus/models/common/setting_type.dart'; import 'package:PiliPlus/pages/about/view.dart'; import 'package:PiliPlus/pages/login/controller.dart'; -import 'package:PiliPlus/pages/setting/extra_setting.dart'; -import 'package:PiliPlus/pages/setting/play_setting.dart'; -import 'package:PiliPlus/pages/setting/privacy_setting.dart'; -import 'package:PiliPlus/pages/setting/recommend_setting.dart'; -import 'package:PiliPlus/pages/setting/style_setting.dart'; -import 'package:PiliPlus/pages/setting/video_setting.dart'; +import 'package:PiliPlus/pages/setting/common_setting.dart'; import 'package:PiliPlus/pages/setting/widgets/multi_select_dialog.dart'; import 'package:PiliPlus/pages/webdav/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; @@ -43,6 +38,7 @@ class _SettingPageState extends State { late SettingType _type = SettingType.privacySetting; final RxBool _noAccount = Accounts.account.isEmpty.obs; late bool _isPortrait; + late ThemeData theme; static const List<_SettingsModel> _items = [ _SettingsModel( @@ -86,9 +82,15 @@ class _SettingPageState extends State { ]; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); + void didChangeDependencies() { + super.didChangeDependencies(); + + theme = Theme.of(context); _isPortrait = MediaQuery.sizeOf(context).isPortrait; + } + + @override + Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( @@ -111,28 +113,19 @@ class _SettingPageState extends State { Expanded( flex: 6, child: switch (_type) { - SettingType.privacySetting => const PrivacySetting( + .privacySetting || + .recommendSetting || + .videoSetting || + .playSetting || + .styleSetting || + .extraSetting => CommonSetting( + settingType: _type, showAppBar: false, ), - SettingType.recommendSetting => const RecommendSetting( + .webdavSetting => const WebDavSettingPage( showAppBar: false, ), - SettingType.videoSetting => const VideoSetting( - showAppBar: false, - ), - SettingType.playSetting => const PlaySetting( - showAppBar: false, - ), - SettingType.styleSetting => const StyleSetting( - showAppBar: false, - ), - SettingType.extraSetting => const ExtraSetting( - showAppBar: false, - ), - SettingType.webdavSetting => const WebDavSettingPage( - showAppBar: false, - ), - SettingType.about => const AboutPage(showAppBar: false), + .about => const AboutPage(showAppBar: false), }, ), ], @@ -149,7 +142,18 @@ class _SettingPageState extends State { void _toPage(SettingType type) { if (_isPortrait) { - Get.toNamed('/${type.name}'); + Get.to( + () => switch (type) { + .privacySetting || + .recommendSetting || + .videoSetting || + .playSetting || + .styleSetting || + .extraSetting => CommonSetting(settingType: type), + .webdavSetting => const WebDavSettingPage(), + .about => const AboutPage(), + }, + ); } else { _type = type; setState(() {}); diff --git a/lib/pages/sponsor_block/block_mixin.dart b/lib/pages/sponsor_block/block_mixin.dart index 907908545..c554fb8a0 100644 --- a/lib/pages/sponsor_block/block_mixin.dart +++ b/lib/pages/sponsor_block/block_mixin.dart @@ -1,6 +1,7 @@ import 'dart:async' show StreamSubscription, Timer; import 'dart:math' as math; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/sponsor_block.dart'; @@ -285,40 +286,32 @@ mixin BlockMixin on GetxController { void _showVoteDialog(SegmentModel segment) { showDialog( context: Get.context!, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text('赞成票', style: TextStyle(fontSize: 14)), - onTap: () { - Get.back(); - _doVote(segment.uuid, 1); - }, - ), - ListTile( - dense: true, - title: const Text('反对票', style: TextStyle(fontSize: 14)), - onTap: () { - Get.back(); - _doVote(segment.uuid, 0); - }, - ), - ListTile( - dense: true, - title: const Text('更改类别', style: TextStyle(fontSize: 14)), - onTap: () { - Get.back(); - _showCategoryDialog(segment); - }, - ), - ], + builder: (context) => SimpleDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 10), + children: [ + DialogOption( + child: const Text('赞成票', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + _doVote(segment.uuid, 1); + }, ), - ), + DialogOption( + child: const Text('反对票', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + _doVote(segment.uuid, 0); + }, + ), + DialogOption( + child: const Text('更改类别', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + _showCategoryDialog(segment); + }, + ), + ], ), ); } @@ -331,54 +324,49 @@ mixin BlockMixin on GetxController { void _showCategoryDialog(SegmentModel segment) { showDialog( context: Get.context!, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: SegmentType.values - .map( - (item) => ListTile( - dense: true, - onTap: () { - Get.back(); - SponsorBlock.voteOnSponsorTime( - uuid: segment.uuid, - category: item, - ).then((i) { - SmartDialog.showToast( - '类别更改${i.isSuccess ? '成功' : '失败: $i'}', - ); - }); - }, - title: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: blockConfig._getColor(item), - ), - ), - style: const TextStyle(fontSize: 14, height: 1), + builder: (context) => SimpleDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 10), + children: SegmentType.values + .map( + (item) => ListTile( + dense: true, + onTap: () { + Get.back(); + SponsorBlock.voteOnSponsorTime( + uuid: segment.uuid, + category: item, + ).then((i) { + SmartDialog.showToast( + '类别更改${i.isSuccess ? '成功' : '失败: $i'}', + ); + }); + }, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: blockConfig._getColor(item), ), - TextSpan( - text: ' ${item.title}', - style: const TextStyle(fontSize: 14, height: 1), - ), - ], + ), + style: const TextStyle(fontSize: 14, height: 1), ), - ), + TextSpan( + text: ' ${item.title}', + style: const TextStyle(fontSize: 14, height: 1), + ), + ], ), - ) - .toList(), - ), - ), + ), + ), + ) + .toList(), ), ); } @@ -386,96 +374,91 @@ mixin BlockMixin on GetxController { void showSBDetail() { showDialog( context: Get.context!, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: _segmentList - .map( - (item) => ListTile( - onTap: () { - Get.back(); - if (isBlock) { - _showVoteDialog(item); - } - }, - dense: true, - title: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: blockConfig._getColor(item.segmentType), - ), - ), - style: const TextStyle(fontSize: 14, height: 1), + builder: (context) => SimpleDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 10), + children: _segmentList + .map( + (item) => ListTile( + onTap: () { + Get.back(); + if (isBlock) { + _showVoteDialog(item); + } + }, + dense: true, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: blockConfig._getColor(item.segmentType), ), - TextSpan( - text: ' ${item.segmentType.title}', - style: const TextStyle(fontSize: 14, height: 1), - ), - ], + ), + style: const TextStyle(fontSize: 14, height: 1), ), - ), - contentPadding: const EdgeInsets.only(left: 16, right: 8), - subtitle: Text( - '${DurationUtils.formatDuration(item.segment.$1 / 1000)} 至 ${DurationUtils.formatDuration(item.segment.$2 / 1000)}', + TextSpan( + text: ' ${item.segmentType.title}', + style: const TextStyle(fontSize: 14, height: 1), + ), + ], + ), + ), + contentPadding: const EdgeInsets.only(left: 16, right: 8), + subtitle: Text( + '${DurationUtils.formatDuration(item.segment.$1 / 1000)} 至 ${DurationUtils.formatDuration(item.segment.$2 / 1000)}', + style: const TextStyle(fontSize: 13), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.skipType.label, style: const TextStyle(fontSize: 13), ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - item.skipType.label, - style: const TextStyle(fontSize: 13), + if (item.segment.$2 != 0) + SizedBox( + width: 36, + height: 36, + child: IconButton( + tooltip: item.skipType == SkipType.showOnly + ? '跳至此片段' + : '跳过此片段', + onPressed: () { + Get.back(); + onSkip( + item, + isSkip: item.skipType != SkipType.showOnly, + isSeek: false, + ); + }, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: Icon( + item.skipType == SkipType.showOnly + ? Icons.my_location + : MdiIcons.debugStepOver, + size: 18, + color: ColorScheme.of( + context, + ).onSurface.withValues(alpha: 0.7), + ), ), - if (item.segment.$2 != 0) - SizedBox( - width: 36, - height: 36, - child: IconButton( - tooltip: item.skipType == SkipType.showOnly - ? '跳至此片段' - : '跳过此片段', - onPressed: () { - Get.back(); - onSkip( - item, - isSkip: item.skipType != SkipType.showOnly, - isSeek: false, - ); - }, - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - icon: Icon( - item.skipType == SkipType.showOnly - ? Icons.my_location - : MdiIcons.debugStepOver, - size: 18, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - ) - else - const SizedBox(width: 10), - ], - ), - ), - ) - .toList(), - ), - ), + ) + else + const SizedBox(width: 10), + ], + ), + ), + ) + .toList(), ), ); } diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index bc6011a4c..84065e0bb 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pbenum.dart' show PlaylistSource; +import 'package:PiliPlus/grpc/dm.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -19,7 +20,6 @@ import 'package:PiliPlus/models/common/sponsor_block/segment_model.dart'; import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/source_type.dart'; -import 'package:PiliPlus/models/common/video/subtitle_pref_type.dart'; import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; @@ -143,8 +143,7 @@ class VideoDetailController extends GetxController Box setting = GStorage.setting; // 预设的解码格式 - late String cacheDecode = Pref.defaultDecode; // def avc - late String cacheSecondDecode = Pref.secondDecode; // def av1 + late List preferCodecs = Pref.preferCodecs; bool get showReply => isFileSource ? false @@ -669,31 +668,38 @@ class VideoDetailController extends GetxController } } - VideoItem findVideoByQa(int qa) { + VideoItem findVideoByQa(int qa, {bool setCodecs = false}) { /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl final videoList = data.dash!.video!.where((i) => i.id == qa).toList(); - final currentDecodeFormats = this.currentDecodeFormats.codes; - final defaultDecodeFormats = VideoDecodeFormatType.fromString( - cacheDecode, - ).codes; - final secondDecodeFormats = VideoDecodeFormatType.fromString( - cacheSecondDecode, - ).codes; - - VideoItem? video; - for (final i in videoList) { - final codec = i.codecs!; - if (currentDecodeFormats.any(codec.startsWith)) { - video = i; - break; - } else if (defaultDecodeFormats.any(codec.startsWith)) { - video = i; - } else if (video == null && secondDecodeFormats.any(codec.startsWith)) { - video = i; + final currentCodes = currentDecodeFormats.codes; + VideoItem? bestVideo; + int bestIndex = preferCodecs.length; + for (final video in videoList) { + final c = video.codecs!; + if (currentCodes.any(c.startsWith)) { + return video; + } + for (int i = 0; i < bestIndex; i++) { + if (preferCodecs[i].codes.any(c.startsWith)) { + bestIndex = i; + bestVideo = video; + break; + } } } - return video ?? videoList.first; + + if (setCodecs) { + if (bestIndex < preferCodecs.length) { + currentDecodeFormats = preferCodecs[bestIndex]; + } else { + currentDecodeFormats = VideoDecodeFormatType.fromString( + videoList.first.codecs!, + ); + } + } + + return bestVideo ?? videoList.first; } /// 更新画质、音质 @@ -706,11 +712,7 @@ class VideoDetailController extends GetxController ..isBuffering.value = false ..buffered.value = Duration.zero; - final video = findVideoByQa(currentVideoQa.code); - if (firstVideo.codecs != video.codecs) { - currentDecodeFormats = VideoDecodeFormatType.fromString(video.codecs!); - } - firstVideo = video; + firstVideo = findVideoByQa(currentVideoQa.code, setCodecs: true); videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls); /// 根据currentAudioQa 重新设置audioUrl @@ -817,6 +819,7 @@ class VideoDetailController extends GetxController Volume? volume; // 视频链接 + /// TODO: merge [DownloadHttp.getVideoUrl]. Future queryVideoUrl({ bool fromReset = false, bool autoFullScreenFlag = false, @@ -897,7 +900,7 @@ class VideoDetailController extends GetxController quality: videoQuality, ); _setVideoHeight(); - currentDecodeFormats = VideoDecodeFormatType.fromString('avc1'); + currentDecodeFormats = VideoDecodeFormatType.AVC; currentVideoQa.value = videoQuality; await _initPlayerIfNeeded(autoFullScreenFlag); isQuerying = false; @@ -929,42 +932,25 @@ class VideoDetailController extends GetxController } currentVideoQa.value = VideoQuality.fromCode(targetVideoQa); + /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 + final supportFormats = data.supportFormats!; + + // 根据画质选编码格式 + currentDecodeFormats = VideoUtils.selectCodec( + supportFormats + .firstWhere( + (e) => e.quality == targetVideoQa, + orElse: () => supportFormats.first, + ) + .codecs!, + preferCodecs, + ); + /// 取出符合当前画质的videoList - final List videosList = videoList + final videosList = videoList .where((e) => e.quality.code == targetVideoQa) .toList(); - /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 - final List supportFormats = data.supportFormats!; - // 根据画质选编码格式 - final List supportDecodeFormats = supportFormats - .firstWhere( - (e) => e.quality == targetVideoQa, - orElse: () => supportFormats.first, - ) - .codecs!; - // 默认从设置中取AV1 - currentDecodeFormats = VideoDecodeFormatType.fromString(cacheDecode); - VideoDecodeFormatType secondDecodeFormats = - VideoDecodeFormatType.fromString(cacheSecondDecode); - // 当前视频没有对应格式返回第一个 - int flag = 0; - for (final e in supportDecodeFormats) { - if (currentDecodeFormats.codes.any(e.startsWith)) { - flag = 1; - break; - } else if (secondDecodeFormats.codes.any(e.startsWith)) { - flag = 2; - } - } - if (flag == 2) { - currentDecodeFormats = secondDecodeFormats; - } else if (flag == 0) { - currentDecodeFormats = VideoDecodeFormatType.fromString( - supportDecodeFormats.first, - ); - } - /// 取出符合当前解码格式的videoItem firstVideo = videosList.firstWhere( (e) => currentDecodeFormats.codes.any(e.codecs!.startsWith), @@ -1050,16 +1036,14 @@ class VideoDetailController extends GetxController RxList subtitles = RxList(); final Map vttSubtitles = {}; - late final RxInt vttSubtitlesIndex = (-1).obs; - late final RxBool showVP = true.obs; - late final RxList viewPointList = [].obs; + late final vttSubtitlesIndex = (-1).obs; + late final showVP = true.obs; + late final viewPointList = [].obs; // 设定字幕轨道 Future setSubtitle(int index) async { if (index <= 0) { - await plPlayerController.videoPlayerController?.setSubtitleTrack( - SubtitleTrack.no(), - ); + await plPlayerController.videoPlayerController?.setSubtitleTrack(.no()); vttSubtitlesIndex.value = index; return; } @@ -1136,9 +1120,9 @@ class VideoDetailController extends GetxController ); if (res case Success(:final response)) { // interactive video + late final introCtr = Get.find(tag: heroTag); if (isUgc && graphVersion == null) { try { - final introCtr = Get.find(tag: heroTag); if (introCtr.videoDetail.value.rights?.isSteinGate == 1) { graphVersion = response.interaction?.graphVersion; getSteinEdgeInfo(); @@ -1150,22 +1134,18 @@ class VideoDetailController extends GetxController if (isUgc && continuePlayingPart) { continuePlayingPart = false; - try { - UgcIntroController ugcIntroController = Get.find( - tag: heroTag, - ); - if ((ugcIntroController.videoDetail.value.pages?.length ?? 0) > 1 && - response.lastPlayCid != null && - response.lastPlayCid != 0) { - if (response.lastPlayCid != cid.value) { - int index = ugcIntroController.videoDetail.value.pages! - .indexWhere((item) => item.cid == response.lastPlayCid); + final lastCid = response.lastPlayCid; + if (lastCid != null && lastCid != 0 && lastCid != cid.value) { + try { + final pages = introCtr.videoDetail.value.pages; + if (pages != null && pages.length > 1) { + final index = pages.indexWhere((item) => item.cid == lastCid); if (index != -1) { onAddItem(index); } } - } - } catch (_) {} + } catch (_) {} + } } if (plPlayerController.showViewPoints && @@ -1184,27 +1164,53 @@ class VideoDetailController extends GetxController } catch (_) {} } - if (response.subtitle?.subtitles?.isNotEmpty == true) { - subtitles.value = response.subtitle!.subtitles!; - - final idx = switch (Pref.subtitlePreferenceV2) { - SubtitlePrefType.off => 0, - SubtitlePrefType.on => 1, - SubtitlePrefType.withoutAi => - subtitles.first.lan.startsWith('ai') ? 0 : 1, - SubtitlePrefType.auto => - !subtitles.first.lan.startsWith('ai') || - (PlatformUtils.isMobile && - (await FlutterVolumeController.getVolume() ?? 0.0) <= - 0.0) - ? 1 - : 0, - }; - await setSubtitle(idx); + if (response.subtitle?.subtitles case final sub? when (sub.isNotEmpty)) { + _setSubtitle(sub); + } else if (!Accounts.main.isLogin) { + final res = await DmGrpc.dmView(aid, cid.value); + if (res case Success(:final response)) { + if (response.hasSubtitle() && + response.subtitle.subtitles.isNotEmpty) { + _setSubtitle( + response.subtitle.subtitles + .map( + (i) => Subtitle( + lan: i.lan, + lanDoc: i.lanDoc, + subtitleUrl: i.subtitleUrl.replaceFirst( + RegExp('^https?:'), + '', + ), + isAi: i.type == .AI, + ), + ) + .toList() + ..sort(), + ); + } + } else { + res.toast(); + } } } } + Future _setSubtitle(List sub) async { + subtitles.value = sub; + final idx = switch (Pref.subtitlePreferenceV2) { + .off => 0, + .on => 1, + .withoutAi => sub.first.lan.startsWith('ai') ? 0 : 1, + .auto => + !sub.first.lan.startsWith('ai') || + (PlatformUtils.isMobile && + (await FlutterVolumeController.getVolume() ?? 0.0) <= 0.0) + ? 1 + : 0, + }; + await setSubtitle(idx); + } + void updateMediaListHistory(int aid) { if (args['sortField'] != null) { VideoHttp.medialistHistory( diff --git a/lib/pages/video/introduction/pgc/controller.dart b/lib/pages/video/introduction/pgc/controller.dart index 21cb48e02..523ccee7c 100644 --- a/lib/pages/video/introduction/pgc/controller.dart +++ b/lib/pages/video/introduction/pgc/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math' show max; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -123,59 +124,42 @@ class PgcIntroController extends CommonIntroController { '${HttpString.baseUrl}/bangumi/play/ep$epId${videoDetailCtr.playedTimePos}'; showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '复制链接', - style: TextStyle(fontSize: 14), - ), - onTap: () { + children: [ + DialogOption( + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + Utils.copyText(videoUrl); + }, + ), + DialogOption( + child: const Text('其它app打开', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + PageUtils.launchURL(videoUrl); + }, + ), + if (PlatformUtils.isMobile) + DialogOption( + child: const Text('分享视频', style: TextStyle(fontSize: 14)), + onPressed: () { + final item = pgcItem.episodes?.firstWhereOrNull( + (item) => item.epId == epId, + ); Get.back(); - Utils.copyText(videoUrl); + ShareUtils.shareText( + '${pgcItem.title}${item != null ? ' ${item.showTitle}' : ''}' + ' - $videoUrl', + ); }, ), - ListTile( - dense: true, - title: const Text( - '其它app打开', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - PageUtils.launchURL(videoUrl); - }, - ), - if (PlatformUtils.isMobile) - ListTile( - dense: true, - title: const Text( - '分享视频', - style: TextStyle(fontSize: 14), - ), - onTap: () { - final item = pgcItem.episodes?.firstWhereOrNull( - (item) => item.epId == epId, - ); - Get.back(); - ShareUtils.shareText( - '${pgcItem.title}${item != null ? ' ${item.showTitle}' : ''}' - ' - $videoUrl', - ); - }, - ), - ListTile( - dense: true, - title: const Text( - '分享至动态', - style: TextStyle(fontSize: 14), - ), - onTap: () { + if (isLogin) + DialogOption( + child: const Text('分享至动态', style: TextStyle(fontSize: 14)), + onPressed: () { Get.back(); final item = pgcItem.episodes?.firstWhereOrNull( (item) => item.epId == epId, @@ -186,15 +170,15 @@ class PgcIntroController extends CommonIntroController { useSafeArea: true, builder: (context) => RepostPanel( rid: epId, - /** - * 1:番剧 // 4097 - 2:电影 // 4098 - 3:纪录片 // 4101 - 4:国创 // 4100 - 5:电视剧 // 4099 - 6:漫画 - 7:综艺 // 4099 - */ + /* + 1:番剧 // 4097 + 2:电影 // 4098 + 3:纪录片 // 4101 + 4:国创 // 4100 + 5:电视剧 // 4099 + 6:漫画 + 7:综艺 // 4099 + */ dynType: switch (pgcItem.type) { 1 => 4097, 2 => 4098, @@ -211,16 +195,16 @@ class PgcIntroController extends CommonIntroController { ); }, ), - ListTile( - dense: true, - title: const Text( + if (isLogin) + DialogOption( + child: const Text( '分享至消息', style: TextStyle(fontSize: 14), ), - onTap: () { + onPressed: () { Get.back(); try { - EpisodeItem item = pgcItem.episodes!.firstWhere( + final item = pgcItem.episodes!.firstWhere( (item) => item.epId == epId, ); final title = @@ -252,8 +236,7 @@ class PgcIntroController extends CommonIntroController { } }, ), - ], - ), + ], ), ); } diff --git a/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart b/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart index 2fec611e3..da1d1d610 100644 --- a/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart +++ b/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart @@ -13,6 +13,7 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; class PgcPanel extends StatefulWidget { @@ -213,11 +214,10 @@ class _PgcPanelState extends State { if (item.badge?.isNotEmpty == true) ...[ const SizedBox(width: 2), if (item.badge == '会员') - Image.asset( + SvgPicture.asset( Assets.vipIcon, height: 16, - cacheHeight: 16.cacheSize(context), - semanticLabel: "大会员", + semanticsLabel: "大会员", ) else Text( diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index dc1b10c92..cbfcc2eda 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -292,60 +292,59 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { String videoUrl = '${HttpString.baseUrl}/video/$bvid'; showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ + children: [ + ListTile( + dense: true, + title: const Text( + '复制链接', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + Utils.copyText(videoUrl); + }, + trailing: playedTimePos.isNotEmpty + ? iconButton( + tooltip: '精确分享', + icon: const Icon(Icons.timer_outlined), + onPressed: () { + Get.back(); + Utils.copyText('$videoUrl$playedTimePos'); + }, + ) + : null, + ), + ListTile( + dense: true, + title: const Text( + '其它app打开', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + PageUtils.launchURL(videoUrl); + }, + ), + if (PlatformUtils.isMobile) ListTile( dense: true, title: const Text( - '复制链接', + '分享视频', style: TextStyle(fontSize: 14), ), onTap: () { Get.back(); - Utils.copyText(videoUrl); - }, - trailing: playedTimePos.isNotEmpty - ? iconButton( - tooltip: '精确分享', - icon: const Icon(Icons.timer_outlined), - onPressed: () { - Get.back(); - Utils.copyText('$videoUrl$playedTimePos'); - }, - ) - : null, - ), - ListTile( - dense: true, - title: const Text( - '其它app打开', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - PageUtils.launchURL(videoUrl); + ShareUtils.shareText( + '${videoDetail.title} ' + 'UP主: ${videoDetail.owner!.name!}' + ' - $videoUrl', + ); }, ), - if (PlatformUtils.isMobile) - ListTile( - dense: true, - title: const Text( - '分享视频', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - ShareUtils.shareText( - '${videoDetail.title} ' - 'UP主: ${videoDetail.owner!.name!}' - ' - $videoUrl', - ); - }, - ), + if (isLogin) ListTile( dense: true, title: const Text( @@ -368,6 +367,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { ); }, ), + if (isLogin) ListTile( dense: true, title: const Text( @@ -394,8 +394,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { } }, ), - ], - ), + ], ), ); } diff --git a/lib/pages/video/member/view.dart b/lib/pages/video/member/view.dart index 0893c101e..4fe42183c 100644 --- a/lib/pages/video/member/view.dart +++ b/lib/pages/video/member/view.dart @@ -20,7 +20,6 @@ import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/pages/video/member/controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -257,13 +256,10 @@ class _HorizontalMemberPageState extends State { ), ), const SizedBox(width: 8), - Image.asset( - BiliUtils.levelName( - memberInfoModel.level!, - isSeniorMember: memberInfoModel.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + memberInfoModel.level!, + isSeniorMember: memberInfoModel.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/video/note/view.dart b/lib/pages/video/note/view.dart index cdd3c97ca..ca8bdab95 100644 --- a/lib/pages/video/note/view.dart +++ b/lib/pages/video/note/view.dart @@ -10,7 +10,6 @@ import 'package:PiliPlus/pages/video/note/controller.dart'; import 'package:PiliPlus/pages/webview/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; @@ -263,13 +262,10 @@ class _NoteListPageState extends State ), ), const SizedBox(width: 6), - Image.asset( - BiliUtils.levelName( - item.author!.level!, - isSeniorMember: item.author!.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + item.author!.level!, + isSeniorMember: item.author!.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart index c4e1c3c89..7da217e0c 100644 --- a/lib/pages/video/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart @@ -184,13 +184,10 @@ class ReplyItemGrpc extends StatelessWidget { ), ), ), - Image.asset( - BiliUtils.levelName( - member.level, - isSeniorMember: member.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + member.level.toInt(), + isSeniorMember: member.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), if (replyItem.mid == upMid) const PBadge( diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 472173c52..e431a48b3 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -12,6 +12,7 @@ import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/route_aware_mixin.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart'; +import 'package:PiliPlus/common/widgets/svg/play_icon.dart'; import 'package:PiliPlus/models/common/episode_panel_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc; @@ -51,7 +52,6 @@ import 'package:PiliPlus/services/shutdown_timer_service.dart' show shutdownTimerService; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/android/bindings.g.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; @@ -1189,12 +1189,7 @@ class _VideoDetailPageVState extends State child: IconButton( tooltip: '播放', onPressed: handlePlay, - icon: Image.asset( - Assets.play, - width: 60, - height: 60, - cacheHeight: 60.cacheSize(context), - ), + icon: const PlayIcon(), ), ), ], diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index 26d54a555..294d16703 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -110,7 +110,7 @@ class _ViewPointsPageState extends State } } final isCurr = currentIndex == index; - return _buildItem(theme, segment, isCurr); + return _buildItem(theme.colorScheme, segment, isCurr); }, ); if (_isNested) { @@ -122,8 +122,11 @@ class _ViewPointsPageState extends State return child; } - Widget _buildItem(ThemeData theme, ViewPointSegment segment, bool isCurr) { - final theme = Theme.of(context); + Widget _buildItem( + ColorScheme colorScheme, + ViewPointSegment segment, + bool isCurr, + ) { return Material( type: MaterialType.transparency, child: InkWell( @@ -137,18 +140,11 @@ class _ViewPointsPageState extends State } : null, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Style.safeSpace, - vertical: 5, - ), + padding: const .symmetric(horizontal: Style.safeSpace, vertical: 5), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - NetworkImgLayer( - src: segment.url, - width: 140.8, - height: 88, - ), + NetworkImgLayer(src: segment.url, width: 140.8, height: 88), const SizedBox(width: 10), Expanded( child: Column( @@ -163,14 +159,14 @@ class _ViewPointsPageState extends State style: isCurr ? TextStyle( fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, + color: colorScheme.primary, ) : null, ), Text( '${segment.from != null ? DurationUtils.formatDuration(segment.from) : ''} - ' '${segment.to != null ? DurationUtils.formatDuration(segment.to) : ''}', - style: TextStyle(color: theme.colorScheme.outline), + style: TextStyle(color: colorScheme.outline), ), ], ), diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 5d1bcca05..b407fda3b 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/danmaku_block.dart'; @@ -789,9 +790,7 @@ class HeaderControlState extends State content: Material( type: MaterialType.transparency, child: ListTileTheme( - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - ), + contentPadding: const .symmetric(horizontal: 24), child: SingleChildScrollView( child: Column( children: [ @@ -1071,28 +1070,26 @@ class HeaderControlState extends State // 选择解码格式 void showSetDecodeFormats() { - final VideoItem firstVideo = videoDetailCtr.firstVideo; + final firstCode = videoDetailCtr.firstVideo.quality.code; // 当前视频可用的解码格式 - final List videoFormat = videoInfo.supportFormats!; - final List? list = videoFormat - .firstWhere((FormatItem e) => e.quality == firstVideo.quality.code) - .codecs; + final videoFormat = videoInfo.supportFormats!; + + final list = videoFormat.firstWhere((e) => e.quality == firstCode).codecs; if (list == null) { SmartDialog.showToast('当前视频不支持选择解码格式'); return; } // 当前选中的解码格式 - final VideoDecodeFormatType currentDecodeFormats = - videoDetailCtr.currentDecodeFormats; + final curCodecs = videoDetailCtr.currentDecodeFormats.codes; showBottomSheet( (context, setState) { - final theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return Padding( padding: const EdgeInsets.all(12), child: Material( clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surface, + color: colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), child: Column( children: [ @@ -1110,30 +1107,22 @@ class HeaderControlState extends State itemBuilder: (context, index) { final item = list[index]; final format = VideoDecodeFormatType.fromString(item); - final isCurr = currentDecodeFormats.codes.any( - item.startsWith, - ); + final isCurr = curCodecs.any(item.startsWith); return ListTile( dense: true, onTap: () { - if (isCurr) { - return; - } + if (isCurr) return; Get.back(); videoDetailCtr ..currentDecodeFormats = format ..updatePlayer(); + SmartDialog.showToast("解码已变为:${format.name}"); }, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - ), + contentPadding: const .symmetric(horizontal: 20), title: Text(format.description), subtitle: Text(item, style: subTitleStyle), trailing: isCurr - ? Icon( - Icons.done, - color: theme.colorScheme.primary, - ) + ? Icon(Icons.done, color: colorScheme.primary) : null, ); }, @@ -1149,69 +1138,100 @@ class HeaderControlState extends State ); } + Future<_SubtitleFormat?> _showFormatDialog() { + return showDialog<_SubtitleFormat>( + context: context, + builder: (context) => SimpleDialog( + title: const Text('选择格式'), + children: [ + DialogOption( + onPressed: () => Get.back(result: _SubtitleFormat.json), + child: const Text('JSON'), + ), + DialogOption( + onPressed: () => Get.back(result: _SubtitleFormat.vtt), + child: const Text('WEBVTT'), + ), + ], + ), + ); + } + void onExportSubtitle() { showDialog( context: context, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), - title: const Text('保存字幕'), - content: SingleChildScrollView( - child: Column( - children: videoDetailCtr.subtitles - .map( - (item) => ListTile( - dense: true, - onTap: () async { - Get.back(); - final url = item.subtitleUrl; - if (url == null || url.isEmpty) return; - try { - final res = await Request.dio.get( - url.http2https, - options: Options( - responseType: ResponseType.bytes, - headers: Constants.baseHeaders, - extra: {'account': const NoAccount()}, - ), + builder: (context) { + final subtitles = videoDetailCtr.subtitles; + return SimpleDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const .only(bottom: 12), + titlePadding: const .fromLTRB(20, 20, 20, 12), + title: const Text('保存字幕'), + children: List.generate(subtitles.length, (i) { + final item = subtitles[i]; + return DialogOption( + onPressed: () async { + Get.back(); + final url = item.subtitleUrl; + if (url == null || url.isEmpty) return; + final format = await _showFormatDialog(); + if (format == null) return; + try { + final Uint8List bytes; + switch (format) { + case .vtt: + var subtitle = videoDetailCtr.vttSubtitles[i]; + if (subtitle == null) { + final res = await VideoHttp.vttSubtitles( + item.subtitleUrl!, ); - if (res.statusCode == 200) { - final bytes = Uint8List.fromList( - Request.responseBytesDecoder( - res.data!, - res.headers.map, - ), - ); - String name = - '${introController.videoDetail.value.title}-${videoDetailCtr.bvid}-${videoDetailCtr.cid.value}-${item.lanDoc}.json'; - if (Platform.isWindows) { - // Reserved characters may not be used in file names. See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions - name = name.replaceAll( - RegExp(r'[<>:/\\|?*"]'), - '', - ); - } - StorageUtils.saveBytes2File( - name: name, - bytes: bytes, - allowedExtensions: const ['json'], - ); - } - } catch (e, s) { - Utils.reportError(e, s); - SmartDialog.showToast(e.toString()); + if (res == null) return; + subtitle = (isData: true, id: res); + videoDetailCtr.vttSubtitles[i] = subtitle; } - }, - title: Text( - item.lanDoc!, - style: const TextStyle(fontSize: 14), - ), - ), - ) - .toList(), - ), - ), - ), + bytes = utf8.encode(subtitle.id); + case .json: + final res = await Request.dio.get( + url.http2https, + options: Options( + responseType: ResponseType.bytes, + headers: Constants.baseHeaders, + extra: {'account': const NoAccount()}, + ), + ); + if (res.statusCode != 200) return; + bytes = Uint8List.fromList( + Request.responseBytesDecoder( + res.data!, + res.headers.map, + ), + ); + } + String name = + '${introController.videoDetail.value.title}-${videoDetailCtr.bvid}-${videoDetailCtr.cid.value}-${item.lanDoc}.${format.name}'; + // Reserved characters may not be used in file names. See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + name = name.replaceAll( + Platform.isWindows ? RegExp(r'[<>:/\\|?*"]') : '/', + '_', + ); + StorageUtils.saveBytes2File( + name: name, + bytes: bytes, + allowedExtensions: [format.name], + ); + } catch (e, s) { + Utils.reportError(e, s); + SmartDialog.showToast(e.toString()); + } + }, + child: Text( + item.lanDoc ?? item.lan, + style: const TextStyle(fontSize: 14), + ), + ); + }), + ); + }, ); } @@ -2065,3 +2085,5 @@ class HeaderControlState extends State ); } } + +enum _SubtitleFormat { json, vtt } diff --git a/lib/pages/whisper/widgets/item.dart b/lib/pages/whisper/widgets/item.dart index cb0b1c265..98f2d9b53 100644 --- a/lib/pages/whisper/widgets/item.dart +++ b/lib/pages/whisper/widgets/item.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' @@ -56,48 +57,39 @@ class WhisperSessionItem extends StatelessWidget { : null, onLongPress: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: DefaultTextStyle( - style: const TextStyle(fontSize: 14), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () { - Get.back(); - onSetTop(item.isPinned, item.id); - }, - title: Text(item.isPinned ? '移除置顶' : '置顶'), - ), - if (item.id.privateId.hasTalkerUid()) - ListTile( - dense: true, - onTap: () { - Get.back(); - onSetMute(item.isMuted, item.id.privateId.talkerUid); - }, - title: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), - ), - if (item.id.privateId.hasTalkerUid()) - ListTile( - dense: true, - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('确定删除该对话?'), - onConfirm: () => - onRemove(item.id.privateId.talkerUid.toInt()), - ); - }, - title: const Text('删除'), - ), - ], + children: [ + DialogOption( + onPressed: () { + Get.back(); + onSetTop(item.isPinned, item.id); + }, + child: Text(item.isPinned ? '移除置顶' : '置顶'), ), - ), + if (item.id.privateId.hasTalkerUid()) + DialogOption( + onPressed: () { + Get.back(); + onSetMute(item.isMuted, item.id.privateId.talkerUid); + }, + child: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), + ), + if (item.id.privateId.hasTalkerUid()) + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('确定删除该对话?'), + onConfirm: () => + onRemove(item.id.privateId.talkerUid.toInt()), + ); + }, + child: const Text('删除'), + ), + ], ), ), onSecondaryTapUp: PlatformUtils.isDesktop diff --git a/lib/pages/whisper_settings/view.dart b/lib/pages/whisper_settings/view.dart index 070a983f6..ae1a77703 100644 --- a/lib/pages/whisper_settings/view.dart +++ b/lib/pages/whisper_settings/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' show IMSettingType, Setting; @@ -82,49 +83,45 @@ class _WhisperSettingsPageState extends State { String? selected; showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: item.redirect.windowSelect.item.map( - (e) { - if (e.selected) { - selected ??= e.text; - } - return ListTile( - dense: true, - onTap: () async { - if (!e.selected) { - Get.back(); - for (final j in item.redirect.windowSelect.item) { - j.selected = false; - } - item.redirect.selectedSummary = e.text; - e.selected = true; - _controller.loadingState.refresh(); - final settings = {key: item}; - final res = await _controller.onSet(settings); - if (!res) { - for (final j in item.redirect.windowSelect.item) { - j.selected = j.text == selected; - } - item.redirect.selectedSummary = selected!; - _controller.loadingState.refresh(); - } + children: item.redirect.windowSelect.item.map( + (e) { + if (e.selected) { + selected ??= e.text; + } + return DialogOption( + onPressed: () async { + if (!e.selected) { + Get.back(); + for (final j in item.redirect.windowSelect.item) { + j.selected = false; } - }, - title: Text( - e.text, - style: TextStyle( - fontSize: 14, - color: e.selected ? theme.colorScheme.primary : null, - ), + item.redirect.selectedSummary = e.text; + e.selected = true; + _controller.loadingState.refresh(); + final settings = {key: item}; + final res = await _controller.onSet(settings); + if (!res) { + for (final j in item.redirect.windowSelect.item) { + j.selected = j.text == selected; + } + item.redirect.selectedSummary = selected!; + _controller.loadingState.refresh(); + } + } + }, + child: Text( + e.text, + style: TextStyle( + fontSize: 14, + color: e.selected ? theme.colorScheme.primary : null, ), - ); - }, - ).toList(), - ), + ), + ); + }, + ).toList(), ), ); } else if (item.redirect.otherPage.hasUrl()) { diff --git a/lib/plugin/pl_player/view/view.dart b/lib/plugin/pl_player/view/view.dart index 901add55e..88a17ab68 100644 --- a/lib/plugin/pl_player/view/view.dart +++ b/lib/plugin/pl_player/view/view.dart @@ -724,19 +724,16 @@ class _PLVideoPlayerState extends State ), ), ), - ...videoDetailController.subtitles.indexed.map((e) { + ...videoDetailController.subtitles.mapIndexed((i, e) { return PopupMenuItem( - value: e.$1 + 1, + value: i + 1, height: 35, - onTap: () => videoDetailController.setSubtitle(e.$1 + 1), + onTap: () => videoDetailController.setSubtitle(i + 1), child: Text( - "${e.$2.lanDoc}", + e.lanDoc ?? e.lan, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - ), + style: const .new(color: Colors.white, fontSize: 13), ), ); }), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index d26c3d522..1fd642e44 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,4 +1,3 @@ -import 'package:PiliPlus/pages/about/view.dart'; import 'package:PiliPlus/pages/article/view.dart'; import 'package:PiliPlus/pages/article_list/view.dart'; import 'package:PiliPlus/pages/audio/view.dart'; @@ -53,18 +52,12 @@ import 'package:PiliPlus/pages/popular_series/view.dart'; import 'package:PiliPlus/pages/search/view.dart'; import 'package:PiliPlus/pages/search_result/view.dart'; import 'package:PiliPlus/pages/search_trending/view.dart'; -import 'package:PiliPlus/pages/setting/extra_setting.dart'; import 'package:PiliPlus/pages/setting/pages/bar_set.dart'; import 'package:PiliPlus/pages/setting/pages/color_select.dart'; import 'package:PiliPlus/pages/setting/pages/display_mode.dart'; import 'package:PiliPlus/pages/setting/pages/font_size_select.dart'; import 'package:PiliPlus/pages/setting/pages/logs.dart'; import 'package:PiliPlus/pages/setting/pages/play_speed_set.dart'; -import 'package:PiliPlus/pages/setting/play_setting.dart'; -import 'package:PiliPlus/pages/setting/privacy_setting.dart'; -import 'package:PiliPlus/pages/setting/recommend_setting.dart'; -import 'package:PiliPlus/pages/setting/style_setting.dart'; -import 'package:PiliPlus/pages/setting/video_setting.dart'; import 'package:PiliPlus/pages/setting/view.dart'; import 'package:PiliPlus/pages/settings_search/view.dart'; import 'package:PiliPlus/pages/space_setting/view.dart'; @@ -72,7 +65,6 @@ import 'package:PiliPlus/pages/sponsor_block/view.dart'; import 'package:PiliPlus/pages/subscription/view.dart'; import 'package:PiliPlus/pages/subscription_detail/view.dart'; import 'package:PiliPlus/pages/video/view.dart'; -import 'package:PiliPlus/pages/webdav/view.dart'; import 'package:PiliPlus/pages/webview/view.dart'; import 'package:PiliPlus/pages/whisper/view.dart'; import 'package:PiliPlus/pages/whisper_detail/view.dart'; @@ -116,26 +108,12 @@ class Routes { // 用户中心 GetPage(name: '/member', page: () => const MemberPage()), GetPage(name: '/memberSearch', page: () => const MemberSearchPage()), - // 推荐流设置 - GetPage(name: '/recommendSetting', page: () => const RecommendSetting()), - // 音视频设置 - GetPage(name: '/videoSetting', page: () => const VideoSetting()), - // 播放器设置 - GetPage(name: '/playSetting', page: () => const PlaySetting()), - // 外观设置 - GetPage(name: '/styleSetting', page: () => const StyleSetting()), - // 隐私设置 - GetPage(name: '/privacySetting', page: () => const PrivacySetting()), - // 其它设置 - GetPage(name: '/extraSetting', page: () => const ExtraSetting()), // GetPage(name: '/blackListPage', page: () => const BlackListPage()), GetPage(name: '/colorSetting', page: () => const ColorSelectPage()), GetPage(name: '/fontSizeSetting', page: () => const FontSizeSelectPage()), // 屏幕帧率 GetPage(name: '/displayModeSetting', page: () => const SetDisplayMode()), - // 关于 - GetPage(name: '/about', page: () => const AboutPage()), // GetPage(name: '/articlePage', page: () => const ArticlePage()), @@ -174,7 +152,6 @@ class Routes { GetPage(name: '/createFav', page: () => const CreateFavPage()), GetPage(name: '/editProfile', page: () => const EditProfilePage()), GetPage(name: '/settingsSearch', page: () => const SettingsSearchPage()), - GetPage(name: '/webdavSetting', page: () => const WebDavSettingPage()), GetPage(name: '/searchTrending', page: () => const SearchTrendingPage()), GetPage(name: '/dynTopic', page: () => const DynTopicPage()), GetPage(name: '/articleList', page: () => const ArticleListPage()), diff --git a/lib/utils/bili_utils.dart b/lib/utils/bili_utils.dart index 96779e859..1555079c1 100644 --- a/lib/utils/bili_utils.dart +++ b/lib/utils/bili_utils.dart @@ -1,3 +1,6 @@ +import 'package:PiliPlus/common/widgets/svg/level_icon.dart'; +import 'package:flutter/material.dart'; + abstract final class BiliUtils { static bool isDefaultFav(int? attr) { if (attr == null) { @@ -21,8 +24,12 @@ abstract final class BiliUtils { return tagid != null && tagid != 0 && tagid != -10 && tagid != -2; } - static String levelName( - Object level, { + // https://s1.hdslb.com/bfs/svg-next/font/2025-10-27/freshspace-zpjpp3aqht.css + static Widget levelPicture( + int level, { bool isSeniorMember = false, - }) => 'assets/images/lv/lv${isSeniorMember ? '6_s' : level}.png'; + double height = 11, + }) { + return UserLevel(level, height: height, flash: isSeniorMember); + } } diff --git a/lib/utils/cache_manager.dart b/lib/utils/cache_manager.dart index 024e90c48..6e8c3027c 100644 --- a/lib/utils/cache_manager.dart +++ b/lib/utils/cache_manager.dart @@ -1,6 +1,5 @@ import 'dart:io' show Directory, File; -import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; @@ -10,18 +9,19 @@ import 'package:path_provider/path_provider.dart'; abstract final class CacheManager { static late final DefaultCacheManager manager; - static Future ensureInitialized() => - DefaultCacheManager.init().then((i) => manager = i); + static Future ensureInitialized() => DefaultCacheManager.init( + maxNrOfCacheLength: Pref.maxCacheSize.toInt(), + ).then((i) => manager = i); // 获取缓存目录 @pragma('vm:notify-debugger-on-exception') static Future loadApplicationCache() async { try { - final Directory tempDirectory = await getTemporaryDirectory(); if (PlatformUtils.isDesktop) { return manager.getTotalLength(); } + final Directory tempDirectory = await getTemporaryDirectory(); if (tempDirectory.existsSync()) { return await getTotalSizeOfFilesInDir(tempDirectory); } @@ -81,22 +81,4 @@ abstract final class CacheManager { } } catch (_) {} } - - static Future autoClearCache() async { - // TODO: remove - Directory( - '${(await getTemporaryDirectory()).path}/libCachedImageData', - ).tryDel(recursive: true); - if (Pref.autoClearCache) { - await clearLibraryCache(); - } else { - final maxCacheSize = Pref.maxCacheSize; - if (maxCacheSize != 0) { - final currCache = await loadApplicationCache(); - if (currCache >= maxCacheSize) { - await clearLibraryCache(); - } - } - } - } } diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index 0ffa177f6..953882afb 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -3,6 +3,7 @@ import 'dart:convert' show ascii, base64; import 'package:PiliPlus/utils/utils.dart'; +import 'package:collection/collection.dart'; import 'package:uuid/v4.dart'; abstract final class IdUtils { @@ -24,12 +25,6 @@ abstract final class IdUtils { static final avRegexExact = RegExp(r'^av(\d+)$', caseSensitive: false); static final digitOnlyRegExp = RegExp(r'^\d+$'); - static void swap(List list, int idx1, int idx2) { - final idx1Value = list[idx1]; - list[idx1] = list[idx2]; - list[idx2] = idx1Value; - } - /// av转bv static String av2bv(int aid) { final bytes = ['B', 'V', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0']; @@ -40,18 +35,18 @@ abstract final class IdUtils { tmp ~/= BASE; } - swap(bytes, 3, 9); - swap(bytes, 4, 7); + bytes + ..swap(3, 9) + ..swap(4, 7); return bytes.join(); } /// bv转av static int bv2av(String bvid) { - final bvidArr = bvid.codeUnits.sublist(3); - - swap(bvidArr, 0, 6); - swap(bvidArr, 1, 4); + final bvidArr = bvid.codeUnits.sublist(3) + ..swap(0, 6) + ..swap(1, 4); final tmp = bvidArr.fold(0, (pre, char) => pre * BASE + invData[char]!); return (tmp & MASK_CODE) ^ XOR_CODE; diff --git a/lib/utils/request_utils.dart b/lib/utils/request_utils.dart index 9deb098a4..36a3dc3cd 100644 --- a/lib/utils/request_utils.dart +++ b/lib/utils/request_utils.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -169,99 +170,84 @@ abstract final class RequestUtils { String text = isSpecialFollowed ? '移除特别关注' : '加入特别关注'; showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () async { - Get.back(); - final res = await MemberHttp.specialAction( - fid: mid, - isAdd: !isSpecialFollowed, - ); - if (res.isSuccess) { - SmartDialog.showToast('$text成功'); - afterMod?.call(isSpecialFollowed ? 2 : -10); - } else { - res.toast(); - } - }, - title: Text( - text, - style: const TextStyle(fontSize: 14), - ), - ), - ListTile( - dense: true, - onTap: () async { - Get.back(); - final result = await showModalBottomSheet>( - context: context, - useSafeArea: true, - isScrollControlled: true, - constraints: BoxConstraints( - maxWidth: min(640, context.mediaQueryShortestSide), - ), - builder: (BuildContext context) { - final maxChildSize = - PlatformUtils.isMobile && - !context.mediaQuerySize.isPortrait - ? 1.0 - : 0.7; - return DraggableScrollableSheet( - minChildSize: 0, - maxChildSize: 1, - snap: true, - expand: false, - snapSizes: [maxChildSize], - initialChildSize: maxChildSize, - builder: (context, scrollController) { - return GroupPanel( - mid: mid, - tags: followStatus!.tag, - scrollController: scrollController, - ); - }, - ); - }, - ); - if (result != null) { - followStatus!.tag = result.toList(); - afterMod?.call(result.contains(-10) ? -10 : 2); - } - }, - title: const Text( - '设置分组', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - dense: true, - onTap: () async { - Get.back(); - final res = await VideoHttp.relationMod( - mid: mid, - act: 2, - reSrc: 11, - ); - if (res.isSuccess) { - SmartDialog.showToast('取消关注成功'); - afterMod?.call(0); - } else { - res.toast(); - } - }, - title: const Text( - '取消关注', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + children: [ + DialogOption( + onPressed: () async { + Get.back(); + final res = await MemberHttp.specialAction( + fid: mid, + isAdd: !isSpecialFollowed, + ); + if (res.isSuccess) { + SmartDialog.showToast('$text成功'); + afterMod?.call(isSpecialFollowed ? 2 : -10); + } else { + res.toast(); + } + }, + child: Text(text, style: const TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final result = await showModalBottomSheet>( + context: context, + useSafeArea: true, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: min(640, context.mediaQueryShortestSide), + ), + builder: (BuildContext context) { + final maxChildSize = + PlatformUtils.isMobile && + !context.mediaQuerySize.isPortrait + ? 1.0 + : 0.7; + return DraggableScrollableSheet( + minChildSize: 0, + maxChildSize: 1, + snap: true, + expand: false, + snapSizes: [maxChildSize], + initialChildSize: maxChildSize, + builder: (context, scrollController) { + return GroupPanel( + mid: mid, + tags: followStatus!.tag, + scrollController: scrollController, + ); + }, + ); + }, + ); + if (result != null) { + followStatus!.tag = result.toList(); + afterMod?.call(result.contains(-10) ? -10 : 2); + } + }, + child: const Text('设置分组', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final res = await VideoHttp.relationMod( + mid: mid, + act: 2, + reSrc: 11, + ); + if (res.isSuccess) { + SmartDialog.showToast('取消关注成功'); + afterMod?.call(0); + } else { + res.toast(); + } + }, + child: const Text('取消关注', style: TextStyle(fontSize: 14)), + ), + ], ), ); } diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 7b9eb1d74..752859b51 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -8,8 +8,7 @@ abstract final class SettingBoxKey { defaultAudioQaCellular = 'defaultAudioQaCellular', autoPlayEnable = 'autoPlayEnable', fullScreenMode = 'fullScreenMode', - defaultDecode = 'defaultDecode', - secondDecode = 'secondDecode', + preferCodecs = 'preferCodecs', defaultToastOp = 'defaultToastOp', defaultPicQa = 'defaultPicQa', enableHA = 'enableHA', @@ -56,7 +55,6 @@ abstract final class SettingBoxKey { banWordForRecommend = 'banWordForRecommend', applyFilterToRelatedVideos = 'applyFilterToRelatedVideos', autoUpdate = 'autoUpdate', - autoClearCache = 'autoClearCache', maxCacheSize = 'maxCacheSize', defaultShowComment = 'defaultShowComment', replySortType = 'replySortType', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 4fc4c1b56..14cdff96a 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math' show pow; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart' show deviceTouchSlop; @@ -246,15 +245,33 @@ abstract final class Pref { defaultValue: AudioQuality.k192.code, ); - static String get defaultDecode => _setting.get( - SettingBoxKey.defaultDecode, - defaultValue: VideoDecodeFormatType.AVC.codes.first, - ); + static List get preferCodecs { + // TODO: remove next 2 version + if (_setting.get('defaultDecode') case String codecStr) { + String? codecStr2 = _setting.get('secondDecode'); + _setting.deleteAll(const ['defaultDecode', 'secondDecode']); + final codecs = [ + VideoDecodeFormatType.values.firstWhere( + (i) => i.codes.contains(codecStr), + ), + if (codecStr2 != null && codecStr2 != codecStr) + VideoDecodeFormatType.values.firstWhere( + (i) => i.codes.contains(codecStr2), + ), + ]; + _setting.put( + SettingBoxKey.preferCodecs, + codecs.map((i) => i.name).toList(), + ); + return codecs; + } - static String get secondDecode => _setting.get( - SettingBoxKey.secondDecode, - defaultValue: VideoDecodeFormatType.AV1.codes.first, - ); + final codecs = _setting.get(SettingBoxKey.preferCodecs); + if (codecs is List && codecs.isNotEmpty) { + return codecs.map((i) => VideoDecodeFormatType.values.byName(i)).toList(); + } + return const []; + } static String get hardwareDecoding => _setting.get( SettingBoxKey.hardwareDecoding, @@ -598,7 +615,7 @@ abstract final class Pref { _setting.get(SettingBoxKey.showPgcTimeline, defaultValue: true); static num get maxCacheSize => - _setting.get(SettingBoxKey.maxCacheSize) ?? pow(1024, 3); + _setting.get(SettingBoxKey.maxCacheSize) ?? 1 << 30; static bool get optTabletNav => _setting.get(SettingBoxKey.optTabletNav, defaultValue: true); @@ -717,9 +734,6 @@ abstract final class Pref { !Platform.isIOS && _setting.get(SettingBoxKey.dynamicColor, defaultValue: true); - static bool get autoClearCache => - _setting.get(SettingBoxKey.autoClearCache, defaultValue: false); - static bool get enableSystemProxy => _setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false); diff --git a/lib/utils/video_utils.dart b/lib/utils/video_utils.dart index fa66cb20b..5f080810f 100644 --- a/lib/utils/video_utils.dart +++ b/lib/utils/video_utils.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/models/common/video/cdn_type.dart'; +import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; @@ -93,4 +94,28 @@ abstract final class VideoUtils { final urlInfo = e.urlInfo.getOrFirst(index); return (liveCdnUrl ?? urlInfo.host) + e.baseUrl + urlInfo.extra; } + + static VideoDecodeFormatType selectCodec( + Iterable codecs, + List preferCodecs, + ) { + if (preferCodecs.isNotEmpty) { + int bestIndex = preferCodecs.length; + for (final e in codecs) { + for (int i = 0; i < bestIndex; i++) { + if (preferCodecs[i].codes.any(e.startsWith)) { + bestIndex = i; + if (bestIndex == 0) { + return preferCodecs[0]; + } + break; + } + } + } + if (bestIndex < preferCodecs.length) { + return preferCodecs[bestIndex]; + } + } + return VideoDecodeFormatType.fromString(codecs.first); + } } diff --git a/pubspec.lock b/pubspec.lock index f16f4c5e7..92fd9bcea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -207,17 +207,17 @@ packages: description: path: cached_network_image ref: develop - resolved-ref: "5b79ff9f53a9a55b57d4d6fc52eb529fdfdb30d7" - url: "https://github.com/bggRGjQaUbCoE/flutter_cached_network_image_ce.git" + resolved-ref: "6ad91e9bf71254803e222b7d254751576f2f6d7b" + url: "https://github.com/My-Responsitories/flutter_cached_network_image_ce.git" source: git version: "4.6.4" cached_network_image_platform_interface_ce: dependency: transitive description: path: cached_network_image_platform_interface - ref: "5b79ff9f53a9a55b57d4d6fc52eb529fdfdb30d7" - resolved-ref: "5b79ff9f53a9a55b57d4d6fc52eb529fdfdb30d7" - url: "https://github.com/bggRGjQaUbCoE/flutter_cached_network_image_ce.git" + ref: "6ad91e9bf71254803e222b7d254751576f2f6d7b" + resolved-ref: "6ad91e9bf71254803e222b7d254751576f2f6d7b" + url: "https://github.com/My-Responsitories/flutter_cached_network_image_ce.git" source: git version: "5.2.0" canvas_danmaku: diff --git a/pubspec.yaml b/pubspec.yaml index 72a02fc64..79f61bb12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: brotli: ^0.6.0 cached_network_image_ce: git: - url: https://github.com/bggRGjQaUbCoE/flutter_cached_network_image_ce.git + url: https://github.com/My-Responsitories/flutter_cached_network_image_ce.git path: cached_network_image ref: develop canvas_danmaku: @@ -224,6 +224,11 @@ dependency_overrides: url: https://github.com/bggRGjQaUbCoE/screen_brightness.git path: screen_brightness_android ref: main + cached_network_image_ce: + git: + url: https://github.com/My-Responsitories/flutter_cached_network_image_ce.git + path: cached_network_image + ref: develop dev_dependencies: flutter_test: @@ -265,7 +270,6 @@ flutter: assets: - path: assets/images/ - - path: assets/images/lv/ - path: assets/images/logo/ - path: assets/images/logo/ico/ platforms: [windows]