?$iyL%GiyV^Z)bRYX1hZ{XamIYaN zIDZkEK(@-pcC#aRH7;cP3(zN3trdMA&KG_zdbxLSmlCnukD26LjfI3(fyb;epJ;n13=7x-;gP!Ho-%zVE(jy jpsM z7yP$G8=B{?AR8)_#o`VlmebwIPL2(0`n}aI7F(x0Km>mSyl&i)vnoUU!=l0$0(Q4i z&yo ^F{Hyxp@6mGL4s zBCp?@@(V %2D#~qzPl;%U!gD(w(dGfE-7Ier^N%N9hyE%wtSw35>s(|7)yHo z)S{#66Az~7d(lLg{CsmkQ{o$yZlRH=E6{z4e@Z5>C@oZ}dIwHay)!A0jDfm{rpD2K zB)&aHFd51Qv$1emd|wY&16oQ!;Ew+yP}d>$3j)UYxMIKD(m46DUTrp~d!M!y$Q}#T zwQMjHP&}?U|9OPHazZqR8X?>Tdd~`i+`9T^UVhOgImdUcDxc4cKij2V`e&wFf`OS| z1#CObv0~aOFGMS@5Kio5#=g2fs;sMmNQA;%T;Gcv)Qz=13ND rNrmp6dF0!ZC&~jeAHbRzorK#k}`Ik$>TqaaaQkv8(rG9AOCX&>C4qU_a{pr za~@DfEhjzV_10^}s;;^V{=F0R{Y*I0LNbWy *N4E2xes zHYiTo%3gV1g)s+K$7589oIzJdhv3aBo7VE3 m6aVo@3g3-{@}h;D_paU!pyH2e$vNPzGhA%CwXo zVW;Ykjv|Lw#$U+xd-JZo-yIN`WDqu^RfhY$0Njo!GPwkC;4PV%yqUdg=sjsi^Qc8( zCa}25>v1NUY>zUysz@EL-SaS2Ilxcq{AJDNM2U}^?Hl`%#E7mzn bIp-<{4qOHexu~ry+2W~AP_j#&$w$oEeQdmT(R8V zcCWxFTWjTa<% NCAZ_`-l6<;O zx^`R&oF5M+1evLCj-<>$0uw?lxmzob!VpFQCrWt~nNZPjY4!z^Ojm|YMs8?IF$oxz zx7ibGMf>15av_htj3Pamsw}!C=I@&K9rgf&%|Zt8$M+%4GfbITeliO_YjgJ?+gN#C zyfx8qb2@+Y^djw^2NmAgiw!}+lx~hz>i@kEHTDjj_3uBpXu5*C;76<8DutRuK<{=! z9hoexU@Z&5j>=Dn>N5DUV7N@+^r2*vYyy$#)B>2uhkpUaEeq+b9`s>V5tIdaq4M7; zOptVigJRbKRh|>)w)fDv6aQU|%2eQtyXbuF5f)5ITdSl|@#rXp6`fJqS@wYK-&R0u zP7^&RM8CucRt2qzxNG}uC 8Bvq#x!*Pb+iBX$Mo>#QO^xvKw$Z_~6`A|5?th${?)lwQNhz?J!g zp3O=nMu5F~;KO-${&JhI?_aQrw*N~rU AM`5=+gu`fQevwJpF&SnYl(V1 dyvDBpKYeMq9hPUF&2=gn3Y%S!*Baa=mEmU=@b zLeb8$3LM7h^_l&9<)1@+3V&6)F%W{33SUVtc!vMml6bH?O8r4n0-B&GSX87`$_3Zw zliH_mvgUI?bNbvw=wb#jDW(nHr3F9qQsgn@bWBEM M57_Qgobgm#{ka@Cz?h1T;YJnh 1fpO>goVk#nN@vEm1%R}g1qebYcUU>81&jYOYuv157 zZz63c(V;LPMy9%cs}TNgfLF-6V})8L42h6L4+tj$6`FNbhnwaLUKQh~n{TbeBMSe| zK*$(*JBQZNb9fbFN+(okr9$|w{qL5bsEfJJ56~wZ*N*a7N@GKm1S#aF8Da1EArU2- zdBlF;RY^3Q2;mt#>&`^}K12v`z9{%x7!pou-$MP>nsPfoI4JOuT>s4gknwSG#{>{7 zOeTLk%|o47wHSOPXg}!usF0Oz7vxUz?YewE-K-vZB!d}N $LgR%#H zkYn2x!EFVk7kUec|9gv}tKhn#IHz Wrxf`WZ(aJM|6nP?I!;QB72XF za#}&S-0sl!#NJLalr~46|M#j6BPU@}2NKR`>%;aSfrD;V{=b(&hlGJO`QsHfd0h%K zzcoFNeN;LR|925Ww&rrz4%x@&({8m8^?O<@AHuo~; z`upD|V1X70tF8oG9HcS{!?XfaW$|78s)*j>`2X&)Z>`nL`5krOYmn>IC!x|2htzkU zfmZ(E|5iu gctY|!d9RGb6x%4V_3>>ID-;LaZatz9VCg%{ESLM|9b#6 ze;106K2=_yTt5#@7hb4n0ovap#}Pc$|8@@%d=8}zLd4fRb9u&9MNpS{4)N0c|IL+N z2Jcimn>A#Nfn&hw`uh`0nHV_Gsqh`#$fFRXgf{NTaq=#bhq|Akdmt8+vFczduAu)7 zlA-+aBnV&sObZfTX+=;x#>4R?97WD3F=JAoVqI*_Kusnn)7-i7drG~@e@6|FvWH=e z$^B>l)DlSj!~iQWU3@!*{AdO*Nz6-q#GP8iX?zSNd1THYCyv}eB*3yT7OotMGQW =Tjowc84&dwK*|nWsix_@ z*gB7jpk&W~dt3gHA8;U$Uc~~Lc*7vJ)Sr;?7Rjfg*f;5l>S|@!GlgItn691*JFw uVH!CwM_Je_3dTXJ~{y3H8n{bRgnO$^+yfiU^ zwuQ(R5SI??=bkk;7)@tFFCIDoNAOGA9R9p$AucEl_4o O^X5&k(1=(|FY0l=Ob;`M0bKk5RaiWq#J2tnfUYnZl^t=AA!Ap6143 -1$>HnL?ryO?JH ro$YFn;G;@C@nbcIN~N( zz-j--tQFD+Jc>nx)hvIN-FqfAkm-C8;gi?y-(RCH`;hNUHxMNi7Cgt3E77oy)M3Jl zworeVpS=ZvoDQkU=+6g{q5g$vcrG)A+d=4&squM$oPrJ0=5_!dH}VSvyk9HUf08OK zaMrgxUErMmyUZ)ksqt@CgZbF&^Ie2crDuLn8832EJe9AcM(Ee!jR0fT3i3y#nm)K* zN#}#4PN63vf_5(1`}?zVcPIFJ3Mm<+dq7wjxCpURMgidtfxw)1K%OChzETRs6|GUT zwu41j^WSLxqeHwh#GSD9v5sQB&GLn5nMM2fji|U-wW;uG3)Ct4Y7KYL`ZLf% y!jpidHy)wlxjB$R#ihrzB1lVa4RYC=FJfQ+18OTa`5zV8GSJ2=f!!jme s6;$Vr%%67o|E#1Q5Z}{vj1|@HDE?rrtFma3IXlK*ljkgq?|` zxs`v<%7C0NwA&(e41=U<-|csc6LLCqr;j`M?V)~-+cCVy3bhTA zY7cp=F+M+qd=#aTC-XWiGW~S)ui*>5E&-D#y?I3Z7bY1m6BEh}HFat!Dr3T|!G|_I zXcj=k^|D5|U2&@5C;j;RniRy<{?3k`RiuQtPwMqFlA^sQPIS|qJbqbk|0%%QO@534 zo3q)A-)I0Z+(4c-0}4SyCB-@8BP#0lkZ4a)ak+zn!X{-VQ&cqw+^cOi zCWAjI_Ybhk)xkBY0Ada!r{03NP*)q{^ZsnA8zl)Suf#*t8Sg*?wWF=9{dK>uqKv)@ z!iLKWOi##X)V;Z}&8x~EZq#dbAPZOd_*#Myx5u%9`w#!^9);B>4c=?Ex{bbno0Vf8 z5!4lp{m%Q `{ofw}4vZ~V5WUbxKqZF93tUJCh>{74*4uHE>~c8ThQB7 (t&HO|y)O`>p@tscU!6iW!9>z*=PJhzq{_^7i zt3&0_FlyM`3Pg&OQaMz9_`#ib?eYG9a%hB=#-QF1+kf}VdifDdHerPmm&G&VA6AOV z0 T2M|`T31HKje?8F#qqG=U zD820qAOCl=tldkZt-0MqnG>ybqwRQ%UG@=mb{IF=p+hm5_uRJnPoU3=l@2wM( zD7^8Dyw8yoz7N{k2cB^BE&iIW5Rq7c2GL5&-S5}_1#!ws@y|#9m3BN0BI&6>-b_d8 zYFygcA~T4DQ~0Q|>Fa?_(uE-)sz}$f%Nz1Y`!PHabp0sYTCVZpr{C<=2yyu@(sI}4 z+PJ&`{)}3qT_n=cXupur=X?OnmCv;6N^mTKT-kkYz9YEovwyx7FFs9F4_Al9tFjns zD&B2C>1_?c7fk=M(LtTwSdO?kYc-vwU*pk^<|EEIP^);5uM#^@;X_f=jxr7FkVnxp z`%UitkG2 4^Fbc$Pv;qf*8lGlAvm(dEn zj>m%sg!X#} UOHSNY7G1mN!{=#mH#d)1XN+@*iF}^T=`Cd!_<{NMF1vtBN?5 zUZ8J;5+Z8_3);Ho1nAcmRYkJ7^X%RQ6C6FgkDH*)Vg74C4IH~2B1`I9EsmOn=2T`0 zmdl1C;iwZp1hkA{(g%K+bN@vo^z&EG`*Om?nfi}GG(c0U5NzZm?7 Se$*O5-;vldAg0b#OCQ>Mq$ja197(Dgt((gSXR|dID@vB9w%^< z%rsk1SI{BDfdFNVbX+% Y}W!9RbR7ID>{#5|y3<%6fA2DtB z-;xhABjW*keq@XKtB8^i{UKj?tqamyA_A-mkIh)sa%p?s-up7vd-f=p?(b}k5SNZx z;YxDXOCNy2qvK!OMOzsJ+mS5HTw`SKvDYOzQr86)7E6C*iX>j@47^ed4z^{q168&i zm>3!j^Y$i!`aJZ@bZ7^-znXcFJ{Ea1ljH*>08brW^A;oo?Fs~N`{aPJQ$=B<3v`WC zEx6&xxBuuIJ%39G8dlup+9B9RYW3#UQk7?sQLnPZOTMN&h!ZGh 9Ub#J{=ire 4N%P$%SCjv=NwxF0 zW@U|go;XJzR-2TlW1{!N<%;#*S}MCv_>n)2ykG;c@RnL$Rdrt4MNa$$A+ig2=JuVo z9-FLpAD=Ou9v0OLLBKosc}J-j{FxsCc-@H!FNA_7XUFo^TeQK#V*;NMFXibtIrM?o zLF6boX%$TzQmddEQ^|_II1l||CxBT4`ZIK^G*zogn;8Ol++Mn4gVGfW`D0&eF(^lc zl{4!#cC&EN^gdsw!Z!EMVUQakp-qK2BmfHZtl>Ob1A8^H4EX_FdLn2w=Ie1h)16&} z-x-|Ii>=e2fsG&?o6@fDnGrV#i3EF^p^~3f+-CaKhjGGP_JIrhopj0{q^;Jz*0m-L z5)uLm@3T~i(BOeU8O=>c6R%OGj;db%S+3#(RJl@$t@$?s(x3}ZgzYEDC@yj%QdIbh z>i{j|ph6v?ew22cTJ2sVxBv;*MdM^ G9~t;(MP_#f#;-5(bM3vA#ptPq(eBg2Acuzse^M(9qccs zRr_ykXTHOF`At2KYvJX6S_=;VQpMnAtsLPvVu>2bF7g#wZGiz`v?(-tiHMb}8(&{b z`vXqD(aDrjM(wKGbGzA&I*dqty5zf&6YA;NACmO1e=@Fpi9_@WH;y8!#1A8r)=n9s zh|LZoJ+p_LTpmFp1GtUh%#Jm@C)%dpNVs+bnz`;zHi2jJX g)jQ(>0UfCDcO(+odyAAQ&h}UZo<#E(lXW? zSH%fjS!&V 3p!U zZ(Z;7Z%ry>JKl<6=4J*?<`Ld9#?caO2irQQc4ln_tN>f=e~b)FTppv|Vq$nmf|cnb zFTb1%=enp9)84#_jfU_awdMLC6u~16r8xPpapv1tp;%FH01*!rsl@SeTrq0Vdv{7> zv$Nl92?CNz=alW^kS{>ZE=0 #G>lgl1T7^Kwi3<=2YB6~(4xpNqpL0Anh2 zb$)YFAqy`BSgA2<;&y##>a=otI7fSL+1xkN%DS^`plw!Su#w?9z=Nh%v%O6Na{8%E z{ZgT=J-tox_g$a7v)9vCh!&p*bnLVVgJ0UHD3JBIo9<3y)n7G!qTl$yTF5hGK? 3U6Q6dHy!fdzm^7uo*6(i4FDgD!;U+@_JLq>*eJ8OZ!zGJX%a*t! zp-=as&dsa_L|okF1SnH?(|IHwQ_37HcO8O-S_m^U+A{Y0*cYJE^lUtC&G;VW4vFRB zZN_TlU~MDwn^-Mv{sRTU8S4#$qZwPnUi^g(FK0g`tnP@!Uu*r&VP>TIE)Q+%?BVeU z1#81G+kL1Bi9&o{NX4n7YV3XpcIdp?egPwA<#_>xuc7~}hWvuP&@O2ppL*>vMeCbN zLi`^1@gkpNFAM*>g;9m36Je2Fs(=t(K4(Oj>?@i1W(Mq0n^j$o9yec5uiHs*dX?Nc z|Gb1rF*jrySX(hO|G)PxPF*8}p<^&I_ztmzVXXYa#ZCu<-eL|*uT
WP8AzDgGX1>$ P9k$frt=%DTq75H&p508`XcR-#Zc)y!Zj&d_Z~_EBBfMx z+}}}fk?%q3{^sPFS+!SvOsGAC-?;J(m?y_I0@X&4tNiM+Ha%RRkkt3k25dfrx#}Bf zF9B*lG}hzCM@|3ur3n9A#=Rg;aFp~ye^6W9&GlQGJAg6ofYdDsiJX _#%>duGK(1k%;YEdEQCqh@ =ATtGdpIUzy`w0T5Rsr)WDHDDCoHVKfu9X-Lhy zMYXe~M)Ay28rwTP(*_@c8N`8ydrK0#wgH2=e9dpth{oXbcVe)-STxNp5WXEdC*D3B zZ?8y8jb?^aP)@w3)yoH$-0uZ9vFrPN=TJ{uJP2X2pmmQBj3`H1Q=u<<`jx6c>0anu z<*1`D6t`jk-Lqol?ouJhiFE&ag_SJpJW|RR3OBN+?~?+*9iW!>Gjl+)I^)XIGmQ># z_^oQMKo80Aa(3vu@+In^zn6pH-K6(rkHIM|pvT1h$q~oO&;_3124S@tTxcV|5uCBV zhi0ahZkd%j`u!jB1xoY_HW75*O14*%?Du0(2umh uz}kpgk)$01H0grfT!xWj>O(ufMbFd90%w9|LL>0;jZR1CJ5e+0L~DxV6$@ z6>!P;;dS!s*$eJ8vuvGxKe1gl&3ttwu%mUynK`Hx7^G7{=YFi)@rWxJI4Sx&p)aea zVcFN0_pd+y0$k^>6S=8n=id`tAUn4KR~{NC9%2FJg5cYx3--SJdg0vL2*$tVzusRx zv%cQ{^$&aCw&Jqw)(W!)fb-+Lx_cJ;mT%dc16(BjW`W~(#{Ii5{g3;97qpAx_qVNQ zbwRUg44ZBN=PdG~Ii~?n*;xKE^Y_iSrof$Mzw596-~X??!5^ez-mf>`LmKjdOooaM kAkol1${fz&@ZvwSa2;#b#>z|4z}RN+boFyt=akR{02=|Im;e9( literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt b/app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt new file mode 100644 index 0000000..d0422ee --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt @@ -0,0 +1,257 @@ +package com.dirror.lyricviewx + +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.Layout +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import androidx.annotation.Px +import java.io.File + +const val GRAVITY_CENTER = 0 // 居中 +const val GRAVITY_LEFT = 1 // 左 +const val GRAVITY_RIGHT = 2 // 右 + +fun Int.toLayoutAlign(): Layout.Alignment { + return when (this) { + GRAVITY_LEFT -> Layout.Alignment.ALIGN_NORMAL + GRAVITY_CENTER -> Layout.Alignment.ALIGN_CENTER + GRAVITY_RIGHT -> Layout.Alignment.ALIGN_OPPOSITE + else -> Layout.Alignment.ALIGN_CENTER + } +} + +/** + * LyricViewX 接口 + * 从 LyricViewX 提取,方便管理 + * + * @author Moriafly + * @since 2021年1月28日16:29:16 + */ +interface LyricViewXInterface { + + /** + * 设置整句之间的间隔高度 + * @param height px + */ + fun setSentenceDividerHeight(@Px height: Float) + + /** + * 设置原句与翻译之间的间隔高度 + * @param height px + */ + fun setTranslateDividerHeight(@Px height: Float) + + /** + * 设置歌词整体的垂直偏移值,配合[setHorizontalOffsetPercent]使用 + * @param offset px + * + * @see [setHorizontalOffsetPercent] + */ + fun setHorizontalOffset(@Px offset: Float) + + /** + * 设置歌词整体的垂直偏移,相对于控件高度的百分比,0.5f即表示居中,配合[setHorizontalOffset]使用 + * + * @param percent 0.0f ~ 1.0f + * + * @see [setHorizontalOffset] + */ + fun setHorizontalOffsetPercent(@FloatRange(from = 0.0, to = 1.0) percent: Float) + + /** + * 设置翻译相对与原词之间的缩放比例值 + * @param scaleValue 一般来说 0.8f 是个不错的值 + */ + fun setTranslateTextScaleValue(@FloatRange(from = 0.1, to = 2.0) scaleValue: Float) + + /** + * 设置文字的对齐方向 + */ + fun setTextGravity(gravity: Int) + + /** + * 设置非当前行歌词字体颜色 [normalColor] + */ + fun setNormalColor(@ColorInt normalColor: Int) + + /** + * 普通歌词文本字体大小 [size],单位 px + */ + fun setNormalTextSize(@Px size: Float) + + /** + * 当前歌词文本字体大小 + */ + fun setCurrentTextSize(size: Float) + + /** + * 设置当前行歌词的字体颜色 + */ + fun setCurrentColor(currentColor: Int) + + /** + * 设置拖动歌词时选中歌词的字体颜色 + */ + fun setTimelineTextColor(timelineTextColor: Int) + + /** + * 设置拖动歌词时时间线的颜色 + */ + fun setTimelineColor(timelineColor: Int) + + /** + * 设置拖动歌词时右侧时间字体颜色 + */ + fun setTimeTextColor(timeTextColor: Int) + + /** + * 设置歌词为空时屏幕中央显示的文字 [label],如“暂无歌词” + */ + fun setLabel(label: String) + + /** + * 加载歌词文本 + * 两种语言的歌词时间戳需要一致 + * + * @param mainLyricText 第一种语言歌词文本 + * @param secondLyricText 可选,第二种语言歌词文本 + */ + fun loadLyric(mainLyricText: String?, secondLyricText: String? = null) + + /** + * 加载歌词 [LyricEntry] 集合 + * 如果你在 Service 等地方自行解析歌词包装成 [LyricEntry] 集合,那么可以使用此方法载入歌词 + * + * @param lyricEntries 歌词集合 + * @since 1.3.1 + */ + fun loadLyric(lyricEntries: List ) + + /** + * 刷新歌词 + * + * @param time 当前播放时间 + */ + fun updateTime(time: Long, force: Boolean = false) + + /** + * 设置歌词是否允许拖动 + * + * @param draggable 是否允许拖动 + * @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器,如果允许拖动,则不能为 null + */ + fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?) + + /** + * 设置单击 + */ + fun setOnSingerClickListener(onSingerClickListener: OnSingleClickListener?) + + /** + * 获取当前歌词每句实体,可用于歌词分享 + * + * @return LyricEntry 集合 + */ + fun getLyricEntryList(): List + + /** + * 设置当前歌词每句实体 + */ + fun setLyricEntryList(newList: List ) + + /** + * 获取当前行歌词 + */ + fun getCurrentLineLyricEntry(): LyricEntry? + + /** + * 为歌词设置自定义的字体 + * + * @param file 字体文件 + */ + fun setLyricTypeface(file: File) + + /** + * 为歌词设置自定义的字体 + * + * @param path 字体文件路径 + */ + fun setLyricTypeface(path: String) + + /** + * 为歌词设置自定义的字体,可为空,若为空则应清除字体 + * + * @param typeface 字体对象 + */ + fun setLyricTypeface(typeface: Typeface?) + + /** + * 为歌词的过渡动画设置阻尼比(数值越大,回弹次数越多) + * + * @param dampingRatio 阻尼比 详见[androidx.dynamicanimation.animation.SpringForce] + */ + fun setDampingRatioForLyric(dampingRatio: Float) + + /** + * 为歌词视图的滚动动画设置阻尼比(数值越大,回弹次数越多) + * + * @param dampingRatio 阻尼比 详见[androidx.dynamicanimation.animation.SpringForce] + */ + fun setDampingRatioForViewPort(dampingRatio: Float) + + /** + * 为歌词的过渡动画设置刚度(数值越大,动画越短) + * + * @param stiffness 刚度 详见[androidx.dynamicanimation.animation.SpringForce] + */ + fun setStiffnessForLyric(stiffness: Float) + + /** + * 为歌词视图的滚动动画设置刚度(数值越大,动画越短) + * + * @param stiffness 刚度 详见[androidx.dynamicanimation.animation.SpringForce] + */ + fun setStiffnessForViewPort(stiffness: Float) + + /** + * 设置跳转播放按钮 + */ + fun setPlayDrawable(drawable: Drawable) + + /** + * 设置是否绘制歌词翻译 + */ + fun setIsDrawTranslation(isDrawTranslation: Boolean) + + /** + * 是否开启特定的模糊效果 + */ + fun setIsEnableBlurEffect(isEnableBlurEffect: Boolean) + + /** + * 设置元素的偏移百分比,0.5f即表示居中 + * + * @param itemOffsetPercent 0f ~ 1f 偏移百分比 + */ + fun setItemOffsetPercent(@FloatRange(from = 0.0, to = 1.0) itemOffsetPercent: Float) +} + +/** + * 播放按钮点击监听器,点击后应该跳转到指定播放位置 + */ +interface OnPlayClickListener { + /** + * 播放按钮被点击,应该跳转到指定播放位置 + * + * @return 是否成功消费该事件,如果成功消费,则会更新UI + */ + fun onPlayClick(time: Long): Boolean +} + +/** + * 点击歌词布局 + */ +interface OnSingleClickListener { + fun onClick() +} \ No newline at end of file diff --git a/app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt b/app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt new file mode 100644 index 0000000..17c590a --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt @@ -0,0 +1,82 @@ +package com.dirror.lyricviewx + +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint + +/** + * 一行歌词实体 + * @since 2021年1月19日09:51:40 Moriafly 基于 LrcEntry 改造,转换为 kt ,移除部分过时方法 + * @param time 歌词时间 + * @param text 歌词文本 + */ +class LyricEntry(val time: Long, val text: String) : Comparable { + + /** + * 第二文本 + */ + var secondText: String? = null + + /** + * staticLayout + */ + var staticLayout: StaticLayout? = null + private set + + var secondStaticLayout: StaticLayout? = null + private set + + @Deprecated("存在不显示翻译的情况,会导致offset发生改变,故不再固定存储offset") + /** + * 歌词距离视图顶部的距离 + */ + var offset = Float.MIN_VALUE + + /** + * 初始化 + * @param textPaint 文本画笔 + * @param width 宽度 + * @param align 位置 + */ + fun init( + textPaint: TextPaint, + secondTextPaint: TextPaint, + width: Int, align: Layout.Alignment + ) { + staticLayout = createStaticLayout(text, textPaint, width, align) + secondStaticLayout = createStaticLayout(secondText, secondTextPaint, width, align) + offset = Float.MIN_VALUE + } + + /** + * 继承 Comparable 比较 + * @param other LyricEntry + * @return 时间差 + */ + override fun compareTo(other: LyricEntry): Int { + return (time - other.time).toInt() + } + + companion object { + fun createStaticLayout( + text: String?, + paint: TextPaint, + width: Number, + align: Layout.Alignment + ): StaticLayout? { + if (text == null || text.isEmpty()) return null + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + StaticLayout.Builder + .obtain(text, 0, text.length, paint, width.toInt()) + .setAlignment(align) + .setLineSpacing(0f, 1f) + .setIncludePad(false) + .build() + } else { + StaticLayout(text, paint, width.toInt(), align, 1f, 0f, false) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt b/app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt new file mode 100644 index 0000000..cb3b4b4 --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt @@ -0,0 +1,289 @@ +package com.dirror.lyricviewx + +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.graphics.Rect +import android.text.TextUtils +import android.text.format.DateUtils +import android.view.MotionEvent +import java.io.* +import java.net.HttpURLConnection +import java.net.URL +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.regex.Pattern + +/** + * 工具类 + * 原 LrcUtils 转 Kotlin + */ +object LyricUtil { + + private val PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}])+)(.+)") + private val PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})]") + private val argbEvaluator = ArgbEvaluator() + + /** + * 从文件解析双语歌词 + */ + fun parseLrc(lrcFiles: Array ?): List ? { + if (lrcFiles == null || lrcFiles.size != 2 || lrcFiles[0] == null) { + return null + } + val mainLrcFile = lrcFiles[0] + val secondLrcFile = lrcFiles[1] + val mainEntryList = parseLrc(mainLrcFile) + val secondEntryList = parseLrc(secondLrcFile) + if (mainEntryList != null && secondEntryList != null) { + for (mainEntry in mainEntryList) { + for (secondEntry in secondEntryList) { + if (mainEntry.time == secondEntry.time) { + mainEntry.secondText = secondEntry.text + } + } + } + } + return mainEntryList + } + + /** + * 从文件解析歌词 + */ + private fun parseLrc(lrcFile: File?): List ? { + if (lrcFile == null || !lrcFile.exists()) { + return null + } + val entryList: MutableList = ArrayList() + try { + val br = + BufferedReader(InputStreamReader(FileInputStream(lrcFile), StandardCharsets.UTF_8)) + var line: String + while (br.readLine().also { line = it } != null) { + val list = parseLine(line) + if (list != null && list.isNotEmpty()) { + entryList.addAll(list) + } + } + br.close() + } catch (e: IOException) { + e.printStackTrace() + } + entryList.sort() + return entryList + } + + /** + * 从文本解析双语歌词 + */ + fun parseLrc(lrcTexts: Array ?): List ? { + if (lrcTexts == null || lrcTexts.size != 2 || TextUtils.isEmpty(lrcTexts[0])) { + return null + } + val mainLrcText = lrcTexts[0] + val secondLrcText = lrcTexts[1] + val mainEntryList = mainLrcText?.let { parseLrc(it) } + + /** + * 当输入的secondLrcText为空时,按如下格式解析歌词 + * (音乐标签下载的第二种歌词格式) + * + * [00:21.11]いつも待ち合わせより15分前集合 + * [00:21.11]总会比相约时间早15分钟集合 + * [00:28.32]駅の改札ぬける + * [00:28.32]穿过车站的检票口 + * [00:31.39]ざわめきにわくわくだね + * [00:31.39]嘈杂声令内心兴奋不已 + * [00:35.23]どこへ向かうかなんて + * [00:35.23]不在意接下来要去哪里 + */ + if (TextUtils.isEmpty(secondLrcText)) { + var lastEntry: LyricEntry? = null + return mainEntryList?.filter { now -> + if (lastEntry == null) { + lastEntry = now + return@filter true + } + + if (lastEntry!!.time == now.time) { + lastEntry!!.secondText = now.text + lastEntry = null + return@filter false + } + + lastEntry = now + true + } + } + + val secondEntryList = secondLrcText?.let { parseLrc(it) } + if (mainEntryList != null && secondEntryList != null) { + for (mainEntry in mainEntryList) { + for (secondEntry in secondEntryList) { + if (mainEntry.time == secondEntry.time) { + mainEntry.secondText = secondEntry.text + } + } + } + } + return mainEntryList + } + + /** + * 从文本解析歌词 + */ + private fun parseLrc(lrcText: String): List ? { + var lyricText = lrcText.trim() + if (TextUtils.isEmpty(lyricText)) return null + + if (lyricText.startsWith("\uFEFF")) { + lyricText = lyricText.replace("\uFEFF", "") + } + + // 针对传入 Language="Media Monkey Format"; Lyrics="......"; 的情况 + lyricText = lyricText.substringAfter("Lyrics=\"") + .substringBeforeLast("\";") + + val entryList: MutableList = ArrayList() + val array = lyricText.split("\\n".toRegex()).toTypedArray() + for (line in array) { + val list = parseLine(line) + if (!list.isNullOrEmpty()) { + entryList.addAll(list) + } + } + entryList.sort() + return entryList + } + + /** + * 获取网络文本,需要在工作线程中执行 + */ + fun getContentFromNetwork(url: String?, charset: String?): String? { + var lrcText: String? = null + try { + val url = URL(url) + val conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "GET" + conn.connectTimeout = 10000 + conn.readTimeout = 10000 + if (conn.responseCode == 200) { + val `is` = conn.inputStream + val bos = ByteArrayOutputStream() + val buffer = ByteArray(1024) + var len: Int + while (`is`.read(buffer).also { len = it } != -1) { + bos.write(buffer, 0, len) + } + `is`.close() + bos.close() + lrcText = bos.toString(charset) + } + } catch (e: Exception) { + e.printStackTrace() + } + return lrcText + } + + /** + * 解析一行歌词 + */ + private fun parseLine(line: String): List ? { + var lyricLine = line + if (TextUtils.isEmpty(lyricLine)) { + return null + } + lyricLine = lyricLine.trim { it <= ' ' } + // [00:17.65]让我掉下眼泪的 + val lineMatcher = PATTERN_LINE.matcher(lyricLine) + if (!lineMatcher.matches()) { + return null + } + val times = lineMatcher.group(1)!! + val text = lineMatcher.group(3)!! + val entryList: MutableList = ArrayList() + + // [00:17.65] + val timeMatcher = PATTERN_TIME.matcher(times) + while (timeMatcher.find()) { + val min = timeMatcher.group(1)!!.toLong() + val sec = timeMatcher.group(2)!!.toLong() + val milString = timeMatcher.group(3)!! + var mil = milString.toLong() + // 如果毫秒是两位数,需要乘以 10,when 新增支持 1 - 6 位毫秒,很多获取的歌词存在不同的毫秒位数 + when (milString.length) { + 1 -> mil *= 100 + 2 -> mil *= 10 + 4 -> mil /= 10 + 5 -> mil /= 100 + 6 -> mil /= 1000 + } + val time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil + entryList.add(LyricEntry(time, text)) + } + return entryList + } + + /** + * 转为[分:秒] + */ + fun formatTime(milli: Long): String { + val m = (milli / DateUtils.MINUTE_IN_MILLIS).toInt() + val s = (milli / DateUtils.SECOND_IN_MILLIS % 60).toInt() + val mm = String.format(Locale.getDefault(), "%02d", m) + val ss = String.format(Locale.getDefault(), "%02d", s) + return "$mm:$ss" + } + + /** + * BUG java.lang.NoSuchFieldException: No field sDurationScale in class Landroid/animation/ValueAnimator; #3 + */ + @SuppressLint("SoonBlockedPrivateApi") + @Deprecated("") + fun resetDurationScale() { + try { + val mField = ValueAnimator::class.java.getDeclaredField("sDurationScale") + mField.isAccessible = true + mField.setFloat(null, 1f) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * 结合fraction,计算两个值之间的比例 + */ + fun calcScaleValue(a: Float, b: Float, f: Float, reverse: Boolean = false): Float { + if (b == 0f) return 1f + return 1f + ((a - b) / b) * (if (reverse) 1f - f else f) + } + + /** + * 颜色值插值函数 + */ + fun lerpColor(a: Int, b: Int, f: Float): Int { + return argbEvaluator.evaluate(f, a, b) as Int + } + + /** + * 简单的插值函数 + */ + fun lerp(from: Float, to: Float, fraction: Float): Float { + return from + (to - from) * fraction + } + + /** + * 判断MotionEvent是否发生在Rect中 + */ + fun MotionEvent.insideOf(rect: Rect?): Boolean { + rect ?: return false + return rect.contains(x.toInt(), y.toInt()) + } + + fun normalize(min: Float, max: Float, value: Float, limit: Boolean = false): Float { + if (min == max) return 1f + return ((value - min) / (max - min)).let { + if (limit) it.coerceIn(0f, 1f) else it + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt b/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt new file mode 100644 index 0000000..71315e6 --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt @@ -0,0 +1,1099 @@ + +package com.dirror.lyricviewx +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.os.Looper +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import android.text.format.DateUtils +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import android.widget.Scroller +import androidx.annotation.FloatRange +import androidx.core.content.ContextCompat +import androidx.dynamicanimation.animation.SpringForce +import androidx.dynamicanimation.animation.springAnimationOf +import androidx.dynamicanimation.animation.withSpringForceProperties +import com.dirror.lyricviewx.LyricUtil.calcScaleValue +import com.dirror.lyricviewx.LyricUtil.formatTime +import com.dirror.lyricviewx.LyricUtil.insideOf +import com.dirror.lyricviewx.LyricUtil.lerp +import com.dirror.lyricviewx.LyricUtil.lerpColor +import com.dirror.lyricviewx.LyricUtil.normalize +import com.dirror.lyricviewx.extension.BlurMaskFilterExt +import com.lalilu.easeview.EaseView +import com.lalilu.easeview.animatevalue.BoolValue +import com.lalilu.easeview.animatevalue.FloatListAnimateValue +import com.muqingbfq.R +import java.io.File +import kotlin.concurrent.thread + +import kotlin.math.abs +import kotlin.math.max + +/** + * LyricViewX + * + * Based on https://github.com/zion223/NeteaseCloudMusic-MVVM Kotlin + * + * Thanks: + * https://github.com/cy745 + */ +open class LyricViewX : EaseView, LyricViewXInterface { + constructor(context: Context) : super(context) { + init(null) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(attrs) + } + + protected val readyHelper = ReadyHelper() + private val blurMaskFilterExt = BlurMaskFilterExt() + + /** 单句歌词集合 */ + private val lyricEntryList: MutableList = ArrayList() + + /** 主歌词画笔 */ + private val lyricPaint = TextPaint() + + /** 副歌词(一般为翻译歌词)画笔 */ + private val secondLyricPaint = TextPaint() + + /** 时间文字画笔 */ + private val timePaint = TextPaint() + + private var timeFontMetrics: Paint.FontMetrics? = null + + /** 跳转播放按钮 */ + private var playDrawable: Drawable? = null + + private var translateDividerHeight = 0f + private var sentenceDividerHeight = 0f + private var animationDuration: Long = 0 + private var normalTextColor = 0 + private var normalTextSize = 0f + private var currentTextColor = 0 + private var currentTextSize = 0f + private var translateTextScaleValue = 1f + private var timelineTextColor = 0 + private var timelineColor = 0 + private var timeTextColor = 0 + private var drawableWidth = 0 + private var timeTextWidth = 0 + private var defaultLabel: String? = null + private var lrcPadding = 0f + private var onPlayClickListener: OnPlayClickListener? = null + private var onSingerClickListener: OnSingleClickListener? = null + private var animator: ValueAnimator? = null + private var gestureDetector: GestureDetector? = null + private var scroller: Scroller? = null + private var flag: Any? = null + private var isTouching = false + private var isFling = false + private var textGravity = GRAVITY_CENTER // 歌词显示位置,靠左 / 居中 / 靠右 + private var horizontalOffset: Float = 0f + private var horizontalOffsetPercent: Float = 0.5f + private var itemOffsetPercent: Float = 0.5f + private var dampingRatioForLyric: Float = SpringForce.DAMPING_RATIO_LOW_BOUNCY + private var dampingRatioForViewPort: Float = SpringForce.DAMPING_RATIO_NO_BOUNCY + private var stiffnessForLyric: Float = SpringForce.STIFFNESS_LOW + private var stiffnessForViewPort: Float = SpringForce.STIFFNESS_VERY_LOW + + private var currentLine = 0 // 当前高亮显示的歌词 + private val focusLine: Int // 当前焦点歌词 + get() = if (isTouching || isFling) centerLine else currentLine + + /** + * 获取当前在视图中央的行数 + */ + private val centerLine: Int + get() { + var centerLine = 0 + var minDistance = Float.MAX_VALUE + var tempDistance: Float + + for (i in lyricEntryList.indices) { + tempDistance = abs(mViewPortOffset - getOffset(i)) + if (tempDistance < minDistance) { + minDistance = tempDistance + centerLine = i + } + } + return centerLine + } + + /** + * 获取歌词宽度 + */ + open val lrcWidth: Float + get() = width - lrcPadding * 2 + + /** + * 歌词整体的垂直偏移值 + */ + open val startOffset: Float + get() = height.toFloat() * horizontalOffsetPercent + horizontalOffset + + + /** + * 原有的mOffset被拆分成两个独立的offset,这样可以更好地让进度和拖拽滚动独立开来 + */ + private var mCurrentOffset = 0f // 实际的歌词进度Offset + private var mViewPortOffset = 0f // 歌词显示窗口的Offset + + private var animateProgress = 0f // 动画进度 + private var animateTargetOffset = 0f // 动画目标Offset + private var animateStartOffset = 0f // 动画起始Offset + + private val viewPortSpringAnimator = springAnimationOf( + getter = { mViewPortOffset }, + setter = { value -> + if (!isShowTimeline.value && !isTouching && !isFling) { + mViewPortOffset = value + invalidate() + } + } + ).withSpringForceProperties { + dampingRatio = dampingRatioForViewPort + stiffness = stiffnessForViewPort + finalPosition = 0f + } + + /** + * 弹性动画Scroller + */ + private val progressSpringAnimator = springAnimationOf( + getter = { mCurrentOffset }, + setter = { value -> + animateProgress = normalize(animateStartOffset, animateTargetOffset, value) + mCurrentOffset = value + + if (!isShowTimeline.value && !isTouching && !isFling) { + viewPortSpringAnimator.animateToFinalPosition(animateTargetOffset) + } + invalidate() + } + ).withSpringForceProperties { + dampingRatio = dampingRatioForLyric + stiffness = stiffnessForLyric + finalPosition = 0f + } + + @SuppressLint("CustomViewStyleable") + private fun init(attrs: AttributeSet?) { + readyHelper.readyState = STATE_INITIALIZING + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LyricView) + currentTextSize = typedArray.getDimension(R.styleable.LyricView_lrcTextSize, resources.getDimension(R.dimen.lrc_text_size)) + normalTextSize = typedArray.getDimension(R.styleable.LyricView_lrcNormalTextSize, resources.getDimension(R.dimen.lrc_text_size)) + if (normalTextSize == 0f) { + normalTextSize = currentTextSize + } + + sentenceDividerHeight = + typedArray.getDimension(R.styleable.LyricView_lrcSentenceDividerHeight, resources.getDimension(R.dimen.lrc_sentence_divider_height)) + translateDividerHeight = + typedArray.getDimension(R.styleable.LyricView_lrcTranslateDividerHeight, resources.getDimension(R.dimen.lrc_translate_divider_height)) + val defDuration = resources.getInteger(R.integer.lrc_animation_duration) + animationDuration = typedArray.getInt(R.styleable.LyricView_lrcAnimationDuration, defDuration).toLong() + animationDuration = + if (animationDuration < 0) defDuration.toLong() else animationDuration + + normalTextColor = typedArray.getColor( + R.styleable.LyricView_lrcNormalTextColor, + ContextCompat.getColor(context, R.color.lrc_normal_text_color) + ) + currentTextColor = typedArray.getColor( + R.styleable.LyricView_lrcCurrentTextColor, + ContextCompat.getColor(context, R.color.lrc_current_text_color) + ) + timelineTextColor = typedArray.getColor( + R.styleable.LyricView_lrcTimelineTextColor, + ContextCompat.getColor(context, R.color.lrc_timeline_text_color) + ) + defaultLabel = typedArray.getString(R.styleable.LyricView_lrcLabel) + defaultLabel = if (defaultLabel.isNullOrEmpty()) "暂无歌词" else defaultLabel + lrcPadding = typedArray.getDimension(R.styleable.LyricView_lrcPadding, 0f) + timelineColor = typedArray.getColor( + R.styleable.LyricView_lrcTimelineColor, + ContextCompat.getColor(context, R.color.lrc_timeline_color) + ) + val timelineHeight = typedArray.getDimension( + R.styleable.LyricView_lrcTimelineHeight, + resources.getDimension(R.dimen.lrc_timeline_height) + ) + playDrawable = typedArray.getDrawable(R.styleable.LyricView_lrcPlayDrawable) + playDrawable = if (playDrawable == null) ContextCompat.getDrawable( + context, + R.drawable.zt + ) else playDrawable + timeTextColor = typedArray.getColor( + R.styleable.LyricView_lrcTimeTextColor, + ContextCompat.getColor(context, R.color.lrc_time_text_color) + ) + val timeTextSize = typedArray.getDimension( + R.styleable.LyricView_lrcTimeTextSize, + resources.getDimension(R.dimen.lrc_time_text_size) + ) + textGravity = typedArray.getInteger(R.styleable.LyricView_lrcTextGravity, GRAVITY_CENTER) + translateTextScaleValue = typedArray.getFloat(R.styleable.LyricView_lrcTranslateTextScaleValue, 1f) + horizontalOffset = typedArray.getDimension(R.styleable.LyricView_lrcHorizontalOffset, 0f) + horizontalOffsetPercent = typedArray.getDimension(R.styleable.LyricView_lrcHorizontalOffsetPercent, 0.5f) + itemOffsetPercent = typedArray.getDimension(R.styleable.LyricView_lrcItemOffsetPercent, 0.5f) + isDrawTranslation = typedArray.getBoolean(R.styleable.LyricView_lrcIsDrawTranslation, false) + typedArray.recycle() + drawableWidth = resources.getDimension(R.dimen.lrc_drawable_width).toInt() + timeTextWidth = resources.getDimension(R.dimen.lrc_time_width).toInt() + lyricPaint.isAntiAlias = true + lyricPaint.textSize = currentTextSize + lyricPaint.textAlign = Paint.Align.LEFT +// lyricPaint.setShadowLayer(0.1f, 0f, 1f, Color.DKGRAY) + secondLyricPaint.isAntiAlias = true + secondLyricPaint.textSize = currentTextSize + secondLyricPaint.textAlign = Paint.Align.LEFT +// secondLyricPaint.setShadowLayer(0.1f, 0f, 1f, Color.DKGRAY) + timePaint.isAntiAlias = true + timePaint.textSize = timeTextSize + timePaint.textAlign = Paint.Align.CENTER + timePaint.strokeWidth = timelineHeight + timePaint.strokeCap = Paint.Cap.ROUND + timeFontMetrics = timePaint.fontMetrics + gestureDetector = GestureDetector(context, mSimpleOnGestureListener) + gestureDetector!!.setIsLongpressEnabled(false) + scroller = Scroller(context) + } + + /** + * 歌词是否有效 + * @return true,如果歌词有效,否则false + */ + private fun hasLrc(): Boolean { + return lyricEntryList.isNotEmpty() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + if (changed) { + initPlayDrawable() + initEntryList() + if (hasLrc()) { + smoothScrollTo(currentLine) + } + } + readyHelper.readyState = STATE_INITIALIZED + } + + private val isShowTimeline = BoolValue().also(::registerValue) + private val isEnableBlurEffect = BoolValue().also(::registerValue) + private val progressKeeper = FloatListAnimateValue().also(::registerValue) + private val blurProgressKeeper = FloatListAnimateValue().also(::registerValue) + + private val heightKeeper = LinkedHashMap () + private val offsetKeeper = LinkedHashMap () + private val minOffsetKeeper = LinkedHashMap () + private val maxOffsetKeeper = LinkedHashMap () + + private var viewPortStartOffset: Float = 0f + private var isDrawTranslationValue = 0f + private var isDrawTranslation: Boolean = false + set(value) { + if (field == value) return + field = value + viewPortStartOffset = mViewPortOffset + isDrawTranslationAnimator.animateToFinalPosition(if (value) 1000f else 0f) + } + private val isDrawTranslationAnimator = springAnimationOf( + getter = { isDrawTranslationValue * 1000f }, + setter = { + isDrawTranslationValue = it / 1000f + + if (!isTouching && !isFling) { + viewPortSpringAnimator.cancel() + + val targetOffset = if (isDrawTranslation) getMaxOffset(focusLine) else getMinOffset(focusLine) + val animateValue = if (isDrawTranslation) isDrawTranslationValue else 1f - isDrawTranslationValue + + mViewPortOffset = lerp(viewPortStartOffset, targetOffset, animateValue) + } + invalidate() + }, + ).withSpringForceProperties { + dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY + stiffness = SpringForce.STIFFNESS_LOW + finalPosition = if (isDrawTranslation) 1000f else 0f + } + + override fun onPreDraw(canvas: Canvas): Boolean { + // 无歌词,只渲染一句无歌词的提示语句 + if (!hasLrc()) { + lyricPaint.color = currentTextColor + lyricPaint.textSize = normalTextSize + LyricEntry.createStaticLayout( + defaultLabel, + lyricPaint, + lrcWidth, + Layout.Alignment.ALIGN_CENTER + )?.let { + drawText( + canvas = canvas, + staticLayout = it, + calcHeightOnly = false, + yOffset = startOffset, + yClipPercentage = 1f + ) + } + return false + } + return super.onPreDraw(canvas) + } + + override fun onDoDraw(canvas: Canvas): Boolean { + val centerY = startOffset + val currentCenterLine = centerLine + + // 当显示时间线时,需要绘制时间线 + if (isShowTimeline.value || isShowTimeline.animateValue > 0f) { + val alpha = (isShowTimeline.animateValue * 255f).toInt() + + // 绘制播放按钮 + playDrawable?.let { + it.alpha = alpha + it.draw(canvas) + } + + // 绘制时间线 + timePaint.color = timelineColor + timePaint.alpha = alpha + canvas.drawLine( + timeTextWidth.toFloat(), centerY, + (width - timeTextWidth).toFloat(), centerY, timePaint + ) + + // 绘制当前时间 + val timeText = formatTime(lyricEntryList[currentCenterLine].time) + val timeX = width - timeTextWidth.toFloat() / 2 + val timeY = centerY - (timeFontMetrics!!.descent + timeFontMetrics!!.ascent) / 2 + timePaint.color = timeTextColor + timePaint.alpha = alpha + canvas.drawText(timeText, timeX, timeY, timePaint) + } + + canvas.translate(0f, mViewPortOffset) + + var yOffset = 0f + var yMinOffset = 0f + var yMaxOffset = 0f + var scaleValue: Float + var progress: Float + var radius: Int + var calcHeightOnly: Boolean + + for (i in lyricEntryList.indices) { + // 根据上一项所计算得到的offset值,判断当前元素是否在需要绘制的区间,如果不在,则只需要计算高度不进行绘制相关计算 + calcHeightOnly = getOffset(i - 1) !in (mViewPortOffset - height)..(mViewPortOffset + height) + progressKeeper.updateTargetValue(i, if (currentLine == i) animateProgress else 0f) + progress = progressKeeper.getValueByIndex(i) + scaleValue = 1f + radius = 0 + + if (!calcHeightOnly) { + when { + // 当前行动画未结束 + progress > 0f -> { + scaleValue = calcScaleValue(currentTextSize, normalTextSize, progress) + lyricPaint.color = lerpColor(normalTextColor, currentTextColor, progress.coerceIn(0f, 1f)) + } + + isShowTimeline.value && i == currentCenterLine -> { + lyricPaint.color = timelineTextColor + } + + else -> { + lyricPaint.color = normalTextColor + } + } + lyricPaint.textSize = normalTextSize + secondLyricPaint.textSize = lyricPaint.textSize * translateTextScaleValue + secondLyricPaint.color = lyricPaint.color + + if (isEnableBlurEffect.value || isEnableBlurEffect.animateValue > 0f) { + radius = when (i) { + currentCenterLine -> 0 + currentCenterLine + 1 -> 3 + currentCenterLine + 2, currentCenterLine - 1 -> 7 + currentCenterLine + 3, currentCenterLine - 2 -> 11 + currentCenterLine + 4, currentCenterLine - 3 -> 20 + else -> 20 + } + blurProgressKeeper.updateTargetValue(i, radius.toFloat()) + radius = blurProgressKeeper.getValueByIndex(i).toInt() + radius = (radius * isEnableBlurEffect.animateValue).toInt() + } + } + + val itemHeight = drawLyricEntry( + canvas = canvas, + entry = lyricEntryList[i], + calcHeightOnly = calcHeightOnly, + yOffset = yOffset, + scaleValue = scaleValue, + blurRadius = radius, + ) { minHeight, maxHeight -> + minOffsetKeeper[i] = yMinOffset + calcOffsetOfItem(minHeight, sentenceDividerHeight) + yMinOffset += minHeight + + maxOffsetKeeper[i] = yMaxOffset + calcOffsetOfItem(maxHeight, sentenceDividerHeight) + yMaxOffset += maxHeight + } + heightKeeper[i] = itemHeight + offsetKeeper[i] = yOffset + calcOffsetOfItem(itemHeight, sentenceDividerHeight) + yOffset += itemHeight + } + return super.onDoDraw(canvas) + } + + /** + * 画一组歌词语句 + * + * @param calcHeightOnly 是否只计算高度 + * @param yOffset 歌词中心 Y 坐标 + * @param scaleValue 缩放比例 + * @param blurRadius 模糊半径 + * + * @return 该组歌词的实际绘制高度 + */ + private fun drawLyricEntry( + canvas: Canvas, + entry: LyricEntry, + calcHeightOnly: Boolean, + yOffset: Float, + scaleValue: Float, + blurRadius: Int, + callback: (minHeight: Float, maxHeight: Float) -> Unit = { _, _ -> } + ): Float { + var tempHeight = 0f + var minTempHeight = 0f + var maxTempHeight = 0f + + entry.staticLayout?.let { + tempHeight += drawText( + canvas = canvas, + staticLayout = it, + calcHeightOnly = calcHeightOnly, + yOffset = yOffset, + yClipPercentage = 1f, + scale = scaleValue, + blurRadius = blurRadius + ) + minTempHeight = tempHeight + maxTempHeight = tempHeight + + entry.secondStaticLayout?.let { second -> + tempHeight += translateDividerHeight * isDrawTranslationValue + maxTempHeight += translateDividerHeight + + tempHeight += drawText( + canvas = canvas, + staticLayout = second, + calcHeightOnly = calcHeightOnly, + yOffset = yOffset + tempHeight, + yClipPercentage = isDrawTranslationValue, + alpha = isDrawTranslationValue, + scale = scaleValue, + blurRadius = blurRadius + ) { _, max -> + maxTempHeight += max + } + } + tempHeight += sentenceDividerHeight + minTempHeight += sentenceDividerHeight + maxTempHeight += sentenceDividerHeight + } + callback(minTempHeight, maxTempHeight) + return tempHeight + } + + /** + * 画一行歌词 + * + * @param calcHeightOnly 是否只计算高度 + * @param yOffset 歌词中心 Y 坐标 + * @param yClipPercentage 垂直裁剪比例 + * @param scale 缩放比例 + * @param alpha 透明度 + * @param blurRadius 模糊半径 实现类似AppleMusic的歌词语句的模糊效果 + * + * @return 实际绘制高度 + */ + private fun drawText( + canvas: Canvas, + staticLayout: StaticLayout, + calcHeightOnly: Boolean = false, + yOffset: Float, + @FloatRange(from = 0.0, to = 1.0) + yClipPercentage: Float = 1f, + scale: Float = 1f, + alpha: Float = 1f, + blurRadius: Int = 0, + callback: (minHeight: Float, maxHeight: Float) -> Unit = { _, _ -> } + ): Float { + if (staticLayout.lineCount == 0) { + callback(0f, 0f) + return 0f + } + if (calcHeightOnly) { + callback(0f, staticLayout.height.toFloat()) + return staticLayout.height * yClipPercentage + } + val lineHeight = staticLayout.height.toFloat() / staticLayout.lineCount.toFloat() + + var yTemp = 0f // y轴临时偏移量 + var pivotYTemp: Float // 缩放中心Y坐标 + var itemActualHeight: Float // 单行实际绘制高度 + var actualHeight = 0f // 实际绘制高度 + + staticLayout.paint.alpha = (alpha * 255f).toInt() + staticLayout.paint.maskFilter = blurMaskFilterExt.get(blurRadius) + + /** + * 由于对StaticLayout整个缩放会使其中间的行间距也被缩放(通过TextPaint的textSize缩放则不会), + * 导致其真实渲染高度大于StaticLayout的height属性的值,同时也没有其他的接口能实现相同的缩放效果(对TextSize缩放会显得卡卡的) + * + * 所以通过Canvas的clipRect,来分别对StaticLayout的每一行文字进行缩放和绘制(StaticLayout的各行高度是一致的) + */ + repeat(staticLayout.lineCount) { + itemActualHeight = lineHeight * yClipPercentage + pivotYTemp = yTemp + itemActualHeight - staticLayout.paint.descent() // TextPaint修改textSize所实现的缩放效果应该就是descent线上的缩放(感觉效果差不多) + + canvas.save() + canvas.translate(lrcPadding, yOffset) + canvas.clipRect(-lrcPadding, yTemp, staticLayout.width.toFloat() + lrcPadding, yTemp + itemActualHeight) + + // 根据文字的gravity设置缩放基点坐标 + when (textGravity) { + GRAVITY_LEFT -> canvas.scale(scale, scale, 0f, pivotYTemp) + GRAVITY_RIGHT -> { + canvas.scale(scale, scale, staticLayout.width.toFloat(), pivotYTemp) + } + + GRAVITY_CENTER -> { + canvas.scale(scale, scale, staticLayout.width / 2f, pivotYTemp) + } + } + staticLayout.draw(canvas) + canvas.restore() + yTemp += itemActualHeight + actualHeight += itemActualHeight + } + callback(0f, staticLayout.height.toFloat()) + return actualHeight + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { + isTouching = false + if (hasLrc() && !isFling) { + // TODO 应该为Timeline独立设置一个Enable开关, 这样就可以不需要等待Timeline消失 + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME) + } + } + return gestureDetector!!.onTouchEvent(event) + } + + /** + * 手势监听器 + */ + private val mSimpleOnGestureListener: SimpleOnGestureListener = + object : SimpleOnGestureListener() { + + override fun onDown(e: MotionEvent): Boolean { + // 有歌词并且设置了 mOnPlayClickListener + if (hasLrc() && onPlayClickListener != null) { + scroller!!.forceFinished(true) + removeCallbacks(hideTimelineRunnable) + isTouching = true + invalidate() + return true + } + return super.onDown(e) + } + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + if (hasLrc()) { + // 如果没显示 Timeline 的时候,distanceY 一段距离后再显示时间线 + if (!isShowTimeline.value && abs(distanceY) >= 10) { + // 滚动显示时间线 + isShowTimeline.value = true + } + mViewPortOffset += -distanceY + mViewPortOffset.coerceIn(getOffset(lyricEntryList.size - 1), getOffset(0)) + invalidate() + return true + } + return super.onScroll(e1, e2, distanceX, distanceY) + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (hasLrc()) { + scroller!!.fling( + 0, mViewPortOffset.toInt(), 0, + velocityY.toInt(), 0, 0, + getOffset(lyricEntryList.size - 1).toInt(), + getOffset(0).toInt() + ) + isFling = true + return true + } + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (!hasLrc() || !isShowTimeline.value || !e.insideOf(playDrawable?.bounds)) { + onSingerClickListener?.onClick() + return super.onSingleTapConfirmed(e) + } + + val centerLine = centerLine + val centerLineTime = lyricEntryList[centerLine].time + // onPlayClick 消费了才更新 UI + if (onPlayClickListener?.onPlayClick(centerLineTime) == true) { + isShowTimeline.value = false + removeCallbacks(hideTimelineRunnable) + smoothScrollTo(centerLine) + invalidate() + return true + } + return super.onSingleTapConfirmed(e) + } + } + + private val hideTimelineRunnable = Runnable { + if (hasLrc() && isShowTimeline.value) { + isShowTimeline.value = false + smoothScrollTo(currentLine) + } + } + + override fun computeScroll() { + if (scroller!!.computeScrollOffset()) { + mViewPortOffset = scroller!!.currY.toFloat() + invalidate() + } + if (isFling && scroller!!.isFinished) { + isFling = false + if (hasLrc() && !isTouching) { + adjustCenter() + postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME) + } + } + } + + override fun onDetachedFromWindow() { + removeCallbacks(hideTimelineRunnable) + super.onDetachedFromWindow() + } + + private fun onLrcLoaded(entryList: List ?) { + if (!entryList.isNullOrEmpty()) { + lyricEntryList.addAll(entryList) + } + lyricEntryList.sort() + initEntryList() + invalidate() + } + + private fun initPlayDrawable() { + val l = (timeTextWidth - drawableWidth) / 2 + val t = startOffset.toInt() - drawableWidth / 2 + val r = l + drawableWidth + val b = t + drawableWidth + playDrawable!!.setBounds(l, t, r, b) + } + + private fun initEntryList() { + if (!hasLrc() || width == 0) { + return + } + /** + * StaticLayout 根据初始化时传入的 TextSize 计算换行的位置 + * 如果 [currentTextSize] 与 [normalTextSize] 相差较大, + * 则会导致歌词渲染时溢出边界,或行间距不足挤压在一起 + * + * 故计算出可能的最大 TextSize 以后,用其初始化,使 StaticLayout 拥有足够的高度 + */ + lyricPaint.textSize = max(currentTextSize, normalTextSize) + secondLyricPaint.textSize = lyricPaint.textSize * translateTextScaleValue + for (lrcEntry in lyricEntryList) { + lrcEntry.init( + lyricPaint, secondLyricPaint, + lrcWidth.toInt(), textGravity.toLayoutAlign() + ) + } + mCurrentOffset = startOffset + mViewPortOffset = startOffset + } + + private fun reset() { + // TODO 待完善reset的逻辑 + scroller!!.forceFinished(true) + isShowTimeline.value = false + isTouching = false + isFling = false + removeCallbacks(hideTimelineRunnable) + lyricEntryList.clear() + mCurrentOffset = 0f + mViewPortOffset = 0f + currentLine = 0 + invalidate() + } + + /** + * 将中心行微调至正中心 + */ + private fun adjustCenter() { + smoothScrollTo(currentLine) + } + + /** + * 平滑滚动过渡到某一行 + * + * @param line 行号 + */ + private fun smoothScrollTo(line: Int) { + val offset = getOffset(line) + animateStartOffset = mCurrentOffset + animateTargetOffset = offset + progressSpringAnimator.animateToFinalPosition(offset) + } + + /** + * 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数) + */ + private fun findShowLine(time: Long): Int { + var left = 0 + var right = lyricEntryList.size + while (left <= right) { + val middle = (left + right) / 2 + val middleTime = lyricEntryList[middle].time + if (time < middleTime) { + right = middle - 1 + } else { + if (middle + 1 >= lyricEntryList.size || time < lyricEntryList[middle + 1].time) { + return middle + } + left = middle + 1 + } + } + return 0 + } + + /** + * 计算单个歌词元素的偏移量,用于控制歌词对其中线的位置 + * + * 计算出来的歌词高度包含了分割线的高度,所以需要减去分割线的高度 + * + * @param itemHeight 歌词元素的高度 + * @param dividerHeight 分割线的高度 + * + * @return 歌词元素的偏移量 + */ + protected open fun calcOffsetOfItem(itemHeight: Float, dividerHeight: Float): Float { + return (itemHeight - dividerHeight) * itemOffsetPercent + } + + /** + * 因为添加了 [translateDividerHeight] 用来间隔开歌词与翻译, + * 所以直接从 [LyricEntry] 获取高度不可行, + * 故使用该 [getLyricHeight] 方法来计算 [LyricEntry] 的高度 + */ + @Deprecated("不再单独计算歌词的高度,在绘制时计算并进行更新缓存,所见即所得") + open fun getLyricHeight(line: Int): Int { + var height = lyricEntryList[line].staticLayout?.height ?: return 0 + lyricEntryList[line].secondStaticLayout?.height?.let { + height += (it + translateDividerHeight).toInt() + } + return height + } + + /** + * 获取歌词距离视图顶部的距离 + */ + private fun getOffset(line: Int): Float { + return startOffset - (offsetKeeper[line] ?: 0f) + } + + private fun getMinOffset(line: Int): Float { + return startOffset - (minOffsetKeeper[line] ?: 0f) + } + + private fun getMaxOffset(line: Int): Float { + return startOffset - (maxOffsetKeeper[line] ?: 0f) + } + + /** + * 在主线程中运行 + */ + private fun runOnMain(r: Runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + r.run() + } else { + post(r) + } + } + + /** + * 以下是公共部分 + * 用法见接口 [LyricViewXInterface] + */ + + override fun setSentenceDividerHeight(height: Float) { + sentenceDividerHeight = height + if (hasLrc()) { + smoothScrollTo(currentLine) + } + postInvalidate() + } + + override fun setTranslateDividerHeight(height: Float) { + translateDividerHeight = height + if (hasLrc()) { + smoothScrollTo(currentLine) + } + postInvalidate() + } + + override fun setHorizontalOffset(offset: Float) { + horizontalOffset = offset + initPlayDrawable() + postInvalidate() + } + + override fun setHorizontalOffsetPercent(percent: Float) { + horizontalOffsetPercent = percent + initPlayDrawable() + postInvalidate() + } + + override fun setTranslateTextScaleValue(scaleValue: Float) { + translateTextScaleValue = scaleValue + initEntryList() + if (hasLrc()) { + smoothScrollTo(currentLine) + } + } + + override fun setTextGravity(gravity: Int) { + textGravity = gravity + initEntryList() + if (hasLrc()) { + smoothScrollTo(currentLine) + } + } + + override fun setNormalColor(normalColor: Int) { + normalTextColor = normalColor + postInvalidate() + } + + override fun setNormalTextSize(size: Float) { + normalTextSize = size + initEntryList() + if (hasLrc()) { + smoothScrollTo(currentLine) + } + } + + override fun setCurrentTextSize(size: Float) { + currentTextSize = size + initEntryList() + if (hasLrc()) { + smoothScrollTo(currentLine) + } + } + + override fun setCurrentColor(currentColor: Int) { + currentTextColor = currentColor + postInvalidate() + } + + override fun setTimelineTextColor(timelineTextColor: Int) { + this.timelineTextColor = timelineTextColor + postInvalidate() + } + + override fun setTimelineColor(timelineColor: Int) { + this.timelineColor = timelineColor + postInvalidate() + } + + override fun setTimeTextColor(timeTextColor: Int) { + this.timeTextColor = timeTextColor + postInvalidate() + } + + override fun setLabel(label: String) { + runOnMain { + defaultLabel = label + this@LyricViewX.invalidate() + } + } + + override fun loadLyric(mainLyricText: String?, secondLyricText: String?) { + runOnMain { + reset() + val sb = StringBuilder("file://") + sb.append(mainLyricText) + if (secondLyricText != null) { + sb.append("#").append(secondLyricText) + } + val flag = sb.toString() + this@LyricViewX.flag = flag + thread { + val lrcEntries = LyricUtil.parseLrc(arrayOf(mainLyricText, secondLyricText)) + runOnMain { + if (flag === flag) { + onLrcLoaded(lrcEntries) + this@LyricViewX.flag = null + } + } + } + } + } + + override fun loadLyric(lyricEntries: List ) { + runOnMain { + reset() + onLrcLoaded(lyricEntries) + } + } + + override fun updateTime(time: Long, force: Boolean) { + // 将方法的执行延后至 View 创建完成后执行 + readyHelper.whenReady { + if (!it) return@whenReady + if (hasLrc()) { + val line = findShowLine(time) + if (line != currentLine) { + runOnMain { + currentLine = line + smoothScrollTo(line) + } + } + } + } + } + + override fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?) { + this.onPlayClickListener = if (draggable) { + requireNotNull(onPlayClickListener) { "if draggable == true, onPlayClickListener must not be null" } + onPlayClickListener + } else { + null + } + } + + override fun setOnSingerClickListener(onSingerClickListener: OnSingleClickListener?) { + this.onSingerClickListener = onSingerClickListener + } + + override fun getLyricEntryList(): List { + return lyricEntryList.toList() + } + + override fun setLyricEntryList(newList: List ) { + reset() + onLrcLoaded(newList) + this@LyricViewX.flag = null + } + + override fun getCurrentLineLyricEntry(): LyricEntry? { + if (currentLine <= lyricEntryList.lastIndex) { + return lyricEntryList[currentLine] + } + return null + } + + override fun setLyricTypeface(file: File) { + val typeface = file.takeIf { it.exists() } + ?.runCatching { Typeface.createFromFile(this) } + ?.getOrNull() ?: return + + setLyricTypeface(typeface) + } + + override fun setLyricTypeface(path: String) { + setLyricTypeface(File(path)) + } + + override fun setLyricTypeface(typeface: Typeface?) { + lyricPaint.typeface = typeface + secondLyricPaint.typeface = typeface + postInvalidate() + } + + override fun setDampingRatioForLyric(dampingRatio: Float) { + dampingRatioForLyric = dampingRatio + progressSpringAnimator.spring.dampingRatio = dampingRatio + } + + override fun setDampingRatioForViewPort(dampingRatio: Float) { + dampingRatioForViewPort = dampingRatio + viewPortSpringAnimator.spring.dampingRatio = dampingRatio + } + + override fun setStiffnessForLyric(stiffness: Float) { + stiffnessForLyric = stiffness + progressSpringAnimator.spring.stiffness = stiffness + } + + override fun setStiffnessForViewPort(stiffness: Float) { + stiffnessForViewPort = stiffness + viewPortSpringAnimator.spring.stiffness = stiffness + } + + override fun setPlayDrawable(drawable: Drawable) { + playDrawable = drawable + } + + override fun setIsDrawTranslation(isDrawTranslation: Boolean) { + this.isDrawTranslation = isDrawTranslation + postInvalidate() + } + + override fun setIsEnableBlurEffect(isEnableBlurEffect: Boolean) { + this.isEnableBlurEffect.value = isEnableBlurEffect + postInvalidate() + } + + override fun setItemOffsetPercent(itemOffsetPercent: Float) { + this.itemOffsetPercent = itemOffsetPercent + postInvalidate() + } + + companion object { + + private const val TAG = "LyricViewX" + + // 时间线持续时间 + private const val TIMELINE_KEEP_TIME = 3 * DateUtils.SECOND_IN_MILLIS + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt b/app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt new file mode 100644 index 0000000..9b81331 --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt @@ -0,0 +1,51 @@ +package com.dirror.lyricviewx + +import androidx.annotation.IntDef + +const val STATE_CREATED = 1 +const val STATE_INITIALIZING = 2 +const val STATE_INITIALIZED = 3 +const val STATE_ERROR = 4 + +@IntDef( + STATE_CREATED, + STATE_INITIALIZING, + STATE_INITIALIZED, + STATE_ERROR +) +@Retention(AnnotationRetention.SOURCE) +annotation class ReadyState + +/** + * 简单的状态机,根据 [readyState] 的状态决定当前任务的执行或延后与否 + */ +open class ReadyHelper { + private var readyCallback: (Boolean) -> Unit = {} + + @ReadyState + var readyState: Int = STATE_CREATED + set(value) { + if (field == value) return + when (value) { + STATE_INITIALIZED, + STATE_ERROR -> synchronized(readyCallback) { + field = value + readyCallback.invoke(value != STATE_ERROR) + } + else -> field = value + } + } + + fun whenReady(performAction: (Boolean) -> Unit): Boolean { + return when (readyState) { + STATE_CREATED, STATE_INITIALIZING -> { + readyCallback = performAction + false + } + else -> { + performAction(readyState != STATE_ERROR) + true + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dirror/lyricviewx/SmoothInterpolator.kt b/app/src/main/java/com/dirror/lyricviewx/SmoothInterpolator.kt new file mode 100644 index 0000000..2ad4a4b --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/SmoothInterpolator.kt @@ -0,0 +1,20 @@ +package com.dirror.lyricviewx + +import android.animation.TimeInterpolator +import kotlin.math.pow + +/** + * Smooth 插值器 + * @author Moriafly + */ +@Deprecated("过时") +class SmoothInterpolator: TimeInterpolator { + override fun getInterpolation(input: Float): Float { + val a = 1.11571230005336 + val b = -1.99852071205059 + val c = 0.272428743837376 + val d = -1.15835562067601E-05 + return ((a - d) / (1.0 + (input.toDouble() / c).pow(b)) + d).toFloat() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/dirror/lyricviewx/extension/BlurMaskFilterExt.kt b/app/src/main/java/com/dirror/lyricviewx/extension/BlurMaskFilterExt.kt new file mode 100644 index 0000000..f5a8558 --- /dev/null +++ b/app/src/main/java/com/dirror/lyricviewx/extension/BlurMaskFilterExt.kt @@ -0,0 +1,15 @@ +package com.dirror.lyricviewx.extension + +import android.graphics.BlurMaskFilter +import android.util.SparseArray + +class BlurMaskFilterExt { + private val maskFilterCache = SparseArray () + + fun get(radius: Int): BlurMaskFilter? { + if (radius == 0 || radius > 25) return null + + return maskFilterCache[radius] ?: BlurMaskFilter(radius.toFloat(), BlurMaskFilter.Blur.NORMAL) + .also { maskFilterCache.put(radius, it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/MediaPlayer.java b/app/src/main/java/com/muqingbfq/MediaPlayer.java new file mode 100644 index 0000000..efcacb0 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/MediaPlayer.java @@ -0,0 +1,138 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.muqingbfq.api.url; +import com.muqingbfq.fragment.mp3; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +public class MediaPlayer extends android.media.MediaPlayer { + public MediaPlayer() { + this.setOnCompletionListener(mediaPlayer -> { + if (!home.db.view.isShown()) { + home.db.view.setVisibility(View.VISIBLE); + } + int i = bfqkz.getmti(bfqkz.ms); + bfqkz.xm = bfqkz.list.get(i); + new Thread() { + @Override + public void run() { + super.run(); + bfqkz.mp3(url.hq(bfqkz.xm)); + } + }.start(); + }); + this.setOnErrorListener((mediaPlayer, i, i1) -> { + bfqkz.list.remove(bfqkz.xm); + return false; + }); + resumeTimer(); + } + + @Override + public void pause() throws IllegalStateException { + if (isPlaying()) { + super.pause(); + //暂停 + if (bfq.kg != null) { + bfq.kg.setImageResource(R.drawable.zt); + } + home.db.txa.setImageResource(R.drawable.zt); + bfqkz.updateNotification(); + } + } + + public Timer timer; + public TimerTask timerTask; + public void pauseTimer() { + if (timer != null) { + timer.cancel(); + } + } + public void resumeTimer() { + timer = new Timer();//定时器 + timerTask = new TimerTask() { + @Override + public void run() { + if (bfqkz.mt.isPlaying() && bfq.getVisibility()) { + int currentPosition = bfqkz.mt.getCurrentPosition(); + bfq.tdt.setProgress(currentPosition); + bfq.lrcView.updateTime(currentPosition, true); + } + } + }; + timer.scheduleAtFixedRate(timerTask, 0, 500); + } + + @Override + public void start() throws IllegalStateException { + super.start(); + if (bfqkz.xm == null) { + bfq_an.xyq(); + return; + } + //开始 + if (bfq.kg != null) { + bfq.kg.setImageResource(R.drawable.bf); + } + home.db.txa.setImageResource(R.drawable.bf); + bfqkz.updateNotification(); + } + + @SuppressLint("NotifyDataSetChanged") + @Override + public void setDataSource(String path) throws IOException, IllegalArgumentException, IllegalStateException, SecurityException { + super.setDataSource(path); + prepare(); + bfqkz.tdt_max = getDuration(); + bfqkz.tdt_wz = getCurrentPosition(); + Glide.with(main.context) + .asBitmap() + .load(bfqkz.xm.picurl) + .addListener(new RequestListener () { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, + @NonNull Target target, boolean isFirstResource) { + bfqkz.notify.setBitmap(null); + return false; + } + + @Override + public boolean onResourceReady(@NonNull Bitmap bitmap, @NonNull Object model, Target target, + @NonNull DataSource dataSource, boolean isFirstResource) { + bfqkz.notify.setBitmap(bitmap); + return false; + } + }) + .submit(); + start(); + main.handler.post(() -> { + if (bfq.name != null) { + bfq.tdt.setMax((int) bfqkz.tdt_max); + bfq.tdt.setProgress((int) bfqkz.tdt_wz); + bfq.time_a.setText(bfq_an.getTime(bfqkz.tdt_max)); + bfq.name.setText(bfqkz.xm.name); + bfq.zz.setText(bfqkz.xm.zz); + bfq_an.islike(bfq.like.getContext()); + } + home.db.name.setText(bfqkz.xm.name); + home.db.zz.setText(bfqkz.xm.zz); + if (mp3.lbspq != null) { + mp3.lbspq.notifyDataSetChanged(); + } + }); + } +} diff --git a/app/src/main/java/com/muqingbfq/MyButtonClickReceiver.java b/app/src/main/java/com/muqingbfq/MyButtonClickReceiver.java new file mode 100644 index 0000000..b8af591 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/MyButtonClickReceiver.java @@ -0,0 +1,155 @@ +package com.muqingbfq; + +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.view.KeyEvent; + +import java.util.Timer; +import java.util.TimerTask; + +public class MyButtonClickReceiver extends BroadcastReceiver { + private Timer timer = new Timer(); + private static int clickCount; + + public MyButtonClickReceiver() { + super(); + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { + int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, 0); + //蓝牙断开 + if (bluetoothState == BluetoothAdapter.STATE_DISCONNECTED) { + receiverPause(); + } + return; + } + if (action.equals("android.intent.action.HEADSET_PLUG")) { + if (intent.hasExtra("state")) { + if (intent.getIntExtra("state", 2) == 0) { + //拔出 + if (bfqkz.mt.isPlaying()) { + receiverPause(); + } + } else if (intent.getIntExtra("state", 2) == 1) { + receiverPlay(); + //插入 + } + } + return; + } + if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { + KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK && keyEvent.getAction() == KeyEvent.ACTION_UP) { + clickCount = clickCount + 1; + if (clickCount == 1) { + HeadsetTimerTask headsetTimerTask = new HeadsetTimerTask(); + timer.schedule(headsetTimerTask, 500); + } + } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_NEXT && keyEvent.getAction() == KeyEvent.ACTION_UP) { + handler(2); + } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PREVIOUS && keyEvent.getAction() == KeyEvent.ACTION_UP) { + handler(3); + } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE && keyEvent.getAction() == KeyEvent.ACTION_UP) { + handler(4); + } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY && keyEvent.getAction() == KeyEvent.ACTION_UP) { + handler(5); + } + return; + } + + switch (action) { + case "kg": + playOrPause(); + break; + case "syq": + bfq_an.syq(); + break; + case "xyq": + bfq_an.xyq(); + break; + } + // 处理按钮点击事件的逻辑 + } + + class HeadsetTimerTask extends TimerTask { + @Override + public void run() { + try { + if (clickCount == 1) { + handler(1); + } else if (clickCount == 2) { + handler(2); + } else if (clickCount >= 3) { + handler(3); + } + clickCount = 0; + } catch (Exception e) { + e.printStackTrace(); + } + } + } + private void handler(int a) { + switch (a) { + case 1: + playOrPause(); + break; + case 2: + playNext(); + break; + case 3: + playPrevious(); + break; + case 4: + receiverPause(); + break; + case 5: + receiverPlay(); + break; + default: + break; + } + } + + + // * 对蓝牙 播放 + public void receiverPlay() { + bfqkz.mt.start(); + } + + // * 对蓝牙 暂停 + public void receiverPause() { + bfqkz.mt.pause(); + } + + /** + * 对蓝牙 播放-暂停 + */ + public static void playOrPause() { +// gj.sc(isMusicServiceBound); + // 播放/暂停按钮点击事件 if (isMusicServiceBound) + if (bfqkz.mt.isPlaying()) { + bfqkz.mt.pause(); + } else { + bfqkz.mt.start(); + } + } + + /** + * 对蓝牙 下一首 + */ + public void playNext() { + bfq_an.xyq(); + } + + /** + * 对蓝牙 上一首 + */ + public void playPrevious() { + bfq_an.syq(); + } +} diff --git a/app/src/main/java/com/muqingbfq/activity_about_software.java b/app/src/main/java/com/muqingbfq/activity_about_software.java new file mode 100644 index 0000000..6ada786 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/activity_about_software.java @@ -0,0 +1,36 @@ +package com.muqingbfq; + +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +public class activity_about_software extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about_software); + Toolbar toolbar = findViewById(R.id.toolbar); + try { + String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + toolbar.setSubtitle(versionName + " Base"); + } catch (PackageManager.NameNotFoundException e) { + yc.start(this, e); + } + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + } + return super.onOptionsItemSelected(item); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/activity_search.java b/app/src/main/java/com/muqingbfq/activity_search.java new file mode 100644 index 0000000..bf7eb9b --- /dev/null +++ b/app/src/main/java/com/muqingbfq/activity_search.java @@ -0,0 +1,270 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.muqingbfq.fragment.search; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class activity_search extends AppCompatActivity { + private EditText editText; + private ArrayAdapter adapter; + + private JSONObject json = new JSONObject(); + private List json_list = new ArrayList<>(); + private final List list = new ArrayList<>(); + ListView listPopupWindow; + public static AppCompatActivity appCompatActivity; + + @SuppressLint({"RestrictedApi", "NotifyDataSetChanged"}) + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_search); + appCompatActivity = this; + setSupportActionBar(findViewById(R.id.toolbar)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + RecyclerView recyclerView = findViewById(R.id.list_recycler); + SearchRecordAdapter recordAdapter = new SearchRecordAdapter(); + recyclerView.setAdapter(recordAdapter); + + editText = findViewById(R.id.editview); + editText.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + String str = v.getText().toString(); + if (!str.equals("")) { +// 退出activity并返回str数据 + start(str); + } + } + return false; + }); + findViewById(R.id.deleat).setOnClickListener(v -> new MaterialAlertDialogBuilder(v.getContext()) + .setTitle("删除") + .setMessage("清空历史记录?") + .setNegativeButton("取消", null) + .setPositiveButton("确定", (dialogInterface, i) -> { + wj.sc(wj.filesdri + wj.lishi_json); + json = new JSONObject(); + json_list.clear(); + recordAdapter.notifyDataSetChanged(); + findViewById(R.id.xxbj1).setVisibility(View.GONE); + }) + .show()); + listPopupWindow = findViewById(R.id.search_recycler); + + adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, list); + listPopupWindow.setAdapter(adapter); + + //设置项点击监听 + listPopupWindow.setOnItemClickListener((adapterView, view, i, l) -> { + editText.clearFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + View v = getWindow().peekDecorView(); + if (null != v) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + editText.setText(list.get(i));//把选择的选项内容展示在EditText上 + dismiss();//如果已经选择了,隐藏起来 + start(editText.getText().toString()); + + }); + editText.setOnFocusChangeListener((view, b) -> { + if (b) { + dismiss(); + } + }); + editText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + list.clear(); + if (s.length() < 1) { + list.clear(); + adapter.notifyDataSetChanged(); + editText.clearFocus(); + dismiss(); + return; + } + if (!editText.hasFocus()) { + dismiss(); + return; + } + listPopupWindow.setVisibility(View.VISIBLE); + new Thread() { + @Override + public void run() { + String hq = wl.hq("/search/suggest?keywords=" + s + "&type=mobile"); + try { + JSONArray jsonArray = new JSONObject(hq).getJSONObject("result") + .getJSONArray("allMatch"); + int length = jsonArray.length(); + for (int i = 0; i < length; i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String keyword = jsonObject.getString("keyword"); + list.add(keyword); + } + main.handler.post(() -> adapter.notifyDataSetChanged()); + } catch (Exception e) { + gj.ts(activity_search.this, e); + } + super.run(); + } + }.start(); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + fragmentManager = getSupportFragmentManager(); + fragmentTransaction = fragmentManager.beginTransaction(); + } + + public void dismiss() { + listPopupWindow.setVisibility(View.GONE); + } + + private void addSearchRecord(String name) { + try { + if (!json.has("list")) { + json.put("list", new JSONArray()); + } + if (!json_list.contains(name)) { + json_list.add(name); + JSONObject record = new JSONObject(); + record.put("name", name); + json.getJSONArray("list").put(record); + wj.xrwb(wj.filesdri + wj.lishi_json, json.toString()); + adapter.notifyDataSetChanged(); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.home, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + } else if (itemId == R.id.menu_search) { + start(editText.getText().toString()); + } + return super.onOptionsItemSelected(item); + } + + + FragmentManager fragmentManager; + FragmentTransaction fragmentTransaction; + + com.muqingbfq.fragment.search search; + public void start(String name) { + dismiss(); + if (name.equals("")) { + return; + } + if (search == null) { + search = new search(name); + } + if (!search.isVisible()) { + getSupportFragmentManager().beginTransaction() + .add(R.id.search_fragment, search) + .addToBackStack(null).commit(); + } else { + search.setStart(name); + } + addSearchRecord(name); + } + + class SearchRecordAdapter extends RecyclerView.Adapter { + public SearchRecordAdapter() { + String dqwb = wj.dqwb(wj.filesdri + wj.lishi_json); + if (dqwb != null) { + try { + json = new JSONObject(dqwb); + JSONArray list1 = json.getJSONArray("list"); + int length = list1.length(); + for (int i = length - 1; i >= 0; i--) { + json_list.add(list1. + getJSONObject(i).getString("name")); + } + } catch (JSONException e) { + yc.start(activity_search.this, e); + } + } + if (json_list.isEmpty()) { + findViewById(R.id.xxbj1).setVisibility(View.INVISIBLE); + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = View.inflate(parent.getContext(), android.R.layout.simple_list_item_1, null); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + String keyword = json_list.get(position); + holder.recordTextView.setText(keyword); + holder.recordTextView.setOnClickListener(v -> { + editText.setText(keyword); + start(keyword); + }); + } + + @Override + public int getItemCount() { + return json_list.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView recordTextView; + + public ViewHolder(View itemView) { + super(itemView); + recordTextView = itemView.findViewById(android.R.id.text1); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/api/playlist.java b/app/src/main/java/com/muqingbfq/api/playlist.java new file mode 100644 index 0000000..b608ef5 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/api/playlist.java @@ -0,0 +1,124 @@ +package com.muqingbfq.api; + +import android.annotation.SuppressLint; + +import com.muqingbfq.bfqkz; +import com.muqingbfq.fragment.gd; +import com.muqingbfq.fragment.mp3; +import com.muqingbfq.main; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; +import com.muqingbfq.xm; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.List; + +public class playlist extends Thread { + public static final String api = "/playlist/track/all?id="; + private String uid; + + public playlist(String uid) { + this.uid = uid; + start(); + } + + @SuppressLint("NotifyDataSetChanged") + public static boolean hq(List list, String uid) { + list.clear(); + try { + String hq; + if (wj.cz(wj.gd + uid)) { + hq = wj.dqwb(wj.gd + uid); + } else { + hq = wl.hq(api + uid + "&limit=30"); + } + JSONObject json = new JSONObject(hq); + JSONArray songs = json.getJSONArray("songs"); + int length = songs.length(); + for (int i = 0; i < length; i++) { + JSONObject jsonObject = songs.getJSONObject(i); + String id = jsonObject.getString("id"); + String name = jsonObject.getString("name"); + + JSONObject al = jsonObject.getJSONObject("al"); + JSONArray ar = jsonObject.getJSONArray("ar"); + StringBuilder zz = new StringBuilder(); + int length_a = ar.length(); + for (int j = 0; j < length_a; j++) { + zz.append(ar.getJSONObject(j).getString("name")) + .append("/"); + } + zz.append("-").append(al.getString("name")); + String picUrl = al.getString("picUrl"); + list.add(new xm(id, name, zz.toString(), picUrl)); + } +// main.handler.post(new mp3.lbspq_sx()); + return true; + } catch (Exception e) { + gj.sc("失败的错误 " + e); + } + return false; + } + + public static void hq_like(List list) { + list.clear(); + try { + JSONObject json = gd.like; + for (Iterator it = json.keys(); it.hasNext(); ) { + String id = it.next(); + JSONObject jsonObject = json.getJSONObject(id); + String name = jsonObject.getString("name"); + String zz = jsonObject.getString("zz"); + String picUrl = jsonObject.getString("picUrl"); + list.add(new xm(id, name, zz, picUrl)); + } + main.handler.post(new mp3.lbspq_sx()); + } catch (Exception e) { + gj.sc("失败的错误 " + e); + } + } + + public static void hq_xz(List list) { + list.clear(); + try { + JSONArray json = new JSONObject(wj.dqwb(wj.mp3_xz)) + .getJSONArray("songs"); + int length = json.length(); + for (int i = 0; i < length; i++) { + JSONObject jsonObject = json.getJSONObject(i); + String id = jsonObject.getString("id"); + String name = jsonObject.getString("name"); + String zz = jsonObject.getString("zz"); + String picUrl = jsonObject.getString("picUrl"); + list.add(new xm(id, name, zz, picUrl)); + } +// main.handler.post(new mp3.lbspq_sx()); + } catch (Exception e) { + gj.sc("失败的错误 " + e); + wj.sc(wj.mp3_xz); + } + } + + @Override + public void run() { + super.run(); +/* if (uid.equals(wj.mp3_xz)) { + playlist.hq_xz(mp3.list); + } else if (uid.equals(wj.mp3_like)) { + playlist.hq_like(mp3 + .list); + } else { + playlist.hq(mp3.list, uid); + } + if (bfqkz.list == null || bfqkz.list.isEmpty()) { + int size = mp3.list.size(); + for (int i = 0; i < size; i++) { + bfqkz.list.add(mp3.list.get(i)); + } + }*/ + } +} diff --git a/app/src/main/java/com/muqingbfq/api/resource.java b/app/src/main/java/com/muqingbfq/api/resource.java new file mode 100644 index 0000000..d87699d --- /dev/null +++ b/app/src/main/java/com/muqingbfq/api/resource.java @@ -0,0 +1,107 @@ +package com.muqingbfq.api; + +import com.muqingbfq.R; +import com.muqingbfq.main; +import com.muqingbfq.start; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.List; +import com.muqingbfq.xm; +public class resource { + + public static void recommend(List list) { + String hq; + JSONObject json; + try { + if (wj.cz(wj.gd_json)&& start.time>System.currentTimeMillis()-3600000) { + hq = wj.dqwb(wj.gd_json); + json = new JSONObject(hq); + } else { + hq = wl.hq("/recommend/resource?cookie="+wl.Cookie); + if (hq == null && wj.cz(wj.gd_json)) { + hq = wj.dqwb(wj.gd_json); + json = new JSONObject(hq); + } + json = new JSONObject(hq); + if (json.getInt("code") == 200) { + wj.xrwb(wj.gd_json, hq); + start.time = System.currentTimeMillis(); + main.edit.putLong(main.Time, start.time); + main.edit.commit(); + } + } + JSONArray recommend = json.getJSONArray("recommend"); + int length = recommend.length(); + for (int i = 0; i < length; i++) { + JSONObject jsonObject = recommend.getJSONObject(i); + add(jsonObject, list); + } + } catch (Exception e) { + gj.sc("resource tuijian" + e); + } + } + + public static void 排行榜(List list) { + String hq; + try { + if (wj.cz(wj.gd_phb)) { + hq = wj.dqwb(wj.gd_phb); + } else { + hq = wl.hq("/toplist"); + if (hq == null) { + return; + } + wj.xrwb(wj.gd_phb, hq); + } + JSONObject jsonObject = new JSONObject(hq); + if (jsonObject.getInt("code") == 200) { + JSONArray list_array = jsonObject.getJSONArray("list"); + int length = list_array.length(); + for (int i = 0; i < length; i++) { + JSONObject get = list_array.getJSONObject(i); + String id = get.getString("id"); + String name = get.getString("name") + "\n" + get.getString("description"); + boolean cz = wj.cz(wj.gd + id); + String coverImgUrl = get.getString("coverImgUrl"); + list.add(new xm(id, name, coverImgUrl, cz)); + } + } + } catch (Exception e) { + gj.sc(e); + } + } + + public static void 下载(List list) { +// list.add(new xm("hc.json", "缓存", R.drawable.icon, true)); + list.add(new xm("mp3_like.json", "喜欢", R.mipmap.like, true)); + list.add(new xm("mp3_xz.json", "下载", R.drawable.icon, true)); + try { + // JSONArray date = jsonObject.getJSONArray(""); + JSONObject date = new JSONObject(wj.dqwb(wj.gd_xz)); + for (Iterator it = date.keys(); it.hasNext(); ) { + String id = it.next(); + boolean cz = wj.cz(wj.gd + id); + JSONObject jsonObject = date.getJSONObject(id); + String name = jsonObject.getString("name"); + String picUrl = jsonObject.getString("picUrl"); + list.add(new xm(id, name, picUrl, cz)); + } + } catch (Exception e) { + gj.sc(e); + } + } + + private static void add(JSONObject jsonObject, List list) throws Exception { + String id = jsonObject.getString("id"); + boolean cz = wj.cz(wj.gd + id); + String name = jsonObject.getString("name"); + String picUrl = jsonObject.getString("picUrl"); + list.add(new xm(id, name, picUrl, cz)); + } +} diff --git a/app/src/main/java/com/muqingbfq/api/url.java b/app/src/main/java/com/muqingbfq/api/url.java new file mode 100644 index 0000000..40776b4 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/api/url.java @@ -0,0 +1,99 @@ +package com.muqingbfq.api; + +import android.view.View; + +import com.muqingbfq.bfq; +import com.muqingbfq.home; +import com.muqingbfq.main; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; +import com.muqingbfq.xm; +import com.muqingbfq.yc; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class url extends Thread { + public static String api = "/song/url/v1"; + xm x; + + public url(xm x) { + this.x = x; + start(); + } + + public static String hq(xm x) { + if (bfq.getVisibility() && bfq.lrcView != null && bfq.lrcView.getVisibility() == View.VISIBLE) { + gc(x.id); + } else { + lrc = null; + } + try { + if (wj.cz(wj.mp3 + x.id)) { + return wj.mp3 + x.id; + } + String level = "standard"; + if (gj.isWiFiConnected()) { + level = "exhigh"; + } + String hq = wl.hq(api + "?id=" + x.id + "&level=" + + level + "&cookie=" + wl.Cookie); + gj.sc(hq); + if (hq == null) { + return null; + } + JSONObject json = new JSONObject(hq); + JSONArray data = json.getJSONArray("data"); + JSONObject jsonObject = data.getJSONObject(0); + + String url = jsonObject.getString("url"); + if (wl.xz(url, x)) { + url = wj.mp3 + x.id; + } + return url; + } catch (JSONException e) { + yc.start("url hq :" + e); + } + return null; + } + + @Override + public void run() { + super.run(); + com.muqingbfq.bfqkz.mp3(hq(x)); + } + + + public static String lrc, tlyric; + + public static void gc(String id) { + lrc = null; + tlyric = null; + JSONObject jsonObject = new JSONObject(); + try { + jsonObject = new JSONObject(wl.hq("/lyric?id=" + id)); + lrc = jsonObject.getJSONObject("lrc").getString("lyric"); + } catch (JSONException e) { + gj.sc("url gc(int id) lrc: " + e); + } + try { + tlyric = jsonObject.getJSONObject("tlyric").getString("lyric"); + } catch (JSONException e) { + gj.sc("url gc(int id) tlyric: " + e); + } + bfq.lrcView.loadLyric(lrc, tlyric); + } + + public static String picurl(String id) { + String hq = wl.hq("/song/detail?ids=" + id); + try { + return new JSONObject(hq).getJSONArray("songs").getJSONObject(0) + .getJSONObject("al").getString("picUrl"); + } catch (Exception e) { + yc.start(main.context, e); + } + return null; + } +} diff --git a/app/src/main/java/com/muqingbfq/bfq.java b/app/src/main/java/com/muqingbfq/bfq.java new file mode 100644 index 0000000..5358851 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/bfq.java @@ -0,0 +1,196 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + +import com.bumptech.glide.Glide; + +import org.json.JSONObject; + +public class bfq extends AppCompatActivity { + @SuppressLint("StaticFieldLeak") + public static SeekBar tdt; + @SuppressLint("StaticFieldLeak") + public static TextView name, zz, time_a, time_b; + @SuppressLint("StaticFieldLeak") + public static ImageView tx; + @SuppressLint("StaticFieldLeak") + public static ImageView kg, syq, xyq, like; + public static com.dirror.lyricviewx.LyricViewX lrcView; + @SuppressLint("ResourceType") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FrameLayout frameLayout = new FrameLayout(this); + // 设置 FrameLayout 的布局参数(可以根据自己的需要进行设置) + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, // 宽度为 Match Parent + FrameLayout.LayoutParams.MATCH_PARENT); // 高度为 Match Parent + frameLayout.setLayoutParams(params); + frameLayout.setId(1); + setContentView(frameLayout); + getSupportFragmentManager().beginTransaction() + .add(frameLayout.getId(), new fragment(this)) + .commit(); +/* TypedValue typedValue = new TypedValue(); + getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true); + // 设置背景颜色 + bj = typedValue.data;*/ + } + @SuppressLint("StaticFieldLeak") + public static View inflate; + private static AppCompatActivity context; + public static Bitmap bitmap; + public static class fragment extends Fragment { + + public fragment(AppCompatActivity context) { + bfq.context = context; + } + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (inflate != null) { + tx.setImageBitmap(bitmap); + return inflate; + } + inflate = inflater.inflate(R.layout.fragment_bfq, container, false); + lrcView = inflate.findViewById(R.id.gc); + Toolbar toolbar = inflate.findViewById(R.id.toolbar); + name = inflate.findViewById(R.id.name); + zz = inflate.findViewById(R.id.zz); + kg = inflate.findViewById(R.id.kg); + xyq = inflate.findViewById(R.id.xyq); + syq = inflate.findViewById(R.id.syq); + tx = inflate.findViewById(R.id.mttx); + tdt = inflate.findViewById(R.id.tdt); + time_a = inflate.findViewById(R.id.time_a); + time_b = inflate.findViewById(R.id.time_b); + +// lrcView.setIsEnableBlurEffect(true); + View kp = inflate.findViewById(R.id.kp1); + kp.setOnClickListener(v -> { + if (lrcView != null) { + v.setVisibility(View.GONE); + lrcView.setVisibility(View.VISIBLE); + if (com.muqingbfq.api.url.lrc == null) { + new Thread() { + @Override + public void run() { + super.run(); + com.muqingbfq.api.url.gc(bfqkz.xm.id); + } + }.start(); + } + } + }); + toolbar.setNavigationIcon(R.drawable.end); + toolbar.setNavigationOnClickListener(view1 -> context.finish()); + toolbar.inflateMenu(R.menu.bfq); + toolbar.setOnMenuItemClickListener(item -> { + if (item.getItemId() == R.id.fx) { + com.muqingbfq.mq.gj.fx(context, + "音乐名称:" + name.getText().toString() + + "\n 作者:" + zz.getText().toString() + + "\n 链接:https://music.163.com/#/song?id=" + bfqkz.id); + } + return false; + }); + lrcView.setDraggable(true, (time) -> { + com.muqingbfq.bfqkz.mt.seekTo(Math.toIntExact(time)); + return true; + }); + lrcView.setOnSingerClickListener(() -> { + lrcView.setVisibility(View.GONE); + kp.setVisibility(View.VISIBLE); + }); + inflate.findViewById(R.id.layout).setOnClickListener(view1 -> { + lrcView.setVisibility(View.GONE); + kp.setVisibility(View.VISIBLE); + }); + inflate.findViewById(R.id.bfq_list_mp3). + setOnClickListener(view1 -> com.muqingbfq.fragment.bflb_db.start(context)); + + bfq_an.kz kz = new bfq_an.kz(); + kg.setOnClickListener(kz); + syq.setOnClickListener(kz); + xyq.setOnClickListener(kz); + like = inflate.findViewById(R.id.like); + ImageView control = inflate.findViewById(R.id.control); + control.setOnClickListener(new bfq_an.control(control)); + UI(inflate); + return inflate; + } + private void UI(View view) { +// tdt.getProgressDrawable(). +// setColorFilter(ContextCompat.getColor(this, R.color.text_tm), PorterDuff.Mode.MULTIPLY); +// tdt.getThumb(). +// setColorFilter(ContextCompat.getColor(this, R.color.text), PorterDuff.Mode.SRC_IN); + tdt.setOnSeekBarChangeListener(new bfq_an.tdt()); + + like.setOnClickListener(view1 -> { + try { + if (bfqkz.like_bool) { + like.setImageTintList(ContextCompat.getColorStateList(view.getContext(), R.color.text)); + com.muqingbfq.fragment.gd.like.remove(String.valueOf(bfqkz.xm.id)); + } else { + like.setImageTintList(ContextCompat. + getColorStateList(view.getContext(), android.R.color.holo_red_dark)); + JSONObject json = new JSONObject(); + json.put("name", bfqkz.xm.name); + json.put("zz", bfqkz.xm.zz); + json.put("picUrl", bfqkz.xm.picurl); + com.muqingbfq.fragment.gd.like.put(String.valueOf(bfqkz.xm.id), json); + } + com.muqingbfq.mq.wj.xrwb(com.muqingbfq.mq.wj.mp3_like, + com.muqingbfq.fragment.gd.like.toString()); + bfqkz.like_bool = !bfqkz.like_bool; + } catch (Exception e) { + e.printStackTrace(); + } + }); + if (bfqkz.xm != null) { + xm xm = bfqkz.xm; + name.setText(xm.name); + zz.setText(xm.zz); + time_a.setText(bfq_an.getTime(bfqkz.tdt_max)); + tdt.setMax((int) bfqkz.tdt_max); + if (bfqkz.mt.isPlaying()) { + kg.setImageResource(R.drawable.bf); + } + bfq_an.islike(context); + Glide.with(context).load(xm.picurl) + .placeholder(R.drawable.icon) + .into(tx); + } + } + } + + public static void start(AppCompatActivity context) { + Intent intent = new Intent(context, bfq.class); + context.startActivity(intent); +// home.dialog.show(context.getSupportFragmentManager(), "bfq"); + } + + public static boolean getVisibility() { + if (inflate == null) { + return false; + } + return inflate.isShown(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/bfq_an.java b/app/src/main/java/com/muqingbfq/bfq_an.java new file mode 100644 index 0000000..85dbeb4 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/bfq_an.java @@ -0,0 +1,146 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.View; +import android.widget.ImageView; +import android.widget.SeekBar; + +import androidx.core.content.ContextCompat; + +import com.muqingbfq.api.url; +import com.muqingbfq.fragment.gd; +import com.muqingbfq.fragment.mp3; +import com.muqingbfq.mq.gj; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class bfq_an { + public static class kz implements View.OnClickListener { + @Override + public void onClick(View view) { + int id = view.getId(); + if (id == R.id.kg) { + MyButtonClickReceiver.playOrPause(); + } else if (id == R.id.syq) { + syq(); + } else if (id == R.id.xyq) { + xyq(); + } + } + } + + public static void syq() { + bfqkz.mt.pause(); + int i = bfqkz.list.indexOf(bfqkz.xm) - 1; + if (i < 0) { + i = 0; + } + bfqkz.xm = bfqkz.list.get(i); + new url(bfqkz.xm); + } + + public static void xyq() { + bfqkz.mt.pause(); + int ms = bfqkz.ms; + if (bfqkz.ms == 0) { + ms = 1; + } + bfqkz.xm = bfqkz.list.get(bfqkz.getmti(ms)); + new url(bfqkz.xm); + } + + public static class tdt implements SeekBar.OnSeekBarChangeListener { + // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm"); + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + bfq.time_b.setText(getTime(progress)); + +// bfq.time_b.setText(simpleDateFormat.format(new Date(progress))); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // 暂停播放 + bfqkz.mt.pauseTimer(); +// bfqkz.mt.pause(); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // 播放音乐到指定位置 + bfqkz.mt.seekTo(seekBar.getProgress()); + bfqkz.mt.resumeTimer(); +// bfqkz.mt.start(); + } + } + + public static class control implements View.OnClickListener { + public control(ImageView imageView) { + switch (bfqkz.ms) { + case 0: + imageView.setImageResource(R.drawable.mt_xh); + break; + case 1: + imageView.setImageResource(R.drawable.mt_sx); + break; + case 2: + imageView.setImageResource(R.drawable.mt_sj); + break; + } + } + + @Override + public void onClick(View v) { + ImageView imageView = (ImageView) v; + switch (bfqkz.ms) { + case 0: + bfqkz.ms = 1; + imageView.setImageResource(R.drawable.mt_sx); +// 顺序 + break; + case 1: + bfqkz.ms = 2; + imageView.setImageResource(R.drawable.mt_sj); +// 随机 + break; + case 2: + bfqkz.ms = 0; + imageView.setImageResource(R.drawable.mt_xh); +// 循环 + break; + } + main.edit.putInt("ms", bfqkz.ms); + main.edit.commit(); +// imageView.setImageDrawable(); + } + } + + @SuppressLint("SimpleDateFormat") + static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss"); + public static String getTime(long time) { + return simpleDateFormat.format(new Date(time)); + } + + public static void UI(boolean bool) { + if (bfq.getVisibility()) { + bfq.xyq.setEnabled(bool); + bfq.syq.setEnabled(bool); + bfq.kg.setEnabled(bool); + bfq.tdt.setEnabled(bool); + } + } + public static void islike(Context context) { + try { + gd.like.getJSONObject(String.valueOf(bfqkz.xm.id)); + bfq.like.setImageTintList(ContextCompat. + getColorStateList(context, android.R.color.holo_red_dark)); + bfqkz.like_bool = true; + } catch (Exception e) { + bfq.like.setImageTintList(ContextCompat.getColorStateList(context, R.color.text)); + gj.sc("bfq_an islike() :" + e); + bfqkz.like_bool = false; + } + } +} diff --git a/app/src/main/java/com/muqingbfq/bfqkz.java b/app/src/main/java/com/muqingbfq/bfqkz.java new file mode 100644 index 0000000..aad4750 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/bfqkz.java @@ -0,0 +1,131 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.MediaBrowserServiceCompat; + +import com.muqingbfq.api.url; +import com.muqingbfq.mq.gj; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class bfqkz extends MediaBrowserServiceCompat { + public static com.muqingbfq.MediaPlayer mt = new com.muqingbfq.MediaPlayer(); + public static String id; + public static List list = new ArrayList<>(); + public static long tdt_max, tdt_wz; + public static int ms; + // 0 循环 1 顺序 2 随机 + public static xm xm; + public static boolean like_bool; + @SuppressLint("StaticFieldLeak") + public static com.muqingbfq.mq.NotificationManagerCompat notify; + public static int getmti(int s) { + int i = bfqkz.list.indexOf(xm); + if (s == 1) { + i = bfqkz.list.indexOf(xm) + 1; + if (i >= bfqkz.list.size()) { + i = 0; + } + } else if (s == 2) { + i = new Random().nextInt(bfqkz.list.size()); + } + return i; + } + + @SuppressLint("NotifyDataSetChanged") + public static void mp3(String id) { + try { + if (id == null) { + return; + } + if (xm.picurl == null || xm.picurl.equals("")) { + xm.picurl = url.picurl(xm.id); + } + mt.reset(); + mt.setDataSource(id); + } catch (Exception e) { + gj.sc("bfqkz mp3(String id) :" + e); + } + } + public static MediaSessionCompat mSession; + + @Override + public void onCreate() { + super.onCreate(); + mSession = new MediaSessionCompat(this,"MusicService"); + mSession.setCallback(new MediaSessionCompat.Callback() { + @Override + public void onPlay() { + mt.start(); + // 处理播放音乐逻辑 + } + + @Override + public void onPause() { + // 处理暂停音乐逻辑 + mt.pause(); + } + + @Override + public void onSkipToNext() { + // 处理切换到下一首音乐逻辑 + } + @Override + public void onSkipToPrevious() { + // 处理切换到上一首音乐逻辑 + } + });//设置回调 +/* Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(this, start.class));//用ComponentName得到class对象 + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);// 关键的一步,设置启动模式,两种情况 + MediaButtonReceiver.handleIntent(mSession,intent);*/ + + MediaMetadataCompat build = new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "歌手名称") + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "专辑名称") + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "歌曲名称") + .build(); + mSession.setMetadata(build); + mSession.setActive(true); + setSessionToken(mSession.getSessionToken()); + notify = new com.muqingbfq.mq.NotificationManagerCompat(this); +/* ; +// 激活MediaSessionCompat + */ + // 初始化通知栏 + } + + @Nullable + @Override + public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + return null; + } + + @Override + public void onLoadChildren(@NonNull String parentId, @NonNull Result > result) { + + } + + public static void updateNotification() { + try { + // 更新通知栏的播放状态 + if (notify.notificationBuilder != null) { + notify.tzl_an(); + } + } catch (Exception e) { + gj.sc("bfqkz updateNotification:" + e); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/fragment/bflb_db.java b/app/src/main/java/com/muqingbfq/fragment/bflb_db.java new file mode 100644 index 0000000..1ccb6e7 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/fragment/bflb_db.java @@ -0,0 +1,101 @@ +package com.muqingbfq.fragment; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.muqingbfq.R; +import com.muqingbfq.api.url; +import com.muqingbfq.bfqkz; +import com.muqingbfq.list.MyViewHoder; +import com.muqingbfq.main; +import com.muqingbfq.xm; +import com.muqingbfq.yc; + +public class bflb_db extends BottomSheetDialog { + public static String gdid; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fragment_bflb_db); + // 获取对话框窗口 +/* Window window = getWindow(); + if (window != null) { + // 设置高度为默认值(例如:500dp) + WindowManager.LayoutParams params = window.getAttributes(); + params.height = + window.setAttributes(params); + }*/ + // 设置默认弹出高度和最大上拉高度为 400dp + int height = main.g - main.g / 2 / 2; + getBehavior().setPeekHeight(height); + getBehavior().setMaxHeight(height); + + try { + RecyclerView lb = findViewById(R.id.lb); + lb.setAdapter(new spq()); + if (bfqkz.xm != null) { + lb.smoothScrollToPosition(bfqkz.list.indexOf(bfqkz.xm)); + } + findViewById(R.id.xxbj). + setOnClickListener(v -> { + if (bfqkz.xm != null) { + lb.smoothScrollToPosition(bfqkz.list.indexOf(bfqkz.xm)); + } + }); + } catch (Exception e) { + yc.start(getContext(), e); + } + } + + public bflb_db(Context context) { + super(context); + } + + public static void start(Context context) { + new bflb_db(context).show(); + } + + class spq extends RecyclerView.Adapter
{ + @NonNull + @Override + public MyViewHoder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_mp3, parent, false); + return new MyViewHoder(view); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHoder holder, int position) { + xm x = bfqkz.list.get(position); + holder.name.setText(x.name); + holder.zz.setText(x.zz); + int color = ContextCompat.getColor(holder.getContext(), R.color.text); + if (bfqkz.xm != null && x.id.equals(bfqkz.xm.id)) { + color = ContextCompat.getColor(holder.getContext(), R.color.text_cz); + } + holder.name.setTextColor(color); + holder.zz.setTextColor(color); + holder.view.setOnClickListener(view -> { + bfqkz.id = x.id; + if (bfqkz.xm != x) { + bfqkz.xm = x; + new url(x); + } + }); + } + + @Override + public int getItemCount() { + return bfqkz.list.size(); + } + } + +} diff --git a/app/src/main/java/com/muqingbfq/fragment/bfq_db.java b/app/src/main/java/com/muqingbfq/fragment/bfq_db.java new file mode 100644 index 0000000..58e051b --- /dev/null +++ b/app/src/main/java/com/muqingbfq/fragment/bfq_db.java @@ -0,0 +1,57 @@ +package com.muqingbfq.fragment; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.muqingbfq.R; +import com.muqingbfq.bfq; +import com.muqingbfq.bfq_an; +import com.muqingbfq.bfqkz; +import com.muqingbfq.home; +import com.muqingbfq.xm; + +import java.lang.reflect.Type; +import java.util.List; + +public class bfq_db extends Fragment { + public View view; + public TextView name, zz; + public ImageView txa; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + view = inflater.inflate(R.layout.fragment_bfq_db, container, false); + name = view.findViewById(R.id.name); + zz = view.findViewById(R.id.zz); + txa = view.findViewById(R.id.kg); + txa.setOnClickListener(new bfq_an.kz()); + + view.findViewById(R.id.txb).setOnClickListener(view -> bflb_db.start(getContext())); + view.setOnClickListener(view12 -> bfq.start(home.appCompatActivity)); + +// 恢复列表数据 + SharedPreferences sharedPreferences = this.getContext().getSharedPreferences("MyPrefs", Context.MODE_PRIVATE); + String jsonList = sharedPreferences.getString("listData", ""); // 获取保存的 JSON 字符串 + if (!jsonList.isEmpty()) { + Gson gson = new Gson(); + Type type = new TypeToken >() { + }.getType(); + bfqkz.list = gson.fromJson(jsonList, type); // 将 JSON 字符串转换回列表数据 + } else { + view.setVisibility(View.GONE); + } + return view; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/fragment/gd.java b/app/src/main/java/com/muqingbfq/fragment/gd.java new file mode 100644 index 0000000..9a717a1 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/fragment/gd.java @@ -0,0 +1,203 @@ +package com.muqingbfq.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.tabs.TabLayout; +import com.muqingbfq.R; +import com.muqingbfq.api.playlist; +import com.muqingbfq.api.resource; +import com.muqingbfq.bfq_an; +import com.muqingbfq.bfqkz; +import com.muqingbfq.list.list_gd; +import com.muqingbfq.main; +import com.muqingbfq.mq.wj; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import com.muqingbfq.xm; + +public class gd extends Fragment { + public static String gdid; + public static BaseAdapter lbspq; + public static List
list = new ArrayList<>(); + public static JSONObject like = new JSONObject(); + GridView gridView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_gd, container, false); + lbspq = new baseadapter(view.getContext()); + gridView = view.findViewById(R.id.wgbj); + gridView.setAdapter(lbspq); + if (gdid == null) { + gdid = main.mp3_csh; + } + TabLayout tabLayout = view.findViewById(R.id.tablayout); +// tabLayout.removeAllTabs(); + for (String name : new String[]{"推荐", "排行榜", "下载"}) { + TabLayout.Tab tab = tabLayout.newTab(); + tab.setText(name); + tabLayout.addTab(tab); + } + new thread("推荐"); + tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + list.clear(); + lbspq.notifyDataSetChanged(); + new thread(tab.getText().toString()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + }); + try { + if (wj.cz(wj.mp3_like)) { + like = new JSONObject(wj.dqwb(wj.mp3_like)); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + return view; + } + + class baseadapter extends BaseAdapter { + Context context; + LayoutInflater layoutInflater; + + public baseadapter(Context context) { + this.context = context; + layoutInflater = LayoutInflater.from(context); + } + + @Override + public int getCount() { + return list.size(); + } + + @Override + public Object getItem(int i) { + return list.get(i); + } + + @Override + public long getItemId(int i) { + return i; + } + + @SuppressLint({"ResourceAsColor", "InflateParams", "ClickableViewAccessibility"}) + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + ViewHoder viewHoder; + xm xm = list.get(i); + if (view == null) { + viewHoder = new ViewHoder(); + view = layoutInflater.inflate(R.layout.list_gd, null, false); + viewHoder.textView = view.findViewById(R.id.wb1); + viewHoder.imageView = view.findViewById(R.id.fh); + viewHoder.cardView = view.findViewById(R.id.cardview); + viewHoder.kg = view.findViewById(R.id.kg); + view.setTag(viewHoder); + } else { + viewHoder = (ViewHoder) view.getTag(); + } + list_gd gd = new list_gd(xm); + viewHoder.cardView.setOnClickListener(gd); + viewHoder.cardView.setOnLongClickListener(gd); + viewHoder.textView.setText(xm.name); + viewHoder.kg.setOnClickListener(view1 -> { + ImageView tx = (ImageView) view1; + new Thread() { + @Override + public void run() { + super.run(); + boolean an=playlist.hq(bfqkz.list, xm.id); + main.handler.post(() -> { + if (an) { + bfq_an.xyq(); + tx.setImageResource(R.drawable.bf); + main.edit.putString(main.mp3, xm.id); + main.edit.commit(); + main.mp3_csh = gdid = xm.id; + } + com.muqingbfq.fragment.gd.lbspq.notifyDataSetChanged(); + }); + } + }.start(); + }); + int color = ContextCompat.getColor(context, R.color.text); + Drawable color_kg = ContextCompat.getDrawable(context, R.drawable.zt); + if (xm.id.equals(gdid)) { + color = ContextCompat.getColor(context, R.color.text_cz); + color_kg = ContextCompat.getDrawable(context, R.drawable.bf); + } else if (xm.cz) { + color = ContextCompat.getColor(context, R.color.text_cz_tm); + } + viewHoder.kg.setImageDrawable(color_kg); + viewHoder.textView.setTextColor(color); + Glide.with(context).load(xm.picurl).apply(new RequestOptions().placeholder(R.drawable.icon)) + .into(viewHoder.imageView); +// new wl(xm.picurl, Glide.with(context)).loadImage(bitmap -> viewHoder.imageView.setImageBitmap(bitmap)); + return view; + } + } + + class ViewHoder { + TextView textView; + ImageView imageView, kg; + MaterialCardView cardView; + } + + class thread extends Thread { + String name; + + public thread(String name) { + this.name = name; + start(); + } + + @Override + public void run() { + super.run(); + switch (name) { + case "推荐": + resource.recommend(list); + break; + case "下载": + resource.下载(list); + break; + case "排行榜": + resource.排行榜(list); + break; + } + main.handler.post(() -> lbspq.notifyDataSetChanged()); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/fragment/mp3.java b/app/src/main/java/com/muqingbfq/fragment/mp3.java new file mode 100644 index 0000000..4504922 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/fragment/mp3.java @@ -0,0 +1,141 @@ +package com.muqingbfq.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.muqingbfq.R; +import com.muqingbfq.api.playlist; +import com.muqingbfq.api.url; +import com.muqingbfq.bfq; +import com.muqingbfq.bfqkz; +import com.muqingbfq.list.MyViewHoder; +import com.muqingbfq.main; +import com.muqingbfq.xm; + +import java.util.ArrayList; +import java.util.List; + +public class mp3 extends AppCompatActivity { + private final List list = new ArrayList<>(); + public static RecyclerView.Adapter lbspq; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fragment_mp3); + Intent intent = getIntent(); + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(intent.getStringExtra("name")); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + lbspq = new spq(); + RecyclerView lb = findViewById(R.id.lb); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + lb.setLayoutManager(layoutManager); + lb.setAdapter(lbspq); + String id = intent.getStringExtra("id"); + new start(id); + } + + @SuppressLint("NotifyDataSetChanged") + class start extends Thread { + String id; + + public start(String id) { + this.id = id; + list.clear(); + mp3.lbspq.notifyDataSetChanged(); + start(); + } + + @Override + public void run() { + super.run(); + if (id.equals("mp3_xz.json")) { + playlist.hq_xz(list); + } else if (id.equals("mp3_like.json")) { + playlist.hq_like(list); + } else { + playlist.hq(list, id); + } + main.handler.post(new lbspq_sx()); + } + } + + public static class lbspq_sx implements Runnable { + @SuppressLint("NotifyDataSetChanged") + @Override + public void run() { + lbspq.notifyDataSetChanged(); + } + } + + class spq extends RecyclerView.Adapter { + + @NonNull + @Override + public MyViewHoder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_mp3, parent, false); + return new MyViewHoder(view); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHoder holder, int position) { + xm x = list.get(position); + holder.name.setText(x.name); + holder.zz.setText(x.zz); + int color = ContextCompat.getColor(holder.getContext(), R.color.text); + if (bfqkz.xm != null && x.id.equals(bfqkz.xm.id)) { + color = ContextCompat.getColor(holder.getContext(), R.color.text_cz); + } + holder.name.setTextColor(color); + holder.zz.setTextColor(color); + holder.view.setOnClickListener(view -> { + bfqkz.id = x.id; + if (bfqkz.xm == null || !bfqkz.xm.id.equals(x.id)) { + bfqkz.xm = x; + new url(x); + } +// if (!gd.gdid.equals(bflb_db.gdid)) { + bfqkz.list.clear(); + int size = list.size(); + for (int i = 0; i < size; i++) { + bfqkz.list.add(list.get(i)); + } + bfq.start(mp3.this); + }); + } + + @Override + public int getItemCount() { + return list.size(); + } + } + + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + return super.onOptionsItemSelected(item); + } + + public static void startactivity(Context context, String id) { + context.startActivity(new Intent(context, mp3.class).putExtra("id", id)); + } +} diff --git a/app/src/main/java/com/muqingbfq/fragment/search.java b/app/src/main/java/com/muqingbfq/fragment/search.java new file mode 100644 index 0000000..e6c5da0 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/fragment/search.java @@ -0,0 +1,149 @@ +package com.muqingbfq.fragment; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.muqingbfq.R; +import com.muqingbfq.activity_search; +import com.muqingbfq.api.url; +import com.muqingbfq.bfq; +import com.muqingbfq.bfqkz; +import com.muqingbfq.list.MyViewHoder; +import com.muqingbfq.main; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wl; +import com.muqingbfq.xm; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class search extends Fragment { + + String name; + + public search(String name) { + this.name = name; + } + + View view; + RecyclerView.Adapter lbspq; + List list = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + view = inflater.inflate(R.layout.fragment_search, container, false); + TypedValue typedValue = new TypedValue(); + requireContext().getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true); + // 设置背景颜色 + view.setBackgroundColor(typedValue.data); + RecyclerView lb = view.findViewById(R.id.recyclerview); + LinearLayoutManager manager = new LinearLayoutManager(getContext()); + lb.setLayoutManager(manager); + lbspq = new spq(); + lb.setAdapter(lbspq); + new start().start(); + return view; + } + + @SuppressLint("NotifyDataSetChanged") + public void setStart(String name) { + this.name = name; + list.clear(); + lbspq.notifyDataSetChanged(); + new start().start(); + + } + + public class start extends Thread { + @SuppressLint("NotifyDataSetChanged") + @Override + public void run() { + super.run(); + String hq = wl.hq("/search?keywords=" + name); + try { + JSONArray jsonArray = new JSONObject(hq).getJSONObject("result") + .getJSONArray("songs"); + int length = jsonArray.length(); + for (int i = 0; i < length; i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String id = jsonObject.getString("id"); + String name = jsonObject.getString("name"); + JSONArray artists = jsonObject.getJSONArray("artists"); + int length1 = artists.length(); + StringBuilder zz = null; + for (int j = 0; j < length1; j++) { + JSONObject josn = artists.getJSONObject(j); + String name_zz = josn.getString("name"); + if (zz == null) { + zz = new StringBuilder(name_zz); + } else { + zz.append("/").append(name_zz); + } + } + list.add(new xm(id, name, zz.toString(), null)); + } + main.handler.post(() -> lbspq.notifyDataSetChanged()); + } catch (Exception e) { + gj.sc(e); + } + } + } + + class spq extends RecyclerView.Adapter { + @NonNull + @Override + public MyViewHoder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_mp3, parent, false); + return new MyViewHoder(view); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHoder holder, int position) { + xm x = list.get(position); + holder.name.setText(x.name); + holder.zz.setText(x.zz); + int color = ContextCompat.getColor(holder.getContext(), R.color.text); + if (bfqkz.xm != null && x.id.equals(bfqkz.xm.id)) { + color = ContextCompat.getColor(holder.getContext(), R.color.text_cz); + } + holder.name.setTextColor(color); + holder.zz.setTextColor(color); + holder.view.setOnClickListener(view1 -> { + bfqkz.id = x.id; + if (bfqkz.xm == null || !bfqkz.xm.id.equals(x.id)) { + bfqkz.xm = x; + new url(x); + } + if (!com.muqingbfq.fragment.gd.gdid.equals(bflb_db.gdid)) { + bfqkz.list.clear(); + int size = list.size(); + for (int i = 0; i < size; i++) { + bfqkz.list.add(list.get(i)); + } + } + bfqkz.mt.start(); + bfq.start(activity_search.appCompatActivity); + }); + } + + @Override + public int getItemCount() { + return list.size(); + } + } +} diff --git a/app/src/main/java/com/muqingbfq/fragment/sz.java b/app/src/main/java/com/muqingbfq/fragment/sz.java new file mode 100644 index 0000000..0f73eb2 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/fragment/sz.java @@ -0,0 +1,74 @@ +package com.muqingbfq.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.muqingbfq.R; +import com.muqingbfq.activity_about_software; +import com.muqingbfq.login.user_logs; +import com.muqingbfq.login.user_message; +import com.muqingbfq.mq.gj; + +public class sz { + + @SuppressLint("StaticFieldLeak") + public static TextView name, jieshao; + @SuppressLint("StaticFieldLeak") + public static ImageView imageView; + Context context; + + public sz(Context context, View view) { + this.context = context; + name = view.findViewById(R.id.sz_text1); + jieshao = view.findViewById(R.id.sz_text2); + imageView = view.findViewById(R.id.image); + view.findViewById(R.id.xdbj). + setOnClickListener(v -> { + if (name.getText().equals("登录")) { + context.startActivity(new Intent(context, user_logs.class)); + } + }); + new user_message(); + } + + public static void switch_sz(Context context, int id) { + if (id == R.id.a) { + gj.llq(context, "https://rust.coldmint.top/ftp/muqing/"); + } else if (id == R.id.b) { + context.startActivity(new Intent(context, com.muqingbfq.sz.class)); +// 设置中心 + } else if (id == R.id.c) { +// 储存清理 + } else if (id == R.id.d) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse("mqqapi://card/show_pslcard?card_type=group&uin=" + + 674891685)); + context.startActivity(intent); + } catch (Exception e) { + Toast.makeText(context, "无法打开 QQ", Toast.LENGTH_SHORT).show(); + } + // 如果没有安装 QQ 客户端或无法打开 QQ,您可以在此处理异常 +// 官方聊群 + } else if (id == R.id.e) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse( + "mqqwpa://im/chat?chat_type=wpa&uin=" + "1966944300")); + context.startActivity(intent); + } catch (Exception e) { + // 如果没有安装 QQ 或无法跳转,则会抛出异常 + Toast.makeText(context, "无法打开 QQ", Toast.LENGTH_SHORT).show(); + } +// 联系作者 + } else if (id == R.id.f) { + context.startActivity(new Intent(context, activity_about_software.class)); +// 关于软件 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/home.java b/app/src/main/java/com/muqingbfq/home.java new file mode 100644 index 0000000..d255f14 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/home.java @@ -0,0 +1,178 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.LeadingMarginSpan; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.google.android.material.navigation.NavigationView; +import com.google.gson.Gson; +import com.muqingbfq.fragment.bfq_db; +import com.muqingbfq.mq.gj; + +public class home extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { + @SuppressLint("StaticFieldLeak") + public static Toolbar toolbar; + public static AppCompatActivity appCompatActivity; + + @SuppressLint("CommitTransaction") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_home); + appCompatActivity = this; + try { + toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + DrawerLayout drawerLayout = findViewById(R.id.chct); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawerLayout, toolbar, R.string.app_name, R.string.app_name); + drawerLayout.addDrawerListener(toggle); + toggle.syncState(); + NavigationView chb = findViewById(R.id.chb); + chb.setNavigationItemSelectedListener(this); + Menu menu = chb.getMenu(); + for (int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + SpannableString spannableString = new SpannableString(item.getTitle()); + spannableString.setSpan(new LeadingMarginSpan.Standard( + 26, 26), 0, spannableString.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + item.setTitle(spannableString); + } + new com.muqingbfq.fragment.sz(this, chb.getHeaderView(0)); + + db = new bfq_db(); + getSupportFragmentManager().beginTransaction() + .add(R.id.bfq_db, db).commit(); + mediaBrowser = new MediaBrowserCompat(this, + new ComponentName(this, bfqkz.class), connectionCallbacks, null); + mediaBrowser.connect(); + } catch (Exception e) { + gj.sc(e); + } + } + + private MediaBrowserCompat mediaBrowser; + private final MediaBrowserCompat.ConnectionCallback connectionCallbacks = + new MediaBrowserCompat.ConnectionCallback() { + @Override + public void onConnected() { + // 连接成功后执行的操作 + MediaControllerCompat mediaController; + try { + mediaController = new MediaControllerCompat(home.this, + mediaBrowser.getSessionToken()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + MediaControllerCompat.setMediaController(home.this, mediaController); + + } + + @Override + public void onConnectionSuspended() { + // 连接暂停时执行的操作 +// gj.ts(home.this,"zangting"); + } + + @Override + public void onConnectionFailed() { + // 连接失败时执行的操作 +// gj.ts(home.this,"shibai"); + } + }; + + @SuppressLint("StaticFieldLeak") + public static bfq_db db; + + @Override + protected void onPause() { + super.onPause(); + // 保存列表数据 + SharedPreferences sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + Gson gson = new Gson(); + String jsonList = gson.toJson(bfqkz.list); // 将列表数据转换为 JSON 字符串 + editor.putString("listData", jsonList); // 保存 JSON 字符串到 SharedPreferences + editor.apply(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mediaBrowser.disconnect(); + } + @Override + public void onResume() { + super.onResume(); + setVolumeControlStream(AudioManager.STREAM_MUSIC); + } + + @Override + public void onStop() { + super.onStop(); + // (see "stay in sync with the MediaSession") + if (MediaControllerCompat.getMediaController(home.this) != null) { +// MediaControllerCompat.getMediaController(home.this).unregisterCallback(controllerCallback); + } + mediaBrowser.disconnect(); + } + + private long time; + + @Override + public void onBackPressed() { + if (bfqkz.mt.isPlaying()) { + Intent home = new Intent(Intent.ACTION_MAIN); + home.addCategory(Intent.CATEGORY_HOME); + startActivity(home); + } else { + if (time < System.currentTimeMillis() - 1000) { + time = System.currentTimeMillis(); + gj.ts(this, "再按一次退出软件"); + } else { + finish(); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.home, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_search) { + Intent intent = new Intent(this, activity_search.class); + startActivity(intent); + } + return super.onOptionsItemSelected(item); + } + + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + com.muqingbfq.fragment.sz.switch_sz(this, item.getItemId()); + return false; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/list/MyViewHoder.java b/app/src/main/java/com/muqingbfq/list/MyViewHoder.java new file mode 100644 index 0000000..be8a866 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/list/MyViewHoder.java @@ -0,0 +1,26 @@ +package com.muqingbfq.list; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.muqingbfq.R; + +public class MyViewHoder extends RecyclerView.ViewHolder { + public TextView name, zz; + public View view; + + public MyViewHoder(@NonNull View itemView) { + super(itemView); + view = itemView; + name = itemView.findViewById(R.id.wb1); + zz = itemView.findViewById(R.id.zz); + } + + public Context getContext() { + return view.getContext(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/list/file_list.java b/app/src/main/java/com/muqingbfq/list/file_list.java new file mode 100644 index 0000000..dcfd3e0 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/list/file_list.java @@ -0,0 +1,5 @@ +package com.muqingbfq.list; + +public class file_list { + +} diff --git a/app/src/main/java/com/muqingbfq/list/list_gd.java b/app/src/main/java/com/muqingbfq/list/list_gd.java new file mode 100644 index 0000000..4130e4a --- /dev/null +++ b/app/src/main/java/com/muqingbfq/list/list_gd.java @@ -0,0 +1,95 @@ +package com.muqingbfq.list; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.view.View; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.muqingbfq.R; +import com.muqingbfq.api.playlist; +import com.muqingbfq.fragment.gd; +import com.muqingbfq.main; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; +import com.muqingbfq.xm; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.Objects; + +public class list_gd implements View.OnClickListener, View.OnLongClickListener { + xm xm; + + public list_gd(com.muqingbfq.xm xm) { + this.xm = xm; + } + + @SuppressLint("NotifyDataSetChanged") + @Override + public void onClick(View view) { +// if (!gd.gdid.equals(xm.id)) { +// gd.gdid = xm.id; + Context context = view.getContext(); + Intent intent = new Intent(context, com.muqingbfq.fragment.mp3.class); + intent.putExtra("id", xm.id); + intent.putExtra("name", xm.name); + context.startActivity(intent); +// mp3.startactivity(view.getContext(),xm.id); + } + + + @Override + public boolean onLongClick(View view) { + String[] stringArray = view.getResources() + .getStringArray(R.array.gd_list); + new MaterialAlertDialogBuilder(view.getContext()).setItems(stringArray, (dialog, id) -> { + new Thread() { + @Override + public void run() { + if (id == 0) { + String hq = wl.hq(playlist.api + xm.id + "&limit=30"); + if (hq != null) { + wj.xrwb(wj.gd + xm.id, hq); + xm.cz = true; + try { + JSONObject jsonObject = new JSONObject(); + if (wj.cz(wj.gd_xz)) { + jsonObject = new JSONObject(Objects.requireNonNull(wj.dqwb(wj.gd_xz))); + } + JSONObject json = new JSONObject(); + json.put("name", xm.name); + json.put("picUrl", xm.picurl); + jsonObject.put(xm.id, json); + wj.xrwb(wj.gd_xz, jsonObject.toString()); + } catch (JSONException e) { + gj.sc("list gd onclick thear " + e); + } + } + + } else if (id == 2) { + wj.sc(wj.gd + xm.id); + if (xm.id.equals("mp3_xz.json")) { + wj.sc(new File(wj.mp3)); + } + xm.cz = false; + try { + JSONObject jsonObject = new JSONObject(Objects.requireNonNull(wj.dqwb(wj.gd_xz))); + jsonObject.remove(xm.id); + wj.xrwb(wj.gd_xz, jsonObject.toString()); + } catch (JSONException e) { + gj.sc(e); + } + } + main.handler.post(() -> gd.lbspq.notifyDataSetChanged()); + } + }.start(); + // 在这里处理菜单项的点击事件 + dialog.dismiss(); + }).show(); + return false; + } +} diff --git a/app/src/main/java/com/muqingbfq/list/yylb.java b/app/src/main/java/com/muqingbfq/list/yylb.java new file mode 100644 index 0000000..71c6f3b --- /dev/null +++ b/app/src/main/java/com/muqingbfq/list/yylb.java @@ -0,0 +1,25 @@ +package com.muqingbfq.list; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.muqingbfq.R; +import com.muqingbfq.api.url; +import com.muqingbfq.bfq; +import com.muqingbfq.bfqkz; +import com.muqingbfq.fragment.bflb_db; +import com.muqingbfq.fragment.mp3; +import com.muqingbfq.home; +import com.muqingbfq.xm; + +import java.util.List; + +public class yylb { +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/login/enroll.java b/app/src/main/java/com/muqingbfq/login/enroll.java new file mode 100644 index 0000000..4ef0982 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/login/enroll.java @@ -0,0 +1,85 @@ +package com.muqingbfq.login; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import androidx.appcompat.app.AppCompatActivity; +import com.muqingbfq.R; +import com.muqingbfq.main; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wl; +import com.muqingbfq.yc; + +import org.json.JSONObject; + +public class enroll extends AppCompatActivity { + EditText eduser,edpassword; + String user, password; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_enroll); + setSupportActionBar(findViewById(R.id.toolbar)); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + Intent intent = getIntent(); + eduser = findViewById(R.id.edit_user); + eduser.setText( + intent.getStringExtra("user")); + edpassword = findViewById(R.id.edit_password); + findViewById(R.id.edit_cookie).setOnClickListener(view -> new user_logs.erweima(view.getContext())); + findViewById(R.id.enroll).setOnClickListener(view -> a()); + } + + public void a() { + user = eduser.getText().toString(); + password = edpassword.getText().toString(); + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + View v = getWindow().peekDecorView(); + if (null != v) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + new thread().start(); + } + + private void end() { + Intent intent = new Intent(); // 创建一个新意图 + Bundle bundle = new Bundle(); // 创建一个新包裹 + // 往包裹存入名叫response_time的字符串 + bundle.putString("user", user); + // 往包裹存入名叫response_content的字符串 + bundle.putString("password", password); + intent.putExtras(bundle); // 把快递包裹塞给意图 + + // 携带意图返回上一个页面。RESULT_OK表示处理成功 + setResult(Activity.RESULT_OK, intent); + + finish(); // 结束当前的活动页面 + } + + class thread extends Thread { + @Override + public void run() { + super.run(); + String s = wl.get("http://139.196.224.229/muqing/enroll.php?user=" + user + "&password=" + password + + "&cookie" + wl.Cookie); + try { + JSONObject jsonObject = new JSONObject(s); + int code = jsonObject.getInt("code"); + String msg = jsonObject.getString("msg"); + main.handler.post(() -> gj.ts(enroll.this, msg)); + if (code == 200) { + end(); + } + } catch (Exception e) { + yc.start(e); + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/login/user_logs.java b/app/src/main/java/com/muqingbfq/login/user_logs.java new file mode 100644 index 0000000..2cc53d7 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/login/user_logs.java @@ -0,0 +1,222 @@ +package com.muqingbfq.login; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.util.Base64; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.muqingbfq.R; +import com.muqingbfq.main; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wl; + +import org.json.JSONObject; + +import java.util.Objects; + +public class user_logs extends AppCompatActivity { + + EditText edituser, editpassword; + Toolbar toolbar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_user_logs); + toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); + edituser = findViewById(R.id.edit_user); + editpassword = findViewById(R.id.edit_password); + findViewById(R.id.login).setOnClickListener(view -> new CloudUser(edituser.getText().toString() + , editpassword.getText().toString())); + findViewById(R.id.enroll).setOnClickListener(view -> { + Intent intent = new Intent(user_logs.this, enroll.class); + intent.putExtra("user", edituser.getText().toString()); + startActivityForResult(intent, 0); + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data != null && requestCode == 0 && resultCode == Activity.RESULT_OK) { + Bundle bundle = data.getExtras(); + String user = bundle.getString("user"); + String password = bundle.getString("password"); + edituser.setText(user); + editpassword.setText(password); + } + } + //some statement + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + public static Bitmap stringToBitmap(String string) { + Bitmap bitmap = null; + try { + byte[] bitmapArray = Base64.decode(string.split(",")[1], Base64.DEFAULT); + bitmap = BitmapFactory.decodeByteArray(bitmapArray, 0, bitmapArray.length); + } catch (Exception e) { + e.printStackTrace(); + } + return bitmap; + } + + class CloudUser extends Thread { + String user, password; + + public CloudUser(String user, String password) { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + View v = getWindow().peekDecorView(); + if (null != v) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + this.user = user; + this.password = password; + start(); + } + + @Override + public void run() { + super.run(); + String s = wl.get(main.http + "/user.php?" + "user=" + user + "&password=" + password); + try { + JSONObject jsonObject = new JSONObject(s); + int code = jsonObject.getInt("code"); + String msg = jsonObject.getString("msg"); + main.handler.post(() -> gj.ts(user_logs.this, msg)); + if (code == 200) { + String cookie = jsonObject.getString("cookie"); + if (wl.iskong()) { + new visitor(); + } + wl.setcookie(cookie); + new user_message(); + user_logs.this.finish(); + } + } catch (Exception e) { + gj.sc(e); + } + } + } + + public static class erweima extends Thread { + int code = 800; + String unikey, qrimg, hq; + private long time=0; + ImageView imageView; + TextView textView; + MaterialAlertDialogBuilder materialAlertDialogBuilder; + + public erweima(Context context) { + View inflate = LayoutInflater.from(context).inflate(R.layout.erweima, null); + imageView = inflate.findViewById(R.id.image); + textView = inflate.findViewById(R.id.text); +// 创建布局参数对象 + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(main.g, main.k); +// 设置视图的布局参数 + imageView.setLayoutParams(layoutParams); + materialAlertDialogBuilder = new MaterialAlertDialogBuilder(context) { + }; + materialAlertDialogBuilder.setOnDismissListener(dialog -> { + // 对话框消失时触发的操作 + // 可以在这里处理一些额外的逻辑 + code = 0; + }); + materialAlertDialogBuilder.setView(inflate).setTitle("请使用网易云音乐扫码"); + materialAlertDialogBuilder.show(); + start(); + } + + @Override + public void run() { + super.run(); + while (code != 0) { + try { + hq = wl.hq("/login/qr/check?key=" + unikey + Time()); + if (hq != null) { + JSONObject json = new JSONObject(hq); + code = json.getInt("code"); + switch (code) { + case 800: + case 400: + setwb("二维码过期"); + hqkey(); + break; + case 801: + setwb("等待扫码"); + break; + case 802: + setwb("等待确认"); + break; + case 803: + setwb("登录成功"); + wl.setcookie(json.getString("cookie")); + main.handler.postDelayed(() -> materialAlertDialogBuilder.create().cancel(), + 500); + code = 0; + break; + default: + code = 0; + // 默认情况下的操作 + break; + } + } + sleep(1000); + } catch (Exception e) { + gj.sc(e); + } + } + } + + private void hqkey() throws Exception { + unikey = new JSONObject(Objects.requireNonNull(wl.hq("/login/qr/key"))). + getJSONObject("data").getString("unikey"); + JSONObject jsonObject = new JSONObject(Objects.requireNonNull(wl.hq("/login/qr/create?key=" + + unikey + + "&qrimg=base64"))); + qrimg = jsonObject.getJSONObject("data").getString("qrimg"); + main.handler.post(() -> imageView.setImageBitmap(stringToBitmap(qrimg))); + } + + private String Time() { + if (time < System.currentTimeMillis() - 1000) { + time = System.currentTimeMillis(); + } + return "×tamp" + time; + } + + private void setwb(String wb) { + main.handler.post(() -> textView.setText(wb)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/login/user_message.java b/app/src/main/java/com/muqingbfq/login/user_message.java new file mode 100644 index 0000000..090338b --- /dev/null +++ b/app/src/main/java/com/muqingbfq/login/user_message.java @@ -0,0 +1,40 @@ +package com.muqingbfq.login; + +import com.bumptech.glide.Glide; +import com.muqingbfq.R; +import com.muqingbfq.fragment.sz; +import com.muqingbfq.main; +import com.muqingbfq.mq.wl; +import com.muqingbfq.yc; + +import org.json.JSONObject; + +public class user_message extends Thread { + + public user_message() { + start(); + } + + @Override + public void run() { + super.run(); + String hq = wl.hq("/user/account" + "?cookie=" + wl.Cookie); + try { + JSONObject jsonObject = new JSONObject(hq).getJSONObject("profile"); + String nickname = jsonObject.getString("nickname"); + String signature = jsonObject.getString("signature"); + String avatarUrl = jsonObject.getString("avatarUrl"); + main.handler.post(() -> { + sz.name.setText(nickname); + sz.jieshao.setText(signature); + Glide.with(sz.imageView) + .load(avatarUrl) + .placeholder(R.drawable.icon)//图片加载出来前,显示的图片 + .error(R.drawable.deleat)//图片加载失败后,显示的图片 + .into(sz.imageView); + }); + } catch (Exception e) { + yc.start(e); + } + } +} diff --git a/app/src/main/java/com/muqingbfq/login/visitor.java b/app/src/main/java/com/muqingbfq/login/visitor.java new file mode 100644 index 0000000..9a4e392 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/login/visitor.java @@ -0,0 +1,53 @@ +package com.muqingbfq.login; + +import android.content.Intent; + +import androidx.appcompat.app.AppCompatActivity; + +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; +import com.muqingbfq.yc; + +import org.json.JSONException; +import org.json.JSONObject; + +public class visitor extends Thread { + + AppCompatActivity activity; + Intent intent; + public visitor(AppCompatActivity activity, Intent intent) { + this.activity = activity; + this.intent = intent; + start(); + } + + public visitor() { + start(); + } + @Override + public void run() { + super.run(); + String hq = wl.hq("/register/anonimous"); + try { + JSONObject jsonObject = new JSONObject(hq); + wl.setcookie(jsonObject.getString("cookie")); + if (wj.filesdri == null) { + new wj(activity); + } + if (activity != null) { + activity.startActivity(intent); + activity.finish(); + } + } catch (JSONException e) { + yc.start(activity, e); + } + try { + sleep(1000); + } catch (InterruptedException e) { + yc.start(activity,e); + } + if (activity != null) { + activity.finish(); + } + } +} diff --git a/app/src/main/java/com/muqingbfq/main.java b/app/src/main/java/com/muqingbfq/main.java new file mode 100644 index 0000000..3ab7311 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/main.java @@ -0,0 +1,66 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; + +import com.muqingbfq.mq.MyExceptionHandler; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; + +public class main extends Application { + @SuppressLint("StaticFieldLeak") + public static Context context; + public static Handler handler = new Handler(Looper.getMainLooper()); + public static String api = "http://139.196.224.229:3000"; + public static String http = "http://139.196.224.229/muqing"; + public static int k, g; + public static SharedPreferences sp; + public static SharedPreferences.Editor edit; + + public static String mp3 = "mp3", mp3_csh, + Time = "Time", Cookie = "Cookie"; + + @Override + public void onCreate() { + super.onCreate(); + context = this; + new wj(this); + sp = getSharedPreferences("Set_up", MODE_PRIVATE); + edit = sp.edit(); + if (sp.getLong(Time, 0) == 0) { + edit.putLong(Time, System.currentTimeMillis()); + } + start.time = sp.getLong(Time, System.currentTimeMillis()); + boolean bj = false; + try { + mp3_csh = sp.getString(mp3, ""); + } catch (Exception e) { + edit.putString(mp3, ""); + edit.commit(); + mp3_csh = ""; + } + try { + com.muqingbfq.bfqkz.ms = sp.getInt("ms", 1); + } catch (Exception e) { + edit.putInt("ms", 1); + bj = true; + com.muqingbfq.bfqkz.ms = 1; + } + try { + wl.Cookie = sp.getString(Cookie, ""); + } catch (Exception e) { + edit.putString(Cookie, ""); + wl.Cookie = ""; + bj = true; + } + if (bj) { + edit.commit(); + } + // 创建全局异常处理器实例 设置全局异常处理器 + Thread.setDefaultUncaughtExceptionHandler(new MyExceptionHandler(this)); + } +} diff --git a/app/src/main/java/com/muqingbfq/mq/BluetoothMusicController.java b/app/src/main/java/com/muqingbfq/mq/BluetoothMusicController.java new file mode 100644 index 0000000..1715726 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/BluetoothMusicController.java @@ -0,0 +1,50 @@ +package com.muqingbfq.mq; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.content.ComponentName; +import android.content.Context; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.support.v4.media.session.MediaSessionCompat; + +import com.muqingbfq.MyButtonClickReceiver; + +public class BluetoothMusicController { + private Context context; + private MediaSessionCompat mediaSession; + + public BluetoothMusicController(Context context) { + this.context = context; +// bluetoothReceiver.setOnHeadsetListener(this); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); + + intentFilter.addAction("android.intent.action.HEADSET_PLUG"); + context.registerReceiver(new MyButtonClickReceiver(), intentFilter); + registerHeadsetReceiver(); +// audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); +// setupMediaSession(); + } + + public void stop() { + unregisterHeadsetReceiver(); + } + + // 注册媒体按钮事件接收器 + @SuppressLint("ObsoleteSdkInt") + public void registerHeadsetReceiver() { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + ComponentName name = new ComponentName(context.getPackageName(), MyButtonClickReceiver.class.getName()); + audioManager.registerMediaButtonEventReceiver(name); + } + + // 注销媒体按钮事件接收器 + public void unregisterHeadsetReceiver() { + if (mediaSession != null) { + mediaSession.setActive(false); + mediaSession.release(); + mediaSession = null; + } + } +} diff --git a/app/src/main/java/com/muqingbfq/mq/MyExceptionHandler.java b/app/src/main/java/com/muqingbfq/mq/MyExceptionHandler.java new file mode 100644 index 0000000..600fae7 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/MyExceptionHandler.java @@ -0,0 +1,32 @@ +package com.muqingbfq.mq; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.muqingbfq.yc; + +public class MyExceptionHandler implements Thread.UncaughtExceptionHandler { + public static Throwable throwable; + private Context mContext; + public MyExceptionHandler(Context context) { + mContext = context; + } + @Override + public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { + // 将异常信息打印到日志中 + MyExceptionHandler.throwable = throwable; + String TAG = "MyExceptionHandler"; + Log.e(TAG, "UncaughtException: ", throwable); + // 在这里执行生成错误报告的逻辑,例如将错误信息保存到文件或发送给服务器 + // 可以使用第三方库,如ACRA、Bugsnag等,或者自行实现错误报告的处理逻辑 + // 生成错误报告的逻辑 + // 发送 Intent 重启应用 + yc.start(mContext, throwable); + // 这里可以进行一些其他的操作,例如记录错误日志、弹出错误提示框等 + // 终止程序 +// android.os.Process.killProcess(android.os.Process.myPid()); +// System.exit(0); + } +} diff --git a/app/src/main/java/com/muqingbfq/mq/NotificationManagerCompat.java b/app/src/main/java/com/muqingbfq/mq/NotificationManagerCompat.java new file mode 100644 index 0000000..21580b4 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/NotificationManagerCompat.java @@ -0,0 +1,164 @@ +package com.muqingbfq.mq; + +import static com.muqingbfq.bfqkz.xm; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.support.v4.media.session.PlaybackStateCompat; + +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.media.session.MediaButtonReceiver; + +import com.muqingbfq.MyButtonClickReceiver; +import com.muqingbfq.R; +import com.muqingbfq.bfq; +import com.muqingbfq.bfqkz; +import com.muqingbfq.start; +import com.muqingbfq.yc; + +public class NotificationManagerCompat { + Service context; + public NotificationCompat.Builder notificationBuilder; + public androidx.core.app.NotificationManagerCompat notificationManager; + + public NotificationManagerCompat(Service context) { + this.context = context; + CharSequence name = context.getString(R.string.app_name); + String zz = context.getString(R.string.zz); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, + NotificationManager.IMPORTANCE_LOW); + channel.setDescription(zz); + NotificationManager systemService = context.getSystemService(NotificationManager.class); + systemService.createNotificationChannel(channel); + if (!systemService.areNotificationsEnabled()) { + return; + } + } + // 适配12.0及以上 + int flag; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flag = PendingIntent.FLAG_IMMUTABLE; + } else { + flag = PendingIntent.FLAG_UPDATE_CURRENT; + } + // 设置启动的程序,如果存在则找出,否则新的启动 + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(context, start.class));//用ComponentName得到class对象 + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);// 关键的一步,设置启动模式,两种情况 + + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flag); + pendingIntent_kg = pendingIntent(context, new Intent(context, MyButtonClickReceiver.class). + setAction("kg")); + pendingIntent_syq = pendingIntent(context, new Intent(context, MyButtonClickReceiver.class). + setAction("syq")); + pendingIntent_xyq = pendingIntent(context, new Intent(context, MyButtonClickReceiver.class). + setAction("xyq")); + // 取消操作的PendingIntent +// 取消操作的PendingIntent + PendingIntent cancelIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, + PlaybackStateCompat.ACTION_STOP); + androidx.media.app.NotificationCompat.MediaStyle style = + new androidx.media.app.NotificationCompat.MediaStyle() + .setShowActionsInCompactView(0, 1, 2) + .setMediaSession(bfqkz.mSession.getSessionToken()) + .setShowCancelButton(true) + .setCancelButtonIntent(cancelIntent); +// + notificationBuilder = getNotificationBuilder(context) + .setSmallIcon(R.drawable.icon) + .setContentTitle(name).setContentText(zz) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true).setAutoCancel(false).setOnlyAlertOnce(true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(pendingIntent) + .setStyle(style); + notificationManager = androidx.core.app.NotificationManagerCompat.from(context); + tzl_an(); +// context.startForeground(1, notificationBuilder.build()); + } catch (Exception e) { + yc.start(context, e); + } + } + + private PendingIntent pendingIntent_kg, + pendingIntent_syq, + pendingIntent_xyq; + private final String CHANNEL_ID = "muqing_yy_id"; + + @SuppressLint("RestrictedApi") + public void tzl_an() { + notificationBuilder.mActions.clear(); + notificationBuilder +/* .addAction(R.drawable.syq, "syq", pendingIntent_syq) // #0 + .addAction(bfqkz.mt.isPlaying() ? R.drawable.bf : R.drawable.zt + , "kg", pendingIntent_kg) // #1 + .addAction(R.drawable.xyq, "xyq", pendingIntent_xyq);*/ + .addAction(android.R.drawable.ic_media_previous, "syq", pendingIntent_syq) // #0 + .addAction(bfqkz.mt.isPlaying() ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play + , "kg", pendingIntent_kg) // #1 + .addAction(android.R.drawable.ic_media_next, "xyq", pendingIntent_xyq); + notificationBuilder.setOngoing(bfqkz.mt.isPlaying()); + notificationManager_notify(); + } + + public void notificationManager_notify() { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + return; + } + notificationManager.notify(1, notificationBuilder.build()); + } + + @SuppressLint({"MissingPermission", "RestrictedApi"}) + public void setBitmap(Bitmap bitmap) { + bfq.bitmap = bitmap; + if (bitmap == null) { + bitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.icon); + } + if (notificationManager != null) { + notificationBuilder.setContentTitle(xm.name); + notificationBuilder.setContentText(xm.zz); + notificationBuilder.setLargeIcon(bitmap); + notificationManager.notify(1, notificationBuilder.build()); + } + if (bfq.tx != null) { + bfq.tx.setImageBitmap(bitmap); + } + } + + private NotificationCompat.Builder getNotificationBuilder(Context context) { + // 适用于Android 8.0及以上版本 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return new NotificationCompat.Builder(context, CHANNEL_ID); + } else { + // Android 7.1及以下版本 + return new NotificationCompat.Builder(context); + } + } + + @SuppressLint("UnspecifiedImmutableFlag") + private PendingIntent pendingIntent(Context context, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE); + } else { + return PendingIntent.getBroadcast(context, 0, intent, 0); + } + } + +} diff --git a/app/src/main/java/com/muqingbfq/mq/gj.java b/app/src/main/java/com/muqingbfq/mq/gj.java new file mode 100644 index 0000000..851a4d9 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/gj.java @@ -0,0 +1,94 @@ +package com.muqingbfq.mq; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import com.muqingbfq.main; +import com.muqingbfq.yc; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; + +public class gj { + public static void ts(Context a, Object b) { + Toast.makeText(a, b.toString(), Toast.LENGTH_SHORT).show(); + } + + public static void xcts(Context context, Object b) { + main.handler.post(() -> Toast.makeText(context, b.toString(), Toast.LENGTH_SHORT).show()); + } + + public static void sc(Object a) { + if (a == null) { + a = "null"; + } + Log.d("云音乐", String.valueOf(a)); + } + + public static void llq(Context context, String str) { + context.startActivity(new Intent(context, llq.class).putExtra("url", str)); + } + + public static void fx(Context context, String str) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, str); + context.startActivity(shareIntent); + } + + public static int isDarkTheme(Context context) { + return context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; +// return flag == Configuration.UI_MODE_NIGHT_YES; + } + + public static boolean isWiFiConnected() { + try { + for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) { + if (networkInterface.isUp() && !networkInterface.isLoopback()) { + if (networkInterface.getDisplayName().contains("wlan")) { + return true; // Wi-Fi网络 + } else if (networkInterface.getDisplayName().contains("rmnet")) { + return false; // 流量网络 + } + } + } + } catch (SocketException e) { + yc.start(main.context, e); + } + return false; // 默认为流量网络 + } + + public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, int cornerRadius) { + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + final RectF rectF = new RectF(rect); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint); + + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + return output; + } + +} diff --git a/app/src/main/java/com/muqingbfq/mq/llq.java b/app/src/main/java/com/muqingbfq/mq/llq.java new file mode 100644 index 0000000..d0c025b --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/llq.java @@ -0,0 +1,199 @@ +package com.muqingbfq.mq; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.muqingbfq.R; + +import java.io.File; +import java.util.Locale; + +public class llq extends AppCompatActivity { + WebView web; + Toolbar toolbar; + @SuppressLint({"DefaultLocale", "ObsoleteSdkInt"}) + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_llq); + + Intent intent = getIntent(); + + toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + web = findViewById(R.id.webview); + web.getSettings().setJavaScriptEnabled(true); + web.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + String title = view.getTitle(); + toolbar.setTitle(title); + toolbar.setSubtitle(url); + // 在这里获取到了网页的标题 + } + }); + web.setDownloadListener((url1, userAgent, contentDisposition, mimetype, contentLength) -> { + String size = "0B"; + if (contentLength > 0) { + final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"}; + int digitGroups = (int) (Math.log10(contentLength) / Math.log10(1024)); + size = String.format("%.1f %s", contentLength + / Math.pow(1024, digitGroups), units[digitGroups]); + } + final String filename = url1.substring(url1.lastIndexOf('/') + 1); + new MaterialAlertDialogBuilder(llq.this) + .setTitle(filename) + .setMessage("文件链接:" + url1 + + "\n文件大小:" + size) + .setNegativeButton("取消", null) + .setNegativeButton("下载", (dialogInterface, i) -> { + // 检查权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + ContextCompat.checkSelfPermission(llq.this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + // 如果没有写入存储的权限,则请求权限 + ActivityCompat.requestPermissions(llq.this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + 1); + } else { + // 执行文件下载操作 + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url1)); + // 设置下载保存路径和文件名 + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); + // 允许使用的网络类型,手机、WIFI + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI); + // 设置通知栏标题和描述信息 + request.setTitle(filename); + request.setDescription("正在下载"); + DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); + // 注册下载完成的广播接收器 + long enqueue = downloadManager.enqueue(request); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (filename.endsWith(".apk")) { + // 打开安装界面 + Cursor cursor = downloadManager.query( + new DownloadManager.Query().setFilterById(enqueue)); + if (cursor.moveToFirst()) { + int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); + int status = cursor.getInt(columnIndex); + if (status == DownloadManager.STATUS_SUCCESSFUL) { + // 下载成功 + columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI); + String localUri = cursor.getString(columnIndex); + if (localUri != null) { + Uri uri = Uri.parse(localUri); + String filePath = uri.getPath(); + File file = new File(filePath); + // 获取下载文件的路径 + Intent installIntent = new Intent(Intent.ACTION_VIEW); + installIntent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); + installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(installIntent); + } + } + } + } + } + }; + registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + } + }).show(); + + }); + final ProgressBar progressBar = findViewById(R.id.webViewProgressBar); + web.setWebChromeClient(new WebChromeClient() { + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + gj.sc(newProgress); + if (newProgress == 100) { + progressBar.setVisibility(View.GONE); + } else { + progressBar.setProgress(newProgress); + if (!progressBar.isShown()) { + progressBar.setVisibility(View.VISIBLE); + } + } + } + }); + loadUrl(intent.getStringExtra("url")); + } + + private void loadUrl(String url) { + web.loadUrl(url); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == 1 && grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // 权限已授予,执行文件下载操作 + gj.ts(this, "权限已授予,请重新执行文件下载操作"); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.llq, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public void onBackPressed() { + if (web.canGoBack()) { + web.goBack(); + } else { + finish(); + } + } + + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + } else if (itemId == R.id.fx) { + gj.fx(this, web.getUrl()); +// 服务中心 + } else if (itemId == R.id.sx) { + web.reload(); + } else if (itemId == R.id.menu_web) { + startActivity(new Intent(Intent.ACTION_VIEW, + Uri.parse(web.getUrl()))); + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/muqingbfq/mq/wj.java b/app/src/main/java/com/muqingbfq/mq/wj.java new file mode 100644 index 0000000..73c12e6 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/wj.java @@ -0,0 +1,164 @@ +package com.muqingbfq.mq; + +import android.content.Context; + +import com.muqingbfq.yc; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class wj { + public static String filesdri; + public static String mp3 = "mp3/"; + public static String lishi_json = "lishi.json"; + public static String gd = "gd/"; + public static String tx = "image/"; + public static String gd_json = "gd.json", mp3_xz = "mp3_xz.json", gd_xz = "gd_xz.json", + gd_phb = "gd_phb.json", mp3_like = "like.json"; + + public wj(Context context) { + try { + wj.filesdri = context.getExternalFilesDir("").getAbsolutePath() + "/"; +// context.getFilesDir().toString() + "/"; + gd_json = filesdri + gd_json; + mp3 = filesdri + mp3; + gd = filesdri + gd; + gd_xz = filesdri + gd_xz; + gd_phb = filesdri + gd_phb; + mp3_xz = gd + mp3_xz; + mp3_like = gd + mp3_like; + tx = filesdri + tx; + if (!new File(mp3).exists()) { + new File(mp3).mkdirs(); + } + if (!new File(gd).exists()) { + new File(gd).mkdirs(); + } + } catch (Exception e) { + yc.start(context, e); + } + } + + public static boolean new_wj(String string) { + File file = new File(string); + if (file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + try { + return file.createNewFile(); + } catch (IOException e) { + gj.sc(e); + } + return false; + } + + /* + * 这里定义的是一个文件保存的方法,写入到文件中,所以是输出流 + * */ + public static boolean xrwb(String url, String text) { + File file = new File(url); +//如果文件不存在,创建文件 + try { + if (file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + if (!file.exists()) + file.createNewFile(); +//创建FileOutputStream对象,写入内容 + FileOutputStream fos = new FileOutputStream(file); +//向文件中写入内容 + fos.write(text.getBytes()); + fos.close(); + } catch (Exception e) { + gj.sc(e); + } + return false; + } + + public static String dqwb(String url) { + try { + File file = new File(url); + FileInputStream fis = new FileInputStream(file); + BufferedReader br = new BufferedReader(new InputStreamReader(fis)); + StringBuilder str = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + str.append(line); + } + br.close(); + fis.close(); + return str.toString(); + } catch (Exception e) { + gj.sc(e); + } + return null; + } + + public static boolean cz(String url) { + return new File(url).exists(); + } + + public static boolean sc(String url) { + File file = new File(url); + return file.delete(); + } + + public static void sc(File url) { + if (url.exists()) { + File[] files = url.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + // 递归调用,删除子文件夹及其内容 + sc(file); + } else { + file.delete(); // 删除文件 + } + } + } + url.delete(); // 删除当前文件夹 + } + } + + + public String convertToMd5(String url) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] messageDigest = md.digest(url.getBytes()); + StringBuilder hexString = new StringBuilder(); + for (byte value : messageDigest) { + String hex = Integer.toHexString(0xFF & value); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + public static class xrwb extends Thread { + String url, text; + + public xrwb(String url, String text) { + this.url = url; + this.text = text; + start(); + } + + @Override + public void run() { + super.run(); + xrwb(url, text); + } + } +} diff --git a/app/src/main/java/com/muqingbfq/mq/wl.java b/app/src/main/java/com/muqingbfq/mq/wl.java new file mode 100644 index 0000000..88a4cd4 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/mq/wl.java @@ -0,0 +1,130 @@ +package com.muqingbfq.mq; + + +import com.muqingbfq.main; +import com.muqingbfq.xm; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class wl { + public static String Cookie; + + public static void setcookie(String cookie) { + wl.Cookie = cookie; + main.edit.putString(main.Cookie, cookie); + main.edit.commit(); + } + + public static boolean iskong() { + return Cookie.equals("") || Cookie == null; + } + public static String hq(String url) { + try { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(main.api + url) + .build(); + Response response = client.newCall(request).execute(); + if (response.body() != null) { + return response.body().string(); + } + } catch (Exception e) { + gj.sc("wl hq(Strnig) " + e); + } + return null; + } + + public static String get(String url) { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(url) + .build(); + try { + Response response = client.newCall(request).execute(); + if (response.body() != null) { + return response.body().string(); + } + } catch (Exception e) { + gj.sc("wl get(Strnig) " + e); + } + return null; + } + + public static class xz extends Thread { + String url; + xm x; + + public xz(String url, xm x) { + this.url = url; + this.x = x; + start(); + } + + @Override + public void run() { + super.run(); + xz(url, x); + } + } + + public static boolean xz(String url, final xm x) { + try { + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + //访问路径 + .url(url) + .build(); + Call call = client.newCall(request); + Response response = call.execute(); + if (response.isSuccessful()) { + ResponseBody body = response.body(); + if (body != null) { + File file = new File(wj.mp3, String.valueOf(x.id)); + InputStream inputStream = body.byteStream(); + FileOutputStream fileOutputStream = + new FileOutputStream(file); + // 替换为实际要保存的文件路径 + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + fileOutputStream.write(buffer, 0, bytesRead); + } + fileOutputStream.close(); + inputStream.close(); + } + } + JSONObject jsonObject = new JSONObject(); + if (wj.cz(wj.mp3_xz)) { + jsonObject = new JSONObject(wj.dqwb(wj.mp3_xz)); + } else { + jsonObject.put("songs", new JSONArray()); + } + JSONArray songs = jsonObject.getJSONArray("songs"); + if (songs.length() > 30) { + songs.remove(0); + } + JSONObject json = new JSONObject(); + json.put("id", x.id); + json.put("name", x.name); + json.put("zz", x.zz); + json.put("picUrl", x.picurl); + songs.put(json); + wj.xrwb(wj.mp3_xz, jsonObject.toString()); + return true; + } catch (Exception e) { + gj.sc("wl xz " + e); + } + return false; + } +} diff --git a/app/src/main/java/com/muqingbfq/start.java b/app/src/main/java/com/muqingbfq/start.java new file mode 100644 index 0000000..3c1e280 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/start.java @@ -0,0 +1,172 @@ +package com.muqingbfq; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.muqingbfq.login.visitor; +import com.muqingbfq.mq.gj; +import com.muqingbfq.mq.wj; +import com.muqingbfq.mq.wl; + +public class start extends AppCompatActivity { + public static int ztl, dhl; + Intent home; + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + home = new Intent(this, home.class); + ztl = getNavigationBarHeight(this); + dhl = getStatusBarHeight(this); + DisplayMetrics dm = getResources().getDisplayMetrics(); + main.k = dm.widthPixels; + main.g = dm.heightPixels; + } catch (Exception e) { + yc.start(this, e); + } + if (Build.VERSION.SDK_INT >= 33) { + int checkPermission = + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS); + if (checkPermission != PackageManager.PERMISSION_GRANTED) { + //动态申请 + ActivityCompat.requestPermissions(this, new String[]{ + Manifest.permission.POST_NOTIFICATIONS}, REQUEST_EXTERNAL_STORAGE); + } else { + startApp(); + } + } else { + startApp(); + } +// checkPermission(); + } + + public static long time; + + @Override + protected void onResume() { + super.onResume(); + + } + + public static int getStatusBarHeight(Context context) { + int result = 0; + @SuppressLint({"InternalInsetResource", "DiscouragedApi"}) int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + private int getNavigationBarHeight(Context context) { + Resources resources = context.getResources(); + @SuppressLint({"InternalInsetResource", "DiscouragedApi"}) int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + return resources.getDimensionPixelSize(resourceId); + } + + private static final int REQUEST_EXTERNAL_STORAGE = 1; + private static final String[] PERMISSIONS_STORAGE = {android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE}; + + private AlertDialog dialog; + + @SuppressLint("ObsoleteSdkInt") + private void checkPermission() { + //检查权限(NEED_PERMISSION)是否被授权 PackageManager.PERMISSION_GRANTED表示同意授权 + if (Build.VERSION.SDK_INT >= 30) { + if (!Environment.isExternalStorageManager()) { + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + dialog = new AlertDialog.Builder(this).setTitle("提示")//设置标题 + .setMessage("请开启文件访问权限,否则无法正常使用本应用!").setNegativeButton("取消", (dialog, i) -> dialog.dismiss()).setPositiveButton("确定", (dialog, which) -> { + dialog.dismiss(); + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + startActivity(intent); + }).create(); + dialog.show(); + } else { + gj.sc("Android 11以上,当前已有权限"); + startApp(); + } + } else { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + //申请权限 + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + dialog = new AlertDialog.Builder(this).setTitle("提示")//设置标题 + .setMessage("请开启文件访问权限,否则无法正常使用本应用!").setPositiveButton("确定", (dialog, which) -> { + dialog.dismiss(); + ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); + }).create(); + dialog.show(); + } else { + gj.sc("Android 6.0以上,11以下,当前已有权限"); + startApp(); + } + } else { + gj.sc("Android 6.0以下,已获取权限"); + startApp(); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_EXTERNAL_STORAGE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "授权成功!", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "授权被拒绝!", Toast.LENGTH_SHORT).show(); + } + } + startApp(); + } + + private void startApp() { + + SharedPreferences theme = getSharedPreferences("theme", MODE_PRIVATE); + @SuppressLint("CommitPrefEdits") SharedPreferences.Editor edit = theme.edit(); + int i = theme.getInt("theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + if (i == -1) { + edit.putInt("theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } + AppCompatDelegate.setDefaultNightMode(i); + + wl.Cookie = main.sp.getString(main.Cookie, ""); + if (wl.Cookie.equals("")) { + new visitor(this, home); + } else { + if (wj.filesdri == null) { + new wj(this); + } + startActivity(home); + finish(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/sz.java b/app/src/main/java/com/muqingbfq/sz.java new file mode 100644 index 0000000..5043912 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/sz.java @@ -0,0 +1,73 @@ +package com.muqingbfq; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; + +import com.google.android.material.materialswitch.MaterialSwitch; + +public class sz extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_sz); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + UI(); + } + + @SuppressLint("ApplySharedPref") + private void UI() { + MaterialSwitch a1 = findViewById(R.id.switch_a1); + MaterialSwitch a2 = findViewById(R.id.switch_a2); + SharedPreferences theme = getSharedPreferences("theme", MODE_PRIVATE); + @SuppressLint("CommitPrefEdits") SharedPreferences.Editor edit = theme.edit(); + int i = theme.getInt("theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + if (i == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { + a1.setChecked(true); + a2.setEnabled(false); + } else { + a1.setChecked(false); + a2.setEnabled(true); + a2.setChecked(i == AppCompatDelegate.MODE_NIGHT_YES); + } + a1.setOnCheckedChangeListener((compoundButton, b) -> { + if (b) { +// 跟随系统设置切换颜色模式 + int ms = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + AppCompatDelegate.setDefaultNightMode(ms); + edit.putInt("theme", ms); + edit.commit(); + } + a2.setEnabled(!b); + }); + a2.setOnCheckedChangeListener((compoundButton, b) -> { + if (compoundButton.isEnabled()) { + int ms; + if (b) { + ms = AppCompatDelegate.MODE_NIGHT_YES; + } else { + ms = AppCompatDelegate.MODE_NIGHT_NO; + } + AppCompatDelegate.setDefaultNightMode(ms); + edit.putInt("theme", ms); + edit.commit(); + } + }); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + return super.onOptionsItemSelected(item); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/xm.java b/app/src/main/java/com/muqingbfq/xm.java new file mode 100644 index 0000000..e8b8f8a --- /dev/null +++ b/app/src/main/java/com/muqingbfq/xm.java @@ -0,0 +1,25 @@ +package com.muqingbfq; + +public class xm { + public String id, name, zz; + public Object picurl; + public boolean cz; + public xm(String id, String name, String zz, String picurl) { + this.id = id; + this.name = name; + this.zz = zz; + this.picurl = picurl; + } + public xm(String id, String name, String picurl, boolean cz) { + this.id = id; + this.name = name; + this.picurl = picurl; + this.cz = cz; + } + public xm(String id, String name, int picurl, boolean cz) { + this.id = id; + this.name = name; + this.picurl = picurl; + this.cz = cz; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/muqingbfq/yc.java b/app/src/main/java/com/muqingbfq/yc.java new file mode 100644 index 0000000..8038a75 --- /dev/null +++ b/app/src/main/java/com/muqingbfq/yc.java @@ -0,0 +1,34 @@ +package com.muqingbfq; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +public class yc extends AppCompatActivity { + public static Object exception; + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_yc); + TextView text = findViewById(R.id.text); + try { + text.setText(exception.toString()); + } catch (Exception e) { + Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show(); + } + } + + public static void start(Object e) { + start(main.context, e); + } + + public static void start(Context context, Object e) { + yc.exception = e; + context.startActivity(new Intent(context,yc.class)); + } +} diff --git a/app/src/main/logo-playstore.png b/app/src/main/logo-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..58ac1095b648bfe8919d8e7f398477a8a28c04f5 GIT binary patch literal 31732 zcmeGEcRbba{|AmgkDU^gQOYP4vPwp_6hbm1^JplW$mX0=NC=fp_TG`b4wYnNkL tGi?{%nNulMWsyM6!v-fo}2a6Hd79@k^uANT9&qk2!Cijt8Mf*>k|JGa##hzR^j z1W}NI9~(}cyAb4Bu5kONrjy~q;1-hB7>$39XNg#J7cPKcGRu`)(lha|rHMGYC=*x> zG&86cetq&A(z>n5o%XAwQEEkGy)@M6L#d1;!@I%gXlMM2+sEadAy<~mpEV4xi`f}C z3heC* O)tNN1JS(t&(Gtto-W0oK z8oQOKqmAicbk(!XNtDm2y!ZK2!2Y%A={>XP8$G9T#SFchTd*gr`6{eMdfJ}4vV62& z_5+{Lu_Q*%zX_Z)&xvW3oY!{Cv6fsOehIanhn`SCXNang5lO#>u6jW43Mrv|_^%Zf z<~Ye=YtnuTEUIA0P(*KEyrW?Ne%vf}GgD1!DQ*%ik)EB{g=R|KAGYq1T$U&19Zozx z&}?5Y8`vSRf^{uu!FG)-FW;a4RvP0pxGMSrF%(FqKXflMm$|3AEgv46^j>PARdQL| z?X5!&n`G#E(F86FzFJzfQ-PGXqgz3$t}eQKQr=#DXn(;xC+VpMVkptQJ9oFAJZXnV zF~4^V&XT$`Y}E;-9z>TbSt^04o2wMlaTwmkbwt;Ce|eYa^Bi_V$BN_8Kx9k9mYUd5 zJEkr}k=uI KKgm78HIH`auIdrGfZK zP!QpRzki)MctV=+0r=m+Uq)nvCnyd+I0ifX>u>;~{~PZAWcPoH0&MF4tm6M09wlE3 z_6ak``N%HkT%3ZG6j9NA4B2`$Jh$~L65ZVpnaN(*vfEX`MQ+-$++sR!GSAYIN!7UH zEH$?IX|CsSpJ?CamQG}V{YKy!sXxyDp y9v9d#Nv!lV zVy^9lV}3hee1P`?Q=sMc-kA)v3z0=djre}-*333i%EEYhIF}H6$`Qy=%>G_VQNVE= z_G90tkFi^9hSb!!o&2JJfW0kZtdv-~`%rFo@4QcskDbJ7+1H}ry7`w|Jkz2sokQfp zeb|Kcbwq8Q$+YO$V-c2)=-5pz$sXa@t>s^Ki&T$5m`fZKUFE7`6nScWLuGGyT+0FO zYJkmxTYk=!J8tNzrzbDJzn)Y1QX 8n&M zIWdV!Ez4J^tX56x%U4jZMZ-Xh_6=-nw*OXVE14oD^*ac&L47Fb-9Z?>{!b*PVf}Ah zJ`a%){{1^jpNE>j6WK&@t6QmWe(p`ixp)y_RQIgeP3qveY5wWSh3MFJ5a-vCB6p+k zH?p%67m-zQF>@gB1v*KraNPmRP`TXlO?0;|D|RdWaLJ!|CRbfX+#5>+gnF??*_C`? z2Y=)9wZXq(avUmvjYB`K9!DJ;hr(-aK>Tlsjb0PB=J_1b{VFZ}H|&+FL2vO78SL>c z#FJI}N~JA~99s$!#!|Y QJK#5O5u9k)=oIY$!XTp7@w%Ah)DLN$r`nNUyO(;DnSm94oPp)yO( zu7QhLr5zRzeRNh%!(x!aH-}lSZSod}u5fq)*8;xu>5bY*IrW95HripKfRkG)N#AY- zaqj%+ELx0^ORsyXwi~e%?3@^o&yu75vZgVOr&x*gK #=dc-~$S;A`O!A}8C4MN%{-7jJ)2MD&`C?_PbA=|$ zySEYbUEN6wPO0^5 zF?0JJf`2nyEc@OQ;|sLsI&}s`?ks!^rj9*j$GN`tXmef8i%E_ytnYz$@sa--HR~fW ziY2MBqV#S1GkW#8ZGA;a(dYy}a`ia#FI9@&DgjmWRO%+0j+-B4KE(1kWrVi#@v66o zzUch=h5|Y;2I)i7n52bDbGy^MNBMKP)!6qYMs;XohN7~Xug%eNC+clm(HvKgyOPbU zdN(>UHO70yo4G5B(#<>0f0OU#?@aDcBkk-V{W3&|b!<34AD xlRUgy?+F-|*7b?w(VO3a }iTp$5-&Q_b5;zG2XR+!U%B}V9;z=$o(kElyT5nT{&8=Dhv1s7}^;|etG zKS>lP_)l7!pFj%5ZliVOXQ$~^o#d?NzIiO)P~lLmO1SC!Gbbgom^tosY=FkNb?Sa^ zC=HZ-;JXfOf{qPx`ene3AIf+$%YJ<2qY1BU5Vb&SWTSjnktbwA^-rFh^$zw<^jhGT zQ!XFqj+n6@S*bGOzUyvY*Sg&!bRGi21Nrsgq0P@gBjnhV*woBxbIujJ`HYWQL_gNH z9P#v^W!6al&Y=30rZ{hK`v}1(58bAYJYfUQ^p(;rv^s4x_i(MctrxiH6Ql!!BfH)e zGfWFjFdX>OL+vU)3-uzi 7<@w6nm^z=xu|_z0U6rlg9u4 z1|uYX`tFliR_wRf`j&j}Q&IzOQA|WWpBTGEbcJt7E!dpxk6`$hTm~(mmD|@wd)?Qy zajRGF;cQ%;cE_9~NiHan`Fy2vJrLgE+9OP%CsNj`r&d?Q*LU>xEYCH;tmwR@FWilf zH{d98@JvNA{!2DNL@1VSf@6z6ZvdQtO)a%otrXRh1hb`=CSO-@ABin3iYE^WnnOEZ zcnw?W=WJMtxEZfDSg$Dmyq3DwUV=ns+*K9#(x=ZsulZjU&qrA6_B^NV_SC(MNWoXH zD%)?Y-ZCZSp?smD?qo)n9Fx;kG;Hfhh+T(~38Mn(yGLpn-nhF*@1M%cwyVziCU3V! zmMGL^-;+i9r@PLO)X{^ucBm}Rp|`tZhLI(%mo_6MgNfe1L#&Y6_{GV-pLOTRd#v6Y zaOKTEgB!6(f9B&((O* w|T?Fyc%|xX9s6Yfe8y~r^Z_!?C$#77m9Vm^><6cMY zw0PHB<1X*K`u9`=vhLBw{0e$BFR!)~wB~F{^N%9(ZaEVt2ec&r7(hIkyeh#LEQL_I z#E*oM?=>e5Ff6g$q4@i(w0UoFsD|7614{py*Yz&PbmFBEKXk=H+uh65d(^t-sC;0J zm)lmT1Ut>wUnp8Kr*_Zt^xn{=`b3+)s4~iVpZ46)eShD&+-iGY(p8B;5`?i}w*bFX z*2*P!tmDyt46w(*B55PSYwikXt4I#hjT*@|c@Nri0}&Sl_c+QduGY?Zt7SdESi8)8 z%V%+%6I)Fi>GFiBOT0LQzN#=N6j>JAt&jNd9X^!IoYdJ}%84znM^r@eTk|AttRLmH z&w72*HAQW#%7=ECKlG6U>$h%yRgY_{;XCJ!h>t&F$60h-4k-Nb1!m^zIL|8ckJc|_ z?r&+SueF)d#tbM)+*}kl+NGoxjyk6CNmOa{h(wpqg(47A8DD2|t#-ce4R*GRV0^9R zuYmuFm(uSh)Z;U6nk{{{zV=f>#3BKnEoVW%FJwC6$FaIXOUGU@@D?A?I-^Lb+4qp< z_#Co$c;z!p!$qzAc>02TX5pre*`Jl{h(ZfN3~*9e?PJMC_vNJS?7{*shxw+(iYNGy zW#}!s$wm)|`Gd4zLgo1DVqc{*(8+x<^;EO2%;$NpPt1u;vrl;J*;U09pZ_JCNyN9V z8@;-vf9&t*f{-m<_f~qMU{%FjalRU*|3V70?S$SG&hv)UMmRR>rk;xM*PpOf`hZs; z+|QAm&KqYJuM{T58c@Fy>W+H0S^_KF JexCqrVX>#H`pQMf*!X5>rgcZ}It( zaH>Wmtt1)FC9(7y#jeCg%9pD2n9NT(1nWTs!R4)YB8tN_Y=>X?<}hY-Yna?M-sho_ zB1te-GjVIdsSqvaws))K{qZkT#n6w6k_QqM^fT| !H+>>(}vT1N1yfe!{pZ;Jhmhua*`8YipcnkAa5As#wkmP-Z1E7Q8D27QOmeyE! zIC_$0ZKh+i1|oHN))}Jjq-(9n9BB%l6sBF2UP^{cCZjGlR8@W)>;IlEd~Ex^s7f8G z61l?kZo|({5fcji@yG}Y)BwAAXXfJ=x8hiqu|_#g@f`16DzQ9kQDf(C#Y$e~?dTIN zWYR}*MK`C;)m1wQ$~-YKVX&6GZe*|CaV+!MElG6cvs+fAe+D$9hM1d$5%}AJHg6r? zaXlM_NXa!-O)RK=g^p>nU!EQ|v;cEz@d~L;Z6?pb%0_-z&H1?GRw~mlcHgf&@nJ8n zq=od4Cz_gsepFSyN>C!7mVawLKBDEYZ6?K#I8xm_m-=>tONnDkc~%AY(~SY*nc&Sz z+~_Er^GrA7xhpGvidg(AePD7mb&Du|N#>xw1Ky`86ISifb^GbYd9J0G{h#Aeq7>^Y zr}N6|(r!9^MCP8|7J+of%I~$&-Rji4tR*z%6C2$*^AhNctCh1_4Cmir>|oXCwyMIn zuZV4pkt2m)Zf<_kF?r#rD9$g7*E1l4#f_7bi;xf*oQiSU4wl!Lc*g1^Ww)QrbmWh} zQX#G0)GqF{$+bLJ(LgjTG_{4{dNfzLxAA#hX-#@KTN*0xN2Vx97-Ytr8aWc=w03dP zm5(7*wkF z#)1G5!A?|Nby6YP@J@95iACxA^siu> zSw#5n8CJQX_$vOF&^vp)RbpQ;eO0N)74~CEs^T(Q5zV?kJWMZ9w{ZW6hW_}+21%&O zRQ+q%nr2}$aqzrOXxSWf%R(ZetaZPL9{`ww H5H?njsURB_A@yV{?X~xyRR}HCLm9 z3ecMlf4^1)v&mCaHkcPb9YS1XANfmakj`W4#YGa-f*efMb&}>j^0U`Tag+O7Nku 9 zf=r#0lmvD67_>i->^8B+6(^4V8a=u88D%d)v{brB@h64I`bfmORjX@n!
jRyI;A8~%boAk zxYZh2v6&LJAL>Cgs{;96PRb4bG*EsNCG_2qxJo~hY)Gx8BDkJaMvMP$J@Iub5tkFc zkyS2lXrw>Bp6zp2$M b?@e-s2Q))Si~v4`cP+P<%0YpjHWK24_GM!rI(6ZvACTsN&7B*JtN#;vlt{f zgH{m7uz=bU&P;JB_NLZ!NfA$(a@?~>P}vN1lKc&mr9#kxhyLz{3?(MF^*D#sSG(j) z;sKuhp; #b|_`}{I zo}cjgu33heuqNdO2~)gIi&e@Je712}o3IRWXk3_7(p*(#kldwYdz!y70Yu&g#=A~l zf5izBOB+g+Y}>{8d*ZJBDC3~?sSP3f{7f7@d(l%c#!aHQ-dZ`q;@eNGq%6zrPsMEV zg9zuoqxV;u%CPEU{vCs7#mx(Kb|rz8$Rl0u8)>@o6sT^X8n4Kp@+a~e=1QDJR!Qb; zx z $% z~p17rnC061_G`2 T<58F36d`S?QeigZeUGBLg0x!rZ#S4PxM?@q1nGxaafebd4@1k>ZzPX7|xTRpc}iMh lG3{c4{ PK*Qh^pQVNr_U2PF zdUT_)(wxTZS{ ;w{|%X27QNuoF^VAFZTM!oD< z6Y0T=l(iT7U~!sG4~CDFhiZJKt}7tazW|bn&?7>vR^2FgPG4J|ROp_dNmK(-4PW(U z-d-vw3OE1*K&;ofMjSU JiJjC=$IiUx8#VTnO zHXYF@3%sJPJx)?|b&EW^`@{hp0kTo9r%>m|UO5B#`{rV9>1jdufaE#W8U1b$n+UDB zVp3>a4CbMwoM_T5apJ1k&=Z%LM2(G3BpykyMsqr73%7Y8uS+{HlU#Y^dEZ*ynLhlo zotd|}Pb`N$qC2-+UqXR;Z!@DPfO!XNpMl*}SX?hQt6?uspWcY?3oyJQPCJXjPPbK9 zV@w<83KojlP1jjc!3k^+FSmjoCZT`Xh?L0aK7g9X?PqWmEUS{W1gLwlVC&`(Lv$=( z=ouEtJ0XpA>a0v%m=N`mSfa~4J?%?Me7Mg6+Yg%SUQFj>>0;-jTCk2yW%+@p(ahD$ z178dTHd7@!SNav~H{06Z#%>X?@0zgXL|OE?w(d55taR6W`Om&-YuVylVbuhLC!>B{ zX=2!`5P$WxrM%9`aWYGte)Y}0r;)f0D@&&AVy+5D@@zJ~8SJ1|;lC6nEv%B+Y1lT0 zZHU|%{sK`IE{ZOF_2?+s2Rdp~>|M@pqN%0>?n-I1skmz&11B{W9+%mb?)kQC{+xC{ zM$$=kjN~HMcWLR_XH%Ka(a~Z~oGUdvWoD<1hYjO-ru|o%U%x~>IZbg#tKlW;Zc=Qt z%4q%lPR@mZhaOxY!E@#tPVGKHMr20C2BQQ{Nd{n?<1T_E>2`Izq|DE+@~1}f^YL_t zewH}YynbR7$OIEc-tl&F&IBCs+~te7lgTF$0NF?rdaod&RD*!DTpZ$WDrVnC%8hME zVMvDmpk8*pexNS-wiM8JjZHG_lTOWFeN~!I-rs#CXB8>Wm3vMWDnK1zM_$JuW+kZ` zX`hR3F+_LOONJYBy4%JSy}kT n-e8zS;^xcBg{eM8b9^6*a%?N!ThAvUiP$% z9fNe*G+LqwPd7V4+h`6N>st3<)f)D7%dvN~zL5sm_nsKGtKneW8ocK&;!X);1rd{y z4qA1s(}**d8Gi4_LmM{{uM> 5M5)p}p+dS7)b`ue8v z1}6!jJ ^g7u^y1OU&OEp3@b&`t%U6HfDf?Vp4?58kinCc|O~SAMmp8 z XHrMd$eW99ILaI#v{_li)uDSOF^}}Vn_>0yNL`XS z^PXiCxglO1oGTL@B!Zn|eGtvF(}lFyyehaGXSZWTet0^d3~=1pPAy#dSk+ghtyu$k zv18_1Tls_!SE~l2$z;)5Q@H(V&lWTUa=u#a2_F&!Kqd04$KOn!Dn_%hl$t*1{)t2Y z`)^LJA32oK8|a6DNs@GnrYZ95F%pi=7uljWM8cLG4x2YI6e#QU+}+E}(J6+7U6|TO zM;_2J_WIk#3Yj!clON7~0!9;~p&b1snTdxx&zfwq>Ppk1^udOkxM0=!ELjONzU=|j z)h*|0g|^pup@TC8sp<;r`8ln) #c|8oSjFohBctbZzbB%_8ME{`AEl z*{_Z;xfdN_Kgkb-yWl{~s@{(-`07cPjXQ>9!j&VH4y`YhXrzB$igY2`usz))`-x`) zm$zBt6iC&;fjWr6-eI#|X5F35+3!+Ci?K6oOtc)%mP-mX#=w=^j6DkXKASidwG5rv z4Dz(=tpK1TA-89TF8Gm!E)`0-rc!S-rgPCu+u92pZqN@FhoE$NevQ;6X=AV8PS)-4 zgiP<*#;J#gb7q1$dl=_vxkk;A?^ywY*A<#cb2zCA8Ooo1Kye{b#xZopiWU9xn 8+0PMmc_f&C%)>f7$6loSVswho4M;y#YJ0PnZ<`<5R_?Q?;JMUnJC zLT`zybIcG8Lhdd%XNRy @YoSek%z3w8D***^lc7VAI65acAs>ula~;W+yiL@A5bTp z9!!fbh9$}0y|ncey%+mtduY8mK@FkhH}f2zv!uuzlWf?bFd+Q?(Y&0xi5qo(dvf&8 z`IMFH%7%*oY3xUSB@u*F*GBFN(wfHC5goQba3>+|s>dI8d++s!`JF%aqvcrNcUxyr zIFHmpBRhFJN1PGzk~=d3iUVRVzg*v)AfJ5`?C>Qf-~~JR&Ekt*moZ7(nAKo6rz^?K zQOORw1JhJ<#doI5O79;ULM#!A)Z3otr_nwG)Q2>mg`$g*zblXMt~EdBAgZHZGHrH-P>_kJc0q!vpl9EJvf-QAHCNqv$_x;BHt zX?WU2R1m>fS8Smb%jG6NKTi86MJ`u)1u8OAb5^X+4xgWiO5Vmi4=(6ALH>b}3r8Qr z@_W &N0?A>oDaqfY?3o0 @I6Hr)Wr~cL)Js$MlbOfaP ze~eD8;6`y4w|6JR8!Gbdy1k(})aA#}%C) -K@^c2ud9dd}eVtq+b zQAz?XG4XWrk=d@^^mnS_^YjU=!B+%hou?Mm>2xibI1Vis4lEd%!rf(nn(;kv<|#AH zA0kGAiaSS)64qo^;Ie x55Z=Y5;zQ V9zpKZLK1C(5t997nZuestYB@jVW zdA*P9=u-9a)w)#ej+Gu^Rgf36LVW;b*%5&rJgKG0|M1XKimSwk72jcawo#KXfHooB zu`iE>k00Go9QQ@veGm`XGxcgKM;2hFPf?(R0HlDRhUm}zD!ph&axt(gt(>NLk`vVb zFEfcy>|=StRh_5oI?YXzOQ^_-`Dg>%TkWr>kEV_nV2Xn%Q1paGP_>JoE_-16^guBs zkN9WmNy(g^&{Tktkyufp7R0zZts9#CPc~?9(5INnN6|n$1QY|c2_(TuihWw>o^g8n z#)5u~trw8odB~RGEN7W5hrMsuQ*jFpZZ5idG*7joQ{VRf9ti=mobY<>(1uBVbSEY} zQ%=*KS4BhyqEKXleB@K#M+T?Tetvj!74-aa(lHy~*g!(YPa X^NdzO`VGpu}kcBu8a^S=o%9`y+1eJZ;>)`m2{>!^eB`>Mjw`1y(++Gcl>NlhHIn z)1-~nct}5m=WxCgkT{j4jm@)R<=UgpTKox5r5dZIqlX!l31~ZNo;N#nC9CREM2)~+ zdxDTRju#{eH?I++;C!R0dc+1}$!C^M4zSUFD)u0`#1f@k%C~1H3=wvRlZqMPz7Z!g zjHKi)bE5zhh-@B}(wh*4FC8=sddW5~90lK6a1L3MI#Rv}$jm&KJ761pAWwQ;0d*&R zYD+X{$?XS~K5;xM5#Vc-%)lZ!x&=OTdLVFTKiS zHgeJ}t zb{ba*P{b$3eo{(Cb4{=E@n^tlIG%tWqq!=2GlP?NHIAjA4s<&^ w@SC3UNQ%$ z3aFlxB*difO~H1toS(LdPjPOUI=4`PB$@UKv>l2xd2tCb?;Uk=CPLjVC^MqI?^jL( z69Jkq@rOrKsmvuWa(U55>dHpgH5N{CvI7};lUEBX2mcbhB$X?evpY)vU^Y#3Bsn%? zb717=0p7!4Aj*NqX`aO@#jiky5ey8bjq3MkpjQFMpqNU&vM2A#Lgoj;AuGj;;Yi+* z6>gQEY5|0Wz5opkHIpa8?oyneHfBlt@`FzS1-J#kX2nK?zRG*&Jzv9g<4d%xTqo42 zHy)IRyC5c@Qm54SShxvBbdr zS&2alnV*~V8ac!h1My^-Je2_$_DOS)gC{#C>DS!D;9nvU9|G@M_Q#k84@gCS@P69& zRBa8lC$**Ag2#(VnJW!Qy?;dYwTna{{-FWr{cOOEbR0-DpK(54CTT#Z?%Rd15!6fQ z7lTfl@h#sfytkZvVpH#$J2N?qwImxF3Bm)gC$d|w-==~iQdzYt>x2Mi|B}Gydpdzf zE$R&m(eWvKQ+mc`xCK8Z$MQ(;#-E51Ujkbq_s4UAi#Lujf4uF3y{ww`Jc*K6O5D7L zntW|OsiIP(M?>TQHnbe~BMo>sU8Qyh_$+F<3H?xlzZ)=vR+jf>Q_a`4_bn}M7zgGq zEe0K@R@cZwoTGBKkyI9Gl|DM$r(lj0z2}dXko7}~$PA(f)3T#pjgyb_3S~H#Yp_pc zoPGmHE_qam92RxqjHYcnTTW2mxwIjgPab#l*o{HFy9GKC%c#};#Q{FbhN_Y~Oqrbh z(>CDn^^QQyWrFs$hNg09S6KHYBn2wY)@o5DZg)S=DCx-MPMPCdO4&i5Ywte6lB9SN zvdl|FA=#(By$JcpcFHwcz3U%|B-CRVQS}*b(#e&tgMzZ>=9vSGQkXts7~aoL$+I(4 zkRM%p3JAIv2>R}t36 5CYA2(!~9xZyl|ajToe{Qw6qV|jInMP zy!=p}s)n-?Bh#Y#DJ8q`^7N cUF$V-lP%U-(5??FJ`PY@eAAl2f% zod%@SbK$ChZyl{`A#8!!(W3lO`LA<}oq%zVwN;)JyR4Rl2xoj0y!`4$&wY^*^5AW3 zQ-!W%3(0}oIjfPY*AH7BV4zf%AaDJ?Rs!tX=p^K3pM_YDRZq4&J;~b2e(QbdUT2pz z2KQ6t`D`I%;*&Z)NkgHMeWl)$t=Mlm;tenJ1EJh22foum-z?$Bhnt=CSC$6LCu$ qX0V&o8GnOM2L-OEw9j?I`Cw0 zTl_HsijR3IGbZLaxuKzVodh)k2o&R1*e9y;@4bi%{A)s-M0=Z`Z(f&m>vw9ful7Ml z#1h5nV162#>A!4!$2(W-VCh~?gT$}8vZr0U@(~%RcOYEMq-1*P3~r1$jCe0U7~xP= z#BJpb?2QAuTvc3oarxtr02luDjX;HJ?Hn&Bcvm1SZiOlC`cXmvCC#*(TRdX9SAI86 z@6`wIL9FW~!g}9 !?2 zqeS~g8n-eYBBOfLJ{zMY748pPc1tebAFMJ-SXeqk%k(Scl_`TY3l1_VCNYL*cI>}a zpT`c;>*BH3`-%i6QwKFSE0IOXm=%`?nEs$XQC*WmD0W%Ccd&!Av)pN&h$OQ;*wy7F z*m2z 4TfbMZSf$$j z*tMfo`)uK)jo5!#r-o~p2FTdt$N0T>_x7gQ`c|6QGzBY4sKZWwRuLF|5}fh$c X>TQxEKg-Akf_pX&@Q z4+n4|m3D0WZZUJR34hMguN&w_^`q>MJ696-%ghgn71zZO?mkl%y7#o=P}nGUwHuSw zjAq;M+m41}GMUgX_eJ|hQTsQt4Qp2`GgaEM+~?C00 NAc%9**TXy- z+kCNxW&y0cT(SKY0>ODGm4MME?qMW<9MG3HSB7)j%1();>7l<+buj1U?wiBQ;n?Cv z(%(2T+BW>x8y1$NxyF9C+?HuawMV+}MJ2vMX(732mX9es*6D9|p0j6fZ*o}f?A_(s zL`TQnlo}u~nw=z?_MOqXeDi!vsW1I5n+<24eIZpEo`D|B;6dgcD|KVB+}$fE;`ukR zT6e!ujnVSpmJwivdw04-Y>e1adWs?5<%o%`?&rVRMU>0_UEOJlALqrhi|5Jgj~q{n zn|R;bmmhkPjOfFvSh0Ax@_Lz$<0OeP(zX(_7pl)8U8-`nV3Lm>0L_3_{M4z6-jEL` zSDlu9bi;B_s8+~7eu3TVtWoyqt0zaOz7@Ysl-72XC}m>o_fS{qa`-_aNnk_Rkx5Gu z-BbTLo87miZ>D9O5yvK91oA2X;+Y c}0Oc~=4>t+!eIoG%2 zNXtlY$5%V}p~##*Y+nnzN&8S&@s^$RRq6CpbIz^WKD`*pL1h}~xVVzx6FC;afGW&n z7)9V@tVnrh2s;wE@<9%0`-!%*DVwn&d@iHj(`k2P!Ii5Jo!ENdm?N}cHiNz_v2pL8 z39Bbr(F{5VITAlIf2}c WrF>M>0@FJ(wMF*>)-i%VsL;x-facB zxrXiPu)8}P(NPGiurjkx=v*xgE>|7!I7r~Hc4$E>->gpYE+-72yH%&UF43YL#YwT+ zGa4@I+!^&0USy6b-va7`C5pxI7EPD7n7sihA y85J#~!EB_?zEfmxQ`C~=4v3Zn z0>VcoScLWhg-!i!nbG=64s6YpMQct#mH<~mre4F0td-`jWA?w-;Ry9)i2GDwvIDKY zqXVb$A|Aa^&c(UqT&EJhL=Otm1bYtyJ5qWB(L0v!tfUCTH*rAW?p`$=zPhF1Mar~M zAwGTNHIseA>URzt!`su4<>>HclnDMhwG5 pDmf&HLrlNVD8Li&nR2DGv_OP6;-09h80B(MVzOZHAamKfo(fmwk_Ptz@Er zdVi8YjqasFcyYz)b~y-tPq%4U;(*w2cr8pxQnGx4T$esgahj!KH)A^&K6o6kM4l3( zR&U9--M>|@D8{UM +(0cRLhV-jg&hw{#koq&>9r6*MqF?pV#PnL z7I)SAY1*%HUPlLU)F`4#ob-V3Nw|DabsY7};PDS%uL5HDHLquz_m?B-*B!e92>Po9 zeNqCXkri6X4Mp>Q)p$>ESAgpfQLLguElg9<&ctxfnj^)>jCOatj8}7zuD~VtABWBk zU-Hd4oiNnmXr<)!PW-BeEu4PqrRIUUfLnI!me7jzQnK4(AUje)lE2VmXw#Y+X!QwL zHRu(ktZXxrBAKi6$4}eAN=_Z(K>d)a$A0iiG5$xwOH2rIC-%S$)L_*gyq~GtNP872 z+b4l?;*v85-GNI0<&fJM2v=rMZ;1Vziz6O9xs#cBp~~<+P%6N^fTSuIhlTE0%J*Tq zU@}22YM8{XOTChRPQ3{Ui1epOGx+CjFNm+uc`-5jVA u@>QpWJ3=*9`b|DT7pU#7K&D=tpx+uZa6LB_msT=qZDNI}g! zJu1zz)!KNU!H$B-)eC>j;7~Z?Oi<}j|6ozW#ORMlo9B{-K4W&R@u2<#x%;65B3R4| z=*ci#p(d{uy=QHlNNXS_qsN^4S2%~Bpab^K+3t+J@9Tn7FX92ZoxFes9iwr8c>num zu(L448)C8&d*|Jk&%1Mt zN`3QUs_uXsO=R8uNO8^!^2oKXB7-sT`& zGM?%Mjl+CTQ7@0aBW10CcpmJg*!Ae3kR=?};d;tpD{Ci#XiUTxtO4EHQl|oxiHDp1 z)8Ua`@+%B>dJ*aHidTSnn|)m_x=7p*5WNna%%MMI2dvv6MPKgIanW%{{ZsX_OMa#C z1or%ALG{qcFrGMsbCJa0j@D(xHo>oj#;bse_vg;yk8R$S@b!Mvde^|#FT#%PV~&n( z4SdvXzOga4p@NZm=uWK8gN f0-_(7#P9{3+Ze^2IC zxvGuoGsR;wa=nO<;PfRce9MyNwKVqo$1)QpJ~7iWw)uZq$7bRUo8=!D-2g)GyP?3K z?(`HsEU`Cy?2jA(OjGJ7UV+sI79}%kTFEI;RxUb9?(*!1qGx)Lv1#3ukWTauU++$2 z;A2*G4P}6Mo8n`7oOz*=sj;oQt>_h^F)F^YM{kO_5eIBOf+s;7K@Dh|bGo~nuLaF& zP}_5e=cSf)FfN3#aWTc;8%2q}7YnQt_J7REm;^3I$UfbO?+g;`%XbQyCStX7;PjHS z??F!x5&B-hko)qtLD%KCRa}F%8zjNquS=M_8rqoA^!v0Rn7+ETkcn+bVtm-;Jizlt z74>S*jy7U23{cfOLOaaG86f?)TD&R9JkoDxEifMU|2<%cJLJ}p6cASLah Zr7Yd2+%IAi8T$^TJod%st1LfO;h4uNA*ZK^HhJhRz-D}vcK@}k5Dm~cZ zO~?##zsC2SvOb@1l`TKE({;`&H QYCT<6n!AV$ n$CZ2a=HS6h*-dv8Ar4;?Des66u|I@ zqd7DJ+O$DG16MD#c4mzgxQeIB?uMY_6EDwoxiVa+GEsgk@Lae%GPx?C@fgvF(2DAZ zJTl}xsPp6>RgEBnh9f3|G Qj9$_2x0NlhLUPwVYq*r>0;?K-Ci{F3_xYI zq5kazN>XcR;*U?!(Sv eBnYO5e(W6Nhd{iYYnD>9iy4%;9$*#6 zN#5OBZ7CR>b0!=)JoG~STnW!P-O`rEEnd_!z~!A~^c~h7P(XE3sKK4BZQev3Fgk&4 zzc6d)cLdbh9j`gwXP6)77}uR7sM4Zt`z+~DdcJxx>Ipw7zGou>Cvmw9Ia$p9Dm&4~ zpm|4p#>-9wcL0q8e>RbR)4|X%A*E_^c-dLAa%9Eo$tAXuGRG5KCzCdOoQ=ikDjbPM z25fzK&m_6u^zwv$9Qt)PCMawzXd)@rK)T+&qidxUH&nV_LcH|l@ud|elSM2yam*1& zkfJrKsxM6A4h2g5!1;a;gH s}Y z{qfc4HAsuPBlK(Fq45lp613CVKAq9vxXNr8CszNRsE{_^s*xZd_owGcy&c-nABjd4 z1G8C@dAwN;j}A0&mFBy|Cd7*nan@R5RnpF*FGCuhe_&lEslL9>scKNcRNqj`QDy(J z=p5oO2j0E_S|ND$%{e-L{Y{4B=rboM$T?3_WeQ*Vh Q9xG!r%hBAPGd()G-K-Oo2`tvh9ku3F8dGW zB#WfHG`ynNU$fr@)hSwG6l`CmOFizHBK8rb7oLY%x1;pWs+>fj?Fno6PTg{}v}4ip zyDy`vb7fu=J(0a-<{-lbE^j#cXaM%HBD&&%fxpU*=@#DlmUe7PJ$8 tvdu#@HxlpY1kaGXSSPJ_&Ku8X3pbtkaV$o `y>o$00M~B(jNbDDhPWvYhhiLt{8SfApLdG^?K&VGRPlf0O z0t`G86#vbtiMaak_*%%SlP?Qu?nH;9rc-ogDM)vXIT3i02KS-3-2SsRWYr0F)N0GU zY=w9%(|K^e)_557?mYSnDN#Gx$4{MI9rw_5ch=KM7cYJDimvu3%Eq45eo#9R9JK%c zE9OxYAhd^we69ao)J$k%&Ha!BT5dZOS|(wrq;(;INxu5Lky@JeYtiNKdzC|`aaF?> z-QCIQ&f3Qd`(B5}_A7UNmY>BxCBMUS`flZrRcmO?;%^q{R$-5iWfbd#2^ zJ@3$UC;~BP+VI%CaB?M)2l+9cxTm=-9UpByxq=>!-|We?-Lv|-+|t^NHtgzdtH&ce z?K_Eb*$ofrwD{}O(~UO=(U|tQE{(#+Z-xQd*KmB{rpJ70$HFrq0%6l+5daU1A*TwK zKuWeW$7yLW^wt}X$nFv<82=h1@M!6xrHH5u`qC+r*&T^K{t~7`=xCu7Lzc&bjA^r~ zDP_V}1!Xmv!tcf>VP7P3k^>}WCD9Tou0jrO(j45455P?uVB_|(FwRnaCb`}Q$mlN6 zhzS?!V;2H!)Hz;PiPe8z!oo-1!K{~RC~wDn3-^tZgyZ8|wZQsYCIRmG@b$r K~;+g~sy~dyCLW@0sYv5r>1<*M!+aO|G-d zfZpB>EK6Z??rsn!r~9vI<&sw8dYF@#HC~(4QRn(SIjJdV?kOtA>tqLzuQkAJ-o2KA zUpk_@)u>=8K?kassR$^|eMhoE)3Jvz9wsf$r(D~taa8^ww!94km+j{?1We(TWnGKn z2q%S3$zI1ZL*B2gv*P=-ml7vqms06sS(3Vj&&9uH38b5t8zMOvG#rURG*Hc@6O3s2 zL&~byJP;)4QiBBZyArIL-4kZ-wezzy_z@=0`a#DzU@#84YeQ_BTdRgxPb6v%?OtJ` zy>%G`UM-j^a+4%4LL1Y*9qSMha-J_c`P-*4Wir~ka_Q598U$q$(Z=+XFFotLS(f}0 z`7uR7Y7yY=@ %3cu2P?)-O#wWRWx)YnhlgycqXfN-}A0&?;}Ycs#o(KE;u`wOYjM%*(v> zFQWougXIJRKSa0xb_|vWLW6w6!ayH;bqQLDqxIQ60USviXbrTF!2{B`3PMBob}!y{ zFV?|Uu-tb?>-gWD(wCoKw3+h )Z9qV~R!g(X <~x*vjY?UqQO5 z#>x{U(#(f`Hw3x^m`hm?j>PO!uVv#7lETj+Da=vXQyD5!&)|dT!nd}u5!A_gOTeSH zkVtc0?i&$1K6!)uh5TVWt~d#;{`7MG)x_B?qFf333MU*}lH*-NRf|3BI5)Q~8Y(J0 zZDVu0LK_MBC>Ws@5Nms1uLJ3<6b#K3OIqr<9lY|0P~$lD^*;ppU&BkXNM|=T9`WKJ z!VO@-uq`$8a nnZFj1xO|l9SB{%w-a3b1=geQC#@CB{{cm zHx=*z)`y`4v8ZrgL%ul$9_bPLOjWo=WN%oC0aaRR@dP3{*)2wQ`_f`%~u^@U^o0 zL03U|cLp?vm*R-4EjyF6>QC;$8)D$uJbtmDMk17!C=5u?K;95T2}z!)TVhUh7xAKH z)JPx7v*-Q)^AISi<~d4{uQ6b(IM)wjTW`Sz38E!byV6)1XbSwU0(hCi$~`P31qFO* zMz2C$j!Wy&v;4`q7eVz44zGd^8a71IT-6;oe}heMFKz4 9T z-y5_ht6uj@(-25=SMZ`e*XIQgdb7r{{|>}XjEW_u2$)H6S0FV8uX$*b*Q1v@Y#;sI zq4*i7cYPDQs6i3?0T7eY8Nj |S$MRhrCE&2OBD9urGv0>||LwD{+tl{oPT?JyI#W{nQL*ETQ8&RP7w4_QIK z@~1;J;_^$@i9P{uK}gj8h1mRAIKX~Xx>XAvdSZ;}^s5tE! GO0wibW0-%gBayr|jV3O%P=#Hwr zh%a6y`i})xQ9uORcb1JRC{+WNi8NC>`ze|aAj1FIq%bJAZF#&>wM^mqYPVm}0+Ns3 z<=p@16+jztSB-XObr=1(=($cMclwm>+B!r35o#>-X%dj_wR%c0lXS9IrQ_QMyP_$k zZ3zs;|Cv)8@XBF;W?>39Yy-qKJz*Jm4*wsB>dz**psJtL$DUmJ(#P d!&Dya(*K+S0Ivz}K44#uhCgb15Y6Gx zW0FzwD~U%B(CH2l(ci}f9^@9! 1ZV zY!aEy-qT9qooi~57X$fitZKG?<(E}9Uv2ccl&w27xyc+3nt&v5zoshKDVV%;OSQ9v zPbR)7Uak|w!1~X++U}>A2Tiv_DRHTD2M!QpmuBk*y9N@`p!yUe&Fb?mS-rQtK%6O z1Y5dxa-N@||NPmQ;A=if1= )xfzJ&)fxsp! W19@R$)62a9EquDX2B*feFd~vhPd(){aBXgtAOz)%XfPc^Po64IVP|v{peA zlQ@TPI8J|lliINaws)R#DH(y6fK&^1AizWdblG)08xQcGUcVR&ePI|hI^m21IR)@# z3>>cX`64F8%C&FgLX@AQ*t~*8rckE&pL_^>zxJ3KOvjhma7$6Qco3QG&|>Yz(u$$u zTfh=nF#5dp;2L^LiBKBPxi!TN*4xQl`HMXL(o+N?PRjNrK5)2%*0J%uip?LEH>T(7 z&5OYID{4sZ?d7cz`lZ+>KZ!dBQ{gLmU+OOYC9`x^BKU?e?@71NRQKq1964fAe!VH* zc!dO#p$y!- <>Z|92Q#vg{1nKFFKz9m&y*QqLR06xV3wany9XoMv z{# zS04-QL5ruKk z&+t{nL}|U`pd1&~kbgA$z*y>UBZ2Aw4_vdD)3NnGD($?_b$k*%-~_ly;jFKPnZYLt zHpO%N(*Q`uX0-G^5q1G9n*NMG&SC1TWu~ -ff;6wo&xp<0)8W}hug09=$y)x)LAahnp_OLRpce8jj z-a^yYY*?zQI?koTT;N~Ft06jnX>dfsLvuoXbzStvQ9TmFdPeCL;MkMRt66eU<)e`{ z=DU`FH8`U}PZJy}AOa=( mpVanVefpkN2Vk14s zys^pA;A*xP q1XX3y@u36!=jI 8Mg`OEsf72@Ohih! z^^Ipo2BXab^*iF^U%#FRQ>lq0?H=M%wv!smRjG_u`#<9^aU{$<4F7E+kr>bD(qVNv zilj*!>=i#DS?v{6ob!hm3Sz^(f1gJMn81UH^b`^5APjAq0e4o2YgdZ+U&*@3y=En( z7z#GU@#iLK^%aI;y54faj6JL_2RV1w{zSb;HK%y z>RnoYv=ydkmO*EH-I~A}dz5~5^gi)IHrSev4r$NA1QEM&HU`Rp?b$=lh4d$G?y@5_ zFbOxdkh@J}qRrV+RQ>IcFT{1QaT^^kGA)TuyZwip+$8{T@(Q7LW+Sfv_XG?eT`nam zqQ8{MS(3vy2lHnN2a@eX=&bf~-*>q|_rYi0!h66;7vyhv$3b2y)%vH;ifbQVqI6Hn z%qr!T1-myg@f5A~buW*M*-$xerQUriX4>XUzs(YGq+qt&5xKImL=XboIG6V(?7{X} zqkrpwq|t6Ddynrss4x1r{$Agwh_o3O)x29y;el^tdZl#}TV3+Ts*h2TqjI{K0JW+5 zfC($}&- u8L)c&u9@FIuG1zXHQT`O}Ds{ znr?O50#%UfZspM%m)v#u)T3apVs2b`S=>Knf5ZFvZ*0imQBmx_XqWMw;Ws~wt>lZz z>DY5m=pY L3eN5ejw%EkY_!h6wBWbIb+y)n2R4Jg^wx*Pk24Ka2dp z(u!E wXe$n%eE+AH`)j+5HFpp}*r4dmO9dvChLN2lVe?#jITX!Iq-#0}LG5uu1B} zA*D2Sew>$?>aM%B9!sgKm=NYs`u>5>^FO($31>FZNwnned9YNEV~gQeF{0afWoeMm zO@}gl3=aBTF9+u$(=Ebgd|FpNC^q-Zz8!y{g`Tuni#GHsIZiFd34*f?jMp}%=n{4+ z*f G<-JJ?w&O9ZYk6Zfla&}k7c7`rOOMiVCS3&tk{5|1b@c92bdp- 5`5!@8q{p|Kj+D05n;WC;{v=Kv~G@z77wzFqP?OlP_gnfj0qpM@=f-2}0$DH=I7 zF*24fj|GMr1Jd_5P{MxzfCzA?XmvEtxd+P_qt;xT=_{|Z%R_zlIw?vwB23T?db3uK zoo*zN^;}bqUPS+togshJ6Vz^nB_-Skk@iVn?h_8f7Upq8 IZb5Vrx2p1w&JkN#=G*8%d9nuQ*1!rZNVGug0fQjG#!>ietOLr+L4 z8wy@J3$K Hig87W0d ;?~xa@nDe@IZJxw0y3o`u-TT!Pl!i zp<}%5^Bd#8yY@prqWV%>7n{r@e*O3$eCAT)lb6?En9{vV%c<)t1sL2LWB{%-4{bH> z>Q0jpT8{0+EtrH^-W?rTF5y_+ty!@ Vsq6i;4d18%=))ItiJ6D*#m2fbnuyFGO^`3-4*FU*@ #;Sp0?J_ z3@{Tl=_~>XP;-Baw4IG>J|ow!D%7{Y*zQup+ZBCH$EdVj;id*P+qrJYC!sAs6Eo2C zf*3A @!vYAVA#Obu znjakx#cQE|ej#ztnkVw-=`KB!!Dt9E!B-88y6bnA5V)X4ZZAV1a8UqmMGNF(yBXVD z>y3z?S6 Y?wqX zNrO~W#jE|A%leOqnkbI@1zC&Mh